From 5fc45480276df5b39c0fb84070357b59f0b19494 Mon Sep 17 00:00:00 2001 From: circle-github-action-bot Date: Tue, 28 Apr 2026 17:59:33 +0000 Subject: [PATCH] chore: sync to arc-node - eba4c9745946a6f25a40c408fbffd0f974b69a2a chore: sync from main@391f3332dec78b32229101d1f597ee7c9bd... by circle-github-action-bot <[internal]> - fb4fcdba4c23a8f6a5bbdd78b4f93e4067dd49b0 chore: sync from main@b5f38a110dcba2f7baf70776a61d619d77f... by circle-github-action-bot <[internal]> - 0e03330c5aa6a0ba9d9298f71f6396d235ab1779 chore: sync from main@3df766646e40b64490597640decee807a33... by circle-github-action-bot <[internal]> - 22d039059f388125f0b80b5d753b90e6a32d2c6a chore: sync from main@e12060a75059eefcb3525a1ce581708bb00... by circle-github-action-bot <[internal]> - 4a9f8425088f54c7bd1c5ac48aac6b0734b9be10 chore: sync from main@1ead54346685b16f798e5b1917dce309f26... by circle-github-action-bot <[internal]> - ae25f503b423df58c6a6c6702dae82fbbf6d1185 chore: sync from main@cf4f9cc5e7d5bbbedb8e03a0f88c8b35589... by circle-github-action-bot <[internal]> - 8881b886e4e582570094dd97b31068014ddb0b90 ci: auto-label external arc-node PRs as pend... by circle-github-action-bot <[internal]> - be663db3644c23ab48e47e18fc907cacf630873d chore: sync from main@27e0db142093ed3a9ad366bda236af50e0d... by circle-github-action-bot <[internal]> - 87bbb904dbf5b02d15b21014151d3cf70e2d9d07 chore: sync from main@6e37777fd23dc50c3571ed9265f99ba5609... by circle-github-action-bot <[internal]> - c58b7f91171ac2344c3fd07e3ac52b31b1278da0 chore: sync from main@feacfe3623dbb3497b3a1c28d9a218460d4... by circle-github-action-bot <[internal]> - 9b4bb61370814d43a53b7f5374ee124108584907 chore: sync from main@8da4b7193194748d2c62c0876e40851dac5... by circle-github-action-bot <[internal]> - b27487fcf5ce097574a88dac76b740a41dc10777 chore: sync from main@0bc52ca1439339369846a0aa4bfbc24c300... by circle-github-action-bot <[internal]> - 50670ab0dd507c63487075642f2ade8d065ce8ff chore: sync from main@df698f343631a7ff354a57228fcad084465... by circle-github-action-bot <[internal]> GitOrigin-RevId: eba4c9745946a6f25a40c408fbffd0f974b69a2a --- .dockerignore | 9 +- .github/workflows/label-external-prs.yml | 46 + .gitignore | 4 + README.md | 12 +- arcup/arcup | 14 +- assets/localdev/genesis.config.ts | 4 +- assets/localdev/genesis.json | 52 +- contracts/README.md | 9 + contracts/scripts/Addresses.sol | 4 +- contracts/scripts/ArtifactHelper.s.sol | 42 +- contracts/src/Precompiles.sol | 27 + contracts/src/batch/Multicall3From.sol | 4 +- contracts/src/memo/Memo.sol | 4 +- contracts/src/mocks/PrecompileCallCode.sol | 4 +- contracts/src/mocks/PrecompileDelegater.sol | 4 +- crates/eth-engine/src/persistence_meter.rs | 44 +- crates/eth-engine/tests/integration.rs | 8 +- crates/evm-node/src/lib.rs | 1 + crates/evm-node/src/node.rs | 103 +- crates/evm-node/src/rebroadcast.rs | 211 ++ crates/evm/src/evm.rs | 147 +- crates/evm/src/executor.rs | 404 +--- .../src/addresses_denylist.rs | 2 +- crates/execution-config/src/call_from.rs | 4 +- .../execution-config/src/protocol_config.rs | 44 +- .../src/actions/payload_utils.rs | 4 +- .../src/actions/produce_invalid_block.rs | 4 +- .../tests/beneficiary_blocklist.rs | 71 + .../tests/beneficiary_mismatch.rs | 167 -- crates/execution-validation/src/consensus.rs | 55 + .../src/handlers/restream_proposal.rs | 166 +- crates/malachite-cli/src/cmd/start.rs | 28 +- crates/mesh-analysis/src/analyze.rs | 11 +- crates/node/src/main.rs | 57 + crates/quake/README.md | 21 +- .../examples/testnet-small-default.toml | 142 ++ .../scenarios/examples/testnet-small.toml | 154 ++ crates/quake/src/infra/terraform.rs | 26 +- crates/quake/src/main.rs | 60 +- crates/quake/src/manifest.rs | 17 + crates/quake/src/report.rs | 2145 +++++++++++++++++ crates/quake/src/testnet.rs | 2 + crates/quake/src/tests/arc_node.rs | 317 ++- crates/quake/src/tests/mesh.rs | 18 +- crates/quake/src/tests/mod.rs | 6 +- crates/quake/src/tests/perf.rs | 18 +- crates/quake/src/tests/sanity.rs | 18 +- crates/quake/terraform/cc.tf | 9 + crates/quake/terraform/nodes.tf | 9 + crates/quake/terraform/variables.tf | 16 + crates/spammer/Dockerfile | 1 + crates/spammer/src/erc20.rs | 2 +- crates/spammer/src/generator.rs | 2 +- crates/test/checks/src/perf.rs | 42 +- crates/types/src/config.rs | 16 +- deployments/Dockerfile.consensus | 22 +- deployments/Dockerfile.engine-bench | 21 +- deployments/Dockerfile.execution | 22 +- deployments/docker-compose.yml | 2 +- docs/installation.md | 55 +- docs/running-an-arc-node-docker.md | 170 -- docs/running-an-arc-node.md | 134 +- foundry.toml | 9 +- scripts/engine-bench-report.py | 956 ++++++++ scripts/genesis/addresses.ts | 14 +- scripts/release-package.sh | 38 + tests/helpers/GasGuzzler.ts | 11 +- tests/helpers/forge-artifact.ts | 49 + tests/helpers/index.ts | 1 + tests/helpers/networks/localdev.ts | 7 +- tests/localdev/NativeFiatToken.test.ts | 8 +- tests/localdev/ProtocolConfig.test.ts | 103 +- tests/localdev/genesis.test.ts | 177 +- tests/simulation/ProtocolConfig.test.ts | 68 - 74 files changed, 5223 insertions(+), 1455 deletions(-) create mode 100644 .github/workflows/label-external-prs.yml create mode 100644 contracts/src/Precompiles.sol create mode 100644 crates/evm-node/src/rebroadcast.rs create mode 100644 crates/execution-e2e/tests/beneficiary_blocklist.rs delete mode 100644 crates/execution-e2e/tests/beneficiary_mismatch.rs create mode 100644 crates/quake/scenarios/examples/testnet-small-default.toml create mode 100644 crates/quake/scenarios/examples/testnet-small.toml create mode 100644 crates/quake/src/report.rs delete mode 100644 docs/running-an-arc-node-docker.md create mode 100755 scripts/engine-bench-report.py create mode 100755 scripts/release-package.sh create mode 100644 tests/helpers/forge-artifact.ts diff --git a/.dockerignore b/.dockerignore index 6880c84..926f969 100644 --- a/.dockerignore +++ b/.dockerignore @@ -57,10 +57,17 @@ docker-compose* .dockerignore docker-bake.hcl -# CI/CD +# CI/CD and tooling .github/ +.claude/ .gitlab-ci.yml .travis.yml +atlantis.yaml + +# JS tooling (not needed for Rust builds) +package.json +package-lock.json +k6/ # Logs *.log diff --git a/.github/workflows/label-external-prs.yml b/.github/workflows/label-external-prs.yml new file mode 100644 index 0000000..4959641 --- /dev/null +++ b/.github/workflows/label-external-prs.yml @@ -0,0 +1,46 @@ +# 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: + issues: write + +concurrency: + group: label-external-pr-${{ github.event.pull_request.number }} + cancel-in-progress: false + # cancel-in-progress: false is correct β€” label ops are idempotent but partial + # cancellation could leave a PR unlabeled. Two simultaneous merges both hit + # `gh label create --force`; one wins, the other is a no-op. + +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: Ensure label exists and apply + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + gh label create pending-import \ + --repo "${REPO}" \ + --color ededed \ + --description "Merged PR awaiting reverse-sync to upstream" \ + --force + gh pr edit "${PR_NUMBER}" --repo "${REPO}" --add-label pending-import diff --git a/.gitignore b/.gitignore index 6318e25..e94b104 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,10 @@ node_modules/ **/*.rs.bk *.pdb +# Python-specific +__pycache__/ +*.pyc + # Test artifacts /test-results/ /coverage/ diff --git a/README.md b/README.md index b08ad1e..ddcdcac 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,17 @@ Arc is an open EVM-compatible layer 1 built on [Malachite](https://github.com/ci - πŸ—³οΈ **[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 -## Run a Node +## Install and Run a Node -See [Installation β€” Build from Source](docs/installation.md#build-from-source) for how to build and install arc-node binaries, and [Running an Arc Node](docs/running-an-arc-node.md) for configuration and startup. +### 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 diff --git a/arcup/arcup b/arcup/arcup index 5d7c9e1..2663176 100755 --- a/arcup/arcup +++ b/arcup/arcup @@ -8,11 +8,16 @@ set -e # 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.0.1" -REPO="circlefin/arc-node" -# TODO: set GPG key fingerprint once release signing is configured +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" BIN_DIR="${ARC_BIN_DIR:-$HOME/.arc/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" CURL_HEADERS=() @@ -333,6 +338,11 @@ main() { 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" diff --git a/assets/localdev/genesis.config.ts b/assets/localdev/genesis.config.ts index dc08927..4615e92 100644 --- a/assets/localdev/genesis.config.ts +++ b/assets/localdev/genesis.config.ts @@ -18,7 +18,7 @@ import fs from 'fs' import { z } from 'zod' import { parseEther, parseGwei, toHex, zeroAddress } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { createBuilderContext, buildGenesis, GenesisConfig, schemaGenesisConfig } from '../../scripts/genesis' +import { createBuilderContext, buildGenesis, GenesisConfig, schemaGenesisConfig, localdevFeeRecipient } from '../../scripts/genesis' import { bigintReplacer } from '../../scripts/genesis/types' import { LocalDevAccountCreator } from '../../scripts/genesis/AccountCreator' @@ -73,7 +73,7 @@ const build = async (options: z.infer) => { const config: GenesisConfig = { timestamp: 1763620028n, - coinbase: proxyAdmin.address, + coinbase: localdevFeeRecipient, hardforks: { zero3Block: 0, ...hardforks, diff --git a/assets/localdev/genesis.json b/assets/localdev/genesis.json index f705ab6..bb902d3 100644 --- a/assets/localdev/genesis.json +++ b/assets/localdev/genesis.json @@ -33,7 +33,7 @@ "gasLimit": "0x1c9c380", "difficulty": "0x0", "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbase": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720", + "coinbase": "0x65E0a200006D4FF91bD59F9694220dafc49dbBC1", "number": "0x0", "alloc": { "0x3600000000000000000000000000000000000000": { @@ -67,18 +67,18 @@ "nonce": "0x1", "code": "0x73fcff98b65f9ea559ec0df36f4072c7e3be0520df30146080604052600436106100355760003560e01c80636ccea6521461003a575b600080fd5b6101026004803603606081101561005057600080fd5b73ffffffffffffffffffffffffffffffffffffffff8235169160208101359181019060608101604082013564010000000081111561008d57600080fd5b82018360208201111561009f57600080fd5b803590602001918460018302840111640100000000831117156100c157600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550610116945050505050565b604080519115158252519081900360200190f35b600061012184610179565b610164578373ffffffffffffffffffffffffffffffffffffffff16610146848461017f565b73ffffffffffffffffffffffffffffffffffffffff16149050610172565b61016f848484610203565b90505b9392505050565b3b151590565b600081516041146101db576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806106296023913960400191505060405180910390fd5b60208201516040830151606084015160001a6101f98682858561042d565b9695505050505050565b60008060608573ffffffffffffffffffffffffffffffffffffffff16631626ba7e60e01b86866040516024018083815260200180602001828103825283818151815260200191508051906020019080838360005b8381101561026f578181015183820152602001610257565b50505050905090810190601f16801561029c5780820380516001836020036101000a031916815260200191505b50604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181529181526020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fffffffff000000000000000000000000000000000000000000000000000000009098169790971787525181519196909550859450925090508083835b6020831061036957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161032c565b6001836020036101000a038019825116818451168082178552505050505050905001915050600060405180830381855afa9150503d80600081146103c9576040519150601f19603f3d011682016040523d82523d6000602084013e6103ce565b606091505b50915091508180156103e257506020815110155b80156101f9575080517f1626ba7e00000000000000000000000000000000000000000000000000000000906020808401919081101561042057600080fd5b5051149695505050505050565b60007f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a08211156104a8576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806106726026913960400191505060405180910390fd5b8360ff16601b141580156104c057508360ff16601c14155b15610516576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602681526020018061064c6026913960400191505060405180910390fd5b600060018686868660405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa158015610572573d6000803e3d6000fd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff811661061f57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f45435265636f7665723a20696e76616c6964207369676e617475726500000000604482015290519081900360640190fd5b9594505050505056fe45435265636f7665723a20696e76616c6964207369676e6174757265206c656e67746845435265636f7665723a20696e76616c6964207369676e6174757265202776272076616c756545435265636f7665723a20696e76616c6964207369676e6174757265202773272076616c7565a2646970667358221220289705d6ae8701c9ed586541fa0ba342f754518aa4f0e59dbbda380bc9f2323964736f6c634300060c0033" }, - "0x3DF536744A4F88c5F881C5A841093535E8A0F0Aa": { + "0xcaC224cc7F15866F9454a22f735D0d4ae001D0a2": { "balance": "0x0", "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b5060043610610127575f3560e01c80638456cb59116100a95780639fd0506d1161006e5780639fd0506d14610420578063da980fed14610428578063e30c39781461043b578063f2fde38b14610443578063f77c479114610456575f5ffd5b80638456cb59146102165780638da5cb5b1461021e5780638e207848146102265780639242164f146102395780639fd02a361461033d575f5ffd5b806341a56c59116100ef57806341a56c5914610181578063554bab3c146101ca5780635c975abb146101dd578063715018a61461020657806379ba50971461020e575f5ffd5b8063032b901b1461012b57806306cb5b66146101405780632bbdb79f146101535780633466e3d4146101665780633f4ba83a14610179575b5f5ffd5b61013e610139366004610ec5565b61045e565b005b61013e61014e366004610ee7565b61050c565b61013e610161366004610f0d565b6105a3565b61013e610174366004610ee7565b6106b0565b61013e610739565b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385204546001600160a01b03165b6040516001600160a01b0390911681526020015b60405180910390f35b61013e6101d8366004610ee7565b610785565b5f5160206114105f395f51905f5254600160a01b900460ff1660405190151581526020016101c1565b61013e610809565b61013e61081c565b61013e610869565b6101ad6108bb565b61013e610234366004610f24565b6108ef565b6102d76040805160c0810182525f80825260208201819052918101829052606081018290526080810182905260a08101829052905f5160206113f05f395f51905f526040805160c08101825282546001600160401b038082168352600160401b820481166020840152600160801b9091041691810191909152600182015460608201526002820154608082015260039091015460a082015292915050565b6040516101c191905f60c0820190506001600160401b0383511682526001600160401b0360208401511660208301526001600160401b036040840151166040830152606083015160608301526080830151608083015260a083015160a083015292915050565b61041360408051610100810182525f80825260208201819052918101829052606081018290526080810182905260a0810182905260c0810182905260e08101829052905f5160206113f05f395f51905f52604080516101008101825260059092015461ffff80821684526201000082048116602085015264010000000082048116928401929092526601000000000000810482166060840152600160401b810482166080840152600160501b8104821660a0840152600160601b8104821660c0840152600160701b90041660e082015292915050565b6040516101c19190610f3d565b6101ad610a45565b61013e610436366004610fdd565b610a5a565b6101ad610c38565b61013e610451366004610ee7565b610c60565b6101ad610ce5565b610466610d0d565b61046e610d58565b5f8161ffff16116104925760405163811da5f160e01b815260040160405180910390fd5b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205805461ffff191661ffff83161781556040515f5160206113f05f395f51905f52917faba8150f8eae6a1acc6824830944cdc19c670da2fae4fe28f4dfa04c0d6932d4916105009190610fef565b60405180910390a15050565b610514610d90565b6001600160a01b03811661053b576040516371ab47bb60e11b815260040160405180910390fd5b7f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af0080546001600160a01b0319166001600160a01b03831690811782556040517f1304018cfe79741dcf02ba6b61d39cc4757d59395d03224d9925c7aa83002146905f90a25050565b6105ab610d0d565b6105b3610d58565b5f81116105d357604051635251cbd760e01b815260040160405180910390fd5b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385203819055604080515f5160206113f05f395f51905f5280546001600160401b03808216845281851c81166020850152608091821c16938301939093527f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852015460608301527f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385202549282019290925260a081018390527f413a821e8d0eadcc30efbe627a655111b9f28f5999003a85b7718a14b0e329ef9060c001610500565b6106b8610d0d565b6106c0610d58565b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec8438520480546001600160a01b0319166001600160a01b0383169081179091556040515f5160206113f05f395f51905f5291907fdec90d8bfa3fe33f0b0d876fc2cfc0e936e625e503ed170de964f15e6e17d15c905f90a25050565b610741610dc2565b5f5160206114105f395f51905f52805460ff60a01b191681556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b33905f90a150565b61078d610d90565b6001600160a01b0381166107b45760405163a74995ab60e01b815260040160405180910390fd5b5f5160206114105f395f51905f5280546001600160a01b0319166001600160a01b03831690811782556040517fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a604905f90a25050565b610811610d90565b61081a5f610dfa565b565b3380610826610c38565b6001600160a01b03161461085d5760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b61086681610dfa565b50565b610871610dc2565b5f5160206114105f395f51905f52805460ff60a01b1916600160a01b1781556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff625905f90a150565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b6108f7610d0d565b6108ff610d58565b606461090e6020830183611077565b6001600160401b0316111561093657604051633e82ffd960e21b815260040160405180910390fd5b6127106109496040830160208401611077565b6001600160401b031611156109715760405163f87b4d6d60e01b815260040160405180910390fd5b80608001358160600135111561099a576040516336b1b98d60e11b815260040160405180910390fd5b5f8160a00135116109be57604051635251cbd760e01b815260040160405180910390fd5b6127106109d16060830160408401611077565b6001600160401b031611156109f95760405163279eca6760e21b815260040160405180910390fd5b5f5160206113f05f395f51905f528181610a138282611092565b9050507f413a821e8d0eadcc30efbe627a655111b9f28f5999003a85b7718a14b0e329ef82604051610500919061114f565b5f805f5160206114105f395f51905f526108df565b610a62610d0d565b610a6a610d58565b5f610a786020830183610ec5565b61ffff1611610a9a5760405163811da5f160e01b815260040160405180910390fd5b5f610aab6040830160208401610ec5565b61ffff1611610acd5760405163055dd8c360e01b815260040160405180910390fd5b5f610ade6060830160408401610ec5565b61ffff1611610b0057604051632de6d8ed60e11b815260040160405180910390fd5b5f610b116080830160608401610ec5565b61ffff1611610b3357604051634c14d46960e11b815260040160405180910390fd5b5f610b4460a0830160808401610ec5565b61ffff1611610b66576040516396e398e160e01b815260040160405180910390fd5b5f610b7760c0830160a08401610ec5565b61ffff1611610b9957604051632218f17b60e21b815260040160405180910390fd5b5f610baa60e0830160c08401610ec5565b61ffff1611610bcc57604051631805fa5160e11b815260040160405180910390fd5b5f5160206113f05f395f51905f52817f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205610c0682826111d8565b9050507faba8150f8eae6a1acc6824830944cdc19c670da2fae4fe28f4dfa04c0d6932d482604051610500919061133e565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006108df565b610c68610d90565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610cac6108bb565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b5f807f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af006108df565b7f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af0080546001600160a01b0316331461086657604051630e971e6360e11b815260040160405180910390fd5b5f5160206114105f395f51905f528054600160a01b900460ff16156108665760405163ab35696f60e01b815260040160405180910390fd5b33610d996108bb565b6001600160a01b03161461081a5760405163118cdaa760e01b8152336004820152602401610854565b5f5160206114105f395f51905f5280546001600160a01b031633146108665760405163daeefd6560e01b815260040160405180910390fd5b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610e3282610e36565b5050565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b61ffff81168114610866575f5ffd5b8035610ec081610ea6565b919050565b5f60208284031215610ed5575f5ffd5b8135610ee081610ea6565b9392505050565b5f60208284031215610ef7575f5ffd5b81356001600160a01b0381168114610ee0575f5ffd5b5f60208284031215610f1d575f5ffd5b5035919050565b5f60c0828403128015610f35575f5ffd5b509092915050565b5f6101008201905061ffff835116825261ffff602084015116602083015261ffff60408401511660408301526060830151610f7e606084018261ffff169052565b506080830151610f94608084018261ffff169052565b5060a0830151610faa60a084018261ffff169052565b5060c0830151610fc060c084018261ffff169052565b5060e0830151610fd660e084018261ffff169052565b5092915050565b5f610100828403128015610f35575f5ffd5b815461ffff8082168352601082901c166020830152610100820190602081901c61ffff166040840152603081901c61ffff166060840152604081901c61ffff166080840152605081901c61ffff1660a0840152606081901c61ffff1660c0840152607081901c61ffff1660e0840152610fd6565b6001600160401b0381168114610866575f5ffd5b5f60208284031215611087575f5ffd5b8135610ee081611063565b813561109d81611063565b6001600160401b03811690508154816001600160401b0319821617835560208401356110c881611063565b6fffffffffffffffff00000000000000008160401b16836fffffffffffffffffffffffffffffffff198416171784555050505f604083013561110981611063565b825467ffffffffffffffff60801b1916608091821b67ffffffffffffffff60801b161783556060840135600184015583013560028301555060a090910135600390910155565b60c08101823561115e81611063565b6001600160401b03168252602083013561117781611063565b6001600160401b03166020830152604083013561119381611063565b6001600160401b03166040830152606083810135908301526080808401359083015260a092830135929091019190915290565b5f81356111d281610ea6565b92915050565b81356111e381610ea6565b61ffff8116905081548161ffff198216178355602084013561120481610ea6565b63ffff00008160101b168363ffffffff1984161717845550505061124b61122d604084016111c6565b825465ffff00000000191660209190911b65ffff0000000016178255565b61127c61125a606084016111c6565b825467ffff000000000000191660309190911b67ffff00000000000016178255565b6112b161128b608084016111c6565b825469ffff0000000000000000191660409190911b69ffff000000000000000016178255565b6112e06112c060a084016111c6565b82805461ffff60501b191660509290921b61ffff60501b16919091179055565b61130f6112ef60c084016111c6565b82805461ffff60601b191660609290921b61ffff60601b16919091179055565b610e3261131e60e084016111c6565b82805461ffff60701b191660709290921b61ffff60701b16919091179055565b6101008101823561134e81610ea6565b61ffff168252602083013561136281610ea6565b61ffff16602083015261137760408401610eb5565b61ffff16604083015261138c60608401610eb5565b61ffff1660608301526113a160808401610eb5565b61ffff1660808301526113b660a08401610eb5565b61ffff1660a08301526113cb60c08401610eb5565b61ffff1660c08301526113e060e08401610eb5565b61ffff811660e0840152610fd656fe668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852000642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a00a264697066735822122006fbb4f04fa93630fb87aa47c9d37004bbdef7ba36847c53a4088e6fbf18fe5764736f6c634300081d0033" + "code": "0x608060405234801561000f575f5ffd5b5060043610610127575f3560e01c80638456cb59116100a95780639fd0506d1161006e5780639fd0506d14610420578063da980fed14610428578063e30c39781461043b578063f2fde38b14610443578063f77c479114610456575f5ffd5b80638456cb59146102165780638da5cb5b1461021e5780638e207848146102265780639242164f146102395780639fd02a361461033d575f5ffd5b806341a56c59116100ef57806341a56c5914610181578063554bab3c146101ca5780635c975abb146101dd578063715018a61461020657806379ba50971461020e575f5ffd5b8063032b901b1461012b57806306cb5b66146101405780632bbdb79f146101535780633466e3d4146101665780633f4ba83a14610179575b5f5ffd5b61013e610139366004610ec5565b61045e565b005b61013e61014e366004610ee7565b61050c565b61013e610161366004610f0d565b6105a3565b61013e610174366004610ee7565b6106b0565b61013e610739565b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385204546001600160a01b03165b6040516001600160a01b0390911681526020015b60405180910390f35b61013e6101d8366004610ee7565b610785565b5f5160206114105f395f51905f5254600160a01b900460ff1660405190151581526020016101c1565b61013e610809565b61013e61081c565b61013e610869565b6101ad6108bb565b61013e610234366004610f24565b6108ef565b6102d76040805160c0810182525f80825260208201819052918101829052606081018290526080810182905260a08101829052905f5160206113f05f395f51905f526040805160c08101825282546001600160401b038082168352600160401b820481166020840152600160801b9091041691810191909152600182015460608201526002820154608082015260039091015460a082015292915050565b6040516101c191905f60c0820190506001600160401b0383511682526001600160401b0360208401511660208301526001600160401b036040840151166040830152606083015160608301526080830151608083015260a083015160a083015292915050565b61041360408051610100810182525f80825260208201819052918101829052606081018290526080810182905260a0810182905260c0810182905260e08101829052905f5160206113f05f395f51905f52604080516101008101825260059092015461ffff80821684526201000082048116602085015264010000000082048116928401929092526601000000000000810482166060840152600160401b810482166080840152600160501b8104821660a0840152600160601b8104821660c0840152600160701b90041660e082015292915050565b6040516101c19190610f3d565b6101ad610a45565b61013e610436366004610fdd565b610a5a565b6101ad610c38565b61013e610451366004610ee7565b610c60565b6101ad610ce5565b610466610d0d565b61046e610d58565b5f8161ffff16116104925760405163811da5f160e01b815260040160405180910390fd5b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205805461ffff191661ffff83161781556040515f5160206113f05f395f51905f52917faba8150f8eae6a1acc6824830944cdc19c670da2fae4fe28f4dfa04c0d6932d4916105009190610fef565b60405180910390a15050565b610514610d90565b6001600160a01b03811661053b576040516371ab47bb60e11b815260040160405180910390fd5b7f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af0080546001600160a01b0319166001600160a01b03831690811782556040517f1304018cfe79741dcf02ba6b61d39cc4757d59395d03224d9925c7aa83002146905f90a25050565b6105ab610d0d565b6105b3610d58565b5f81116105d357604051635251cbd760e01b815260040160405180910390fd5b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385203819055604080515f5160206113f05f395f51905f5280546001600160401b03808216845281851c81166020850152608091821c16938301939093527f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852015460608301527f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385202549282019290925260a081018390527f413a821e8d0eadcc30efbe627a655111b9f28f5999003a85b7718a14b0e329ef9060c001610500565b6106b8610d0d565b6106c0610d58565b7f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec8438520480546001600160a01b0319166001600160a01b0383169081179091556040515f5160206113f05f395f51905f5291907fdec90d8bfa3fe33f0b0d876fc2cfc0e936e625e503ed170de964f15e6e17d15c905f90a25050565b610741610dc2565b5f5160206114105f395f51905f52805460ff60a01b191681556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b33905f90a150565b61078d610d90565b6001600160a01b0381166107b45760405163a74995ab60e01b815260040160405180910390fd5b5f5160206114105f395f51905f5280546001600160a01b0319166001600160a01b03831690811782556040517fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a604905f90a25050565b610811610d90565b61081a5f610dfa565b565b3380610826610c38565b6001600160a01b03161461085d5760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b61086681610dfa565b50565b610871610dc2565b5f5160206114105f395f51905f52805460ff60a01b1916600160a01b1781556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff625905f90a150565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b6108f7610d0d565b6108ff610d58565b606461090e6020830183611077565b6001600160401b0316111561093657604051633e82ffd960e21b815260040160405180910390fd5b6127106109496040830160208401611077565b6001600160401b031611156109715760405163f87b4d6d60e01b815260040160405180910390fd5b80608001358160600135111561099a576040516336b1b98d60e11b815260040160405180910390fd5b5f8160a00135116109be57604051635251cbd760e01b815260040160405180910390fd5b6127106109d16060830160408401611077565b6001600160401b031611156109f95760405163279eca6760e21b815260040160405180910390fd5b5f5160206113f05f395f51905f528181610a138282611092565b9050507f413a821e8d0eadcc30efbe627a655111b9f28f5999003a85b7718a14b0e329ef82604051610500919061114f565b5f805f5160206114105f395f51905f526108df565b610a62610d0d565b610a6a610d58565b5f610a786020830183610ec5565b61ffff1611610a9a5760405163811da5f160e01b815260040160405180910390fd5b5f610aab6040830160208401610ec5565b61ffff1611610acd5760405163055dd8c360e01b815260040160405180910390fd5b5f610ade6060830160408401610ec5565b61ffff1611610b0057604051632de6d8ed60e11b815260040160405180910390fd5b5f610b116080830160608401610ec5565b61ffff1611610b3357604051634c14d46960e11b815260040160405180910390fd5b5f610b4460a0830160808401610ec5565b61ffff1611610b66576040516396e398e160e01b815260040160405180910390fd5b5f610b7760c0830160a08401610ec5565b61ffff1611610b9957604051632218f17b60e21b815260040160405180910390fd5b5f610baa60e0830160c08401610ec5565b61ffff1611610bcc57604051631805fa5160e11b815260040160405180910390fd5b5f5160206113f05f395f51905f52817f668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205610c0682826111d8565b9050507faba8150f8eae6a1acc6824830944cdc19c670da2fae4fe28f4dfa04c0d6932d482604051610500919061133e565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006108df565b610c68610d90565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610cac6108bb565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b5f807f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af006108df565b7f958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af0080546001600160a01b0316331461086657604051630e971e6360e11b815260040160405180910390fd5b5f5160206114105f395f51905f528054600160a01b900460ff16156108665760405163ab35696f60e01b815260040160405180910390fd5b33610d996108bb565b6001600160a01b03161461081a5760405163118cdaa760e01b8152336004820152602401610854565b5f5160206114105f395f51905f5280546001600160a01b031633146108665760405163daeefd6560e01b815260040160405180910390fd5b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610e3282610e36565b5050565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b61ffff81168114610866575f5ffd5b8035610ec081610ea6565b919050565b5f60208284031215610ed5575f5ffd5b8135610ee081610ea6565b9392505050565b5f60208284031215610ef7575f5ffd5b81356001600160a01b0381168114610ee0575f5ffd5b5f60208284031215610f1d575f5ffd5b5035919050565b5f60c0828403128015610f35575f5ffd5b509092915050565b5f6101008201905061ffff835116825261ffff602084015116602083015261ffff60408401511660408301526060830151610f7e606084018261ffff169052565b506080830151610f94608084018261ffff169052565b5060a0830151610faa60a084018261ffff169052565b5060c0830151610fc060c084018261ffff169052565b5060e0830151610fd660e084018261ffff169052565b5092915050565b5f610100828403128015610f35575f5ffd5b815461ffff8082168352601082901c166020830152610100820190602081901c61ffff166040840152603081901c61ffff166060840152604081901c61ffff166080840152605081901c61ffff1660a0840152606081901c61ffff1660c0840152607081901c61ffff1660e0840152610fd6565b6001600160401b0381168114610866575f5ffd5b5f60208284031215611087575f5ffd5b8135610ee081611063565b813561109d81611063565b6001600160401b03811690508154816001600160401b0319821617835560208401356110c881611063565b6fffffffffffffffff00000000000000008160401b16836fffffffffffffffffffffffffffffffff198416171784555050505f604083013561110981611063565b825467ffffffffffffffff60801b1916608091821b67ffffffffffffffff60801b161783556060840135600184015583013560028301555060a090910135600390910155565b60c08101823561115e81611063565b6001600160401b03168252602083013561117781611063565b6001600160401b03166020830152604083013561119381611063565b6001600160401b03166040830152606083810135908301526080808401359083015260a092830135929091019190915290565b5f81356111d281610ea6565b92915050565b81356111e381610ea6565b61ffff8116905081548161ffff198216178355602084013561120481610ea6565b63ffff00008160101b168363ffffffff1984161717845550505061124b61122d604084016111c6565b825465ffff00000000191660209190911b65ffff0000000016178255565b61127c61125a606084016111c6565b825467ffff000000000000191660309190911b67ffff00000000000016178255565b6112b161128b608084016111c6565b825469ffff0000000000000000191660409190911b69ffff000000000000000016178255565b6112e06112c060a084016111c6565b82805461ffff60501b191660509290921b61ffff60501b16919091179055565b61130f6112ef60c084016111c6565b82805461ffff60601b191660609290921b61ffff60601b16919091179055565b610e3261131e60e084016111c6565b82805461ffff60701b191660709290921b61ffff60701b16919091179055565b6101008101823561134e81610ea6565b61ffff168252602083013561136281610ea6565b61ffff16602083015261137760408401610eb5565b61ffff16604083015261138c60608401610eb5565b61ffff1660608301526113a160808401610eb5565b61ffff1660808301526113b660a08401610eb5565b61ffff1660a08301526113cb60c08401610eb5565b61ffff1660c08301526113e060e08401610eb5565b61ffff811660e0840152610fd656fe668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec843852000642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a00a2646970667358221220477a036c9ec804860715e0657fe49c602f65271f42c96af40095bcf35a04f75164736f6c634300081d0033" }, "0x3600000000000000000000000000000000000001": { "balance": "0x0", "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220b46184e97ca15761bb56cc4f4af0c9790e85df702b2581fb83f0e8f093b3543564736f6c634300081d0033", + "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", "storage": { "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000a0ee7a142d267c1f36714e4a8f75612f20a79720", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x0000000000000000000000003df536744a4f88c5f881c5a841093535e8a0f0aa", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000cac224cc7f15866f9454a22f735d0d4ae001d0a2", "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001", "0x958f8fec699b51a1249f513eceda5429078000657f74abd1721bba363087af00": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", @@ -91,18 +91,18 @@ "0x668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385205": "0x0000000000000000000000000000000001f403e801f403e801f403e801f40bb8" } }, - "0x2aE97f6138243d48579E8545bEeF557854cfB056": { + "0xbE869B9952F16af1c0B3C68C17eB3aB61DccaEAb": { "balance": "0x0", "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100e5575f3560e01c80638da5cb5b11610088578063c4899f0c11610063578063c4899f0c146101cc578063e30c3978146101df578063f2fde38b146101e7578063f94e1867146101fa575f5ffd5b80638da5cb5b14610184578063b5d89627146101a4578063b86444b1146101c4575f5ffd5b80634ebae617116100c35780634ebae61714610138578063715018a61461014d57806377db16691461015557806379ba50971461017c575f5ffd5b806303a9b132146100e957806324408a68146101105780632469d67214610125575b5f5ffd5b6100fd5f51602061138e5f395f51905f5281565b6040519081526020015b60405180910390f35b61011861020d565b6040516101079190610ffc565b6100fd61013336600461108e565b6103ed565b61014b61014636600461114f565b610598565b005b61014b61065b565b7fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204546100fd565b61014b61066e565b61018c6106b6565b6040516001600160a01b039091168152602001610107565b6101b76101b236600461114f565b6106ea565b6040516101079190611166565b6100fd61080f565b61014b6101da366004611178565b61082d565b61018c610996565b61014b6101f5366004611199565b6109be565b61014b61020836600461114f565b610a43565b60605f51602061138e5f395f51905f525f6102477fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d201610c4d565b9050806001600160401b038111156102615761026161105f565b6040519080825280602002602001820160405280156102b557816020015b6102a26040805160608101909152805f8152606060208201525f60409091015290565b81526020019060019003908161027f5790505b5092505f5b818110156103e7575f6102d06001850183610c56565b5f818152602086905260409081902081516060810190925280549293509091829060ff16600281111561030557610305610f61565b600281111561031657610316610f61565b815260200160018201805461032a906111bf565b80601f0160208091040260200160405190810160405280929190818152602001828054610356906111bf565b80156103a15780601f10610378576101008083540402835291602001916103a1565b820191905f5260205f20905b81548152906001019060200180831161038457829003601f168201915b5050509183525050600291909101546001600160401b031660209091015285518690849081106103d3576103d36111f7565b6020908102919091010152506001016102ba565b50505090565b5f6103f6610c68565b602083511461041857604051630717c3c160e01b815260040160405180910390fd5b82516020808501919091205f8181527fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d2039092526040909120545f51602061138e5f395f51905f529190819060ff1615610490576040516338a14a4160e01b815260040161048791815260200190565b60405180910390fd5b50600482018054905f6104a28361121f565b909155506040805160608101909152909350806001815260208082018890526001600160401b0387166040928301525f868152908590522081518154829060ff191660018360028111156104f8576104f8610f61565b0217905550602082015160018201906105119082611283565b50604091820151600291909101805467ffffffffffffffff19166001600160401b039092169190911790555f82815260038401602052819020805460ff191660011790555183907fea20c1f9de86768933c1d4eff47ce667e26f886c4877d1bedc253e42b2f04a7c90610587908790899061133d565b60405180910390a250505b92915050565b6105a0610c68565b5f8181525f51602061138e5f395f51905f5260208190526040909120805460019060ff1660028111156105d5576105d5610f61565b1483906105f8576040516355b54a8d60e01b815260040161048791815260200190565b50805460ff191660021781556106116001830184610c9a565b5060028101546040516001600160401b03909116815283907fbc5136437746415b39af863272d0ca406634cf14bb7c3e5fa03855781add87e59060200160405180910390a2505050565b610663610c68565b61066c5f610ca5565b565b3380610678610996565b6001600160a01b0316146106aa5760405163118cdaa760e01b81526001600160a01b0382166004820152602401610487565b6106b381610ca5565b50565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b61070d6040805160608101909152805f8152606060208201525f60409091015290565b5f8281525f51602061138e5f395f51905f5260208190526040918290208251606081019093528054919291829060ff16600281111561074e5761074e610f61565b600281111561075f5761075f610f61565b8152602001600182018054610773906111bf565b80601f016020809104026020016040519081016040528092919081815260200182805461079f906111bf565b80156107ea5780601f106107c1576101008083540402835291602001916107ea565b820191905f5260205f20905b8154815290600101906020018083116107cd57829003601f168201915b5050509183525050600291909101546001600160401b03166020909101529392505050565b5f5f51602061138e5f395f51905f5261082781610ce1565b91505090565b610835610c68565b5f5f51602061138e5f395f51905f525f848152602082905260408120919250815460ff16600281111561086a5761086a610f61565b1415849061088e576040516355b54a8d60e01b815260040161048791815260200190565b5060028101546001600160401b0390811690841681036108c157604051632bf0186d60e21b815260040160405180910390fd5b6002825460ff1660028111156108d9576108d9610f61565b1480156108ee57505f816001600160401b0316115b801561090157506001600160401b038416155b1561092f57600161091184610ce1565b1161092f5760405163aff8365b60e01b815260040160405180910390fd5b60028201805467ffffffffffffffff19166001600160401b03868116918217909255604080519284168352602083019190915286917f4a7e1cc2e075be9a3c913bf00ec8ff10f6630f23433f65eee31e20ef69110ae4910160405180910390a25050505050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006106da565b6109c6610c68565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610a0a6106b6565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b610a4b610c68565b5f5f51602061138e5f395f51905f525f838152602082905260408120919250815460ff166002811115610a8057610a80610f61565b14158390610aa4576040516355b54a8d60e01b815260040161048791815260200190565b506002815460ff166002811115610abd57610abd610f61565b148015610ad6575060028101546001600160401b031615155b15610b04576001610ae683610ce1565b11610b045760405163aff8365b60e01b815260040160405180910390fd5b5f816001018054610b14906111bf565b80601f0160208091040260200160405190810160405280929190818152602001828054610b40906111bf565b8015610b8b5780601f10610b6257610100808354040283529160200191610b8b565b820191905f5260205f20905b815481529060010190602001808311610b6e57829003601f168201915b5050835160208501206002870154949550936001600160401b03169250610bb89150506001860187610d47565b505f868152602086905260408120805460ff1916815590610bdc6001830182610f17565b50600201805467ffffffffffffffff191690555f828152600386016020908152604091829020805460ff1916905590516001600160401b038316815287917f70d022b699831c183688ed9d9d1a1cbf586698b506eaacfd5195a4e3738d285b910160405180910390a2505050505050565b5f610592825490565b5f610c618383610d52565b9392505050565b33610c716106b6565b6001600160a01b03161461066c5760405163118cdaa760e01b8152336004820152602401610487565b5f610c618383610d78565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610cdd82610dc4565b5050565b5f5f610cef83600101610c4d565b90505f5b81811015610d40575f610d096001860183610c56565b5f818152602087905260409020600201549091506001600160401b031615610d3757610d348461121f565b93505b50600101610cf3565b5050919050565b5f610c618383610e34565b5f825f018281548110610d6757610d676111f7565b905f5260205f200154905092915050565b5f818152600183016020526040812054610dbd57508154600181810184555f848152602080822090930184905584548482528286019093526040902091909155610592565b505f610592565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f8181526001830160205260408120548015610f0e575f610e56600183611366565b85549091505f90610e6990600190611366565b9050808214610ec8575f865f018281548110610e8757610e876111f7565b905f5260205f200154905080875f018481548110610ea757610ea76111f7565b5f918252602080832090910192909255918252600188019052604090208390555b8554869080610ed957610ed9611379565b600190038181905f5260205f20015f90559055856001015f8681526020019081526020015f205f905560019350505050610592565b5f915050610592565b508054610f23906111bf565b5f825580601f10610f32575050565b601f0160209004905f5260205f20908101906106b391905b80821115610f5d575f8155600101610f4a565b5090565b634e487b7160e01b5f52602160045260245ffd5b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b5f815160038110610fc257634e487b7160e01b5f52602160045260245ffd5b80845250602082015160606020850152610fdf6060850182610f75565b6040938401516001600160401b0316949093019390935250919050565b5f602082016020835280845180835260408501915060408160051b8601019250602086015f5b8281101561105357603f1987860301845261103e858351610fa3565b94506020938401939190910190600101611022565b50929695505050505050565b634e487b7160e01b5f52604160045260245ffd5b80356001600160401b0381168114611089575f5ffd5b919050565b5f5f6040838503121561109f575f5ffd5b82356001600160401b038111156110b4575f5ffd5b8301601f810185136110c4575f5ffd5b80356001600160401b038111156110dd576110dd61105f565b604051601f8201601f19908116603f011681016001600160401b038111828210171561110b5761110b61105f565b604052818152828201602001871015611122575f5ffd5b816020840160208301375f6020838301015280945050505061114660208401611073565b90509250929050565b5f6020828403121561115f575f5ffd5b5035919050565b602081525f610c616020830184610fa3565b5f5f60408385031215611189575f5ffd5b8235915061114660208401611073565b5f602082840312156111a9575f5ffd5b81356001600160a01b0381168114610c61575f5ffd5b600181811c908216806111d357607f821691505b6020821081036111f157634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b5f600182016112305761123061120b565b5060010190565b601f82111561127e57805f5260205f20601f840160051c8101602085101561125c5750805b601f840160051c820191505b8181101561127b575f8155600101611268565b50505b505050565b81516001600160401b0381111561129c5761129c61105f565b6112b0816112aa84546111bf565b84611237565b6020601f8211600181146112e2575f83156112cb5750848201515b5f19600385901b1c1916600184901b17845561127b565b5f84815260208120601f198516915b8281101561131157878501518255602094850194600190920191016112f1565b508482101561132e57868401515f19600387901b60f8161c191681555b50505050600190811b01905550565b6001600160401b0383168152604060208201525f61135e6040830184610f75565b949350505050565b818103818111156105925761059261120b565b634e487b7160e01b5f52603160045260245ffdfeb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d200a26469706673582212209c21c0c4281833b1fc2b883d82bf7763240bf7e0c2ccdf8617b149314c4fac9864736f6c634300081d0033" + "code": "0x608060405234801561000f575f5ffd5b50600436106100e5575f3560e01c80638da5cb5b11610088578063c4899f0c11610063578063c4899f0c146101cc578063e30c3978146101df578063f2fde38b146101e7578063f94e1867146101fa575f5ffd5b80638da5cb5b14610184578063b5d89627146101a4578063b86444b1146101c4575f5ffd5b80634ebae617116100c35780634ebae61714610138578063715018a61461014d57806377db16691461015557806379ba50971461017c575f5ffd5b806303a9b132146100e957806324408a68146101105780632469d67214610125575b5f5ffd5b6100fd5f51602061138e5f395f51905f5281565b6040519081526020015b60405180910390f35b61011861020d565b6040516101079190610ffc565b6100fd61013336600461108e565b6103ed565b61014b61014636600461114f565b610598565b005b61014b61065b565b7fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204546100fd565b61014b61066e565b61018c6106b6565b6040516001600160a01b039091168152602001610107565b6101b76101b236600461114f565b6106ea565b6040516101079190611166565b6100fd61080f565b61014b6101da366004611178565b61082d565b61018c610996565b61014b6101f5366004611199565b6109be565b61014b61020836600461114f565b610a43565b60605f51602061138e5f395f51905f525f6102477fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d201610c4d565b9050806001600160401b038111156102615761026161105f565b6040519080825280602002602001820160405280156102b557816020015b6102a26040805160608101909152805f8152606060208201525f60409091015290565b81526020019060019003908161027f5790505b5092505f5b818110156103e7575f6102d06001850183610c56565b5f818152602086905260409081902081516060810190925280549293509091829060ff16600281111561030557610305610f61565b600281111561031657610316610f61565b815260200160018201805461032a906111bf565b80601f0160208091040260200160405190810160405280929190818152602001828054610356906111bf565b80156103a15780601f10610378576101008083540402835291602001916103a1565b820191905f5260205f20905b81548152906001019060200180831161038457829003601f168201915b5050509183525050600291909101546001600160401b031660209091015285518690849081106103d3576103d36111f7565b6020908102919091010152506001016102ba565b50505090565b5f6103f6610c68565b602083511461041857604051630717c3c160e01b815260040160405180910390fd5b82516020808501919091205f8181527fb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d2039092526040909120545f51602061138e5f395f51905f529190819060ff1615610490576040516338a14a4160e01b815260040161048791815260200190565b60405180910390fd5b50600482018054905f6104a28361121f565b909155506040805160608101909152909350806001815260208082018890526001600160401b0387166040928301525f868152908590522081518154829060ff191660018360028111156104f8576104f8610f61565b0217905550602082015160018201906105119082611283565b50604091820151600291909101805467ffffffffffffffff19166001600160401b039092169190911790555f82815260038401602052819020805460ff191660011790555183907fea20c1f9de86768933c1d4eff47ce667e26f886c4877d1bedc253e42b2f04a7c90610587908790899061133d565b60405180910390a250505b92915050565b6105a0610c68565b5f8181525f51602061138e5f395f51905f5260208190526040909120805460019060ff1660028111156105d5576105d5610f61565b1483906105f8576040516355b54a8d60e01b815260040161048791815260200190565b50805460ff191660021781556106116001830184610c9a565b5060028101546040516001600160401b03909116815283907fbc5136437746415b39af863272d0ca406634cf14bb7c3e5fa03855781add87e59060200160405180910390a2505050565b610663610c68565b61066c5f610ca5565b565b3380610678610996565b6001600160a01b0316146106aa5760405163118cdaa760e01b81526001600160a01b0382166004820152602401610487565b6106b381610ca5565b50565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b61070d6040805160608101909152805f8152606060208201525f60409091015290565b5f8281525f51602061138e5f395f51905f5260208190526040918290208251606081019093528054919291829060ff16600281111561074e5761074e610f61565b600281111561075f5761075f610f61565b8152602001600182018054610773906111bf565b80601f016020809104026020016040519081016040528092919081815260200182805461079f906111bf565b80156107ea5780601f106107c1576101008083540402835291602001916107ea565b820191905f5260205f20905b8154815290600101906020018083116107cd57829003601f168201915b5050509183525050600291909101546001600160401b03166020909101529392505050565b5f5f51602061138e5f395f51905f5261082781610ce1565b91505090565b610835610c68565b5f5f51602061138e5f395f51905f525f848152602082905260408120919250815460ff16600281111561086a5761086a610f61565b1415849061088e576040516355b54a8d60e01b815260040161048791815260200190565b5060028101546001600160401b0390811690841681036108c157604051632bf0186d60e21b815260040160405180910390fd5b6002825460ff1660028111156108d9576108d9610f61565b1480156108ee57505f816001600160401b0316115b801561090157506001600160401b038416155b1561092f57600161091184610ce1565b1161092f5760405163aff8365b60e01b815260040160405180910390fd5b60028201805467ffffffffffffffff19166001600160401b03868116918217909255604080519284168352602083019190915286917f4a7e1cc2e075be9a3c913bf00ec8ff10f6630f23433f65eee31e20ef69110ae4910160405180910390a25050505050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006106da565b6109c6610c68565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610a0a6106b6565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b610a4b610c68565b5f5f51602061138e5f395f51905f525f838152602082905260408120919250815460ff166002811115610a8057610a80610f61565b14158390610aa4576040516355b54a8d60e01b815260040161048791815260200190565b506002815460ff166002811115610abd57610abd610f61565b148015610ad6575060028101546001600160401b031615155b15610b04576001610ae683610ce1565b11610b045760405163aff8365b60e01b815260040160405180910390fd5b5f816001018054610b14906111bf565b80601f0160208091040260200160405190810160405280929190818152602001828054610b40906111bf565b8015610b8b5780601f10610b6257610100808354040283529160200191610b8b565b820191905f5260205f20905b815481529060010190602001808311610b6e57829003601f168201915b5050835160208501206002870154949550936001600160401b03169250610bb89150506001860187610d47565b505f868152602086905260408120805460ff1916815590610bdc6001830182610f17565b50600201805467ffffffffffffffff191690555f828152600386016020908152604091829020805460ff1916905590516001600160401b038316815287917f70d022b699831c183688ed9d9d1a1cbf586698b506eaacfd5195a4e3738d285b910160405180910390a2505050505050565b5f610592825490565b5f610c618383610d52565b9392505050565b33610c716106b6565b6001600160a01b03161461066c5760405163118cdaa760e01b8152336004820152602401610487565b5f610c618383610d78565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319168155610cdd82610dc4565b5050565b5f5f610cef83600101610c4d565b90505f5b81811015610d40575f610d096001860183610c56565b5f818152602087905260409020600201549091506001600160401b031615610d3757610d348461121f565b93505b50600101610cf3565b5050919050565b5f610c618383610e34565b5f825f018281548110610d6757610d676111f7565b905f5260205f200154905092915050565b5f818152600183016020526040812054610dbd57508154600181810184555f848152602080822090930184905584548482528286019093526040902091909155610592565b505f610592565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f8181526001830160205260408120548015610f0e575f610e56600183611366565b85549091505f90610e6990600190611366565b9050808214610ec8575f865f018281548110610e8757610e876111f7565b905f5260205f200154905080875f018481548110610ea757610ea76111f7565b5f918252602080832090910192909255918252600188019052604090208390555b8554869080610ed957610ed9611379565b600190038181905f5260205f20015f90559055856001015f8681526020019081526020015f205f905560019350505050610592565b5f915050610592565b508054610f23906111bf565b5f825580601f10610f32575050565b601f0160209004905f5260205f20908101906106b391905b80821115610f5d575f8155600101610f4a565b5090565b634e487b7160e01b5f52602160045260245ffd5b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b5f815160038110610fc257634e487b7160e01b5f52602160045260245ffd5b80845250602082015160606020850152610fdf6060850182610f75565b6040938401516001600160401b0316949093019390935250919050565b5f602082016020835280845180835260408501915060408160051b8601019250602086015f5b8281101561105357603f1987860301845261103e858351610fa3565b94506020938401939190910190600101611022565b50929695505050505050565b634e487b7160e01b5f52604160045260245ffd5b80356001600160401b0381168114611089575f5ffd5b919050565b5f5f6040838503121561109f575f5ffd5b82356001600160401b038111156110b4575f5ffd5b8301601f810185136110c4575f5ffd5b80356001600160401b038111156110dd576110dd61105f565b604051601f8201601f19908116603f011681016001600160401b038111828210171561110b5761110b61105f565b604052818152828201602001871015611122575f5ffd5b816020840160208301375f6020838301015280945050505061114660208401611073565b90509250929050565b5f6020828403121561115f575f5ffd5b5035919050565b602081525f610c616020830184610fa3565b5f5f60408385031215611189575f5ffd5b8235915061114660208401611073565b5f602082840312156111a9575f5ffd5b81356001600160a01b0381168114610c61575f5ffd5b600181811c908216806111d357607f821691505b6020821081036111f157634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b5f600182016112305761123061120b565b5060010190565b601f82111561127e57805f5260205f20601f840160051c8101602085101561125c5750805b601f840160051c820191505b8181101561127b575f8155600101611268565b50505b505050565b81516001600160401b0381111561129c5761129c61105f565b6112b0816112aa84546111bf565b84611237565b6020601f8211600181146112e2575f83156112cb5750848201515b5f19600385901b1c1916600184901b17845561127b565b5f84815260208120601f198516915b8281101561131157878501518255602094850194600190920191016112f1565b508482101561132e57868401515f19600387901b60f8161c191681555b50505050600190811b01905550565b6001600160401b0383168152604060208201525f61135e6040830184610f75565b949350505050565b818103818111156105925761059261120b565b634e487b7160e01b5f52603160045260245ffdfeb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d200a26469706673582212205cd5370d39b2e3b86d8a3f498e338403bf1ea4bc76c4dcb7a720d82b33431ab664736f6c634300081d0033" }, "0x3600000000000000000000000000000000000002": { "balance": "0x0", "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220b46184e97ca15761bb56cc4f4af0c9790e85df702b2581fb83f0e8f093b3543564736f6c634300081d0033", + "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", "storage": { "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000a0ee7a142d267c1f36714e4a8f75612f20a79720", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x0000000000000000000000002ae97f6138243d48579e8545beef557854cfb056", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000be869b9952f16af1c0b3c68c17eb3ab61dccaeab", "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001", "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x0000000000000000000000003600000000000000000000000000000000000003", "0x0499f4c2de309a54da703ff88d2d815fa145b3fb32cf77b2528c8033db8fd7ee": "0x0000000000000000000000000000000000000000000000000000000000000002", @@ -144,18 +144,18 @@ "0xb58da0dce03316992faea3e12c60705b8ac05a309e27e3bc8421e5b271c9d204": "0x0000000000000000000000000000000000000000000000000000000000000006" } }, - "0x971b475B4be17cdDDf218F5dcEAD523811bAf0BF": { + "0x92b26CF0580faECF86cdd16069aF99a81aadc3ad": { "balance": "0x0", "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106101dc575f3560e01c806379ba509711610109578063d2c20cb31161009e578063ead130111161006e578063ead130111461048d578063ec4a44d7146104a1578063f2fde38b146104b5578063f6a74ed7146104c8575f5ffd5b8063d2c20cb31461042b578063d773a7411461043e578063da3003f914610472578063e30c397814610485575f5ffd5b80638da5cb5b116100d95780638da5cb5b146103d25780639fd0506d146103da578063aa5ecb52146103e2578063b429afeb146103f5575f5ffd5b806379ba5097146103b35780637f77403d146103bb5780637ffc51fd146103c35780638456cb59146103ca575f5ffd5b8063485cc9551161017f5780635c975abb1161014f5780635c975abb146103595780635e5feee614610377578063602a9eee1461038a578063715018a6146103ab575f5ffd5b8063485cc955146102b85780634cb4850a146102cb578063554bab3c146102de5780635acac2b6146102f1575f5ffd5b8063263a3402116101ba578063263a34021461028b5780632eb5c6581461029557806337ec36d1146102a85780633f4ba83a146102b0575f5ffd5b8063048544a4146101e057806306433b1b1461022c5780631904bb2e1461026b575b5f5ffd5b6102176101ee366004611498565b6001600160a01b03165f9081525f51602061187e5f395f51905f52602052604090205460ff1690565b60405190151581526020015b60405180910390f35b6102537f000000000000000000000000360000000000000000000000000000000000000281565b6040516001600160a01b039091168152602001610223565b61027e610279366004611498565b6104db565b60405161022391906114e6565b6102936105b6565b005b6102936102a336600461155c565b610661565b610293610745565b6102936107e5565b6102936102c6366004611591565b610831565b6102936102d93660046115c2565b6109bd565b6102936102ec366004611498565b610ade565b6103416102ff366004611498565b6001600160a01b03165f9081527fe90ec3add3e251bfbe914c9e482b511e91a3b187718c1dc10223f64a8a644a0160205260409020546001600160401b031690565b6040516001600160401b039091168152602001610223565b5f51602061185e5f395f51905f5254600160a01b900460ff16610217565b610293610385366004611498565b610b62565b61039d61039836600461166f565b610c3d565b604051908152602001610223565b610293610ce4565b610293610cf7565b610293610d3f565b6103415f81565b610293610dbd565b610253610e0f565b610253610e43565b6102936103f0366004611498565b610e58565b610217610403366004611498565b6001600160a01b03165f9081525f51602061183e5f395f51905f526020526040902054151590565b6102936104393660046116e8565b610f17565b61039d61044c366004611498565b6001600160a01b03165f9081525f51602061183e5f395f51905f52602052604090205490565b610293610480366004611498565b611026565b6102536110e9565b61039d5f51602061183e5f395f51905f5281565b61039d5f51602061187e5f395f51905f5281565b6102936104c3366004611498565b611111565b6102936104d6366004611498565b611196565b6104fe6040805160608101909152805f8152606060208201525f60409091015290565b5f5f51602061183e5f395f51905f526001600160a01b038481165f908152602083905260409081902054905163b5d8962760e01b815260048101829052929350917f00000000000000000000000036000000000000000000000000000000000000029091169063b5d89627906024015f60405180830381865afa158015610587573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526105ae9190810190611730565b949350505050565b6105be611268565b6105c66112a5565b335f9081525f51602061183e5f395f51905f526020819052604091829020549151634ebae61760e01b8152600481018390529091907f00000000000000000000000036000000000000000000000000000000000000026001600160a01b031690634ebae617906024015b5f604051808303815f87803b158015610647575f5ffd5b505af1158015610659573d5f5f3e3d5ffd5b505050505050565b6106696112dd565b6001600160a01b038216610690576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0382165f9081525f51602061183e5f395f51905f52602081905260408220549091036106d6576040516308c2ec5b60e41b815260040160405180910390fd5b6001600160a01b0383165f818152600183016020908152604091829020805467ffffffffffffffff19166001600160401b03871690811790915591519182527fcf03087b939040458ea3c3a6791532c7c4432baf98e770abc930afc9cda81d87910160405180910390a2505050565b61074d6112dd565b7f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03166379ba50976040518163ffffffff1660e01b81526004015f604051808303815f87803b1580156107a5575f5ffd5b505af11580156107b7573d5f5f3e3d5ffd5b50506040517f54b70ab40993761a2b0e96f42fdf47939a56830ede1325df83e05ed33e3e8fd592505f9150a1565b6107ed61130f565b5f51602061185e5f395f51905f52805460ff60a01b191681556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b33905f90a150565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff1615906001600160401b03165f811580156108755750825b90505f826001600160401b031660011480156108905750303b155b90508115801561089e575080155b156108bc5760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff1916600117855583156108e657845460ff60401b1916600160401b1785555b6001600160a01b03871661090d576040516342cad95760e01b815260040160405180910390fd5b6001600160a01b0386166109345760405163a74995ab60e01b815260040160405180910390fd5b61093c611347565b6109458761134f565b5f5f51602061185e5f395f51905f5280546001600160a01b0319166001600160a01b0389161790555083156109b457845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6109c5611268565b6109cd6112a5565b335f9081525f51602061183e5f395f51905f5260208181526040808420547fe90ec3add3e251bfbe914c9e482b511e91a3b187718c1dc10223f64a8a644a01909252909220549091906001600160401b03908116908416811015610a5457604051635af79e0d60e11b81526001600160401b03821660048201526024015b60405180910390fd5b60405163312267c360e21b8152600481018390526001600160401b03851660248201527f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03169063c4899f0c906044015f604051808303815f87803b158015610ac2575f5ffd5b505af1158015610ad4573d5f5f3e3d5ffd5b5050505050505050565b610ae66112dd565b6001600160a01b038116610b0d5760405163a74995ab60e01b815260040160405180910390fd5b5f51602061185e5f395f51905f5280546001600160a01b0319166001600160a01b03831690811782556040517fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a604905f90a25050565b610b6a6112dd565b6001600160a01b038116610b91576040516342cad95760e01b815260040160405180910390fd5b60405163f2fde38b60e01b81526001600160a01b0382811660048301527f0000000000000000000000003600000000000000000000000000000000000002169063f2fde38b906024015f604051808303815f87803b158015610bf1575f5ffd5b505af1158015610c03573d5f5f3e3d5ffd5b50506040516001600160a01b03841692507ff8e2a8d4b93bd709cc57c9f21b79403c532f960ef18f77d0c23403cae0de78d991505f90a250565b5f610c4661138b565b610c4e6112a5565b604051631234eb3960e11b81526001600160a01b037f00000000000000000000000036000000000000000000000000000000000000021690632469d67290610c9c9085905f906004016117fc565b6020604051808303815f875af1158015610cb8573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610cdc9190611826565b90505b919050565b610cec6112dd565b610cf55f61134f565b565b3380610d016110e9565b6001600160a01b031614610d335760405163118cdaa760e01b81526001600160a01b0382166004820152602401610a4b565b610d3c8161134f565b50565b610d47611268565b610d4f6112a5565b335f9081525f51602061183e5f395f51905f52602081905260409182902054915163f94e186760e01b8152600481018390529091907f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03169063f94e186790602401610630565b610dc561130f565b5f51602061185e5f395f51905f52805460ff60a01b1916600160a01b1781556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff625905f90a150565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b5f805f51602061185e5f395f51905f52610e33565b610e606112dd565b6001600160a01b038116610e8757604051634a1653dd60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f51602061187e5f395f51905f52602081905260409091205460ff16610ece576040516335c0bed760e21b815260040160405180910390fd5b6001600160a01b0382165f81815260208390526040808220805460ff19169055517f195d741377fcd6e23556d115769d0b5f54decc24349a916f6de7371077fe7fd99190a25050565b610f1f6112dd565b815f03610f3f576040516309e56b6960e21b815260040160405180910390fd5b6001600160a01b038316610f66576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0383165f9081525f51602061183e5f395f51905f52602081905260409091205415610fab57604051633dd8918760e01b815260040160405180910390fd5b6001600160a01b0384165f8181526020838152604080832087905560018501825291829020805467ffffffffffffffff19166001600160401b03871690811790915591519182528592917fa1f041d5612451bd93cf2d0b47fbfde4782731f7612386774a8ab2b4caf57955910160405180910390a350505050565b61102e6112dd565b6001600160a01b03811661105557604051634a1653dd60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f51602061187e5f395f51905f52602081905260409091205460ff161561109d5760405163348917ed60e01b815260040160405180910390fd5b6001600160a01b0382165f81815260208390526040808220805460ff19166001179055517fd25faf73acccddf9a7ac429be872d2109cd7bd0c2370658b7c019518f3159d8e9190a25050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00610e33565b6111196112dd565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b038316908117825561115d610e0f565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b61119e6112dd565b6001600160a01b0381166111c5576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f51602061183e5f395f51905f526020819052604082205490910361120b576040516308c2ec5b60e41b815260040160405180910390fd5b6001600160a01b0382165f8181526020838152604080832083905560018501909152808220805467ffffffffffffffff19169055517f33d83959be2573f5453b12eb9d43b3499bc57d96bd2f067ba44803c859e811139190a25050565b335f9081525f51602061183e5f395f51905f5260208190526040822054909103610d3c57604051630e971e6360e11b815260040160405180910390fd5b5f51602061185e5f395f51905f528054600160a01b900460ff1615610d3c5760405163ab35696f60e01b815260040160405180910390fd5b336112e6610e0f565b6001600160a01b031614610cf55760405163118cdaa760e01b8152336004820152602401610a4b565b5f51602061185e5f395f51905f5280546001600160a01b03163314610d3c5760405163daeefd6560e01b815260040160405180910390fd5b610cf56113c9565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b031916815561138782611412565b5050565b335f9081525f51602061187e5f395f51905f52602081905260409091205460ff16610d3c5760405163eb4086bb60e01b815260040160405180910390fd5b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff16610cf557604051631afcd79f60e31b815260040160405180910390fd5b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80356001600160a01b0381168114610cdf575f5ffd5b5f602082840312156114a8575f5ffd5b6114b182611482565b9392505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f82516003811061150957634e487b7160e01b5f52602160045260245ffd5b8060208401525060208301516060604084015261152960808401826114b8565b90506001600160401b0360408501511660608401528091505092915050565b6001600160401b0381168114610d3c575f5ffd5b5f5f6040838503121561156d575f5ffd5b61157683611482565b9150602083013561158681611548565b809150509250929050565b5f5f604083850312156115a2575f5ffd5b6115ab83611482565b91506115b960208401611482565b90509250929050565b5f602082840312156115d2575f5ffd5b81356114b181611548565b634e487b7160e01b5f52604160045260245ffd5b604051606081016001600160401b0381118282101715611613576116136115dd565b60405290565b604051601f8201601f191681016001600160401b0381118282101715611641576116416115dd565b604052919050565b5f6001600160401b03821115611661576116616115dd565b50601f01601f191660200190565b5f6020828403121561167f575f5ffd5b81356001600160401b03811115611694575f5ffd5b8201601f810184136116a4575f5ffd5b80356116b76116b282611649565b611619565b8181528560208385010111156116cb575f5ffd5b816020840160208301375f91810160200191909152949350505050565b5f5f5f606084860312156116fa575f5ffd5b61170384611482565b925060208401359150604084013561171a81611548565b809150509250925092565b8051610cdf81611548565b5f60208284031215611740575f5ffd5b81516001600160401b03811115611755575f5ffd5b820160608185031215611766575f5ffd5b61176e6115f1565b81516003811061177c575f5ffd5b815260208201516001600160401b03811115611796575f5ffd5b8201601f810186136117a6575f5ffd5b80516117b46116b282611649565b8181528760208385010111156117c8575f5ffd5b8160208401602083015e5f602083830101528060208501525050506117ef60408301611725565b6040820152949350505050565b604081525f61180e60408301856114b8565b90506001600160401b03831660208301529392505050565b5f60208284031215611836575f5ffd5b505191905056fee90ec3add3e251bfbe914c9e482b511e91a3b187718c1dc10223f64a8a644a000642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a0036c39aeb5f498ae36546fc14573b003abf87227a5a2df6caec16ee566f1ad800a2646970667358221220b1e868f3ce1c9a4dd69d4083f113d5a30c8fa37341377cb5154bb72e95331bf264736f6c634300081d0033" + "code": "0x608060405234801561000f575f5ffd5b50600436106101dc575f3560e01c806379ba509711610109578063d2c20cb31161009e578063ead130111161006e578063ead130111461048d578063ec4a44d7146104a1578063f2fde38b146104b5578063f6a74ed7146104c8575f5ffd5b8063d2c20cb31461042b578063d773a7411461043e578063da3003f914610472578063e30c397814610485575f5ffd5b80638da5cb5b116100d95780638da5cb5b146103d25780639fd0506d146103da578063aa5ecb52146103e2578063b429afeb146103f5575f5ffd5b806379ba5097146103b35780637f77403d146103bb5780637ffc51fd146103c35780638456cb59146103ca575f5ffd5b8063485cc9551161017f5780635c975abb1161014f5780635c975abb146103595780635e5feee614610377578063602a9eee1461038a578063715018a6146103ab575f5ffd5b8063485cc955146102b85780634cb4850a146102cb578063554bab3c146102de5780635acac2b6146102f1575f5ffd5b8063263a3402116101ba578063263a34021461028b5780632eb5c6581461029557806337ec36d1146102a85780633f4ba83a146102b0575f5ffd5b8063048544a4146101e057806306433b1b1461022c5780631904bb2e1461026b575b5f5ffd5b6102176101ee366004611498565b6001600160a01b03165f9081525f51602061187e5f395f51905f52602052604090205460ff1690565b60405190151581526020015b60405180910390f35b6102537f000000000000000000000000360000000000000000000000000000000000000281565b6040516001600160a01b039091168152602001610223565b61027e610279366004611498565b6104db565b60405161022391906114e6565b6102936105b6565b005b6102936102a336600461155c565b610661565b610293610745565b6102936107e5565b6102936102c6366004611591565b610831565b6102936102d93660046115c2565b6109bd565b6102936102ec366004611498565b610ade565b6103416102ff366004611498565b6001600160a01b03165f9081527fe90ec3add3e251bfbe914c9e482b511e91a3b187718c1dc10223f64a8a644a0160205260409020546001600160401b031690565b6040516001600160401b039091168152602001610223565b5f51602061185e5f395f51905f5254600160a01b900460ff16610217565b610293610385366004611498565b610b62565b61039d61039836600461166f565b610c3d565b604051908152602001610223565b610293610ce4565b610293610cf7565b610293610d3f565b6103415f81565b610293610dbd565b610253610e0f565b610253610e43565b6102936103f0366004611498565b610e58565b610217610403366004611498565b6001600160a01b03165f9081525f51602061183e5f395f51905f526020526040902054151590565b6102936104393660046116e8565b610f17565b61039d61044c366004611498565b6001600160a01b03165f9081525f51602061183e5f395f51905f52602052604090205490565b610293610480366004611498565b611026565b6102536110e9565b61039d5f51602061183e5f395f51905f5281565b61039d5f51602061187e5f395f51905f5281565b6102936104c3366004611498565b611111565b6102936104d6366004611498565b611196565b6104fe6040805160608101909152805f8152606060208201525f60409091015290565b5f5f51602061183e5f395f51905f526001600160a01b038481165f908152602083905260409081902054905163b5d8962760e01b815260048101829052929350917f00000000000000000000000036000000000000000000000000000000000000029091169063b5d89627906024015f60405180830381865afa158015610587573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526105ae9190810190611730565b949350505050565b6105be611268565b6105c66112a5565b335f9081525f51602061183e5f395f51905f526020819052604091829020549151634ebae61760e01b8152600481018390529091907f00000000000000000000000036000000000000000000000000000000000000026001600160a01b031690634ebae617906024015b5f604051808303815f87803b158015610647575f5ffd5b505af1158015610659573d5f5f3e3d5ffd5b505050505050565b6106696112dd565b6001600160a01b038216610690576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0382165f9081525f51602061183e5f395f51905f52602081905260408220549091036106d6576040516308c2ec5b60e41b815260040160405180910390fd5b6001600160a01b0383165f818152600183016020908152604091829020805467ffffffffffffffff19166001600160401b03871690811790915591519182527fcf03087b939040458ea3c3a6791532c7c4432baf98e770abc930afc9cda81d87910160405180910390a2505050565b61074d6112dd565b7f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03166379ba50976040518163ffffffff1660e01b81526004015f604051808303815f87803b1580156107a5575f5ffd5b505af11580156107b7573d5f5f3e3d5ffd5b50506040517f54b70ab40993761a2b0e96f42fdf47939a56830ede1325df83e05ed33e3e8fd592505f9150a1565b6107ed61130f565b5f51602061185e5f395f51905f52805460ff60a01b191681556040517f7805862f689e2f13df9f062ff482ad3ad112aca9e0847911ed832e158c525b33905f90a150565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff1615906001600160401b03165f811580156108755750825b90505f826001600160401b031660011480156108905750303b155b90508115801561089e575080155b156108bc5760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff1916600117855583156108e657845460ff60401b1916600160401b1785555b6001600160a01b03871661090d576040516342cad95760e01b815260040160405180910390fd5b6001600160a01b0386166109345760405163a74995ab60e01b815260040160405180910390fd5b61093c611347565b6109458761134f565b5f5f51602061185e5f395f51905f5280546001600160a01b0319166001600160a01b0389161790555083156109b457845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050565b6109c5611268565b6109cd6112a5565b335f9081525f51602061183e5f395f51905f5260208181526040808420547fe90ec3add3e251bfbe914c9e482b511e91a3b187718c1dc10223f64a8a644a01909252909220549091906001600160401b03908116908416811015610a5457604051635af79e0d60e11b81526001600160401b03821660048201526024015b60405180910390fd5b60405163312267c360e21b8152600481018390526001600160401b03851660248201527f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03169063c4899f0c906044015f604051808303815f87803b158015610ac2575f5ffd5b505af1158015610ad4573d5f5f3e3d5ffd5b5050505050505050565b610ae66112dd565b6001600160a01b038116610b0d5760405163a74995ab60e01b815260040160405180910390fd5b5f51602061185e5f395f51905f5280546001600160a01b0319166001600160a01b03831690811782556040517fb80482a293ca2e013eda8683c9bd7fc8347cfdaeea5ede58cba46df502c2a604905f90a25050565b610b6a6112dd565b6001600160a01b038116610b91576040516342cad95760e01b815260040160405180910390fd5b60405163f2fde38b60e01b81526001600160a01b0382811660048301527f0000000000000000000000003600000000000000000000000000000000000002169063f2fde38b906024015f604051808303815f87803b158015610bf1575f5ffd5b505af1158015610c03573d5f5f3e3d5ffd5b50506040516001600160a01b03841692507ff8e2a8d4b93bd709cc57c9f21b79403c532f960ef18f77d0c23403cae0de78d991505f90a250565b5f610c4661138b565b610c4e6112a5565b604051631234eb3960e11b81526001600160a01b037f00000000000000000000000036000000000000000000000000000000000000021690632469d67290610c9c9085905f906004016117fc565b6020604051808303815f875af1158015610cb8573d5f5f3e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610cdc9190611826565b90505b919050565b610cec6112dd565b610cf55f61134f565b565b3380610d016110e9565b6001600160a01b031614610d335760405163118cdaa760e01b81526001600160a01b0382166004820152602401610a4b565b610d3c8161134f565b50565b610d47611268565b610d4f6112a5565b335f9081525f51602061183e5f395f51905f52602081905260409182902054915163f94e186760e01b8152600481018390529091907f00000000000000000000000036000000000000000000000000000000000000026001600160a01b03169063f94e186790602401610630565b610dc561130f565b5f51602061185e5f395f51905f52805460ff60a01b1916600160a01b1781556040517f6985a02210a168e66602d3235cb6db0e70f92b3ba4d376a33c0f3d9434bff625905f90a150565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b5f805f51602061185e5f395f51905f52610e33565b610e606112dd565b6001600160a01b038116610e8757604051634a1653dd60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f51602061187e5f395f51905f52602081905260409091205460ff16610ece576040516335c0bed760e21b815260040160405180910390fd5b6001600160a01b0382165f81815260208390526040808220805460ff19169055517f195d741377fcd6e23556d115769d0b5f54decc24349a916f6de7371077fe7fd99190a25050565b610f1f6112dd565b815f03610f3f576040516309e56b6960e21b815260040160405180910390fd5b6001600160a01b038316610f66576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0383165f9081525f51602061183e5f395f51905f52602081905260409091205415610fab57604051633dd8918760e01b815260040160405180910390fd5b6001600160a01b0384165f8181526020838152604080832087905560018501825291829020805467ffffffffffffffff19166001600160401b03871690811790915591519182528592917fa1f041d5612451bd93cf2d0b47fbfde4782731f7612386774a8ab2b4caf57955910160405180910390a350505050565b61102e6112dd565b6001600160a01b03811661105557604051634a1653dd60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f51602061187e5f395f51905f52602081905260409091205460ff161561109d5760405163348917ed60e01b815260040160405180910390fd5b6001600160a01b0382165f81815260208390526040808220805460ff19166001179055517fd25faf73acccddf9a7ac429be872d2109cd7bd0c2370658b7c019518f3159d8e9190a25050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00610e33565b6111196112dd565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b038316908117825561115d610e0f565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b61119e6112dd565b6001600160a01b0381166111c5576040516371ab47bb60e11b815260040160405180910390fd5b6001600160a01b0381165f9081525f51602061183e5f395f51905f526020819052604082205490910361120b576040516308c2ec5b60e41b815260040160405180910390fd5b6001600160a01b0382165f8181526020838152604080832083905560018501909152808220805467ffffffffffffffff19169055517f33d83959be2573f5453b12eb9d43b3499bc57d96bd2f067ba44803c859e811139190a25050565b335f9081525f51602061183e5f395f51905f5260208190526040822054909103610d3c57604051630e971e6360e11b815260040160405180910390fd5b5f51602061185e5f395f51905f528054600160a01b900460ff1615610d3c5760405163ab35696f60e01b815260040160405180910390fd5b336112e6610e0f565b6001600160a01b031614610cf55760405163118cdaa760e01b8152336004820152602401610a4b565b5f51602061185e5f395f51905f5280546001600160a01b03163314610d3c5760405163daeefd6560e01b815260040160405180910390fd5b610cf56113c9565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b031916815561138782611412565b5050565b335f9081525f51602061187e5f395f51905f52602081905260409091205460ff16610d3c5760405163eb4086bb60e01b815260040160405180910390fd5b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff16610cf557604051631afcd79f60e31b815260040160405180910390fd5b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b80356001600160a01b0381168114610cdf575f5ffd5b5f602082840312156114a8575f5ffd5b6114b182611482565b9392505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f82516003811061150957634e487b7160e01b5f52602160045260245ffd5b8060208401525060208301516060604084015261152960808401826114b8565b90506001600160401b0360408501511660608401528091505092915050565b6001600160401b0381168114610d3c575f5ffd5b5f5f6040838503121561156d575f5ffd5b61157683611482565b9150602083013561158681611548565b809150509250929050565b5f5f604083850312156115a2575f5ffd5b6115ab83611482565b91506115b960208401611482565b90509250929050565b5f602082840312156115d2575f5ffd5b81356114b181611548565b634e487b7160e01b5f52604160045260245ffd5b604051606081016001600160401b0381118282101715611613576116136115dd565b60405290565b604051601f8201601f191681016001600160401b0381118282101715611641576116416115dd565b604052919050565b5f6001600160401b03821115611661576116616115dd565b50601f01601f191660200190565b5f6020828403121561167f575f5ffd5b81356001600160401b03811115611694575f5ffd5b8201601f810184136116a4575f5ffd5b80356116b76116b282611649565b611619565b8181528560208385010111156116cb575f5ffd5b816020840160208301375f91810160200191909152949350505050565b5f5f5f606084860312156116fa575f5ffd5b61170384611482565b925060208401359150604084013561171a81611548565b809150509250925092565b8051610cdf81611548565b5f60208284031215611740575f5ffd5b81516001600160401b03811115611755575f5ffd5b820160608185031215611766575f5ffd5b61176e6115f1565b81516003811061177c575f5ffd5b815260208201516001600160401b03811115611796575f5ffd5b8201601f810186136117a6575f5ffd5b80516117b46116b282611649565b8181528760208385010111156117c8575f5ffd5b8160208401602083015e5f602083830101528060208501525050506117ef60408301611725565b6040820152949350505050565b604081525f61180e60408301856114b8565b90506001600160401b03831660208301529392505050565b5f60208284031215611836575f5ffd5b505191905056fee90ec3add3e251bfbe914c9e482b511e91a3b187718c1dc10223f64a8a644a000642d7922329a434cf4fd17a3c95eb692c24fd95f9f94d0b55420a5d895f4a0036c39aeb5f498ae36546fc14573b003abf87227a5a2df6caec16ee566f1ad800a2646970667358221220b2ba8c761bf75081c2d92a909c7cfe262281693aff1055fb8f3984e0036cc33764736f6c634300081d0033" }, "0x3600000000000000000000000000000000000003": { "balance": "0x0", "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220b46184e97ca15761bb56cc4f4af0c9790e85df702b2581fb83f0e8f093b3543564736f6c634300081d0033", + "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", "storage": { "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000a0ee7a142d267c1f36714e4a8f75612f20a79720", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000971b475b4be17cdddf218f5dcead523811baf0bf", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x00000000000000000000000092b26cf0580faecf86cdd16069af99a81aadc3ad", "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001", "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", "0xa4ec95f560ce81c285cd4e5462195a1e0b5658e60e5765bb1a90935498ab31ef": "0x0000000000000000000000000000000000000000000000000000000000000001", @@ -172,18 +172,18 @@ "0xc15fa7a907ba1d41aff5e5ded523fc00e97bf156e8a98a179bd9b65a05161d7c": "0x0000000000000000000000000000000000000000000000000000000000000001" } }, - "0xb08145cc285EB94C42C6217DA692C87239691481": { + "0x4b5A1f0cf56B193cCA1a0B16fCE0713d44abdDbE": { "balance": "0x0", "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100cb575f3560e01c8063c4d66de811610088578063e8746fed11610063578063e8746fed146101b0578063e877a526146101c3578063f2fde38b146101fa578063fab48ccf1461020d575f5ffd5b8063c4d66de814610182578063ca2f731314610195578063e30c3978146101a8575f5ffd5b806331d798b5146100cf578063715018a61461011b57806379ba5097146101255780637a7ceb161461012d5780638a8ee6ac146101405780638da5cb5b14610162575b5f5ffd5b6101066100dd366004610980565b6001600160a01b03165f9081525f516020610a335f395f51905f52602052604090205460ff1690565b60405190151581526020015b60405180910390f35b610123610220565b005b610123610233565b61012361013b3660046109ad565b610280565b6101545f516020610a535f395f51905f5281565b604051908152602001610112565b61016a610385565b6040516001600160a01b039091168152602001610112565b610123610190366004610980565b6103b9565b6101236101a3366004610980565b6104f5565b61016a6105ab565b6101236101be3660046109ad565b6105d3565b6101066101d1366004610980565b6001600160a01b03165f9081525f516020610a535f395f51905f52602052604090205460ff1690565b610123610208366004610980565b610717565b61012361021b366004610980565b61079c565b610228610855565b6102315f610887565b565b338061023d6105ab565b6001600160a01b0316146102745760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b61027d81610887565b50565b335f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff166102ca57604051637628c31d60e01b815260040160405180910390fd5b5f516020610a535f395f51905f52825f5b8181101561037d575f8686838181106102f6576102f6610a1e565b905060200201602081019061030b9190610980565b6001600160a01b0381165f9081526020869052604090205490915060ff1615610374576001600160a01b0381165f81815260208690526040808220805460ff19169055517fc904e1b03de0c20d7fcf9dbd056daf1bd3815e93f251199de815fd0f0b96e1669190a25b506001016102db565b505050505050565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff16159067ffffffffffffffff165f811580156103fe5750825b90505f8267ffffffffffffffff16600114801561041a5750303b155b905081158015610428575080155b156104465760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561047057845460ff60401b1916600160401b1785555b6001600160a01b0386166104975760405163d92e233d60e01b815260040160405180910390fd5b61049f6108bf565b6104a886610887565b831561037d57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a1505050505050565b6104fd610855565b6001600160a01b0381166105245760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b0381165f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff16156105a7576001600160a01b0382165f818152600183016020526040808220805460ff19169055517f2ee9bf58aeff79a8ee48ae2ebaea69b7fc533af3060aaa8d8956b225785db10a9190a25b5050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006103a9565b335f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff1661061d57604051637628c31d60e01b815260040160405180910390fd5b5f610626610385565b90505f516020610a535f395f51905f52835f5b8181101561070e575f87878381811061065457610654610a1e565b90506020020160208101906106699190610980565b9050846001600160a01b0316816001600160a01b03160361069d5760405163266f648160e01b815260040160405180910390fd5b6001600160a01b0381165f9081526020859052604090205460ff16610705576001600160a01b0381165f81815260208690526040808220805460ff19166001179055517ffa4507bc1f9c730e6e95897024f1fe7d576cf2deb53579d55c14f1ac3439e1149190a25b50600101610639565b50505050505050565b61071f610855565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610763610385565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b6107a4610855565b6001600160a01b0381166107cb5760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b0381165f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff166105a7576001600160a01b0382165f81815260018381016020526040808320805460ff1916909217909155517f1ac0162c9c4096b260eb12be2731ca291739aa6ed7c94780f52c55df88589e7c9190a25050565b3361085e610385565b6001600160a01b0316146102315760405163118cdaa760e01b815233600482015260240161026b565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b03191681556105a7826108c7565b610231610937565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff1661023157604051631afcd79f60e31b815260040160405180910390fd5b5f60208284031215610990575f5ffd5b81356001600160a01b03811681146109a6575f5ffd5b9392505050565b5f5f602083850312156109be575f5ffd5b823567ffffffffffffffff8111156109d4575f5ffd5b8301601f810185136109e4575f5ffd5b803567ffffffffffffffff8111156109fa575f5ffd5b8560208260051b8401011115610a0e575f5ffd5b6020919091019590945092505050565b634e487b7160e01b5f52603260045260245ffdfe1d7e1388d3ae56f3d9c18b1ce8d2b3b1a238a0edf682d2053af5d8a1d2f12f011d7e1388d3ae56f3d9c18b1ce8d2b3b1a238a0edf682d2053af5d8a1d2f12f00a26469706673582212207336c02264a8d24a85b924b547fbe99ee505763075a6697eba1b1b0aec59b79864736f6c634300081d0033" + "code": "0x608060405234801561000f575f5ffd5b50600436106100cb575f3560e01c8063c4d66de811610088578063e8746fed11610063578063e8746fed146101b0578063e877a526146101c3578063f2fde38b146101fa578063fab48ccf1461020d575f5ffd5b8063c4d66de814610182578063ca2f731314610195578063e30c3978146101a8575f5ffd5b806331d798b5146100cf578063715018a61461011b57806379ba5097146101255780637a7ceb161461012d5780638a8ee6ac146101405780638da5cb5b14610162575b5f5ffd5b6101066100dd366004610980565b6001600160a01b03165f9081525f516020610a335f395f51905f52602052604090205460ff1690565b60405190151581526020015b60405180910390f35b610123610220565b005b610123610233565b61012361013b3660046109ad565b610280565b6101545f516020610a535f395f51905f5281565b604051908152602001610112565b61016a610385565b6040516001600160a01b039091168152602001610112565b610123610190366004610980565b6103b9565b6101236101a3366004610980565b6104f5565b61016a6105ab565b6101236101be3660046109ad565b6105d3565b6101066101d1366004610980565b6001600160a01b03165f9081525f516020610a535f395f51905f52602052604090205460ff1690565b610123610208366004610980565b610717565b61012361021b366004610980565b61079c565b610228610855565b6102315f610887565b565b338061023d6105ab565b6001600160a01b0316146102745760405163118cdaa760e01b81526001600160a01b03821660048201526024015b60405180910390fd5b61027d81610887565b50565b335f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff166102ca57604051637628c31d60e01b815260040160405180910390fd5b5f516020610a535f395f51905f52825f5b8181101561037d575f8686838181106102f6576102f6610a1e565b905060200201602081019061030b9190610980565b6001600160a01b0381165f9081526020869052604090205490915060ff1615610374576001600160a01b0381165f81815260208690526040808220805460ff19169055517fc904e1b03de0c20d7fcf9dbd056daf1bd3815e93f251199de815fd0f0b96e1669190a25b506001016102db565b505050505050565b5f807f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c1993005b546001600160a01b031692915050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff16159067ffffffffffffffff165f811580156103fe5750825b90505f8267ffffffffffffffff16600114801561041a5750303b155b905081158015610428575080155b156104465760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561047057845460ff60401b1916600160401b1785555b6001600160a01b0386166104975760405163d92e233d60e01b815260040160405180910390fd5b61049f6108bf565b6104a886610887565b831561037d57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a1505050505050565b6104fd610855565b6001600160a01b0381166105245760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b0381165f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff16156105a7576001600160a01b0382165f818152600183016020526040808220805460ff19169055517f2ee9bf58aeff79a8ee48ae2ebaea69b7fc533af3060aaa8d8956b225785db10a9190a25b5050565b5f807f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c006103a9565b335f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff1661061d57604051637628c31d60e01b815260040160405180910390fd5b5f610626610385565b90505f516020610a535f395f51905f52835f5b8181101561070e575f87878381811061065457610654610a1e565b90506020020160208101906106699190610980565b9050846001600160a01b0316816001600160a01b03160361069d5760405163266f648160e01b815260040160405180910390fd5b6001600160a01b0381165f9081526020859052604090205460ff16610705576001600160a01b0381165f81815260208690526040808220805460ff19166001179055517ffa4507bc1f9c730e6e95897024f1fe7d576cf2deb53579d55c14f1ac3439e1149190a25b50600101610639565b50505050505050565b61071f610855565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b0319166001600160a01b0383169081178255610763610385565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b6107a4610855565b6001600160a01b0381166107cb5760405163d92e233d60e01b815260040160405180910390fd5b6001600160a01b0381165f9081525f516020610a335f395f51905f5260205260409020545f516020610a535f395f51905f529060ff166105a7576001600160a01b0382165f81815260018381016020526040808320805460ff1916909217909155517f1ac0162c9c4096b260eb12be2731ca291739aa6ed7c94780f52c55df88589e7c9190a25050565b3361085e610385565b6001600160a01b0316146102315760405163118cdaa760e01b815233600482015260240161026b565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0080546001600160a01b03191681556105a7826108c7565b610231610937565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff1661023157604051631afcd79f60e31b815260040160405180910390fd5b5f60208284031215610990575f5ffd5b81356001600160a01b03811681146109a6575f5ffd5b9392505050565b5f5f602083850312156109be575f5ffd5b823567ffffffffffffffff8111156109d4575f5ffd5b8301601f810185136109e4575f5ffd5b803567ffffffffffffffff8111156109fa575f5ffd5b8560208260051b8401011115610a0e575f5ffd5b6020919091019590945092505050565b634e487b7160e01b5f52603260045260245ffdfe1d7e1388d3ae56f3d9c18b1ce8d2b3b1a238a0edf682d2053af5d8a1d2f12f011d7e1388d3ae56f3d9c18b1ce8d2b3b1a238a0edf682d2053af5d8a1d2f12f00a264697066735822122057fdea9cb356478df3dad5dbaec8a47476bd3266f5a49729c6d02d8e049ee9b864736f6c634300081d0033" }, - "0x36082bA812806eB06C2758c412522669b5E2ac7b": { + "0x360Eb67EDbA456Bbe01512679f36c2717AA65121": { "balance": "0x0", "nonce": "0x1", - "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea2646970667358221220b46184e97ca15761bb56cc4f4af0c9790e85df702b2581fb83f0e8f093b3543564736f6c634300081d0033", + "code": "0x608060405260043610610049575f3560e01c80633659cfe6146100535780634f1ef286146100725780635c60da1b146100855780638f283970146100b5578063f851a440146100d4575b6100516100e8565b005b34801561005e575f5ffd5b5061005161006d3660046104f3565b6100fa565b61005161008036600461050c565b610134565b348015610090575f5ffd5b50610099610197565b6040516001600160a01b03909116815260200160405180910390f35b3480156100c0575f5ffd5b506100516100cf3660046104f3565b6101a5565b3480156100df575f5ffd5b506100996101c5565b6100f86100f3610197565b6101ce565b565b6101026101ec565b6001600160a01b0316330361012c576101298160405180602001604052805f81525061021e565b50565b6101296100e8565b61013c6101ec565b6001600160a01b0316330361018f5761018a8383838080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061021e92505050565b505050565b61018a6100e8565b5f6101a0610277565b905090565b6101ad6101ec565b6001600160a01b0316330361012c576101298161029e565b5f6101a06101ec565b365f5f375f5f365f845af43d5f5f3e8080156101e8573d5ff35b3d5ffd5b5f7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035b546001600160a01b0316919050565b610227826102f2565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561026b5761018a8282610370565b6102736103e2565b5050565b5f7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc61020f565b7f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f6102c76101ec565b604080516001600160a01b03928316815291841660208301520160405180910390a161012981610401565b806001600160a01b03163b5f0361032c57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b807f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5b80546001600160a01b0319166001600160a01b039290921691909117905550565b60605f5f846001600160a01b03168460405161038c919061058a565b5f60405180830381855af49150503d805f81146103c4576040519150601f19603f3d011682016040523d82523d5f602084013e6103c9565b606091505b50915091506103d9858383610451565b95945050505050565b34156100f85760405163b398979f60e01b815260040160405180910390fd5b6001600160a01b03811661042a57604051633173bdd160e11b81525f6004820152602401610323565b807fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d610361034f565b60608261046657610461826104b0565b6104a9565b815115801561047d57506001600160a01b0384163b155b156104a657604051639996b31560e01b81526001600160a01b0385166004820152602401610323565b50805b9392505050565b8051156104bf57805160208201fd5b60405163d6bda27560e01b815260040160405180910390fd5b80356001600160a01b03811681146104ee575f5ffd5b919050565b5f60208284031215610503575f5ffd5b6104a9826104d8565b5f5f5f6040848603121561051e575f5ffd5b610527846104d8565b9250602084013567ffffffffffffffff811115610542575f5ffd5b8401601f81018613610552575f5ffd5b803567ffffffffffffffff811115610568575f5ffd5b866020828401011115610579575f5ffd5b939660209190910195509293505050565b5f82518060208501845e5f92019182525091905056fea26469706673582212204c0881af9b906ca964ce2bc1cc4525350de8dd57658dacb806266290556eb9ae64736f6c634300081d0033", "storage": { "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103": "0x000000000000000000000000a0ee7a142d267c1f36714e4a8f75612f20a79720", - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000b08145cc285eb94c42c6217da692c87239691481", + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x0000000000000000000000004b5a1f0cf56b193cca1a0b16fce0713d44abddbe", "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300": "0x00000000000000000000000023618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f", "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00": "0x0000000000000000000000000000000000000000000000000000000000000001", "0x7c227092eb1f4e3f31917fac32f58d3c183f1245a2f05a65068f491ee1a24948": "0x0000000000000000000000000000000000000000000000000000000000000001" @@ -221,25 +221,25 @@ "nonce": "0x1", "code": "0x6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f000000000000000000000000000000000000000000000000000000000000053903611b69577f1f520b5ee38ad937955892c3dfc7055e8eeb515d905781b6951e4c687917c53090565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a" }, - "0x1be052abb35D7f41609Bfec8F2fC2A684CB9984f": { + "0x45a834A6bB86F516D4157a8cBcc60f2F35F8398C": { "balance": "0x0", "nonce": "0x1", - "code": "0x60806040526004361061008f575f3560e01c806386fdb4d31161005757806386fdb4d314610137578063c1ee9d5e1461014c578063caeda9a614610161578063cb1ed51814610180578063fa0ff39f14610193575f5ffd5b806332e43a111461009357806357043888146100b95780635e666e4a146100ce578063665c5b98146100f95780636715131914610118575b5f5ffd5b34801561009e575f5ffd5b506100a75f5481565b60405190815260200160405180910390f35b3480156100c4575f5ffd5b506100a760025481565b3480156100d9575f5ffd5b506100a76100e83660046103fb565b60016020525f908152604090205481565b348015610104575f5ffd5b506100a76101133660046103fb565b6101b1565b348015610123575f5ffd5b506100a76101323660046103fb565b61023d565b61014a6101453660046103fb565b610284565b005b348015610157575f5ffd5b506100a760035481565b34801561016c575f5ffd5b506100a761017b3660046103fb565b61034d565b61014a61018e3660046103fb565b6103db565b34801561019e575f5ffd5b5061014a6101ad3660046103fb565b5f55565b5f815f036101c057505f919050565b6002546040516bffffffffffffffffffffffff193360601b16602082015260348101919091525f9060540160408051601f19818403018152919052805160209091012090505f5b8381101561022e578181015f90815260016020819052604090912054939093189201610207565b50506002805490920190915590565b5f805b8281101561027e57604080516020810184905290810182905260600160408051601f1981840301815291905280516020909101209150600101610240565b50919050565b5f30825a6102929190610426565b604080514260248083019190915282518083039091018152604490910182526020810180516001600160e01b031663fa0ff39f60e01b17905290516102d7919061043f565b5f604051808303818686fa925050503d805f8114610310576040519150601f19603f3d011682016040523d82523d5f602084013e610315565b606091505b505090505f81610325575f610328565b60015b60ff1690505b825a1115610348578061034081610455565b91505061032e565b505050565b5f815f0361035c57505f919050565b6003546040516bffffffffffffffffffffffff193360601b16602082015260348101919091525f9060540160408051601f19818403018152919052805160209091012090505f5b838110156103cc578181015f8181526001602081905260409091209490911893849055016103a3565b50506003805490920190915590565b5f5b815a11156103f757806103ef81610455565b9150506103dd565b5050565b5f6020828403121561040b575f5ffd5b5035919050565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561043957610439610412565b92915050565b5f82518060208501845e5f920191825250919050565b5f6001820161046657610466610412565b506001019056fea26469706673582212200033ca55e77271ccb4d26a683599dcbac0fca98edc5e7b1be3fae40eacffef0464736f6c634300081d0033" + "code": "0x60806040526004361061008f575f3560e01c806386fdb4d31161005757806386fdb4d314610137578063c1ee9d5e1461014c578063caeda9a614610161578063cb1ed51814610180578063fa0ff39f14610193575f5ffd5b806332e43a111461009357806357043888146100b95780635e666e4a146100ce578063665c5b98146100f95780636715131914610118575b5f5ffd5b34801561009e575f5ffd5b506100a75f5481565b60405190815260200160405180910390f35b3480156100c4575f5ffd5b506100a760025481565b3480156100d9575f5ffd5b506100a76100e83660046103fb565b60016020525f908152604090205481565b348015610104575f5ffd5b506100a76101133660046103fb565b6101b1565b348015610123575f5ffd5b506100a76101323660046103fb565b61023d565b61014a6101453660046103fb565b610284565b005b348015610157575f5ffd5b506100a760035481565b34801561016c575f5ffd5b506100a761017b3660046103fb565b61034d565b61014a61018e3660046103fb565b6103db565b34801561019e575f5ffd5b5061014a6101ad3660046103fb565b5f55565b5f815f036101c057505f919050565b6002546040516bffffffffffffffffffffffff193360601b16602082015260348101919091525f9060540160408051601f19818403018152919052805160209091012090505f5b8381101561022e578181015f90815260016020819052604090912054939093189201610207565b50506002805490920190915590565b5f805b8281101561027e57604080516020810184905290810182905260600160408051601f1981840301815291905280516020909101209150600101610240565b50919050565b5f30825a6102929190610426565b604080514260248083019190915282518083039091018152604490910182526020810180516001600160e01b031663fa0ff39f60e01b17905290516102d7919061043f565b5f604051808303818686fa925050503d805f8114610310576040519150601f19603f3d011682016040523d82523d5f602084013e610315565b606091505b505090505f81610325575f610328565b60015b60ff1690505b825a1115610348578061034081610455565b91505061032e565b505050565b5f815f0361035c57505f919050565b6003546040516bffffffffffffffffffffffff193360601b16602082015260348101919091525f9060540160408051601f19818403018152919052805160209091012090505f5b838110156103cc578181015f8181526001602081905260409091209490911893849055016103a3565b50506003805490920190915590565b5f5b815a11156103f757806103ef81610455565b9150506103dd565b5050565b5f6020828403121561040b575f5ffd5b5035919050565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561043957610439610412565b92915050565b5f82518060208501845e5f920191825250919050565b5f6001820161046657610466610412565b506001019056fea26469706673582212202b8f1c900a394275a0d8f46b1573db0b518b0424e872e3ddb6309e56e5730c7764736f6c634300081d0033" }, - "0x9702466268ccF55eAB64cdf484d272Ac08d3b75b": { + "0xe4aa7Ed3585AEf598179f873086F75Fcd6D4b755": { "balance": "0x0", "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b506004361061003f575f3560e01c8063c3b2c4f814610043578063ea94cddd14610058578063f884e35514610083575b5f5ffd5b610056610051366004610231565b610099565b005b61006660036003609b1b0181565b6040516001600160a01b0390911681526020015b60405180910390f35b61008b5f5481565b60405190815260200161007a565b5f805481806100a7836102c7565b9091555060405190915081907fb252e055da754c72fbf7542cf424b190808a9b541e912894c5e15b4238c41501905f90a2604051631595ec0b60e01b81525f90819060036003609b1b0190631595ec0b9061010c9033908d908d908d90600401610313565b5f604051808303815f875af1158015610127573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f1916820160405261014e919081019061035d565b915091508161017b578060405163768cb35160e11b81526004016101729190610428565b60405180910390fd5b85896001600160a01b0316336001600160a01b03167feb15ee720798341c37739df41be53acfbbf70ae6802dade35457beec6e47a5e48b8b6040516101c192919061045d565b6040519081900381206101d9918b908b908b9061046c565b60405180910390a4505050505050505050565b5f5f83601f8401126101fc575f5ffd5b50813567ffffffffffffffff811115610213575f5ffd5b60208301915083602082850101111561022a575f5ffd5b9250929050565b5f5f5f5f5f5f60808789031215610246575f5ffd5b86356001600160a01b038116811461025c575f5ffd5b9550602087013567ffffffffffffffff811115610277575f5ffd5b61028389828a016101ec565b90965094505060408701359250606087013567ffffffffffffffff8111156102a9575f5ffd5b6102b589828a016101ec565b979a9699509497509295939492505050565b5f600182016102e457634e487b7160e01b5f52601160045260245ffd5b5060010190565b81835281816020850137505f828201602090810191909152601f909101601f19169091010190565b6001600160a01b038581168252841660208201526060604082018190525f9061033f90830184866102eb565b9695505050505050565b634e487b7160e01b5f52604160045260245ffd5b5f5f6040838503121561036e575f5ffd5b8251801515811461037d575f5ffd5b602084015190925067ffffffffffffffff811115610399575f5ffd5b8301601f810185136103a9575f5ffd5b805167ffffffffffffffff8111156103c3576103c3610349565b604051601f8201601f19908116603f0116810167ffffffffffffffff811182821017156103f2576103f2610349565b604052818152828201602001871015610409575f5ffd5b8160208401602083015e5f602083830101528093505050509250929050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b818382375f9101908152919050565b848152606060208201525f6104856060830185876102eb565b90508260408301529594505050505056fea264697066735822122090e53d508af109e9426e625ab8a9ce00a41728efa42bcd9956104c124b75377e64736f6c634300081d0033" + "code": "0x608060405234801561000f575f5ffd5b506004361061003f575f3560e01c8063c3b2c4f814610043578063ea94cddd14610058578063f884e35514610083575b5f5ffd5b610056610051366004610231565b610099565b005b61006660036003609b1b0181565b6040516001600160a01b0390911681526020015b60405180910390f35b61008b5f5481565b60405190815260200161007a565b5f805481806100a7836102c7565b9091555060405190915081907fb252e055da754c72fbf7542cf424b190808a9b541e912894c5e15b4238c41501905f90a2604051631595ec0b60e01b81525f90819060036003609b1b0190631595ec0b9061010c9033908d908d908d90600401610313565b5f604051808303815f875af1158015610127573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f1916820160405261014e919081019061035d565b915091508161017b578060405163768cb35160e11b81526004016101729190610428565b60405180910390fd5b85896001600160a01b0316336001600160a01b03167feb15ee720798341c37739df41be53acfbbf70ae6802dade35457beec6e47a5e48b8b6040516101c192919061045d565b6040519081900381206101d9918b908b908b9061046c565b60405180910390a4505050505050505050565b5f5f83601f8401126101fc575f5ffd5b50813567ffffffffffffffff811115610213575f5ffd5b60208301915083602082850101111561022a575f5ffd5b9250929050565b5f5f5f5f5f5f60808789031215610246575f5ffd5b86356001600160a01b038116811461025c575f5ffd5b9550602087013567ffffffffffffffff811115610277575f5ffd5b61028389828a016101ec565b90965094505060408701359250606087013567ffffffffffffffff8111156102a9575f5ffd5b6102b589828a016101ec565b979a9699509497509295939492505050565b5f600182016102e457634e487b7160e01b5f52601160045260245ffd5b5060010190565b81835281816020850137505f828201602090810191909152601f909101601f19169091010190565b6001600160a01b038581168252841660208201526060604082018190525f9061033f90830184866102eb565b9695505050505050565b634e487b7160e01b5f52604160045260245ffd5b5f5f6040838503121561036e575f5ffd5b8251801515811461037d575f5ffd5b602084015190925067ffffffffffffffff811115610399575f5ffd5b8301601f810185136103a9575f5ffd5b805167ffffffffffffffff8111156103c3576103c3610349565b604051601f8201601f19908116603f0116810167ffffffffffffffff811182821017156103f2576103f2610349565b604052818152828201602001871015610409575f5ffd5b8160208401602083015e5f602083830101528093505050509250929050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b818382375f9101908152919050565b848152606060208201525f6104856060830185876102eb565b90508260408301529594505050505056fea264697066735822122055e34761f9688c46910c2312ec9a6b566f67e946c47cd1aa4f5cca217b38341364736f6c634300081d0033" }, - "0xEb7cc06E3D3b5F9F9a5fA2B31B477ff72bB9c8b6": { + "0x825F535677d346626cDE45D64cf89C2a426467e0": { "balance": "0x0", "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b50600436106100fb575f3560e01c806372425d9d11610093578063bce38bd711610063578063bce38bd7146101d4578063c3077fa9146101e7578063ea94cddd146101fa578063ee82ac5e14610208575f5ffd5b806372425d9d1461018e57806382ad56cb1461019457806386d516e8146101b4578063a8b0574e146101ba575f5ffd5b8063399542e9116100ce578063399542e9146101455780633e64a6961461016757806342cbb15c1461016d5780634d2301cc14610173575f5ffd5b80630f28c97d146100ff578063252dba421461011457806327e86d6e146101355780633408e4701461013f575b5f5ffd5b425b6040519081526020015b60405180910390f35b61012761012236600461093d565b61021a565b60405161010b9291906109aa565b435f190140610101565b46610101565b610158610153366004610a24565b61038e565b60405161010b93929190610ae8565b48610101565b43610101565b610101610181366004610b0f565b6001600160a01b03163190565b44610101565b6101a76101a236600461093d565b6103ac565b60405161010b9190610b3c565b45610101565b415b6040516001600160a01b03909116815260200161010b565b6101a76101e2366004610a24565b610586565b6101586101f536600461093d565b61072c565b6101bc60036003609b1b0181565b610101610216366004610b4e565b4090565b436060828067ffffffffffffffff81111561023757610237610b65565b60405190808252806020026020018201604052801561026a57816020015b60608152602001906001900390816102555790505b5091505f5b81811015610385575f8060036003609b1b01631595ec0b338a8a8781811061029957610299610b79565b90506020028101906102ab9190610b8d565b6102b9906020810190610b0f565b8b8b888181106102cb576102cb610b79565b90506020028101906102dd9190610b8d565b6102eb906020810190610bab565b6040518563ffffffff1660e01b815260040161030a9493929190610bee565b5f604051808303815f875af1158015610325573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f1916820160405261034c9190810190610c36565b915091508161035d57805160208201fd5b8085848151811061037057610370610b79565b6020908102919091010152505060010161026f565b50509250929050565b5f5f606061039d86868661074a565b91989097509095509350505050565b6060818067ffffffffffffffff8111156103c8576103c8610b65565b60405190808252806020026020018201604052801561040d57816020015b604080518082019091525f8152606060208201528152602001906001900390816103e65790505b5091505f5b8181101561057e575f8060036003609b1b01631595ec0b3389898781811061043c5761043c610b79565b905060200281019061044e9190610cfd565b61045c906020810190610b0f565b8a8a8881811061046e5761046e610b79565b90506020028101906104809190610cfd565b61048e906040810190610bab565b6040518563ffffffff1660e01b81526004016104ad9493929190610bee565b5f604051808303815f875af11580156104c8573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526104ef9190810190610c36565b91509150604051806040016040528083151581526020018281525085848151811061051c5761051c610b79565b602002602001018190525086868481811061053957610539610b79565b905060200281019061054b9190610cfd565b61055c906040810190602001610d11565b158015610567575081155b1561057457805160208201fd5b5050600101610412565b505092915050565b6060818067ffffffffffffffff8111156105a2576105a2610b65565b6040519080825280602002602001820160405280156105e757816020015b604080518082019091525f8152606060208201528152602001906001900390816105c05790505b5091505f5b81811015610723575f8060036003609b1b01631595ec0b3389898781811061061657610616610b79565b90506020028101906106289190610b8d565b610636906020810190610b0f565b8a8a8881811061064857610648610b79565b905060200281019061065a9190610b8d565b610668906020810190610bab565b6040518563ffffffff1660e01b81526004016106879493929190610bee565b5f604051808303815f875af11580156106a2573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526106c99190810190610c36565b915091508780156106d8575081155b156106e557805160208201fd5b604051806040016040528083151581526020018281525085848151811061070e5761070e610b79565b602090810291909101015250506001016105ec565b50509392505050565b5f5f606061073c6001868661074a565b919790965090945092505050565b4380406060838067ffffffffffffffff81111561076957610769610b65565b6040519080825280602002602001820160405280156107ae57816020015b604080518082019091525f8152606060208201528152602001906001900390816107875790505b5091505f5b818110156108ea575f8060036003609b1b01631595ec0b338b8b878181106107dd576107dd610b79565b90506020028101906107ef9190610b8d565b6107fd906020810190610b0f565b8c8c8881811061080f5761080f610b79565b90506020028101906108219190610b8d565b61082f906020810190610bab565b6040518563ffffffff1660e01b815260040161084e9493929190610bee565b5f604051808303815f875af1158015610869573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526108909190810190610c36565b9150915089801561089f575081155b156108ac57805160208201fd5b60405180604001604052808315158152602001828152508584815181106108d5576108d5610b79565b602090810291909101015250506001016107b3565b505093509350939050565b5f5f83601f840112610905575f5ffd5b50813567ffffffffffffffff81111561091c575f5ffd5b6020830191508360208260051b8501011115610936575f5ffd5b9250929050565b5f5f6020838503121561094e575f5ffd5b823567ffffffffffffffff811115610964575f5ffd5b610970858286016108f5565b90969095509350505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b5f604082018483526040602084015280845180835260608501915060608160051b8601019250602086015f5b82811015610a0757605f198786030184526109f285835161097c565b945060209384019391909101906001016109d6565b5092979650505050505050565b8015158114610a21575f5ffd5b50565b5f5f5f60408486031215610a36575f5ffd5b8335610a4181610a14565b9250602084013567ffffffffffffffff811115610a5c575f5ffd5b610a68868287016108f5565b9497909650939450505050565b5f82825180855260208501945060208160051b830101602085015f5b83811015610adc57601f1985840301885281518051151584526020810151905060406020850152610ac5604085018261097c565b6020998a0199909450929092019150600101610a91565b50909695505050505050565b838152826020820152606060408201525f610b066060830184610a75565b95945050505050565b5f60208284031215610b1f575f5ffd5b81356001600160a01b0381168114610b35575f5ffd5b9392505050565b602081525f610b356020830184610a75565b5f60208284031215610b5e575f5ffd5b5035919050565b634e487b7160e01b5f52604160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b5f8235603e19833603018112610ba1575f5ffd5b9190910192915050565b5f5f8335601e19843603018112610bc0575f5ffd5b83018035915067ffffffffffffffff821115610bda575f5ffd5b602001915036819003821315610936575f5ffd5b6001600160a01b038581168252841660208201526060604082018190528101829052818360808301375f818301608090810191909152601f909201601f191601019392505050565b5f5f60408385031215610c47575f5ffd5b8251610c5281610a14565b602084015190925067ffffffffffffffff811115610c6e575f5ffd5b8301601f81018513610c7e575f5ffd5b805167ffffffffffffffff811115610c9857610c98610b65565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610cc757610cc7610b65565b604052818152828201602001871015610cde575f5ffd5b8160208401602083015e5f602083830101528093505050509250929050565b5f8235605e19833603018112610ba1575f5ffd5b5f60208284031215610d21575f5ffd5b8135610b3581610a1456fea2646970667358221220bdc31d12bd258d3bf532370f59ae0020535b3d8fce73e228adb8fc2e5999416a64736f6c634300081d0033" + "code": "0x608060405234801561000f575f5ffd5b50600436106100fb575f3560e01c806372425d9d11610093578063bce38bd711610063578063bce38bd7146101d4578063c3077fa9146101e7578063ea94cddd146101fa578063ee82ac5e14610208575f5ffd5b806372425d9d1461018e57806382ad56cb1461019457806386d516e8146101b4578063a8b0574e146101ba575f5ffd5b8063399542e9116100ce578063399542e9146101455780633e64a6961461016757806342cbb15c1461016d5780634d2301cc14610173575f5ffd5b80630f28c97d146100ff578063252dba421461011457806327e86d6e146101355780633408e4701461013f575b5f5ffd5b425b6040519081526020015b60405180910390f35b61012761012236600461093d565b61021a565b60405161010b9291906109aa565b435f190140610101565b46610101565b610158610153366004610a24565b61038e565b60405161010b93929190610ae8565b48610101565b43610101565b610101610181366004610b0f565b6001600160a01b03163190565b44610101565b6101a76101a236600461093d565b6103ac565b60405161010b9190610b3c565b45610101565b415b6040516001600160a01b03909116815260200161010b565b6101a76101e2366004610a24565b610586565b6101586101f536600461093d565b61072c565b6101bc60036003609b1b0181565b610101610216366004610b4e565b4090565b436060828067ffffffffffffffff81111561023757610237610b65565b60405190808252806020026020018201604052801561026a57816020015b60608152602001906001900390816102555790505b5091505f5b81811015610385575f8060036003609b1b01631595ec0b338a8a8781811061029957610299610b79565b90506020028101906102ab9190610b8d565b6102b9906020810190610b0f565b8b8b888181106102cb576102cb610b79565b90506020028101906102dd9190610b8d565b6102eb906020810190610bab565b6040518563ffffffff1660e01b815260040161030a9493929190610bee565b5f604051808303815f875af1158015610325573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f1916820160405261034c9190810190610c36565b915091508161035d57805160208201fd5b8085848151811061037057610370610b79565b6020908102919091010152505060010161026f565b50509250929050565b5f5f606061039d86868661074a565b91989097509095509350505050565b6060818067ffffffffffffffff8111156103c8576103c8610b65565b60405190808252806020026020018201604052801561040d57816020015b604080518082019091525f8152606060208201528152602001906001900390816103e65790505b5091505f5b8181101561057e575f8060036003609b1b01631595ec0b3389898781811061043c5761043c610b79565b905060200281019061044e9190610cfd565b61045c906020810190610b0f565b8a8a8881811061046e5761046e610b79565b90506020028101906104809190610cfd565b61048e906040810190610bab565b6040518563ffffffff1660e01b81526004016104ad9493929190610bee565b5f604051808303815f875af11580156104c8573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526104ef9190810190610c36565b91509150604051806040016040528083151581526020018281525085848151811061051c5761051c610b79565b602002602001018190525086868481811061053957610539610b79565b905060200281019061054b9190610cfd565b61055c906040810190602001610d11565b158015610567575081155b1561057457805160208201fd5b5050600101610412565b505092915050565b6060818067ffffffffffffffff8111156105a2576105a2610b65565b6040519080825280602002602001820160405280156105e757816020015b604080518082019091525f8152606060208201528152602001906001900390816105c05790505b5091505f5b81811015610723575f8060036003609b1b01631595ec0b3389898781811061061657610616610b79565b90506020028101906106289190610b8d565b610636906020810190610b0f565b8a8a8881811061064857610648610b79565b905060200281019061065a9190610b8d565b610668906020810190610bab565b6040518563ffffffff1660e01b81526004016106879493929190610bee565b5f604051808303815f875af11580156106a2573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526106c99190810190610c36565b915091508780156106d8575081155b156106e557805160208201fd5b604051806040016040528083151581526020018281525085848151811061070e5761070e610b79565b602090810291909101015250506001016105ec565b50509392505050565b5f5f606061073c6001868661074a565b919790965090945092505050565b4380406060838067ffffffffffffffff81111561076957610769610b65565b6040519080825280602002602001820160405280156107ae57816020015b604080518082019091525f8152606060208201528152602001906001900390816107875790505b5091505f5b818110156108ea575f8060036003609b1b01631595ec0b338b8b878181106107dd576107dd610b79565b90506020028101906107ef9190610b8d565b6107fd906020810190610b0f565b8c8c8881811061080f5761080f610b79565b90506020028101906108219190610b8d565b61082f906020810190610bab565b6040518563ffffffff1660e01b815260040161084e9493929190610bee565b5f604051808303815f875af1158015610869573d5f5f3e3d5ffd5b505050506040513d5f823e601f3d908101601f191682016040526108909190810190610c36565b9150915089801561089f575081155b156108ac57805160208201fd5b60405180604001604052808315158152602001828152508584815181106108d5576108d5610b79565b602090810291909101015250506001016107b3565b505093509350939050565b5f5f83601f840112610905575f5ffd5b50813567ffffffffffffffff81111561091c575f5ffd5b6020830191508360208260051b8501011115610936575f5ffd5b9250929050565b5f5f6020838503121561094e575f5ffd5b823567ffffffffffffffff811115610964575f5ffd5b610970858286016108f5565b90969095509350505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b5f604082018483526040602084015280845180835260608501915060608160051b8601019250602086015f5b82811015610a0757605f198786030184526109f285835161097c565b945060209384019391909101906001016109d6565b5092979650505050505050565b8015158114610a21575f5ffd5b50565b5f5f5f60408486031215610a36575f5ffd5b8335610a4181610a14565b9250602084013567ffffffffffffffff811115610a5c575f5ffd5b610a68868287016108f5565b9497909650939450505050565b5f82825180855260208501945060208160051b830101602085015f5b83811015610adc57601f1985840301885281518051151584526020810151905060406020850152610ac5604085018261097c565b6020998a0199909450929092019150600101610a91565b50909695505050505050565b838152826020820152606060408201525f610b066060830184610a75565b95945050505050565b5f60208284031215610b1f575f5ffd5b81356001600160a01b0381168114610b35575f5ffd5b9392505050565b602081525f610b356020830184610a75565b5f60208284031215610b5e575f5ffd5b5035919050565b634e487b7160e01b5f52604160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b5f8235603e19833603018112610ba1575f5ffd5b9190910192915050565b5f5f8335601e19843603018112610bc0575f5ffd5b83018035915067ffffffffffffffff821115610bda575f5ffd5b602001915036819003821315610936575f5ffd5b6001600160a01b038581168252841660208201526060604082018190528101829052818360808301375f818301608090810191909152601f909201601f191601019392505050565b5f5f60408385031215610c47575f5ffd5b8251610c5281610a14565b602084015190925067ffffffffffffffff811115610c6e575f5ffd5b8301601f81018513610c7e575f5ffd5b805167ffffffffffffffff811115610c9857610c98610b65565b604051601f8201601f19908116603f0116810167ffffffffffffffff81118282101715610cc757610cc7610b65565b604052818152828201602001871015610cde575f5ffd5b8160208401602083015e5f602083830101528093505050509250929050565b5f8235605e19833603018112610ba1575f5ffd5b5f60208284031215610d21575f5ffd5b8135610b3581610a1456fea2646970667358221220a157cf3289aec06fad492010b882f74f25341bf2e0bc0fda1acf286411e1150564736f6c634300081d0033" }, - "0xe8e7F64D3d4eA1D5b9722A0769c3e7aC380b1423": { + "0x298122B4bF05CC897662e535C18417f44C7f274b": { "balance": "0x0", "nonce": "0x1", - "code": "0x608060405234801561000f575f5ffd5b5060043610610090575f3560e01c8063313ce56711610063578063313ce567146100fa57806370a082311461010957806395d89b4114610131578063a9059cbb14610139578063dd62ed3e1461014c575f5ffd5b806306fdde0314610094578063095ea7b3146100b257806318160ddd146100d557806323b872dd146100e7575b5f5ffd5b61009c610184565b6040516100a99190610554565b60405180910390f35b6100c56100c03660046105a4565b610214565b60405190151581526020016100a9565b6002545b6040519081526020016100a9565b6100c56100f53660046105cc565b61022d565b604051601281526020016100a9565b6100d9610117366004610606565b6001600160a01b03165f9081526020819052604090205490565b61009c610250565b6100c56101473660046105a4565b61025f565b6100d961015a366004610626565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b60606003805461019390610657565b80601f01602080910402602001604051908101604052809291908181526020018280546101bf90610657565b801561020a5780601f106101e15761010080835404028352916020019161020a565b820191905f5260205f20905b8154815290600101906020018083116101ed57829003601f168201915b5050505050905090565b5f3361022181858561026c565b60019150505b92915050565b5f3361023a85828561027e565b6102458585856102ff565b506001949350505050565b60606004805461019390610657565b5f336102218185856102ff565b610279838383600161035c565b505050565b6001600160a01b038381165f908152600160209081526040808320938616835292905220545f198110156102f957818110156102eb57604051637dc7a0d960e11b81526001600160a01b038416600482015260248101829052604481018390526064015b60405180910390fd5b6102f984848484035f61035c565b50505050565b6001600160a01b03831661032857604051634b637e8f60e11b81525f60048201526024016102e2565b6001600160a01b0382166103515760405163ec442f0560e01b81525f60048201526024016102e2565b61027983838361042e565b6001600160a01b0384166103855760405163e602df0560e01b81525f60048201526024016102e2565b6001600160a01b0383166103ae57604051634a1406b160e11b81525f60048201526024016102e2565b6001600160a01b038085165f90815260016020908152604080832093871683529290522082905580156102f957826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161042091815260200190565b60405180910390a350505050565b6001600160a01b038316610458578060025f82825461044d919061068f565b909155506104c89050565b6001600160a01b0383165f90815260208190526040902054818110156104aa5760405163391434e360e21b81526001600160a01b038516600482015260248101829052604481018390526064016102e2565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b0382166104e457600280548290039055610502565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161054791815260200190565b60405180910390a3505050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b80356001600160a01b038116811461059f575f5ffd5b919050565b5f5f604083850312156105b5575f5ffd5b6105be83610589565b946020939093013593505050565b5f5f5f606084860312156105de575f5ffd5b6105e784610589565b92506105f560208501610589565b929592945050506040919091013590565b5f60208284031215610616575f5ffd5b61061f82610589565b9392505050565b5f5f60408385031215610637575f5ffd5b61064083610589565b915061064e60208401610589565b90509250929050565b600181811c9082168061066b57607f821691505b60208210810361068957634e487b7160e01b5f52602260045260245ffd5b50919050565b8082018082111561022757634e487b7160e01b5f52601160045260245ffdfea2646970667358221220b95acbec508a63f34d153c18ae3bd218ee291bec47f681f7802fcb79840d004e64736f6c634300081d0033", + "code": "0x608060405234801561000f575f5ffd5b5060043610610090575f3560e01c8063313ce56711610063578063313ce567146100fa57806370a082311461010957806395d89b4114610131578063a9059cbb14610139578063dd62ed3e1461014c575f5ffd5b806306fdde0314610094578063095ea7b3146100b257806318160ddd146100d557806323b872dd146100e7575b5f5ffd5b61009c610184565b6040516100a99190610554565b60405180910390f35b6100c56100c03660046105a4565b610214565b60405190151581526020016100a9565b6002545b6040519081526020016100a9565b6100c56100f53660046105cc565b61022d565b604051601281526020016100a9565b6100d9610117366004610606565b6001600160a01b03165f9081526020819052604090205490565b61009c610250565b6100c56101473660046105a4565b61025f565b6100d961015a366004610626565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b60606003805461019390610657565b80601f01602080910402602001604051908101604052809291908181526020018280546101bf90610657565b801561020a5780601f106101e15761010080835404028352916020019161020a565b820191905f5260205f20905b8154815290600101906020018083116101ed57829003601f168201915b5050505050905090565b5f3361022181858561026c565b60019150505b92915050565b5f3361023a85828561027e565b6102458585856102ff565b506001949350505050565b60606004805461019390610657565b5f336102218185856102ff565b610279838383600161035c565b505050565b6001600160a01b038381165f908152600160209081526040808320938616835292905220545f198110156102f957818110156102eb57604051637dc7a0d960e11b81526001600160a01b038416600482015260248101829052604481018390526064015b60405180910390fd5b6102f984848484035f61035c565b50505050565b6001600160a01b03831661032857604051634b637e8f60e11b81525f60048201526024016102e2565b6001600160a01b0382166103515760405163ec442f0560e01b81525f60048201526024016102e2565b61027983838361042e565b6001600160a01b0384166103855760405163e602df0560e01b81525f60048201526024016102e2565b6001600160a01b0383166103ae57604051634a1406b160e11b81525f60048201526024016102e2565b6001600160a01b038085165f90815260016020908152604080832093871683529290522082905580156102f957826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161042091815260200190565b60405180910390a350505050565b6001600160a01b038316610458578060025f82825461044d919061068f565b909155506104c89050565b6001600160a01b0383165f90815260208190526040902054818110156104aa5760405163391434e360e21b81526001600160a01b038516600482015260248101829052604481018390526064016102e2565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b0382166104e457600280548290039055610502565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161054791815260200190565b60405180910390a3505050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b80356001600160a01b038116811461059f575f5ffd5b919050565b5f5f604083850312156105b5575f5ffd5b6105be83610589565b946020939093013593505050565b5f5f5f606084860312156105de575f5ffd5b6105e784610589565b92506105f560208501610589565b929592945050506040919091013590565b5f60208284031215610616575f5ffd5b61061f82610589565b9392505050565b5f5f60408385031215610637575f5ffd5b61064083610589565b915061064e60208401610589565b90509250929050565b600181811c9082168061066b57607f821691505b60208210810361068957634e487b7160e01b5f52602260045260245ffd5b50919050565b8082018082111561022757634e487b7160e01b5f52601160045260245ffdfea2646970667358221220ccdc2d87d4833be51b291c8c0fb8b7a70b9625f6a58ddecdd2aced25e83cf63764736f6c634300081d0033", "storage": { "0x0000000000000000000000000000000000000000000000000000000000000002": "0x0000000000000000000000000000000000000000000d3c21bcecceda10000000", "0x723077b8a1b173adc35e5f0e7e3662fd1208212cb629f9c128551ea7168da722": "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000", diff --git a/contracts/README.md b/contracts/README.md index 4ef880e..d158c99 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -2,6 +2,15 @@ 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 diff --git a/contracts/scripts/Addresses.sol b/contracts/scripts/Addresses.sol index 6928e08..1aeb72d 100644 --- a/contracts/scripts/Addresses.sol +++ b/contracts/scripts/Addresses.sol @@ -36,8 +36,8 @@ library Addresses { // ============ Predeployed Contracts ============ address internal constant DETERMINISTIC_DEPLOYER_PROXY = 0x4e59b44847b379578588920cA78FbF26c0B4956C; address internal constant MULTICALL3 = 0xcA11bde05977b3631167028862bE2a173976CA11; - address internal constant MULTICALL3_FROM = 0xEb7cc06E3D3b5F9F9a5fA2B31B477ff72bB9c8b6; - address internal constant MEMO = 0x9702466268ccF55eAB64cdf484d272Ac08d3b75b; + address internal constant MULTICALL3_FROM = 0x825F535677d346626cDE45D64cf89C2a426467e0; + address internal constant MEMO = 0xe4aa7Ed3585AEf598179f873086F75Fcd6D4b755; // ============ Helpers ============ diff --git a/contracts/scripts/ArtifactHelper.s.sol b/contracts/scripts/ArtifactHelper.s.sol index 4dc6192..a0644ab 100644 --- a/contracts/scripts/ArtifactHelper.s.sol +++ b/contracts/scripts/ArtifactHelper.s.sol @@ -16,6 +16,11 @@ 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"; @@ -154,11 +159,12 @@ contract ArtifactHelper is Script { } 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, "protocol-config/ProtocolConfig.sol/ProtocolConfig.json"), - ".bytecode" + string.concat(arcNetworkContractDir, "ProtocolConfig.sol/ProtocolConfig.json"), + ".bytecode.object" ) ); vm.serializeString("output", "ProtocolConfig", getJsonContractCode(address(protocolConfig))); @@ -167,7 +173,7 @@ contract ArtifactHelper is Script { address denylist = deployDeterministicContract( loadBytecode( string.concat(arcNetworkContractDir, "Denylist.sol/Denylist.json"), - ".bytecode" + ".bytecode.object" ) ); vm.serializeString("output", "Denylist", getJsonContractCode(address(denylist))); @@ -175,8 +181,8 @@ contract ArtifactHelper is Script { // ValidatorRegistry address validatorRegistry = deployDeterministicContract( loadBytecode( - string.concat(arcNetworkContractDir, "validator-manager/ValidatorRegistry.sol/ValidatorRegistry.json"), - ".bytecode" + string.concat(arcNetworkContractDir, "ValidatorRegistry.sol/ValidatorRegistry.json"), + ".bytecode.object" ) ); vm.serializeString("output", "ValidatorRegistry", getJsonContractCode(address(validatorRegistry))); @@ -185,8 +191,8 @@ contract ArtifactHelper is Script { address proxy = deployDeterministicContract( bytes.concat( loadBytecode( - string.concat(arcNetworkContractDir, "proxy/AdminUpgradeableProxy.sol/AdminUpgradeableProxy.json"), - ".bytecode" + string.concat(arcNetworkContractDir, "AdminUpgradeableProxy.sol/AdminUpgradeableProxy.json"), + ".bytecode.object" ), abi.encode(address(validatorRegistry), address(0x0000000000000000000000000000000000000001), hex"") ) @@ -197,8 +203,8 @@ contract ArtifactHelper is Script { address poaManager = deployDeterministicContract( bytes.concat( loadBytecode( - string.concat(arcNetworkContractDir, "validator-manager/PermissionedValidatorManager.sol/PermissionedValidatorManager.json"), - ".bytecode" + string.concat(arcNetworkContractDir, "PermissionedValidatorManager.sol/PermissionedValidatorManager.json"), + ".bytecode.object" ), abi.encode(address(validatorRegistryProxyAddr)) ) @@ -208,8 +214,8 @@ contract ArtifactHelper is Script { // GasGuzzler address gasGuzzler = deployDeterministicContract( loadBytecode( - string.concat(arcNetworkContractDir, "mocks/GasGuzzler.sol/GasGuzzler.json"), - ".bytecode" + string.concat(arcNetworkContractDir, "GasGuzzler.sol/GasGuzzler.json"), + ".bytecode.object" ) ); vm.serializeString("output", "GasGuzzler", getJsonContractCode(address(gasGuzzler))); @@ -217,8 +223,8 @@ contract ArtifactHelper is Script { // TestToken (ERC-20 for spammer load testing) address testToken = deployDeterministicContract( loadBytecode( - string.concat(arcNetworkContractDir, "mocks/TestToken.sol/TestToken.json"), - ".bytecode" + string.concat(arcNetworkContractDir, "TestToken.sol/TestToken.json"), + ".bytecode.object" ) ); vm.serializeString("output", "TestToken", getJsonContractCode(address(testToken))); @@ -226,8 +232,8 @@ contract ArtifactHelper is Script { // Memo address memo = deployDeterministicContract( loadBytecode( - string.concat(arcNetworkContractDir, "memo/Memo.sol/Memo.json"), - ".bytecode" + string.concat(arcNetworkContractDir, "Memo.sol/Memo.json"), + ".bytecode.object" ) ); vm.serializeString("output", "Memo", getJsonContractCode(address(memo))); @@ -235,8 +241,8 @@ contract ArtifactHelper is Script { // Multicall3From address multicall3From = deployDeterministicContract( loadBytecode( - string.concat(arcNetworkContractDir, "batch/Multicall3From.sol/Multicall3From.json"), - ".bytecode" + string.concat(arcNetworkContractDir, "Multicall3From.sol/Multicall3From.json"), + ".bytecode.object" ) ); return vm.serializeString("output", "Multicall3From", getJsonContractCode(address(multicall3From))); @@ -302,7 +308,7 @@ contract ArtifactHelper is Script { vm.serializeString("output", "FiatTokenProxy", getJsonContractCode(fiatTokenProxyAddr)); // Deploy ArcNetwork contracts (extracted to avoid stack too deep) - string memory output = deployArcNetworkContracts("contracts/out/hardhat/contracts/src/", validatorRegistryProxyAddr); + string memory output = deployArcNetworkContracts("contracts/out/forge/", validatorRegistryProxyAddr); vm.writeJson(output, outputPath); } } diff --git a/contracts/src/Precompiles.sol b/contracts/src/Precompiles.sol new file mode 100644 index 0000000..496bb04 --- /dev/null +++ b/contracts/src/Precompiles.sol @@ -0,0 +1,27 @@ +// 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/Multicall3From.sol b/contracts/src/batch/Multicall3From.sol index 5a23de2..253c874 100644 --- a/contracts/src/batch/Multicall3From.sol +++ b/contracts/src/batch/Multicall3From.sol @@ -17,8 +17,8 @@ pragma solidity ^0.8.29; import {ICallFrom} from "../call-from/ICallFrom.sol"; +import {Precompiles} from "../Precompiles.sol"; import {IMulticall3From} from "./IMulticall3From.sol"; -import {Addresses} from "../../scripts/Addresses.sol"; /// @title Multicall3From /// @notice Sender-preserving batch-call contract that mirrors the original @@ -29,7 +29,7 @@ import {Addresses} from "../../scripts/Addresses.sol"; /// Reentrancy is safe: the contract holds no state that could be /// corrupted by a reentrant call. contract Multicall3From is IMulticall3From { - ICallFrom public constant CALL_FROM = ICallFrom(Addresses.CALL_FROM); + ICallFrom public constant CALL_FROM = ICallFrom(Precompiles.CALL_FROM); /// @inheritdoc IMulticall3From function aggregate(Call[] calldata calls) diff --git a/contracts/src/memo/Memo.sol b/contracts/src/memo/Memo.sol index f22e5cd..e9f6abe 100644 --- a/contracts/src/memo/Memo.sol +++ b/contracts/src/memo/Memo.sol @@ -17,8 +17,8 @@ pragma solidity ^0.8.29; import {ICallFrom} from "../call-from/ICallFrom.sol"; +import {Precompiles} from "../Precompiles.sol"; import {IMemo} from "./IMemo.sol"; -import {Addresses} from "../../scripts/Addresses.sol"; /** * @title Memo @@ -31,7 +31,7 @@ contract Memo is IMemo { uint256 public memoIndex; /// @notice The callFrom precompile used to forward subcalls with caller preservation. - ICallFrom public constant CALL_FROM = ICallFrom(Addresses.CALL_FROM); + ICallFrom public constant CALL_FROM = ICallFrom(Precompiles.CALL_FROM); /// @inheritdoc IMemo function memo(address target, bytes calldata data, bytes32 memoId, bytes calldata memoData) external { diff --git a/contracts/src/mocks/PrecompileCallCode.sol b/contracts/src/mocks/PrecompileCallCode.sol index e6ceec1..ebf3975 100644 --- a/contracts/src/mocks/PrecompileCallCode.sol +++ b/contracts/src/mocks/PrecompileCallCode.sol @@ -16,7 +16,7 @@ pragma solidity ^0.8.29; -import {Addresses} from "../../scripts/Addresses.sol"; +import {Precompiles} from "../Precompiles.sol"; /** * @title PrecompileCallCode @@ -40,7 +40,7 @@ contract PrecompileCallCode { bool success; // Get the precompile address - address target = Addresses.NATIVE_COIN_AUTHORITY; + address target = Precompiles.NATIVE_COIN_AUTHORITY; // Use assembly to perform CALLCODE and bubble up errors assembly { diff --git a/contracts/src/mocks/PrecompileDelegater.sol b/contracts/src/mocks/PrecompileDelegater.sol index 4668ae7..3eac3a9 100644 --- a/contracts/src/mocks/PrecompileDelegater.sol +++ b/contracts/src/mocks/PrecompileDelegater.sol @@ -16,7 +16,7 @@ pragma solidity ^0.8.29; -import {Addresses} from "../../scripts/Addresses.sol"; +import {Precompiles} from "../Precompiles.sol"; /** * @title PrecompileDelegater @@ -36,7 +36,7 @@ contract PrecompileDelegater { 6660000000000000000 // 6.66 token (18 decimals) ); - (bool success, bytes memory returnData) = Addresses.NATIVE_COIN_AUTHORITY.delegatecall(mintCall); + (bool success, bytes memory returnData) = Precompiles.NATIVE_COIN_AUTHORITY.delegatecall(mintCall); // Bubble up the error if call failed if (!success) { diff --git a/crates/eth-engine/src/persistence_meter.rs b/crates/eth-engine/src/persistence_meter.rs index 445e10d..eb93cf3 100644 --- a/crates/eth-engine/src/persistence_meter.rs +++ b/crates/eth-engine/src/persistence_meter.rs @@ -56,8 +56,8 @@ const MAX_RECONNECT_BACKOFF: Duration = Duration::from_secs(60); #[cfg_attr(any(test, feature = "mocks"), mockall::automock)] #[async_trait] pub trait PersistenceMeter: Send + Sync { - /// Block until `block_number` (minus the configured threshold) has been - /// persisted by the execution layer. Returns immediately if already satisfied. + /// 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, @@ -122,9 +122,9 @@ struct SharedState { /// /// 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 target -/// block is already persisted, and multiple callers can wait concurrently -/// without contending on a lock. +/// 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`] @@ -137,7 +137,8 @@ struct SharedState { /// counter but backpressure remains suspended until the subscription is live. pub struct PersistedBlockMeter { shared: Arc, - /// Number of blocks the EL may lag behind before backpressure applies. + /// 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<()>, @@ -235,7 +236,6 @@ impl PersistenceMeter for PersistedBlockMeter { block_number: u64, timeout: Duration, ) -> eyre::Result<()> { - let target = block_number.saturating_sub(self.persistence_backpressure_threshold); let started_at = Instant::now(); loop { @@ -254,8 +254,9 @@ impl PersistenceMeter for PersistedBlockMeter { } let current = self.shared.last_persisted_block.load(Ordering::Acquire); - // If we're within the configured threshold, proceed - if current >= target { + 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, @@ -551,7 +552,7 @@ mod tests { #[tokio::test] async fn create_with_fallback_suspends_backpressure_on_connection_failure() { let endpoint = SubscriptionEndpoint::Ws { - url: Url::parse("ws://192.0.2.1:1").unwrap(), // unreachable + 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 @@ -563,7 +564,7 @@ mod tests { #[tokio::test] async fn create_retries_on_unreachable_endpoint() { let endpoint = SubscriptionEndpoint::Ws { - url: Url::parse("ws://192.0.2.1:1").unwrap(), + url: Url::parse("ws://127.0.0.1:1").unwrap(), }; let meter = create(&endpoint, 5).await; // Initial connection failed β€” backpressure suspended (not ACTIVE) @@ -581,7 +582,7 @@ mod tests { .last_persisted_block .store(100, Ordering::Release); - // block_number=10, threshold=5, target=5 β€” persisted=100 >= 5 + // gap=0 (saturating) < threshold=5 assert!(meter .wait_for_persisted_block(10, TEST_TIMEOUT) .await @@ -596,13 +597,26 @@ mod tests { .last_persisted_block .store(0, Ordering::Release); - // block_number=5, threshold=10, target=5.saturating_sub(10)=0 β€” persisted=0 >= 0 + // 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); @@ -689,7 +703,7 @@ mod tests { #[tokio::test] async fn wait_wakes_up_on_notify() { - let meter = PersistedBlockMeter::test_instance(0); + let meter = PersistedBlockMeter::test_instance(2); meter .shared .last_persisted_block @@ -703,7 +717,7 @@ mod tests { }); assert!(meter - .wait_for_persisted_block(50, TEST_TIMEOUT) + .wait_for_persisted_block(51, TEST_TIMEOUT) .await .is_ok()); } diff --git a/crates/eth-engine/tests/integration.rs b/crates/eth-engine/tests/integration.rs index faa908e..a20b32e 100644 --- a/crates/eth-engine/tests/integration.rs +++ b/crates/eth-engine/tests/integration.rs @@ -103,12 +103,9 @@ async fn test_engine_common(engine: &Engine, initial_block_number: &str) { ); // Test generate new block + let fee_recipient = Address::repeat_byte(0xBE); let block = engine - .generate_block( - &block.unwrap(), - Engine::timestamp_now() + 1, - &Address::default(), - ) + .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); @@ -162,6 +159,7 @@ async fn test_engine() { None, true, true, + std::time::Duration::from_secs(0), // disable rebroadcast in integration tests ); let node_handle = NodeBuilder::new(node_config) .testing_node(executor) diff --git a/crates/evm-node/src/lib.rs b/crates/evm-node/src/lib.rs index 3b2db7f..70c0e11 100644 --- a/crates/evm-node/src/lib.rs +++ b/crates/evm-node/src/lib.rs @@ -22,6 +22,7 @@ pub mod engine; pub mod node; pub mod payload; +pub mod rebroadcast; pub mod rpc; pub mod rpc_middleware; diff --git a/crates/evm-node/src/node.rs b/crates/evm-node/src/node.rs index cc3da1b..6c10079 100644 --- a/crates/evm-node/src/node.rs +++ b/crates/evm-node/src/node.rs @@ -69,7 +69,7 @@ use reth_rpc_eth_api::{ }; use reth_rpc_eth_types::{error::FromEvmError, EthApiError}; use reth_rpc_server_types::RethRpcModule; -use reth_tracing::tracing::info; +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}; @@ -100,6 +100,8 @@ pub struct ArcNode { /// When true (default), pending-tx RPCs are restricted (`eth_subscribe("newPendingTransactions")`, `eth_newPendingTransactionFilter`). /// When false, all requests are forwarded. pub filter_pending_txs: bool, + /// Interval between tx rebroadcast rounds. Zero disables rebroadcast. + pub rebroadcast_interval: std::time::Duration, } impl Default for ArcNode { @@ -111,6 +113,7 @@ impl Default for ArcNode { payload_builder_deadline_ms: None, wait_for_payload: true, filter_pending_txs: true, + rebroadcast_interval: crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL, } } } @@ -124,6 +127,7 @@ impl ArcNode { payload_builder_deadline_ms: Option, wait_for_payload: bool, filter_pending_txs: bool, + rebroadcast_interval: std::time::Duration, ) -> Self { Self { rpc_cfg, @@ -132,6 +136,7 @@ impl ArcNode { payload_builder_deadline_ms, wait_for_payload, filter_pending_txs, + rebroadcast_interval, } } @@ -141,6 +146,7 @@ impl ArcNode { addresses_denylist_config: &AddressesDenylistConfig, payload_builder_deadline_ms: Option, wait_for_payload: bool, + rebroadcast_interval: std::time::Duration, ) -> ComponentsBuilder< Node, ArcPoolBuilder, @@ -181,7 +187,7 @@ impl ArcNode { )) .executor(ArcExecutorBuilder::default()) .payload(BasicPayloadServiceBuilder::new(pb_builder)) - .network(ArcNetworkBuilder::default()) + .network(ArcNetworkBuilder::default().with_rebroadcast_interval(rebroadcast_interval)) .consensus(ArcConsensusBuilder::default()) } @@ -496,6 +502,7 @@ where &self.addresses_denylist_config, self.payload_builder_deadline_ms, self.wait_for_payload, + self.rebroadcast_interval, ) } @@ -541,10 +548,25 @@ where } } -/// A basic Arc payload service. -#[derive(Debug, Default, Clone, Copy)] +/// Arc network builder with optional tx rebroadcast. +#[derive(Debug, Clone, Copy)] pub struct ArcNetworkBuilder { - // TODO add closure to modify network + 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 @@ -563,8 +585,36 @@ where 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) } } @@ -648,6 +698,7 @@ mod tests { None, true, true, + crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL, ); assert!(!node.rpc_cfg.enabled); @@ -675,6 +726,7 @@ mod tests { None, true, true, + crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL, ); assert!(node.addresses_denylist_config.is_enabled()); if let AddressesDenylistConfig::Enabled { @@ -708,6 +760,7 @@ mod tests { None, true, false, + crate::rebroadcast::DEFAULT_REBROADCAST_INTERVAL, ); assert!(!node.filter_pending_txs); } @@ -730,7 +783,47 @@ mod tests { None, false, false, + 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, + 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/rebroadcast.rs b/crates/evm-node/src/rebroadcast.rs new file mode 100644 index 0000000..4552b35 --- /dev/null +++ b/crates/evm-node/src/rebroadcast.rs @@ -0,0 +1,211 @@ +// 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(26_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/src/evm.rs b/crates/evm/src/evm.rs index 4cd4ca2..49e0463 100644 --- a/crates/evm/src/evm.rs +++ b/crates/evm/src/evm.rs @@ -90,9 +90,7 @@ use crate::handler::ArcEvmHandler; use crate::opcode::{arc_network_selfdestruct, arc_network_selfdestruct_zero4}; use crate::subcall::{SubcallContinuation, SubcallRegistry}; use arc_execution_config::chainspec::{ArcChainSpec, BlockGasLimitProvider}; -use arc_execution_config::protocol_config::{ - expected_gas_limit, retrieve_fee_params, retrieve_reward_beneficiary, ProtocolConfigError, -}; +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; @@ -1666,23 +1664,6 @@ impl BlockExecutorFactory for ArcEvmConfig { } } -/// Pick the fee recipient for the next block, given the ProtocolConfig lookup result and the -/// CL-provided recipient. The contract value wins when explicitly set; zero or lookup failure -/// defers to the CL. -fn select_fee_recipient( - protocol_config_beneficiary: Result>, - cl_suggested_recipient: Address, -) -> Address { - match protocol_config_beneficiary { - Ok(addr) if !addr.is_zero() => addr, - Ok(_) => cl_suggested_recipient, - Err(err) => { - tracing::warn!(error = %err, "ProtocolConfig rewardBeneficiary() failed; using CL-provided fee recipient"); - cl_suggested_recipient - } - } -} - impl ConfigureEvm for ArcEvmConfig { type Primitives = ::Primitives; type Error = ::Error; @@ -1727,11 +1708,6 @@ impl ConfigureEvm for ArcEvmConfig { })?, ); - attributes.suggested_fee_recipient = select_fee_recipient( - retrieve_reward_beneficiary(&mut system_evm), - attributes.suggested_fee_recipient, - ); - // 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. @@ -2094,9 +2070,6 @@ mod tests { ); let evm_config = ArcEvmConfig::new(inner_config); let mut db = State::builder().build(); - - // ProtocolConfig contract is absent from `db`, so `retrieve_reward_beneficiary` errors - // and `select_fee_recipient` falls back to `attributes.suggested_fee_recipient`. let parent_header = Header { number: 1, gas_limit: 30_000_000, @@ -2124,124 +2097,6 @@ mod tests { ); } - #[test] - fn test_retrieve_reward_beneficiary_nonzero_address() { - use arc_execution_config::protocol_config::{ - retrieve_reward_beneficiary, PROTOCOL_CONFIG_ADDRESS, - }; - use revm::bytecode::opcode; - use revm::state::AccountInfo; - - let expected = Address::repeat_byte(0xAB); - - // Mock contract at PROTOCOL_CONFIG_ADDRESS that returns a non-zero address, - // exercising the `Ok(addr) if !addr.is_zero() => addr` branch. - // rewardBeneficiary() returns (address) ABI-encodes as [12 zero bytes | 20 addr bytes]; - // MSTORE right-aligns the stack value to produce exactly that. - // - // Equivalent assembly: - // ```text - // PUSH20 ; push address as uint256 (zero-fills upper 12 bytes on stack) - // PUSH1 0 ; memory offset for MSTORE - // MSTORE ; memory[0..32] = [12 zero bytes][20 addr bytes] - // PUSH1 0x20 ; return size = 32 - // PUSH1 0 ; return offset - // RETURN - // ``` - let mut code = vec![opcode::PUSH20]; - code.extend_from_slice(expected.as_slice()); - code.extend_from_slice(&[opcode::PUSH1, 0x00, opcode::MSTORE]); - code.extend_from_slice(&[opcode::PUSH1, 0x20, opcode::PUSH1, 0x00, opcode::RETURN]); - let raw = Bytes::from(code); - - let mut db = InMemoryDB::default(); - db.insert_account_info( - PROTOCOL_CONFIG_ADDRESS, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(&raw), - code: Some(Bytecode::new_raw(raw)), - account_id: None, - }, - ); - - let mut evm = create_test_evm(db, ArcHardforkFlags::default()); - assert_eq!(retrieve_reward_beneficiary(&mut evm).unwrap(), expected); - } - - #[test] - fn test_retrieve_reward_beneficiary_zero_address() { - use arc_execution_config::protocol_config::{ - retrieve_reward_beneficiary, PROTOCOL_CONFIG_ADDRESS, - }; - use revm::bytecode::opcode; - use revm::state::AccountInfo; - - // Mock contract returning address(0): rewardBeneficiary() ABI-encodes as 32 zero - // bytes, which EVM memory provides by default β€” no MSTORE needed. - // - // Equivalent assembly: - // ```text - // PUSH1 0x20 ; return size = 32 - // PUSH1 0 ; return offset (memory[0..32] is all zeros by default) - // RETURN ; returns 32 zero bytes = abi.encode(address(0)) - // ``` - let raw = Bytes::from(vec![ - opcode::PUSH1, - 0x20, - opcode::PUSH1, - 0x00, - opcode::RETURN, - ]); - - let mut db = InMemoryDB::default(); - db.insert_account_info( - PROTOCOL_CONFIG_ADDRESS, - AccountInfo { - balance: U256::ZERO, - nonce: 1, - code_hash: keccak256(&raw), - code: Some(Bytecode::new_raw(raw)), - account_id: None, - }, - ); - - let mut evm = create_test_evm(db, ArcHardforkFlags::default()); - assert_eq!( - retrieve_reward_beneficiary(&mut evm).unwrap(), - Address::ZERO - ); - } - - #[test] - fn select_fee_recipient_uses_protocol_config_when_nonzero() { - let pc_beneficiary = Address::repeat_byte(0x11); - let cl_suggested = Address::repeat_byte(0x22); - assert_eq!( - select_fee_recipient(Ok(pc_beneficiary), cl_suggested), - pc_beneficiary, - ); - } - - #[test] - fn select_fee_recipient_falls_back_to_cl_suggested_when_protocol_config_returns_zero() { - let cl_suggested = Address::repeat_byte(0x22); - assert_eq!( - select_fee_recipient(Ok(Address::ZERO), cl_suggested), - cl_suggested, - ); - } - - #[test] - fn select_fee_recipient_falls_back_to_cl_suggested_on_error() { - let cl_suggested = Address::repeat_byte(0x22); - assert_eq!( - select_fee_recipient(Err(ProtocolConfigError::EmptyOutput), cl_suggested), - cl_suggested, - ); - } - /// Under Zero5, Arc self-emits EIP-7708 Transfer logs for CALL/CREATE value transfers. #[test] fn test_transact_one_eip7708_log_under_zero5() { diff --git a/crates/evm/src/executor.rs b/crates/evm/src/executor.rs index d33186c..99181ea 100644 --- a/crates/evm/src/executor.rs +++ b/crates/evm/src/executor.rs @@ -63,8 +63,6 @@ use reth_evm::block::StateChangePostBlockSource; use revm::DatabaseCommit; const ERR_BLOCKLIST_READ_FAILED: &str = "Failed to read beneficiary blocklist status"; -const ERR_BENEFICIARY_MISMATCH: &str = - "Block beneficiary does not match ProtocolConfig.rewardBeneficiary()"; const ERR_BLOCK_NUMBER_CONVERSION_FAILED: &str = "Failed to convert block number to u64"; /// Result of executing an Arc transaction. @@ -143,25 +141,6 @@ where } } -fn validate_expected_beneficiary( - header_beneficiary: Address, - expected_beneficiary: Address, - block_number: u64, -) -> Result<(), BlockExecutionError> { - if expected_beneficiary.is_zero() || header_beneficiary == expected_beneficiary { - return Ok(()); - } - - tracing::warn!( - header_beneficiary = %header_beneficiary, - expected_beneficiary = %expected_beneficiary, - block_number = block_number, - "{}", - ERR_BENEFICIARY_MISMATCH - ); - Err(BlockValidationError::msg(ERR_BENEFICIARY_MISMATCH).into()) -} - fn validate_beneficiary_not_blocklisted( db: &mut DB, header_beneficiary: Address, @@ -361,8 +340,7 @@ where // Spurious Dragon hardfork is enabled self.evm.db_mut().set_state_clear_flag(true); - // For block voter to validate block beneficiary matches ProtocolConfig.rewardBeneficiary() - // Only validate after Zero5 hardfork when this requirement is active + // Zero5+ pre-execution checks: beneficiary blocklist, gas limit validation let block_number = self.block_number_u64()?; if self @@ -374,29 +352,8 @@ where self.system_caller .apply_blockhashes_contract_call(self.ctx.parent_hash, &mut self.evm)?; - let header_beneficiary = self.evm.block().beneficiary(); - if let Ok(expected_beneficiary) = protocol_config::retrieve_reward_beneficiary(&mut self.evm) - .inspect_err(|error| { - // Log and continue to avoid halting the chain (e.g. if ProtocolConfig is deprecated). - tracing::warn!( - error = %error, - block_number = block_number, - "Failed to retrieve reward beneficiary from ProtocolConfig, continuing without validation" - ); - }) - { - validate_expected_beneficiary( - header_beneficiary, - expected_beneficiary, - block_number, - )?; - } - - validate_beneficiary_not_blocklisted( - self.evm.db_mut(), - header_beneficiary, - block_number, - )?; + 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(); @@ -667,52 +624,6 @@ mod tests { )) } - /// Helper function to query the expected beneficiary from ProtocolConfig - fn query_expected_beneficiary( - chain_spec: alloc::sync::Arc, - db: &mut InMemoryDB, - block_number: u64, - ) -> Address { - let evm_config = create_evm_config(chain_spec.clone()); - - let mut temp_block_env = get_mock_block_env(); - temp_block_env.number = U256::from(block_number); - let temp_cfg_env = CfgEnv::new() - .with_chain_id(chain_spec.chain_id()) - .with_spec_and_mainnet_gas_params(SpecId::PRAGUE); - let temp_evm_env = EvmEnv { - cfg_env: temp_cfg_env, - block_env: temp_block_env, - }; - let mut temp_state = State::builder().with_database(db).build(); - let mut temp_evm = evm_config.evm_with_env(&mut temp_state, temp_evm_env); - - protocol_config::retrieve_reward_beneficiary(&mut temp_evm) - .expect("Should be able to query reward beneficiary") - } - - /// Patch ProtocolConfig storage so rewardBeneficiary() returns the given address. - fn set_reward_beneficiary(db: &mut InMemoryDB, beneficiary: Address) { - // ProtocolConfig ERC-7201 base slot from the contract (see ProtocolConfig.sol). - const PROTOCOL_CONFIG_STORAGE_LOCATION_HEX: &str = - "668f09ce856848ead6cb1ddee963f15ef833cea8958030868f867aec84385200"; - let base_slot = U256::from_str_radix(PROTOCOL_CONFIG_STORAGE_LOCATION_HEX, 16).unwrap(); - // rewardBeneficiary is at base slot + 4. - let reward_beneficiary_slot = base_slot.saturating_add(U256::from(4)); - - db.insert_account_storage( - protocol_config::PROTOCOL_CONFIG_ADDRESS, - reward_beneficiary_slot, - StorageValue::from(U256::from_be_slice(beneficiary.as_slice())), - ) - .expect("Insert storage"); - } - - /// Patch ProtocolConfig storage so rewardBeneficiary() returns 0x00. - fn set_beneficiary_to_zero_address(db: &mut InMemoryDB) { - set_reward_beneficiary(db, Address::ZERO); - } - fn mark_address_as_blocklisted(db: &mut InMemoryDB, beneficiary: Address) { let storage_slot = compute_is_blocklisted_storage_slot(beneficiary).into(); db.insert_account_storage( @@ -1077,152 +988,6 @@ mod tests { ); } - #[test] - fn test_beneficiary_validation_succeeds_when_matching() { - // Test that beneficiary validation passes when header beneficiary matches ProtocolConfig - let chain_spec = LOCAL_DEV.clone(); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - - let block_number = 10; - - // Query the expected beneficiary from ProtocolConfig - let expected_beneficiary = - query_expected_beneficiary(chain_spec.clone(), &mut db, block_number); - - // Create executor with correct beneficiary - let evm_config = create_evm_config(chain_spec.clone()); - - let mut block_env = get_mock_block_env(); - block_env.number = U256::from(block_number); - block_env.beneficiary = expected_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 beneficiary matches - let result = executor.apply_pre_execution_changes(); - assert!( - result.is_ok(), - "Beneficiary validation should succeed when beneficiary matches" - ); - } - - #[test] - fn test_beneficiary_validation_fails_when_mismatched() { - // Test that beneficiary validation fails when header beneficiary doesn't match ProtocolConfig - let chain_spec = LOCAL_DEV.clone(); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - - // Set a non-zero expected beneficiary so the mismatch check is active. - let expected_beneficiary = address!("0000000000000000000000000000000000001234"); - set_reward_beneficiary(&mut db, expected_beneficiary); - - let evm_config = create_evm_config(chain_spec.clone()); - - // Use a wrong beneficiary address (different from expected_beneficiary above) - let wrong_beneficiary = address!("0000000000000000000000000000000000000bad"); - - let mut block_env = get_mock_block_env(); - block_env.number = U256::from(10); - 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 fail because beneficiary doesn't match - let result = executor.apply_pre_execution_changes(); - assert!( - result.is_err(), - "Beneficiary validation should fail when beneficiary doesn't match" - ); - - // Verify the error is returned as a validation error. - let err = result.unwrap_err(); - match err { - BlockExecutionError::Validation(validation_err) => { - let err_msg = validation_err.to_string(); - assert!( - err_msg.contains(ERR_BENEFICIARY_MISMATCH), - "Expected validation error containing '{}', got: {}", - ERR_BENEFICIARY_MISMATCH, - err_msg - ); - } - other => panic!( - "Expected BlockExecutionError::Validation containing '{}', got {:?}", - ERR_BENEFICIARY_MISMATCH, other - ), - } - } - - #[test] - fn test_validate_expected_beneficiary_allows_zero_expected() { - let header_beneficiary = address!("0000000000000000000000000000000000000bad"); - let expected_beneficiary = Address::ZERO; - - let result = validate_expected_beneficiary(header_beneficiary, expected_beneficiary, 10); - assert!( - result.is_ok(), - "Expected zero beneficiary should allow proposer-selected header beneficiary" - ); - } - - #[test] - fn test_validate_expected_beneficiary_rejects_mismatch() { - let header_beneficiary = address!("0000000000000000000000000000000000000bad"); - let expected_beneficiary = address!("0000000000000000000000000000000000000bee"); - - let err = validate_expected_beneficiary(header_beneficiary, expected_beneficiary, 10) - .expect_err("Mismatched beneficiary should fail"); - match err { - BlockExecutionError::Validation(validation_err) => { - let err_msg = validation_err.to_string(); - assert!( - err_msg.contains(ERR_BENEFICIARY_MISMATCH), - "Expected validation error containing '{}', got: {}", - ERR_BENEFICIARY_MISMATCH, - err_msg - ); - } - other => panic!( - "Expected BlockExecutionError::Validation containing '{}', got {:?}", - ERR_BENEFICIARY_MISMATCH, other - ), - } - } - #[test] fn test_validate_beneficiary_not_blocklisted_rejects_blocklisted_address() { let mut db = InMemoryDB::default(); @@ -1287,121 +1052,15 @@ mod tests { ); } - #[test] - fn test_beneficiary_validation_continues_when_protocol_config_unavailable() { - // Break ProtocolConfig so rewardBeneficiary() cannot be read - // Should log error and continue (defensive: avoid halting chain) - fn patch_protocol_config_to_invalid_impl(db: &mut InMemoryDB) { - use reth_ethereum::evm::revm::primitives::{StorageKey, StorageValue}; - - db.replace_account_storage( - protocol_config::PROTOCOL_CONFIG_ADDRESS, - HashMap::from_iter([( - StorageKey::from_str_radix( - "360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - 16, - ) - .unwrap(), - StorageValue::from(0u64), - )]), - ) - .expect("Replace storage"); - } - - let chain_spec = LOCAL_DEV.clone(); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - patch_protocol_config_to_invalid_impl(&mut db); - - let evm_config = create_evm_config(chain_spec.clone()); - - // Any beneficiary; should continue despite ProtocolConfig call failure - let mut block_env = get_mock_block_env(); - block_env.number = U256::from(10); - block_env.beneficiary = address!("0000000000000000000000000000000000000bad"); - - 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(); - assert!( - result.is_ok(), - "Should continue when ProtocolConfig call fails (defensive: avoid chain halt)" - ); - } - - #[test] - fn test_beneficiary_validation_skipped_when_zero_address() { - // Test that beneficiary validation is skipped when ProtocolConfig returns 0x00 - // This allows for future proposer-set addresses - let chain_spec = LOCAL_DEV.clone(); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - set_beneficiary_to_zero_address(&mut db); - - let evm_config = create_evm_config(chain_spec.clone()); - - // Use any beneficiary - should pass because ProtocolConfig returns 0x00 - let any_beneficiary = address!("0000000000000000000000000000000000000bad"); - - let mut block_env = get_mock_block_env(); - block_env.number = U256::from(10); - block_env.beneficiary = any_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 for 0x00 - let result = executor.apply_pre_execution_changes(); - assert!( - result.is_ok(), - "Beneficiary validation should be skipped when ProtocolConfig returns 0x00" - ); - } - #[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()); - set_beneficiary_to_zero_address(&mut db); let blocklisted_beneficiary = address!("0000000000000000000000000000000000000bad"); mark_address_as_blocklisted(&mut db, blocklisted_beneficiary); - let expected_beneficiary = query_expected_beneficiary(chain_spec.clone(), &mut db, 10); - assert!( - expected_beneficiary.is_zero(), - "ProtocolConfig.rewardBeneficiary() should be patched to zero for proposer-selected path" - ); let storage_slot = compute_is_blocklisted_storage_slot(blocklisted_beneficiary).into(); let blocklist_status = ::storage( &mut db, @@ -1454,62 +1113,6 @@ mod tests { } } - #[test] - fn test_beneficiary_validation_fails_when_matching_but_blocklisted() { - // Beneficiary matches ProtocolConfig.rewardBeneficiary() (non-zero) but is blocklisted. - // The blocklist check is unconditional β€” even a correctly-set beneficiary must not be blocklisted. - let chain_spec = LOCAL_DEV.clone(); - - let mut db = InMemoryDB::default(); - insert_alloc_into_db(&mut db, chain_spec.genesis()); - - // Set a non-zero expected beneficiary so the mismatch check is active, then blocklist it. - let expected_beneficiary = address!("0000000000000000000000000000000000001234"); - set_reward_beneficiary(&mut db, expected_beneficiary); - - let block_number = 10; - - mark_address_as_blocklisted(&mut db, expected_beneficiary); - - let evm_config = create_evm_config(chain_spec.clone()); - - let mut block_env = get_mock_block_env(); - block_env.number = U256::from(block_number); - block_env.beneficiary = expected_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; @@ -1563,7 +1166,6 @@ mod tests { let mut base_db = InMemoryDB::default(); insert_alloc_into_db(&mut base_db, chain_spec.genesis()); - set_beneficiary_to_zero_address(&mut base_db); let db = BlocklistReadFailingDb::new(base_db); let evm_config = create_evm_config(chain_spec.clone()); diff --git a/crates/execution-config/src/addresses_denylist.rs b/crates/execution-config/src/addresses_denylist.rs index 72c2894..dc63df2 100644 --- a/crates/execution-config/src/addresses_denylist.rs +++ b/crates/execution-config/src/addresses_denylist.rs @@ -33,7 +33,7 @@ pub const ERR_DENYLISTED_ADDRESS: &str = "Address is denylisted"; /// 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!("0x36082bA812806eB06C2758c412522669b5E2ac7b"); + address!("0x360Eb67EDbA456Bbe01512679f36c2717AA65121"); /// 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`). diff --git a/crates/execution-config/src/call_from.rs b/crates/execution-config/src/call_from.rs index b8b2faf..853c8c9 100644 --- a/crates/execution-config/src/call_from.rs +++ b/crates/execution-config/src/call_from.rs @@ -21,7 +21,7 @@ use alloy_primitives::{address, Address}; /// Address of the `Memo` contract (CREATE2-deployed, zero salt). -pub const MEMO_ADDRESS: Address = address!("9702466268ccF55eAB64cdf484d272Ac08d3b75b"); +pub const MEMO_ADDRESS: Address = address!("e4aa7Ed3585AEf598179f873086F75Fcd6D4b755"); /// Address of the `Multicall3From` contract (CREATE2-deployed, zero salt). -pub const MULTICALL3_FROM_ADDRESS: Address = address!("Eb7cc06E3D3b5F9F9a5fA2B31B477ff72bB9c8b6"); +pub const MULTICALL3_FROM_ADDRESS: Address = address!("825F535677d346626cDE45D64cf89C2a426467e0"); diff --git a/crates/execution-config/src/protocol_config.rs b/crates/execution-config/src/protocol_config.rs index da5a8e4..3d7b35e 100644 --- a/crates/execution-config/src/protocol_config.rs +++ b/crates/execution-config/src/protocol_config.rs @@ -45,7 +45,7 @@ pub enum ProtocolConfigError { pub const PROTOCOL_CONFIG_ADDRESS: Address = address!("0x3600000000000000000000000000000000000001"); sol! { - /// ProtocolConfig interface for reward beneficiary and gas parameters + /// ProtocolConfig interface for gas and consensus parameters interface IProtocolConfig { /// FeeParams struct matching the contract definition struct FeeParams { @@ -57,9 +57,6 @@ sol! { uint256 blockGasLimit; } - /// Returns the current reward beneficiary address - function rewardBeneficiary() external view returns (address beneficiary); - /// Returns the current fee parameters function feeParams() external view returns (FeeParams params); } @@ -132,45 +129,6 @@ where Ok(fee_params) } -/// Returns the beneficiary address if successfully queried, -/// or `Err(ProtocolConfigError)` if there was an error during execution, -/// contract deployment issues, or empty output. -pub fn retrieve_reward_beneficiary( - evm: &mut E, -) -> Result> -where - E: Evm, - E::DB: DatabaseCommit, -{ - // Create call data for rewardBeneficiary() function - let call_data = IProtocolConfig::rewardBeneficiaryCall {}.abi_encode(); - - // Make the system call to ProtocolConfig contract - let result_and_state = evm - .transact_system_call( - Address::ZERO, - PROTOCOL_CONFIG_ADDRESS, // contract address - Bytes::from(call_data), - ) - .map_err(|e| ProtocolConfigError::EvmError(format!("{e:?}")))?; - - // Check if the call was successful - if !result_and_state.result.is_success() { - return Err(ProtocolConfigError::SystemCallFailed( - result_and_state.result, - )); - } - - // Decode the returned address from the call result - let output = result_and_state.result.output().unwrap_or_default(); - if output.is_empty() { - return Err(ProtocolConfigError::EmptyOutput); - } - - let beneficiary = IProtocolConfig::rewardBeneficiaryCall::abi_decode_returns(output)?; - Ok(beneficiary) -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/execution-e2e/src/actions/payload_utils.rs b/crates/execution-e2e/src/actions/payload_utils.rs index 8425a06..8e0a0ff 100644 --- a/crates/execution-e2e/src/actions/payload_utils.rs +++ b/crates/execution-e2e/src/actions/payload_utils.rs @@ -18,7 +18,7 @@ use crate::ArcEnvironment; use alloy_eips::eip7685::{Requests, RequestsOrHash}; -use alloy_primitives::{Address, B256}; +use alloy_primitives::{address, B256}; use alloy_rpc_types_engine::{ CancunPayloadFields, ExecutionData, ExecutionPayload, ExecutionPayloadSidecar, ExecutionPayloadV1, ExecutionPayloadV3, ForkchoiceState, PayloadAttributes, PayloadStatusEnum, @@ -65,7 +65,7 @@ pub(crate) async fn build_payload_for_next_block_with_client Result<()> { + reth_tracing::init_test_tracing(); + + let blocklisted_beneficiary = address!("0xbad0000000000000000000000000000000000001"); + let chain_spec = localdev_with_storage_override(Address::ZERO, 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/beneficiary_mismatch.rs b/crates/execution-e2e/tests/beneficiary_mismatch.rs deleted file mode 100644 index a9e0ef9..0000000 --- a/crates/execution-e2e/tests/beneficiary_mismatch.rs +++ /dev/null @@ -1,167 +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 mismatch handling during payload validation. - -use alloy_primitives::{address, Address}; -use alloy_rpc_types_engine::PayloadStatusEnum; -use arc_execution_config::hardforks::ArcHardfork; -use arc_execution_e2e::{ - actions::{build_payload_for_next_block, set_payload_override_and_rehash, submit_payload}, - chainspec::{localdev_with_hardforks, localdev_with_storage_override}, - ArcEnvironment, ArcSetup, -}; -use eyre::Result; - -async fn submit_with_overridden_beneficiary( - setup: ArcSetup, - header_beneficiary: Address, - submit_ok_msg: &str, -) -> 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.fee_recipient = header_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_ok_msg); - - Ok(status) -} - -/// Ensure `apply_pre_execution_changes` rejects payloads whose beneficiary does -/// not match `ProtocolConfig.rewardBeneficiary()`. -#[tokio::test] -async fn test_beneficiary_mismatch_rejected_with_error() -> Result<()> { - reth_tracing::init_test_tracing(); - - // Set a non-zero expected beneficiary so the mismatch check is active. - let expected_beneficiary = address!("0x65E0a200006D4FF91bD59F9694220dafc49dbBC1"); - let chain_spec = localdev_with_storage_override(expected_beneficiary, None); - - let status = submit_with_overridden_beneficiary( - ArcSetup::new().with_chain_spec(chain_spec), - address!("0xbad0000000000000000000000000000000000000"), - "submit_payload should return Ok for beneficiary mismatch", - ) - .await?; - - assert!( - matches!( - &status, - PayloadStatusEnum::Invalid { validation_error } - if validation_error.to_ascii_lowercase().contains("beneficiary") - ), - "Expected INVALID with beneficiary-related validation error, got {:?}", - status - ); - - Ok(()) -} - -/// Ensure beneficiary mismatch is still accepted before Zero5 hardfork activates. -#[tokio::test] -async fn test_beneficiary_mismatch_before_zero5_is_valid() -> Result<()> { - reth_tracing::init_test_tracing(); - - let chain_spec = localdev_with_hardforks(&[ - (ArcHardfork::Zero3, 0), - (ArcHardfork::Zero4, 0), - (ArcHardfork::Zero5, 10), - ]); - let status = submit_with_overridden_beneficiary( - ArcSetup::new().with_chain_spec(chain_spec), - address!("0xbad0000000000000000000000000000000000002"), - "submit_payload should return Ok before Zero5", - ) - .await?; - - assert!( - matches!(status, PayloadStatusEnum::Valid), - "Expected VALID for beneficiary mismatch before Zero5, got {:?}", - status - ); - - Ok(()) -} - -/// Ensure beneficiary validation is skipped when ProtocolConfig.rewardBeneficiary() returns zero. -/// -/// - When ProtocolConfig.rewardBeneficiary() returns zero address -/// - Header beneficiary mismatch check is skipped -/// - A non-blocklisted header beneficiary is accepted (proposer can set their own address) -#[tokio::test] -async fn test_validation_skipped_when_expected_beneficiary_is_zero() -> Result<()> { - reth_tracing::init_test_tracing(); - - // setting ProtocolConfig.rewardBeneficiary() to zero - let chain_spec = localdev_with_storage_override(Address::ZERO, None); - - let status = submit_with_overridden_beneficiary( - ArcSetup::new().with_chain_spec(chain_spec), - address!("0xbad0000000000000000000000000000000000001"), - "submit_payload should return Ok when expected beneficiary is zero", - ) - .await?; - assert!( - matches!(status, PayloadStatusEnum::Valid), - "Expected VALID/SYNCING when expected beneficiary is zero, got {:?}", - status - ); - Ok(()) -} - -/// Ensure proposer-selected beneficiaries are still rejected when blocklisted. -/// -/// - ProtocolConfig.rewardBeneficiary() returns zero address -/// - Header beneficiary is proposer-selected and 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(Address::ZERO, Some(blocklisted_beneficiary)); - - let status = submit_with_overridden_beneficiary( - ArcSetup::new().with_chain_spec(chain_spec), - blocklisted_beneficiary, - "submit_payload should return Ok for blocklisted proposer-selected beneficiary", - ) - .await?; - - 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-validation/src/consensus.rs b/crates/execution-validation/src/consensus.rs index e6d890b..fc0f618 100644 --- a/crates/execution-validation/src/consensus.rs +++ b/crates/execution-validation/src/consensus.rs @@ -69,6 +69,9 @@ where // 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(()) } @@ -256,6 +259,29 @@ fn arc_validate_gas_limit_bounds( + header: &H, + chain_spec: &CS, +) -> Result<(), ConsensusError> { + if !chain_spec.is_fork_active_at_block(ArcHardfork::Zero6, header.number()) { + 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 @@ -993,4 +1019,33 @@ mod tests { "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:?}" + ); + } } diff --git a/crates/malachite-app/src/handlers/restream_proposal.rs b/crates/malachite-app/src/handlers/restream_proposal.rs index 339778b..d072af9 100644 --- a/crates/malachite-app/src/handlers/restream_proposal.rs +++ b/crates/malachite-app/src/handlers/restream_proposal.rs @@ -33,12 +33,12 @@ use crate::store::repositories::UndecidedBlocksRepository; /// /// 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 block's round and valid_round -/// are updated to match the current proposal context before restreaming. +/// 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 round -/// - If there are issues fetching or storing the block in the repository +/// - 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, @@ -48,14 +48,8 @@ pub async fn handle( valid_round: Round, value_id: ValueId, ) -> eyre::Result<()> { - let block_to_restream = get_block_to_restream( - state.store(), - height, - round, - valid_round, - value_id.block_hash(), - ) - .await?; + 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(); @@ -103,50 +97,33 @@ pub async fn restream_proposal( async fn get_block_to_restream( undecided_blocks: impl UndecidedBlocksRepository, height: Height, - round: Round, - valid_round: Round, block_hash: BlockHash, ) -> eyre::Result> { - let block = undecided_blocks + undecided_blocks .get_by_hash(height, block_hash) .await .wrap_err_with(|| { format!( "Failed to fetch block for restreaming \ - (height={height}, round={round}, block_hash={block_hash})" + (height={height}, block_hash={block_hash})" ) - })?; - - if let Some(mut block) = block { - block.round = round; - block.valid_round = valid_round; - - undecided_blocks - .store_undecided_block(block.clone()) - .await - .wrap_err_with(|| { - format!( - "Failed to store updated block before restreaming \ - (height={height}, round={round}, block_hash={block_hash})" - ) - })?; - - Ok(Some(block)) - } else { - Ok(None) - } + }) } #[cfg(test)] mod tests { - use crate::proposal_parts::MockPublishProposalPart; + 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::{Address, BlockHash, Height}; + 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; @@ -167,35 +144,37 @@ mod tests { } } + 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_and_updated() { + async fn get_block_found() { let mut mock_repo = MockUndecidedBlocksRepository::new(); let height = Height::new(10); - let round = Round::new(5); - let valid_round = Round::new(3); 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(original_block.clone()))); - - mock_repo - .expect_store_undecided_block() - .withf(move |b| b.round == round && b.valid_round == valid_round) - .times(1) - .returning(|_| Ok(())); - - let result = - get_block_to_restream(&mock_repo, height, round, valid_round, block_hash).await; + .returning(move |_, _| Ok(Some(from_repo.clone()))); - let block = result.unwrap().expect("block should be found"); - assert_eq!(block.round, round); - assert_eq!(block.valid_round, valid_round); + 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] @@ -203,8 +182,6 @@ mod tests { let mut mock_repo = MockUndecidedBlocksRepository::new(); let height = Height::new(10); - let round = Round::new(5); - let valid_round = Round::new(3); let block_hash = BlockHash::default(); mock_repo @@ -213,8 +190,7 @@ mod tests { .times(1) .returning(|_, _| Ok(None)); - let result = - get_block_to_restream(&mock_repo, height, round, valid_round, block_hash).await; + let result = get_block_to_restream(&mock_repo, height, block_hash).await; assert!(result.unwrap().is_none()); } @@ -223,16 +199,12 @@ mod tests { async fn get_block_repo_error_propagation() { let mut mock_repo = MockUndecidedBlocksRepository::new(); let height = Height::new(10); - let round = Round::new(5); - let valid_round = Round::Nil; mock_repo .expect_get_by_hash() .returning(|_, _| Err(std::io::Error::other("DB connection failed"))); - let result = - get_block_to_restream(&mock_repo, height, round, valid_round, BlockHash::default()) - .await; + let result = get_block_to_restream(&mock_repo, height, BlockHash::default()).await; assert!(result.is_err()); assert!(result @@ -256,4 +228,72 @@ mod tests { 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-cli/src/cmd/start.rs b/crates/malachite-cli/src/cmd/start.rs index 38e134b..0d73ca7 100644 --- a/crates/malachite-cli/src/cmd/start.rs +++ b/crates/malachite-cli/src/cmd/start.rs @@ -235,8 +235,10 @@ pub struct StartCmd { #[clap(long = "execution-persistence-backpressure")] pub execution_persistence_backpressure: bool, - /// Number of blocks the execution layer is allowed to lag behind the - /// consensus layer before persistence backpressure is applied. + /// 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 @@ -669,6 +671,13 @@ impl StartCmd { )); } + 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(()) } @@ -841,6 +850,21 @@ mod tests { ); } + #[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(); diff --git a/crates/mesh-analysis/src/analyze.rs b/crates/mesh-analysis/src/analyze.rs index 0e94f03..39f6e41 100644 --- a/crates/mesh-analysis/src/analyze.rs +++ b/crates/mesh-analysis/src/analyze.rs @@ -247,13 +247,14 @@ pub fn analyze(nodes: &[NodeMetricsData]) -> MeshAnalysis { .cloned() .collect(); - // Indirect paths between validators (through full nodes) + // 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(); - if actual_partitions.len() == 1 { - let vals: Vec<&String> = all_validators.iter().collect(); + 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..] { - // Skip if directly connected if validator_mesh .get(*v1) .map(|p| p.contains(*v2)) @@ -269,8 +270,8 @@ pub fn analyze(nodes: &[NodeMetricsData]) -> MeshAnalysis { } } } - indirect_paths.sort_by(|a, b| a.3.cmp(&b.3).then(a.0.cmp(&b.0)).then(a.1.cmp(&b.1))); } + 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(), diff --git a/crates/node/src/main.rs b/crates/node/src/main.rs index 44aae3f..a11b35e 100644 --- a/crates/node/src/main.rs +++ b/crates/node/src/main.rs @@ -252,6 +252,18 @@ struct ArcExtraCli { )] arc_hide_pending_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", @@ -463,6 +475,8 @@ fn main() { let wait_for_payload = ext.wait_for_payload; let filter_pending_txs = ext.arc_hide_pending_txs; + let rebroadcast_interval = + std::time::Duration::from_secs(ext.txpool_rebroadcast_interval); let handle = builder .node(ArcNode::new( arc_rpc_cfg, @@ -471,6 +485,7 @@ fn main() { payload_builder_deadline_ms, wait_for_payload, filter_pending_txs, + rebroadcast_interval, )) .launch_with_debug_capabilities() .await?; @@ -1128,4 +1143,46 @@ mod tests { "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"); + } + } } diff --git a/crates/quake/README.md b/crates/quake/README.md index 4cb41c5..95e248f 100644 --- a/crates/quake/README.md +++ b/crates/quake/README.md @@ -1354,14 +1354,19 @@ described below. ### Instance sizing The `--node-size` and `--cc-size` flags let you override the default EC2 instance -types when creating remote infrastructure: +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 +quake start --remote --node-size t3.large --node-disk-gb 100 ``` #### Node instances @@ -1379,14 +1384,14 @@ consumer of both memory (~2.5 GiB) and disk (debug logs grow at ~200 MiB/hr). 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 don't change the disk size (that requires a -Terraform change to `root_block_device`), but they provide more RAM headroom, -reducing swap pressure and making the node more resilient to memory spikes. +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** increasing the EBS volume size in -> `crates/quake/terraform/nodes.tf` (for disk). +> 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 @@ -1546,7 +1551,7 @@ Initialize Terraform plugins and state. This step is required only once. 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 ] +./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. diff --git a/crates/quake/scenarios/examples/testnet-small-default.toml b/crates/quake/scenarios/examples/testnet-small-default.toml new file mode 100644 index 0000000..58139af --- /dev/null +++ b/crates/quake/scenarios/examples/testnet-small-default.toml @@ -0,0 +1,142 @@ +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" +el.config.arc.hide_pending_txs = true + +# --- 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 new file mode 100644 index 0000000..bece671 --- /dev/null +++ b/crates/quake/scenarios/examples/testnet-small.toml @@ -0,0 +1,154 @@ +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" +el.config.arc.hide_pending_txs = true + +# --- 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/src/infra/terraform.rs b/crates/quake/src/infra/terraform.rs index 8cf3023..8fbd16a 100644 --- a/crates/quake/src/infra/terraform.rs +++ b/crates/quake/src/infra/terraform.rs @@ -83,12 +83,16 @@ impl Terraform { /// Create the nodes and the Control Center server in the remote infrastructure. /// /// `node_size` and `cc_size` override the Terraform defaults for EC2 instance types. + /// When set, `node_disk_gb` and `cc_disk_gb` configure root EBS volume sizes (GiB). When omitted, + /// Terraform leaves the AMI default root volume size. pub(crate) fn create( &self, dry_run: bool, yes: bool, node_size: Option<&str>, cc_size: Option<&str>, + node_disk_gb: Option, + cc_disk_gb: Option, ) -> Result<()> { // Ensure testnet directory exists if !dry_run { @@ -98,7 +102,13 @@ impl Terraform { let mut args: Vec<&str> = vec![if dry_run { "plan" } else { "apply" }]; - let vars = self.build_variables(&self.node_names, node_size, cc_size)?; + let vars = self.build_variables( + &self.node_names, + node_size, + cc_size, + node_disk_gb, + cc_disk_gb, + )?; args.extend(vars.iter().map(String::as_str)); let state_flag = self.state_flag(); @@ -117,7 +127,7 @@ impl Terraform { pub(crate) fn destroy(&self, yes: bool) -> Result<()> { let mut args: Vec<&str> = vec!["destroy"]; - let vars = self.build_variables(&self.node_names, None, None)?; + let vars = self.build_variables(&self.node_names, None, None, None, None)?; args.extend(vars.iter().map(String::as_str)); let state_flag = self.state_flag(); @@ -158,6 +168,8 @@ impl Terraform { node_names: &[String], node_size: Option<&str>, cc_size: Option<&str>, + node_disk_gb: Option, + cc_disk_gb: Option, ) -> Result> { let mut args: Vec = Vec::new(); @@ -248,6 +260,16 @@ impl Terraform { args.push(format!("cc_size={size}")); } + if let Some(gib) = node_disk_gb { + args.push("-var".to_string()); + args.push(format!("node_volume_size={gib}")); + } + + if let Some(gib) = cc_disk_gb { + args.push("-var".to_string()); + args.push(format!("cc_volume_size={gib}")); + } + Ok(args) } } diff --git a/crates/quake/src/main.rs b/crates/quake/src/main.rs index ba08d35..3468767 100644 --- a/crates/quake/src/main.rs +++ b/crates/quake/src/main.rs @@ -55,6 +55,7 @@ mod node; mod nodekey; mod nodes; mod perturb; +mod report; mod rpc; mod setup; mod shell; @@ -285,6 +286,41 @@ enum Commands { #[arg(short = 'c', long, default_value_t = 1)] count: usize, }, + /// Generate a network testing report (mesh, health, perf, sanity, sync). + /// + /// Collects metrics from a running testnet, optionally runs sanity phases + /// and sync-speed tests, then writes a structured markdown report. + /// + /// Parameters (via `--set key=value`): + /// + /// Key Default Description + /// ───────────────── ─────────────────── ───────────────────────────────────── + /// warmup_s 30 Seconds before first Prometheus scrape + /// duration_s 60 Observation window / load duration + /// load_rate 50 TPS during observation (0 = no load) + /// load_targets RPC_NODES Node names and/or [node_groups] selectors (default group) + /// load_mix transfer=100 Tx type mix + /// block_time_p50_ms 550 Max p50 block time threshold for validators + /// block_time_p99_ms 1000 Max p99 block time threshold for validators + /// sanity true Run sanity phases + /// sync_speed true Run sync speed test (destructive) + /// arc_nodes ARC_NODES group Sanity target nodes (names and/or [node_groups]) + /// snapshot_provider full-circle-5 Snapshot source node + /// reference validator-blue Reference node for tip height + /// sync_nodes full-quicknode-1 Nodes to sync-test + /// sync_min_bps 7.0 Min avg blocks/sec to pass + /// sync_timeout_s 180 Max sync measurement duration + /// sync_downtime_s 120 Seconds to keep node down + /// store_nodes (pruned nodes) Nodes for storage size lookup + #[command(verbatim_doc_comment)] + Report { + /// Output file path for the markdown report + #[arg(short = 'o', long, default_value = "/tmp/quake-report.md")] + output: PathBuf, + /// Pass parameters as key=value pairs (e.g. --set sanity=false) + #[clap(long = "set", value_parser = parse_key_value)] + params: Vec<(String, String)>, + }, /// Start an MCP (Model Context Protocol) server for AI-assisted testnet management. /// /// By default uses stdio transport (for Claude Code, Cursor, etc.). @@ -337,9 +373,10 @@ struct StartArgs { infra_args: InfraArgs, } -/// EC2 instance size overrides for remote infrastructure. +/// EC2 instance type and optional root EBS size for remote infrastructure. /// -/// See README "Instance sizing" for details. +/// See README "Instance sizing" for details. Disk flags apply only when using +/// `quake remote create` or `quake start --remote` (they are ignored for local testnets). #[derive(Args, Debug, Clone)] pub(crate) struct InfraArgs { /// EC2 instance type for nodes [default: t3.medium]. @@ -364,6 +401,18 @@ pub(crate) struct InfraArgs { /// t3.2xlarge β€” heavy Blockscout indexing or many nodes (32 GiB RAM) #[clap(long, verbatim_doc_comment)] cc_size: Option, + /// Root EBS volume size for nodes (GiB). Omit to use the AMI default. + /// + /// Long runs need more than the default root volume when debug logs fill the disk; + /// pair with `--node-size` for RAM headroom. When set, must be at least **8** (typical + /// lower bound vs AMI snapshot size; AWS may still require a larger minimum for a given AMI). + #[clap(long, value_name = "GIB", value_parser = clap::value_parser!(u32).range(8..))] + node_disk_gb: Option, + /// Root EBS volume size for the Control Center (GiB). Omit to use the AMI default. + /// + /// When set, must be at least **8** (see `--node-disk-gb`). + #[clap(long, value_name = "GIB", value_parser = clap::value_parser!(u32).range(8..))] + cc_disk_gb: Option, } #[derive(Args)] @@ -734,6 +783,7 @@ async fn main() -> Result<()> { ); } export::import_shared_testnet(path)?; + return Ok(()); } // Force the use of remote mode on certain sub-commands @@ -878,6 +928,10 @@ async fn main() -> Result<()> { .run_tests(&spec, dry_run, rpc_timeout, ¶ms) .await? } + Commands::Report { output, params } => { + let params = crate::tests::TestParams::from(params); + report::run_report(&testnet, ¶ms, &output).await? + } Commands::Wait { command } => match command { WaitSubcommand::Height { height, @@ -947,6 +1001,8 @@ async fn pre_start( true, args.infra_args.node_size.as_deref(), args.infra_args.cc_size.as_deref(), + args.infra_args.node_disk_gb, + args.infra_args.cc_disk_gb, )?; // Reload testnet with the recently created infra files diff --git a/crates/quake/src/manifest.rs b/crates/quake/src/manifest.rs index 570f024..e3530c3 100644 --- a/crates/quake/src/manifest.rs +++ b/crates/quake/src/manifest.rs @@ -608,6 +608,23 @@ impl Node { &self.follow_endpoints } + /// `true` if the node is configured to prune the Malachite CL store (used e.g. for + /// `quake report` store appendix defaults). + pub fn cl_store_pruning_configured(&self) -> bool { + if self.cl_prune_preset.is_some() { + return true; + } + match &self.cl_config { + NodeClConfig::Modern(cmd) => { + cmd.full + || cmd.minimal + || cmd.prune_certificates_distance > 0 + || cmd.prune_certificates_before > 0 + } + NodeClConfig::Legacy(cfg) => cfg.prune.enabled(), + } + } + /// Returns the execution layer (Reth) CLI flags for this node, defined in the /// manifest file with the `el_config` key. /// If not defined, returns an empty vector. diff --git a/crates/quake/src/report.rs b/crates/quake/src/report.rs new file mode 100644 index 0000000..017049c --- /dev/null +++ b/crates/quake/src/report.rs @@ -0,0 +1,2145 @@ +// 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. + +//! Network testing report generator. +//! +//! Collects mesh, health, performance, and sanity data from a running testnet +//! and produces a structured markdown report. +//! +//! By default the sanity section runs the full `sanity:arc_node` test phases +//! (snapshot recovery, MEV protection, mempool checks, tx forwarding) and the +//! sync speed measurement (destructive β€” kills and restarts a node). +//! Disable with `--set sanity=false` and/or `--set sync_speed=false` for faster +//! or non-destructive reports. +//! +//! **Remote testnets** (via `quake remote import`): Most sections work via SSM +//! tunnels. The sanity section adapts to remote mode: snapshot recovery is +//! skipped (requires local Docker Compose), tx forwarding uses `quake remote +//! load`, and mempool checks report warnings instead of failures (shared +//! testnets have real traffic). +//! +//! # Usage +//! # Report parameters (via `--set key=value`) +//! +//! | Key | Default | Description | +//! |----------------------|------------------|------------------------------------------------------------| +//! | **Observation & Load** | | | +//! | `warmup_s` | `30` | Seconds to wait before first Prometheus scrape | +//! | `duration_s` | `60` | Observation window (load duration or quiet sleep) | +//! | `load_rate` | `50` | TPS during observation (0 = no load) | +//! | `load_targets` | `RPC_NODES` | Comma-separated load selectors (node names and/or manifest `[node_groups]` names, same as `quake load`); default group must exist in the manifest (e.g. `RPC_NODES` on mainnet) | +//! | `load_mix` | `transfer=100` | Tx type mix (e.g. `transfer=70,erc20=30`) | +//! | **Perf thresholds** | | | +//! | `block_time_p50_ms` | `550` | Fail if any validator node's p50 block time exceeds this | +//! | `block_time_p99_ms` | `1000` | Fail if any validator node's p99 block time exceeds this | +//! | **Section toggles** | | | +//! | `sanity` | `true` | Run snapshot recovery + MEV + mempool + tx forwarding | +//! | `sync_speed` | `true` | Run sync speed measurement (destructive) | +//! | **Sanity** | | | +//! | `arc_nodes` | `ARC_NODES` group | Target nodes for all sanity phases (comma-separated node names and/or `[node_groups]`; default uses the `ARC_NODES` group if defined, else skips) | +//! | | | *Snapshot recovery*: each arc-node is restored from snapshot| +//! | | | *MEV protection*: checks `follow_endpoints` of arc-nodes | +//! | | | *Mempool empty*: checks all nodes except arc-nodes + relays| +//! | | | *Tx forwarding*: sends txs to each arc-node, verifies inclusion | +//! | `snapshot_provider` | `full-circle-5` | Node to take the snapshot from (Phase 1) | +//! | `reference` | `validator-blue` | Reference node for tip height (sanity + sync speed) | +//! | **Sync speed** | | | +//! | `sync_nodes` | `full-quicknode-1` | Comma-separated node names to stop/restart and measure | +//! | `sync_min_bps` | `7.0` | Min avg blocks/sec to pass | +//! | `sync_timeout_s` | `180` | Max measurement duration | +//! | `sync_downtime_s` | `120` | Seconds to keep node down before restart | +//! | **Store** | | | +//! | `store_nodes` | (pruned nodes) | Comma-separated node names for storage size lookup (malachite) | +//! +//! "follow nodes" (sanity / MEV docs) = manifest nodes with `follow = true`. +//! "pruned nodes" = nodes for which `Node::cl_store_pruning_configured()` is true (CL `store.db`). +//! +//! # Hardcoded (not configurable) +//! +//! These values are fixed in the sanity phase implementations (`arc_node.rs`): +//! +//! | Constant | Value | Used by | Description | +//! |----------------------|-------------|--------------------------|----------------------------------------------| +//! | `TARGET_HEIGHT` | `120` | Snapshot recovery | Min block height before snapshotting | +//! | `WAIT_TIMEOUT` | `600s` | Snapshot recovery | Max wait for validators/provider to reach height | +//! | `CATCHUP_TIMEOUT` | `120s` | Snapshot recovery | Max wait for arc-node to catch up after restore | +//! | `RESTART_SETTLE` | `10s` | Snapshot recovery | Sleep after restarting a node | +//! | `LOAD_NUM_TXS` | `10` | Tx forwarding | Number of transactions sent per arc-node | +//! | `MEV addr` | `0xf39F..2266` | MEV protection | Address used for pending-state RPC checks | +//! | `pruning_window` | `100` | CL store pruning check | Max certificate records expected | +//! | `pruning_margin` | `50` | CL store pruning check | Added to window for threshold (total 150) | +//! +//! # Example commands +//! +//! ```text +//! quake report # defaults: 60s, 50 TPS, all sections +//! quake report --set sanity=false --set sync_speed=false # metrics only (fast) +//! quake report --set duration_s=600 --set load_rate=0 # 10 min observation, no load +//! quake report --set arc_nodes=arc-1,arc-2 # custom sanity targets +//! quake report --set sync_nodes=arc-node,rpc-full # measure sync on specific nodes +//! quake report --set store_nodes=snapshot,arc-node # storage size information for specific nodes +//! quake report -o my-report.md # custom output path +//! ``` + +use std::fmt::Write as _; +use std::time::Duration; + +use chrono::Utc; +use color_eyre::eyre::{bail, Result}; +use tracing::info; + +use url::Url; + +use crate::mesh::{analyze, classify_all, fetch_all_metrics as fetch_mesh_metrics, MeshTier}; +use crate::testnet::Testnet; +use crate::tests::mesh::{categorize_node, check_strict, NodeCategory}; +use crate::tests::TestParams; +use crate::RemoteSubcommand; + +const DEFAULT_WARMUP_S: u64 = 30; +const DEFAULT_DURATION_S: u64 = 60; +const DEFAULT_LOAD_RATE: u64 = 50; +const DEFAULT_LOAD_MIX: &str = "transfer=100"; +const DEFAULT_P50_MS: u64 = 550; +const DEFAULT_P99_MS: u64 = 1000; +const DEFAULT_REFERENCE: &str = "validator-blue"; +const DEFAULT_SNAPSHOT_PROVIDER: &str = "full-circle-5"; +const DEFAULT_SYNC_NODE: &str = "full-quicknode-1"; +const DEFAULT_SYNC_MIN_BPS: f64 = 7.0; +const DEFAULT_SYNC_TIMEOUT_S: u64 = 180; +const DEFAULT_SYNC_DOWNTIME_S: u64 = 120; + +/// When `load_targets` is unset, `quake report` uses this manifest `[node_groups]` name. +const DEFAULT_REPORT_LOAD_TARGETS: &str = "RPC_NODES"; + +// ── Data structures ───────────────────────────────────────────────────── + +/// Top-level report data collected from the testnet. +struct ReportData { + // Metadata + quake_version: String, + quake_commit: String, + node_versions: Vec, + manifest_name: String, + manifest_path: String, + manifest_content: String, + timestamp: String, + output_path: String, + + // Effective parameters (resolved defaults + overrides) + effective_params: Vec<(String, String)>, + + // Section results + mesh: MeshSection, + health: HealthSection, + perf: PerfSection, + sanity: Option, + sync_speed: Vec, + + // Node liveness (names of nodes that were down) + liveness: LivenessSection, + + // Raw info outputs for appendices + info_mesh: String, + /// `quake info perf`-style text: delta between two scrapes (observation window). + info_perf_observation: String, + /// `quake info perf`-style text: cumulative since process start (final scrape). + info_perf_cumulative: String, + /// `quake info health`-style text: cumulative counters at end of window (final scrape). + info_health_cumulative: String, + info_store: String, +} + +struct NodeVersion { + name: String, + arc_version: String, + reth_version: String, +} + +struct LivenessSection { + total_nodes: usize, + down_before: Vec, + down_after: Vec, +} + +struct MeshSection { + entries: Vec, + max_hops: usize, + max_duplicate_pct: f64, + passed: bool, + failures: Vec, +} + +struct MeshEntry { + name: String, + category: String, + tier: String, + passed: bool, + status_detail: String, +} + +struct HealthSection { + nodes: Vec, + max_decisions: i64, + any_round_gt0: bool, + any_restarts: bool, + any_sync_behind: bool, + passed: bool, + failures: Vec, +} + +struct HealthEntry { + name: String, + decisions: i64, + round_gt0: i64, + restarts: i64, + sync_behind: i64, + /// Per-node result from `check_health_deltas` (same rules as pass/fail for Health). + passed: bool, + /// Short explanation (same as `CheckResult::message` in arc-checks). + check_detail: String, +} + +struct PerfSection { + p50_threshold_ms: u64, + p99_threshold_ms: u64, + nodes: Vec, + validator_names: std::collections::HashSet, + passed: bool, + failures: Vec, +} + +struct PerfEntry { + name: String, + group: String, + block_time_p50_ms: f64, + block_time_p95_ms: f64, + block_time_p99_ms: f64, + block_count: u64, + avg_tx_per_block: f64, + avg_gas_used: f64, + /// `None` if non-validator (not subject to p50/p99 thresholds in this report). + threshold_pass: Option, + threshold_detail: String, +} + +struct SanitySection { + phases: Vec, + passed: bool, + failures: Vec, +} + +struct SanityPhase { + name: String, + passed: bool, + duration: Duration, + detail: String, +} + +fn skipped_sanity_section(detail: impl Into) -> SanitySection { + let detail = detail.into(); + SanitySection { + phases: vec![SanityPhase { + name: "prerequisites".to_string(), + passed: true, + duration: Duration::ZERO, + detail, + }], + passed: true, + failures: Vec::new(), + } +} + +fn failed_sanity_section(detail: impl Into) -> SanitySection { + let detail = detail.into(); + SanitySection { + phases: vec![SanityPhase { + name: "prerequisites".to_string(), + passed: false, + duration: Duration::ZERO, + detail: detail.clone(), + }], + passed: false, + failures: vec![detail], + } +} + +struct SyncSpeedSection { + node: String, + reference: String, + result: Option, + min_bps: f64, + passed: bool, + detail: String, + duration: Duration, +} + +// ── Collection ────────────────────────────────────────────────────────── + +async fn collect_report_data( + testnet: &Testnet, + params: &TestParams, + output: &std::path::Path, +) -> Result { + let warmup_s: u64 = params + .get_or("warmup_s", &DEFAULT_WARMUP_S.to_string()) + .parse() + .unwrap_or(DEFAULT_WARMUP_S); + let duration_s: u64 = params + .get_or("duration_s", &DEFAULT_DURATION_S.to_string()) + .parse() + .unwrap_or(DEFAULT_DURATION_S); + let load_rate: u64 = params + .get_or("load_rate", &DEFAULT_LOAD_RATE.to_string()) + .parse() + .unwrap_or(DEFAULT_LOAD_RATE); + let load_targets_str = params.get_or("load_targets", ""); + let load_mix = params.get_or("load_mix", DEFAULT_LOAD_MIX); + let p50_ms: u64 = params + .get_or("block_time_p50_ms", &DEFAULT_P50_MS.to_string()) + .parse() + .unwrap_or(DEFAULT_P50_MS); + let p99_ms: u64 = params + .get_or("block_time_p99_ms", &DEFAULT_P99_MS.to_string()) + .parse() + .unwrap_or(DEFAULT_P99_MS); + + let load_target_selectors: Vec = if load_targets_str.is_empty() { + vec![DEFAULT_REPORT_LOAD_TARGETS.to_string()] + } else { + load_targets_str + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() + }; + + let load_desc = if load_rate > 0 { + let resolved = testnet + .manifest + .resolve_node_selectors(&load_target_selectors)?; + let s = load_target_selectors.join(", "); + format!( + "{load_rate} TPS ({load_mix}) β†’ {s} ({} node(s) after group expansion)", + resolved.len() + ) + } else { + "none".to_string() + }; + + let manifest_content = std::fs::read_to_string(&testnet.manifest_path) + .unwrap_or_else(|_| "(could not read manifest file)".to_string()); + + let manifest_name = testnet + .manifest + .name + .clone() + .unwrap_or_else(|| testnet.name.clone()); + + info!( + warmup_s, + duration_s, load_rate, %load_desc, + "Collecting report data" + ); + + // ── Liveness check (before) ──────────────────────────────────── + let node_urls = testnet.nodes_metadata.all_execution_urls(); + let heights_before = crate::rpc::fetch_latest_heights(&node_urls).await; + let down_before: Vec = heights_before + .iter() + .filter(|(_, r)| r.is_err()) + .map(|(name, _)| name.clone()) + .collect(); + if !down_before.is_empty() { + info!( + "⚠ {} node(s) unreachable before report: {}", + down_before.len(), + down_before.join(", ") + ); + } + + // ── Warmup ────────────────────────────────────────────────────── + if warmup_s > 0 { + info!("Warming up ({warmup_s}s)..."); + tokio::time::sleep(Duration::from_secs(warmup_s)).await; + } + + // ── Health baseline ───────────────────────────────────────────── + let metrics_urls = testnet.nodes_metadata.all_consensus_metrics_urls(); + let raw_before = arc_checks::fetch_all_metrics(&metrics_urls).await; + let mut health_before = arc_checks::parse_all_health_metrics(&raw_before); + + if health_before.is_empty() { + bail!("No health metrics collected from any node (scrape 1)"); + } + crate::util::assign_node_groups( + health_before + .iter_mut() + .map(|n| (n.name.as_str(), &mut n.group)), + &testnet.manifest.nodes, + ); + + // ── Load / observation ────────────────────────────────────────── + if load_rate > 0 { + info!("Introducing load: {load_desc} for {duration_s}s"); + if testnet.is_remote() { + let args = crate::tests::sanity::build_remote_load_args( + load_rate, + duration_s, + &load_mix, + &load_target_selectors, + ); + testnet.remote(RemoteSubcommand::Load { args }).await?; + } else { + let config = + crate::tests::sanity::build_spammer_config(load_rate, duration_s, &load_mix)?; + testnet.load(load_target_selectors.clone(), &config).await?; + } + } else { + info!("Observing for {duration_s}s (no load)"); + tokio::time::sleep(Duration::from_secs(duration_s)).await; + } + + // ── Health scrape 2 ───────────────────────────────────────────── + let raw_after = arc_checks::fetch_all_metrics(&metrics_urls).await; + let mut health_after = arc_checks::parse_all_health_metrics(&raw_after); + + if health_after.is_empty() { + bail!("No health metrics collected from any node (scrape 2)"); + } + crate::util::assign_node_groups( + health_after + .iter_mut() + .map(|n| (n.name.as_str(), &mut n.group)), + &testnet.manifest.nodes, + ); + + let health_section = build_health_section(&health_before, &health_after); + + // ── Performance ───────────────────────────────────────────────── + let perf_section = build_perf_section(&raw_before, &raw_after, testnet, p50_ms, p99_ms); + + // ── Mesh ──────────────────────────────────────────────────────── + let mesh_section = build_mesh_section(testnet).await?; + + // ── Sanity (snapshot recovery + MEV + mempool + tx forwarding) ── + let run_sanity = params.get_or("sanity", "true") == "true"; + let sanity_section = if run_sanity { + info!("Running sanity phases (snapshot recovery, MEV, mempool, tx forwarding)..."); + Some(run_sanity_phases(testnet, params).await) + } else { + info!("Sanity phases skipped (--set sanity=false)"); + None + }; + + // ── Sync speed (destructive β€” stops/restarts a node) ──────────── + let run_sync = params.get_or("sync_speed", "true") == "true"; + let sync_speed_sections = if run_sync { + info!("Running sync speed tests..."); + run_sync_speed_all(testnet, params).await + } else { + info!("Sync speed skipped (--set sync_speed=false)"); + Vec::new() + }; + + // ── Liveness check (after) ───────────────────────────────────── + let heights_after = crate::rpc::fetch_latest_heights(&node_urls).await; + let down_after: Vec = heights_after + .iter() + .filter(|(_, r)| r.is_err()) + .map(|(name, _)| name.clone()) + .collect(); + if !down_after.is_empty() { + info!( + "⚠ {} node(s) unreachable after report: {}", + down_after.len(), + down_after.join(", ") + ); + } + { + let newly_down: Vec<_> = down_after + .iter() + .filter(|n| !down_before.contains(n)) + .map(|s| s.as_str()) + .collect(); + if !newly_down.is_empty() { + info!( + "⚠ {} node(s) went down during the report: {}", + newly_down.len(), + newly_down.join(", ") + ); + } + } + + let liveness = LivenessSection { + total_nodes: node_urls.len(), + down_before, + down_after, + }; + + // ── Raw info outputs for appendices ────────────────────────────── + let ( + info_mesh, + info_perf_observation, + info_perf_cumulative, + info_health_cumulative, + info_store, + ) = collect_info_appendices(testnet, params, &raw_before, &raw_after, duration_s).await; + + // Resolve effective values for all parameters so the report is reproducible. + let reference = params.get_or("reference", DEFAULT_REFERENCE); + let arc_nodes_resolved = crate::tests::arc_node::resolve_arc_nodes(testnet, params) + .map(|names| names.join(",")) + .unwrap_or_default(); + let sync_nodes_resolved = { + let p = params.get_or("sync_nodes", ""); + if p.is_empty() { + default_sync_nodes().join(",") + } else { + p + } + }; + let store_nodes_resolved = { + let p = params.get_or("store_nodes", ""); + if p.is_empty() { + testnet + .manifest + .nodes + .iter() + .filter(|(_, node)| node.cl_store_pruning_configured()) + .map(|(name, _)| name.clone()) + .collect::>() + .join(",") + } else { + p + } + }; + let snapshot_provider = params.get_or("snapshot_provider", DEFAULT_SNAPSHOT_PROVIDER); + + let mut effective_params: Vec<(String, String)> = vec![ + ("warmup_s".into(), warmup_s.to_string()), + ("duration_s".into(), duration_s.to_string()), + ("load_rate".into(), load_rate.to_string()), + ("load_targets".into(), load_target_selectors.join(",")), + ("load_mix".into(), load_mix.clone()), + ("block_time_p50_ms".into(), p50_ms.to_string()), + ("block_time_p99_ms".into(), p99_ms.to_string()), + ("sanity".into(), run_sanity.to_string()), + ]; + if run_sanity || run_sync { + effective_params.push(("reference".into(), reference.clone())); + } + if run_sanity { + effective_params.push(("arc_nodes".into(), arc_nodes_resolved)); + effective_params.push(("snapshot_provider".into(), snapshot_provider)); + } + effective_params.push(("sync_speed".into(), run_sync.to_string())); + if run_sync { + effective_params.push(("sync_nodes".into(), sync_nodes_resolved)); + effective_params.push(( + "sync_min_bps".into(), + params.get_or("sync_min_bps", &DEFAULT_SYNC_MIN_BPS.to_string()), + )); + effective_params.push(( + "sync_timeout_s".into(), + params.get_or("sync_timeout_s", &DEFAULT_SYNC_TIMEOUT_S.to_string()), + )); + effective_params.push(( + "sync_downtime_s".into(), + params.get_or("sync_downtime_s", &DEFAULT_SYNC_DOWNTIME_S.to_string()), + )); + } + if !store_nodes_resolved.is_empty() { + effective_params.push(("store_nodes".into(), store_nodes_resolved)); + } + + // ── Collect per-node versions (EL + CL) ──────────────────────── + let node_versions = collect_node_versions(testnet).await; + + Ok(ReportData { + quake_version: arc_version::GIT_VERSION.to_string(), + quake_commit: arc_version::GIT_COMMIT_HASH[..8].to_string(), + node_versions, + manifest_name, + manifest_path: testnet.manifest_path.display().to_string(), + manifest_content, + timestamp: Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), + output_path: output.display().to_string(), + effective_params, + mesh: mesh_section, + health: health_section, + perf: perf_section, + sanity: sanity_section, + sync_speed: sync_speed_sections, + liveness, + info_mesh, + info_perf_observation, + info_perf_cumulative, + info_health_cumulative, + info_store, + }) +} + +fn build_health_section( + before: &[arc_checks::NodeHealthData], + after: &[arc_checks::NodeHealthData], +) -> HealthSection { + let deltas = arc_checks::compute_health_deltas(before, after); + let report = arc_checks::check_health_deltas(&deltas); + + let mut max_decisions = 0i64; + let mut any_round_gt0 = false; + let mut any_restarts = false; + let mut any_sync_behind = false; + let mut nodes = Vec::new(); + + for d in &deltas { + let round_gt0 = d.delta_round_1 + d.delta_round_gt1; + max_decisions = max_decisions.max(d.delta_decisions); + if round_gt0 > 0 { + any_round_gt0 = true; + } + if d.delta_height_restarts > 0 { + any_restarts = true; + } + if d.delta_sync_fell_behind > 0 { + any_sync_behind = true; + } + let check = report.checks.iter().find(|c| c.name == d.name); + let (passed, check_detail) = if let Some(c) = check { + (c.passed, c.message.clone()) + } else { + (true, "internal: no matching check row for node".to_string()) + }; + nodes.push(HealthEntry { + name: d.name.clone(), + decisions: d.delta_decisions, + round_gt0, + restarts: d.delta_height_restarts, + sync_behind: d.delta_sync_fell_behind, + passed, + check_detail, + }); + } + + let failures: Vec = report + .checks + .iter() + .filter(|c| !c.passed) + .map(|c| format!("{}: {}", c.name, c.message)) + .collect(); + + HealthSection { + nodes, + max_decisions, + any_round_gt0, + any_restarts, + any_sync_behind, + passed: report.passed(), + failures, + } +} + +fn build_perf_section( + raw_before: &[(String, String)], + raw_after: &[(String, String)], + testnet: &Testnet, + p50_ms: u64, + p99_ms: u64, +) -> PerfSection { + let perf_nodes = crate::util::parse_perf_metrics_delta_with_groups( + raw_before, + raw_after, + &testnet.manifest.nodes, + ); + + let validator_names: std::collections::HashSet = testnet + .manifest + .validator_names() + .into_iter() + .map(|n| n.to_string()) + .collect(); + let val_before: Vec<_> = raw_before + .iter() + .filter(|(name, _)| validator_names.contains(name.as_str())) + .cloned() + .collect(); + let val_after: Vec<_> = raw_after + .iter() + .filter(|(name, _)| validator_names.contains(name.as_str())) + .cloned() + .collect(); + + let perf_report = arc_checks::check_block_time_delta(&val_before, &val_after, p50_ms, p99_ms); + + let check_by_name: std::collections::HashMap<&str, &arc_checks::CheckResult> = perf_report + .checks + .iter() + .map(|c| (c.name.as_str(), c)) + .collect(); + + let failures: Vec = perf_report + .checks + .iter() + .filter(|c| !c.passed) + .map(|c| format!("{}: {}", c.name, c.message)) + .collect(); + + let nodes: Vec = perf_nodes + .iter() + .map(|n| { + let bt = n.block_time.as_ref(); + let tx = n.block_tx_count.as_ref(); + let gas = n.block_gas_used.as_ref(); + let is_validator = validator_names.contains(n.name.as_str()); + let (threshold_pass, threshold_detail) = if is_validator { + if let Some(c) = check_by_name.get(n.name.as_str()) { + (Some(c.passed), c.message.clone()) + } else { + ( + Some(true), + "no threshold check row (unexpected for validator)".to_string(), + ) + } + } else { + ( + None, + "not thresholded (non-validator; shown for context)".to_string(), + ) + }; + PerfEntry { + name: n.name.clone(), + group: n.group.clone().unwrap_or_default(), + block_time_p50_ms: bt.map(|s| s.p50 * 1000.0).unwrap_or(0.0), + block_time_p95_ms: bt.map(|s| s.p95 * 1000.0).unwrap_or(0.0), + block_time_p99_ms: bt.map(|s| s.p99 * 1000.0).unwrap_or(0.0), + block_count: bt.map(|s| s.count).unwrap_or(0), + avg_tx_per_block: tx.map(|s| s.avg).unwrap_or(0.0), + avg_gas_used: gas.map(|s| s.avg).unwrap_or(0.0), + threshold_pass, + threshold_detail, + } + }) + .collect(); + + PerfSection { + p50_threshold_ms: p50_ms, + p99_threshold_ms: p99_ms, + passed: perf_report.passed(), + failures, + nodes, + validator_names, + } +} + +async fn build_mesh_section(testnet: &Testnet) -> Result { + let metrics_urls = testnet.nodes_metadata.all_consensus_metrics_urls(); + let raw_metrics = fetch_mesh_metrics(&metrics_urls).await; + let nodes_data = crate::mesh::parse_and_classify_metrics(&raw_metrics, &testnet.manifest.nodes); + if nodes_data.is_empty() { + bail!("No mesh metrics collected from any node"); + } + + let analysis = analyze(&nodes_data); + let classifications = classify_all(&analysis); + + let has_external_validators = classifications.iter().any(|(moniker, _, _)| { + testnet + .manifest + .nodes + .get(moniker.as_str()) + .map(|n| categorize_node(moniker, n, testnet) == NodeCategory::ExternalValidator) + .unwrap_or(false) + }); + + let mut max_hops: usize = 0; + let mut max_duplicate_pct: f64 = 0.0; + let mut entries = Vec::new(); + let mut failures = Vec::new(); + + for (moniker, _lib_node_type, tier) in &classifications { + let category = testnet + .manifest + .nodes + .get(moniker.as_str()) + .map(|n| categorize_node(moniker, n, testnet)) + .unwrap_or(NodeCategory::Excluded); + + let (passed, status_detail) = if category == NodeCategory::Excluded { + (true, "skipped".to_string()) + } else { + match check_strict(category, *tier, has_external_validators) { + Ok(detail) => (true, detail), + Err(reason) => { + failures.push(format!("{moniker} ({category}): {reason}")); + (false, reason) + } + } + }; + + if *tier == MeshTier::MultiHop { + max_hops = max_hops.max(2); + } + + entries.push(MeshEntry { + name: moniker.clone(), + category: category.to_string(), + tier: tier.to_string(), + passed, + status_detail, + }); + } + + for node in &nodes_data { + let pct = node.message_counts.duplicate_pct(); + if pct > max_duplicate_pct { + max_duplicate_pct = pct; + } + } + + // Compute max diameter from validator connectivity + for vc in &analysis.validator_connectivity { + max_hops = max_hops.max(vc.max_diameter); + } + + entries.sort_by(|a, b| a.name.cmp(&b.name)); + + Ok(MeshSection { + passed: failures.is_empty(), + entries, + max_hops, + max_duplicate_pct, + failures, + }) +} + +// ── Version summary rendering ──────────────────────────────────────────── + +/// Render node versions as a compact summary: groups by (arc, reth) version pair, +/// shows the majority as "All nodes on version ..." and lists outliers individually. +fn render_node_versions_summary(out: &mut String, versions: &[NodeVersion]) { + use std::collections::HashMap; + + let mut groups: HashMap<(&str, &str), Vec<&str>> = HashMap::new(); + for nv in versions { + groups + .entry((&nv.arc_version, &nv.reth_version)) + .or_default() + .push(&nv.name); + } + + if groups.len() == 1 { + let ((arc, reth), _) = groups.into_iter().next().unwrap(); + let _ = writeln!( + out, + "All {} nodes on version `{arc}` (reth `{reth}`)", + versions.len() + ); + return; + } + + let mut sorted: Vec<_> = groups.into_iter().collect(); + sorted.sort_by(|a, b| b.1.len().cmp(&a.1.len())); + + let ((maj_arc, maj_reth), maj_nodes) = sorted.remove(0); + let _ = writeln!( + out, + "{} node(s) on version `{maj_arc}` (reth `{maj_reth}`)", + maj_nodes.len() + ); + let _ = writeln!(out, "\nExceptions:\n"); + let _ = writeln!(out, "| Node | Arc Version | Reth Version |"); + let _ = writeln!(out, "|------|-------------|--------------|"); + for ((arc, reth), nodes) in &sorted { + for name in nodes { + let _ = writeln!(out, "| {name} | {arc} | {reth} |"); + } + } +} + +// ── Markdown rendering ────────────────────────────────────────────────── + +fn render_markdown(data: &ReportData) -> String { + let mut out = String::with_capacity(8192); + + // ── Header ────────────────────────────────────────────────────── + let _ = writeln!(out, "# Network Testing Report\n"); + let _ = writeln!(out, "| Field | Value |"); + let _ = writeln!(out, "|-------|-------|"); + let _ = writeln!(out, "| Date | {} |", data.timestamp); + let _ = writeln!(out, "| Quake Version | `{}` |", data.quake_version); + let _ = writeln!(out, "| Quake Commit | `{}` |", data.quake_commit); + let _ = writeln!(out, "| Manifest | `{}` |", data.manifest_name); + + if !data.node_versions.is_empty() { + let _ = writeln!(out, "\n### Node Versions\n"); + render_node_versions_summary(&mut out, &data.node_versions); + } + + let _ = writeln!(out, "\n### Parameters\n"); + let _ = writeln!(out, "| Parameter | Value |"); + let _ = writeln!(out, "|-----------|-------|"); + for (key, value) in &data.effective_params { + let _ = writeln!(out, "| `{key}` | {value} |"); + } + + // ── Summary ───────────────────────────────────────────────────── + let _ = writeln!(out, "\n## Summary\n"); + let _ = writeln!(out, "| Section | Result | Details |"); + let _ = writeln!(out, "|---------|--------|---------|"); + + let mesh_detail = format_summary_with_failures( + &format_mesh_summary(&data.mesh), + data.mesh.passed, + &data.mesh.failures, + ); + let health_detail = format_summary_with_failures( + &format_health_summary(&data.health), + data.health.passed, + &data.health.failures, + ); + let perf_detail = format_summary_with_failures( + &format_perf_summary(&data.perf), + data.perf.passed, + &data.perf.failures, + ); + + let _ = writeln!( + out, + "| Mesh | {} | {} |", + status_icon(data.mesh.passed), + mesh_detail + ); + let _ = writeln!( + out, + "| Health | {} | {} |", + status_icon(data.health.passed), + health_detail + ); + let _ = writeln!( + out, + "| Performance | {} | {} |", + status_icon(data.perf.passed), + perf_detail + ); + if let Some(ref sanity) = data.sanity { + for phase in &sanity.phases { + let _ = writeln!( + out, + "| Sanity: {} | {} | {} ({:.0}s) |", + phase.name, + status_icon(phase.passed), + phase.detail, + phase.duration.as_secs_f64(), + ); + } + } + for sync in &data.sync_speed { + let _ = writeln!( + out, + "| Sync Speed ({}) | {} | {} |", + sync.node, + status_icon(sync.passed), + format_sync_speed_summary(sync), + ); + } + + // ── Liveness note ─────────────────────────────────────────────── + render_liveness_note(&mut out, &data.liveness); + + // ── Mesh section ──────────────────────────────────────────────── + let _ = writeln!( + out, + "\n## Mesh Topology β€” {}\n", + status_icon(data.mesh.passed) + ); + render_mesh_details(&mut out, &data.mesh); + + // ── Health section ────────────────────────────────────────────── + let _ = writeln!(out, "\n## Health β€” {}\n", status_icon(data.health.passed)); + render_health_details(&mut out, &data.health); + + // ── Performance section ───────────────────────────────────────── + let _ = writeln!( + out, + "\n## Performance β€” {}\n", + status_icon(data.perf.passed) + ); + let _ = writeln!( + out, + "Pass/fail uses **validator** block-time thresholds only (see table below). Other nodes are listed for context.\n" + ); + render_perf_details(&mut out, &data.perf); + + // ── Sanity section ──────────────────────────────────────────────── + if let Some(ref sanity) = data.sanity { + let _ = writeln!( + out, + "\n## Sanity (Arc Node) β€” {}\n", + status_icon(sanity.passed) + ); + render_sanity_details(&mut out, sanity); + } + + // ── Sync Speed section ───────────────────────────────────────── + if !data.sync_speed.is_empty() { + let all_sync_passed = data.sync_speed.iter().all(|s| s.passed); + let _ = writeln!(out, "\n## Sync Speed β€” {}\n", status_icon(all_sync_passed)); + for sync in &data.sync_speed { + let _ = writeln!(out, "### {} β€” {}\n", sync.node, status_icon(sync.passed)); + render_sync_speed_details(&mut out, sync); + let _ = writeln!(out); + } + } + + // ── Failures ──────────────────────────────────────────────────── + let mut all_failures: Vec<(&str, &[String])> = vec![ + ("Mesh", data.mesh.failures.as_slice()), + ("Health", data.health.failures.as_slice()), + ("Performance", data.perf.failures.as_slice()), + ]; + if let Some(ref sanity) = data.sanity { + all_failures.push(("Sanity", sanity.failures.as_slice())); + } + let sync_failures: Vec = data + .sync_speed + .iter() + .filter(|s| !s.passed) + .map(|s| format!("{}: {}", s.node, s.detail)) + .collect(); + if !sync_failures.is_empty() { + all_failures.push(("Sync Speed", &sync_failures)); + } + let all_failures: Vec<(&str, &[String])> = all_failures + .into_iter() + .filter(|(_, f)| !f.is_empty()) + .collect(); + + if !all_failures.is_empty() { + let _ = writeln!(out, "\n## Failures\n"); + for (section, failures) in all_failures { + let _ = writeln!(out, "### {section}\n"); + for f in failures { + let _ = writeln!(out, "- {f}"); + } + let _ = writeln!(out); + } + } + + // ── Appendices ─────────────────────────────────────────────────── + let _ = writeln!(out, "\n---\n"); + let _ = writeln!(out, "# Appendices\n"); + + let _ = writeln!(out, "## Appendix A: Mesh (`quake info mesh`)\n"); + let _ = writeln!(out, "```\n{}\n```", data.info_mesh.trim()); + + let _ = writeln!(out, "\n## Appendix B: Performance (`quake info perf`)\n"); + let _ = writeln!( + out, + "> The **Performance** section above uses **observation-window** (delta) data for pass/fail. \ + This appendix includes both the window view and **cumulative since process start** so you can compare two reports (e.g. different `duration_s`, load, or manifests).\n" + ); + let _ = writeln!(out, "### B.1 Observation window (delta between scrapes)\n"); + let _ = writeln!(out, "```\n{}\n```", data.info_perf_observation.trim()); + let _ = writeln!(out, "\n### B.2 Cumulative (since process start)\n"); + let _ = writeln!(out, "```\n{}\n```", data.info_perf_cumulative.trim()); + + let _ = writeln!(out, "\n## Appendix C: Health (`quake info health`)\n"); + let _ = writeln!( + out, + "> The **Health** section above uses **observation-window** (delta) data for pass/fail. \ + This appendix shows **cumulative** counters at the end of the run (since process start), \ + so you can compare two reports (e.g. different `duration_s`, load, or manifests).\n" + ); + let _ = writeln!(out, "```\n{}\n```", data.info_health_cumulative.trim()); + + let _ = writeln!(out, "\n## Appendix D: Store (`quake info store`)\n"); + let _ = writeln!(out, "{}", data.info_store.trim()); + + let _ = writeln!(out, "\n## Appendix E: Manifest\n"); + let _ = writeln!(out, "**Path:** `{}`\n", data.manifest_path); + let _ = writeln!(out, "```toml\n{}\n```", data.manifest_content.trim()); + + let _ = writeln!(out, "\n## Appendix F: Reproduce\n"); + let _ = write!(out, "```\nquake report"); + for (key, value) in &data.effective_params { + if value.contains(' ') || value.contains(',') { + let _ = write!(out, " \\\n --set '{key}={value}'"); + } else { + let _ = write!(out, " \\\n --set {key}={value}"); + } + } + let _ = writeln!(out, " \\\n -o {}", data.output_path); + let _ = writeln!(out, "```"); + + out +} + +fn status_icon(passed: bool) -> &'static str { + if passed { + "PASS" + } else { + "FAIL" + } +} + +fn render_liveness_note(out: &mut String, liveness: &LivenessSection) { + let all_healthy = liveness.down_before.is_empty() && liveness.down_after.is_empty(); + if all_healthy { + let _ = writeln!( + out, + "\n> **Node Liveness**: All {} nodes healthy\n", + liveness.total_nodes + ); + return; + } + + let _ = writeln!(out, "\n> **Node Liveness**"); + + if !liveness.down_before.is_empty() { + let _ = writeln!( + out, + "> - Down **before** report: {}", + liveness.down_before.join(", "), + ); + } + if !liveness.down_after.is_empty() { + let newly_down: Vec<_> = liveness + .down_after + .iter() + .filter(|n| !liveness.down_before.contains(n)) + .map(|s| s.as_str()) + .collect(); + let still_down: Vec<_> = liveness + .down_after + .iter() + .filter(|n| liveness.down_before.contains(n)) + .map(|s| s.as_str()) + .collect(); + if !newly_down.is_empty() { + let _ = writeln!( + out, + "> - Went down **during** report: {}", + newly_down.join(", "), + ); + } + if !still_down.is_empty() { + let _ = writeln!( + out, + "> - Still down **after** report: {}", + still_down.join(", "), + ); + } + } + let recovered: Vec<_> = liveness + .down_before + .iter() + .filter(|n| !liveness.down_after.contains(n)) + .map(|s| s.as_str()) + .collect(); + if !recovered.is_empty() { + let _ = writeln!( + out, + "> - **Recovered** during report: {}", + recovered.join(", "), + ); + } + let _ = writeln!(out); +} + +/// When a section failed, append the first few [`failures`] entries so the Summary +/// table explains *why* without reading the `## Failures` section. Values are +/// single-lined and `|`-safe for markdown tables. +fn format_summary_with_failures(summary: &str, passed: bool, failures: &[String]) -> String { + if passed || failures.is_empty() { + return summary.to_string(); + } + let mut note: String = failures + .iter() + .take(2) + .map(|f| f.replace('\n', " ").replace('|', "/")) + .collect::>() + .join("; "); + if failures.len() > 2 { + use std::fmt::Write; + let _ = write!( + &mut note, + "; +{} more (see ## Failures)", + failures.len() - 2 + ); + } + format!("{summary} β€” {note}") +} + +fn format_mesh_summary(mesh: &MeshSection) -> String { + let total = mesh.entries.len(); + let excluded = mesh + .entries + .iter() + .filter(|e| e.category == "excluded") + .count(); + let validators = mesh + .entries + .iter() + .filter(|e| e.category.contains("validator")) + .count(); + let fully_connected = mesh + .entries + .iter() + .filter(|e| e.tier == "fully-connected" && e.category != "excluded") + .count(); + let active = total - excluded; + + format!( + "{fully_connected}/{active} fully connected, {validators} validators, max {max_hops} hops, dup {dup:.1}%", + max_hops = mesh.max_hops, + dup = mesh.max_duplicate_pct, + ) +} + +fn format_health_summary(health: &HealthSection) -> String { + let node_count = health.nodes.len(); + let mut parts = Vec::new(); + parts.push(format!( + "{} heights observed across {node_count} nodes", + health.max_decisions + )); + if health.any_round_gt0 { + let count = health.nodes.iter().filter(|n| n.round_gt0 > 0).count(); + parts.push(format!("{count} node(s) with round>0")); + } else { + parts.push("all round-0".into()); + } + if health.any_restarts { + let count = health.nodes.iter().filter(|n| n.restarts > 0).count(); + parts.push(format!("{count} node(s) restarted")); + } else { + parts.push("no restarts".into()); + } + if health.any_sync_behind { + let count = health.nodes.iter().filter(|n| n.sync_behind > 0).count(); + parts.push(format!("{count} node(s) fell behind")); + } else { + parts.push("no sync-behind".into()); + } + parts.join(", ") +} + +fn format_perf_summary(perf: &PerfSection) -> String { + let validators = perf + .nodes + .iter() + .filter(|n| perf.validator_names.contains(&n.name)); + let worst_p50 = validators + .clone() + .map(|n| n.block_time_p50_ms) + .fold(0.0f64, f64::max); + let worst_p99 = validators + .map(|n| n.block_time_p99_ms) + .fold(0.0f64, f64::max); + format!( + "validators worst p50={:.0}ms (threshold {}ms), worst p99={:.0}ms (threshold {}ms)", + worst_p50, perf.p50_threshold_ms, worst_p99, perf.p99_threshold_ms + ) +} + +fn render_sanity_details(out: &mut String, sanity: &SanitySection) { + let _ = writeln!(out, "| Phase | Result | Duration | Details |"); + let _ = writeln!(out, "|-------|--------|----------|---------|"); + for phase in &sanity.phases { + let _ = writeln!( + out, + "| {} | {} | {:.1}s | {} |", + phase.name, + status_icon(phase.passed), + phase.duration.as_secs_f64(), + phase.detail, + ); + } +} + +fn format_sync_speed_summary(sync: &SyncSpeedSection) -> String { + match &sync.result { + Some(r) => { + let status = if r.caught_up { "caught up" } else { "partial" }; + format!( + "{}: {:.1} blk/s ({status}, {} blocks in {:.0}s, min {:.1})", + sync.node, + r.avg_bps, + r.total_blocks, + r.elapsed.as_secs_f64(), + sync.min_bps, + ) + } + None => sync.detail.clone(), + } +} + +fn render_sync_speed_details(out: &mut String, sync: &SyncSpeedSection) { + let _ = writeln!(out, "| Field | Value |"); + let _ = writeln!(out, "|-------|-------|"); + let _ = writeln!(out, "| Node | {} |", sync.node); + let _ = writeln!(out, "| Reference | {} |", sync.reference); + let _ = writeln!(out, "| Min required | {:.1} blk/s |", sync.min_bps); + let _ = writeln!( + out, + "| Total duration | {:.1}s |", + sync.duration.as_secs_f64() + ); + + if let Some(ref r) = sync.result { + let _ = writeln!(out, "| Start height | {} |", r.start_height); + let _ = writeln!(out, "| Target height | {} |", r.target_height); + let _ = writeln!(out, "| Final height | {} |", r.final_height); + let _ = writeln!(out, "| Blocks synced | {} |", r.total_blocks); + let _ = writeln!(out, "| Sync time | {:.1}s |", r.elapsed.as_secs_f64()); + let _ = writeln!(out, "| Avg speed | {:.1} blk/s |", r.avg_bps); + let _ = writeln!( + out, + "| Caught up | {} |", + if r.caught_up { "yes" } else { "no" } + ); + let _ = writeln!(out, "| Result | {} |", status_icon(sync.passed)); + } else { + let _ = writeln!(out, "| Result | {} β€” {} |", status_icon(false), sync.detail); + } +} + +fn render_mesh_details(out: &mut String, mesh: &MeshSection) { + let _ = writeln!(out, "### Checks\n"); + let _ = writeln!(out, "- **Circle validators** must be **FullyConnected** unless any **external** validator is in the manifest, in which case **MultiHop** is allowed (indirect paths to external validators)."); + let _ = writeln!(out, "- **External validators** must be reachable (not `NotConnected`); **MultiHop** is ok behind a sentry."); + let _ = writeln!( + out, + "- **Other consensus** (non-validator) nodes should be connected (not `NotConnected`)." + ); + let _ = writeln!( + out, + "- **Excluded** nodes are not considered for mesh health.\n" + ); + + // Group by category for a compact summary + let mut by_category: Vec<(&str, Vec<&MeshEntry>)> = Vec::new(); + for entry in &mesh.entries { + if let Some((_, vec)) = by_category + .iter_mut() + .find(|(c, _)| *c == entry.category.as_str()) + { + vec.push(entry); + } else { + by_category.push((&entry.category, vec![entry])); + } + } + + for (category, entries) in &by_category { + let tier_summary: Vec = { + let mut counts: Vec<(&str, usize)> = Vec::new(); + for e in entries { + if let Some((_, c)) = counts.iter_mut().find(|(t, _)| *t == e.tier.as_str()) { + *c += 1; + } else { + counts.push((&e.tier, 1)); + } + } + counts + .iter() + .map(|(tier, count)| format!("{count} {tier}")) + .collect() + }; + let _ = writeln!( + out, + "- **{category}** ({} nodes): {}", + entries.len(), + tier_summary.join(", ") + ); + } + + let _ = writeln!(out, "- **Max hop distance**: {}", mesh.max_hops); + let _ = writeln!( + out, + "- **Max duplicate rate**: {:.1}%", + mesh.max_duplicate_pct + ); + + let _ = writeln!(out, "\n### Per-Node Classification\n"); + let _ = writeln!(out, "| Node | Category | Tier | Result | Detail |"); + let _ = writeln!(out, "|------|----------|------|--------|--------|"); + for entry in &mesh.entries { + let (result, detail) = if entry.passed { + (status_icon(true), entry.status_detail.clone()) + } else { + (status_icon(false), entry.status_detail.clone()) + }; + let _ = writeln!( + out, + "| {} | {} | {} | {} | {} |", + entry.name, entry.category, entry.tier, result, detail + ); + } +} + +fn render_health_details(out: &mut String, health: &HealthSection) { + let _ = writeln!(out, "### Checks\n"); + let _ = writeln!(out, "Per node, deltas over the observation window: **R>0 = 0** (all decisions at round 0), **height restarts = 0**, **sync fell behind = 0**. A negative `decisions` delta is treated as pass/skipped (likely restart or counter reset).\n"); + let _ = writeln!( + out, + "Largest `decisions` delta in the window (any node): **{}**.\n", + health.max_decisions + ); + + let _ = writeln!( + out, + "| Node | Decisions (Ξ”) | Round >0 (Ξ”) | Restarts (Ξ”) | Sync behind (Ξ”) | Result | Notes |" + ); + let _ = writeln!( + out, + "|------|--------------:|-------------:|-------------:|----------------:|--------|-------|" + ); + for entry in &health.nodes { + let _ = writeln!( + out, + "| {} | {} | {} | {} | {} | {} | {} |", + entry.name, + entry.decisions, + entry.round_gt0, + entry.restarts, + entry.sync_behind, + status_icon(entry.passed), + entry.check_detail + ); + } +} + +fn render_perf_details(out: &mut String, perf: &PerfSection) { + let _ = writeln!(out, "### Checks\n"); + let _ = writeln!( + out, + "Validators only (same as report pass/fail): block time **p50** < **{}** ms and **p99** < **{}** ms, using **deltas** between the two scrapes. Other nodes: **N/A** in the result column (not thresholded here).\n", + perf.p50_threshold_ms, perf.p99_threshold_ms + ); + + let _ = writeln!(out, "#### Block Time\n"); + let _ = writeln!( + out, + "| Node | Group | p50 (ms) | p95 (ms) | p99 (ms) | Blocks | Result | Notes |" + ); + let _ = writeln!( + out, + "|------|-------|----------:|----------:|----------:|-------:|--------|-------|" + ); + for entry in &perf.nodes { + let (res_col, note) = match entry.threshold_pass { + None => ("N/A".to_string(), entry.threshold_detail.clone()), + Some(true) => ( + status_icon(true).to_string(), + entry.threshold_detail.clone(), + ), + Some(false) => ( + status_icon(false).to_string(), + entry.threshold_detail.clone(), + ), + }; + let _ = writeln!( + out, + "| {} | {} | {:.0} | {:.0} | {:.0} | {} | {} | {} |", + entry.name, + entry.group, + entry.block_time_p50_ms, + entry.block_time_p95_ms, + entry.block_time_p99_ms, + entry.block_count, + res_col, + note + ); + } + + let has_throughput = perf.nodes.iter().any(|n| n.avg_tx_per_block > 0.0); + if has_throughput { + let _ = writeln!(out, "\n#### Throughput (informational)\n"); + let _ = writeln!(out, "| Node | Avg Tx/Block | Avg Gas Used |"); + let _ = writeln!(out, "|------|-------------:|-------------:|"); + for entry in &perf.nodes { + let _ = writeln!( + out, + "| {} | {:.1} | {:.0} |", + entry.name, entry.avg_tx_per_block, entry.avg_gas_used + ); + } + } +} + +// ── Sanity (snapshot recovery + MEV + mempool + tx forwarding) ────────── + +async fn run_sanity_phases(testnet: &Testnet, params: &TestParams) -> SanitySection { + use crate::tests::RpcClientFactory; + + let factory = RpcClientFactory::new(Duration::from_secs(10)); + let mut phases = Vec::new(); + let mut failures = Vec::new(); + + // Resolve arc-node names: explicit param (names and/or node-group names) or + // the manifest's `ARC_NODES` group when unset. + let arc_node_names = match crate::tests::arc_node::resolve_arc_nodes(testnet, params) { + Ok(names) => names, + Err(e) => { + let msg = format!("Failed to resolve arc nodes: {e:#}"); + info!("Sanity: {msg}"); + return failed_sanity_section(msg); + } + }; + if arc_node_names.is_empty() { + let msg = format!( + "Skipped: no arc nodes resolved (set --set arc_nodes=… or define the \ + {} node-group in the manifest)", + crate::tests::arc_node::DEFAULT_ARC_NODES_GROUP + ); + info!("Sanity: {msg}"); + return skipped_sanity_section(msg); + } + let snapshot_provider = params.get_or("snapshot_provider", DEFAULT_SNAPSHOT_PROVIDER); + let reference = params.get_or("reference", DEFAULT_REFERENCE); + + let arc_node_urls: Vec<_> = testnet + .nodes_metadata + .all_execution_urls() + .into_iter() + .filter(|(name, _)| arc_node_names.contains(name)) + .collect(); + + let required_nodes: Vec<&str> = arc_node_names + .iter() + .map(|s| s.as_str()) + .chain(std::iter::once(reference.as_str())) + .chain(std::iter::once(snapshot_provider.as_str())) + .collect(); + let missing: Vec<&&str> = required_nodes + .iter() + .filter(|n| testnet.nodes_metadata.execution_http_url(n).is_none()) + .collect(); + if !missing.is_empty() { + let msg = format!( + "Skipped: missing nodes {}", + missing.iter().map(|n| **n).collect::>().join(", ") + ); + info!("Sanity: {msg}"); + return skipped_sanity_section(msg); + } + + // Phase 1: Snapshot recovery (auto-skips in remote mode inside arc_node.rs) + let t0 = std::time::Instant::now(); + info!("[Sanity Phase 1] Snapshot recovery"); + let phase1 = crate::tests::arc_node::snapshot_recovery( + testnet, + &factory, + &reference, + &snapshot_provider, + &arc_node_urls, + ) + .await; + let d1 = t0.elapsed(); + let detail = if testnet.is_remote() { + "Skipped (remote mode)".to_string() + } else { + format!( + "Snapshot from \"{snapshot_provider}\", restored [{}]", + arc_node_names + .iter() + .map(|n| format!("\"{n}\"")) + .collect::>() + .join(", ") + ) + }; + match &phase1 { + Ok(()) => { + phases.push(SanityPhase { + name: "Snapshot recovery".to_string(), + passed: true, + duration: d1, + detail, + }); + } + Err(e) => { + let msg = format!("Snapshot recovery: {e:#}"); + phases.push(SanityPhase { + name: "Snapshot recovery".to_string(), + passed: false, + duration: d1, + detail: msg.clone(), + }); + failures.push(msg); + } + } + + // Phase 2: MEV protection + let t0 = std::time::Instant::now(); + info!("[Sanity Phase 2] MEV protection"); + let phase2 = crate::tests::arc_node::mev_protection( + testnet, + &arc_node_names, + arc_checks::mev::DEFAULT_ADDR, + ) + .await; + let d2 = t0.elapsed(); + match &phase2 { + Ok(()) => { + phases.push(SanityPhase { + name: "MEV protection".to_string(), + passed: true, + duration: d2, + detail: format!( + "Pending state blocked on [{}]", + arc_node_names + .iter() + .map(|n| format!("\"{n}\"")) + .collect::>() + .join(", ") + ), + }); + } + Err(e) => { + let msg = format!("MEV protection: {e:#}"); + phases.push(SanityPhase { + name: "MEV protection".to_string(), + passed: false, + duration: d2, + detail: msg.clone(), + }); + failures.push(msg); + } + } + + // Phase 3: Mempool empty + let t0 = std::time::Instant::now(); + info!("[Sanity Phase 3] Mempool empty"); + let phase3 = + crate::tests::arc_node::mempool_empty(testnet, &arc_node_names, &arc_node_urls).await; + let d3 = t0.elapsed(); + match &phase3 { + Ok(()) => { + phases.push(SanityPhase { + name: "Mempool empty".to_string(), + passed: true, + duration: d3, + detail: format!( + "Mempools empty on [{}]", + arc_node_names + .iter() + .map(|n| format!("\"{n}\"")) + .collect::>() + .join(", ") + ), + }); + } + Err(e) => { + let msg = format!("Mempool empty: {e:#}"); + phases.push(SanityPhase { + name: "Mempool empty".to_string(), + passed: false, + duration: d3, + detail: msg.clone(), + }); + failures.push(msg); + } + } + + // Phase 4: Transaction forwarding + let t0 = std::time::Instant::now(); + info!("[Sanity Phase 4] Transaction forwarding"); + let phase4 = + crate::tests::arc_node::tx_forwarding(testnet, &factory, &reference, &arc_node_urls).await; + let d4 = t0.elapsed(); + match &phase4 { + Ok(()) => { + phases.push(SanityPhase { + name: "Tx forwarding".to_string(), + passed: true, + duration: d4, + detail: format!( + "Txs forwarded and included on [{}]", + arc_node_names + .iter() + .map(|n| format!("\"{n}\"")) + .collect::>() + .join(", ") + ), + }); + } + Err(e) => { + let msg = format!("Tx forwarding: {e:#}"); + phases.push(SanityPhase { + name: "Tx forwarding".to_string(), + passed: false, + duration: d4, + detail: msg.clone(), + }); + failures.push(msg); + } + } + + let passed = failures.is_empty(); + SanitySection { + phases, + passed, + failures, + } +} + +// ── Sync speed ────────────────────────────────────────────────────────── + +fn default_sync_nodes() -> Vec { + vec![DEFAULT_SYNC_NODE.to_string()] +} + +async fn run_sync_speed_all(testnet: &Testnet, params: &TestParams) -> Vec { + let node_param = params.get_or("sync_nodes", ""); + let nodes: Vec = if node_param.is_empty() { + default_sync_nodes() + } else { + node_param + .split(',') + .map(|s| s.trim().to_owned()) + .filter(|s| !s.is_empty()) + .collect() + }; + + let reference = params.get_or("reference", DEFAULT_REFERENCE); + let min_bps: f64 = params + .get_or("sync_min_bps", &DEFAULT_SYNC_MIN_BPS.to_string()) + .parse() + .unwrap_or(DEFAULT_SYNC_MIN_BPS); + let timeout_s: u64 = params + .get_or("sync_timeout_s", &DEFAULT_SYNC_TIMEOUT_S.to_string()) + .parse() + .unwrap_or(DEFAULT_SYNC_TIMEOUT_S); + let downtime_s: u64 = params + .get_or("sync_downtime_s", &DEFAULT_SYNC_DOWNTIME_S.to_string()) + .parse() + .unwrap_or(DEFAULT_SYNC_DOWNTIME_S); + + let mut results = Vec::new(); + for node in &nodes { + info!("Running sync speed for {node}..."); + results.push( + run_sync_speed_single(testnet, node, &reference, min_bps, timeout_s, downtime_s).await, + ); + } + results +} + +async fn run_sync_speed_single( + testnet: &Testnet, + node: &str, + reference: &str, + min_bps: f64, + timeout_s: u64, + downtime_s: u64, +) -> SyncSpeedSection { + let node_url = match testnet.nodes_metadata.execution_http_url(node) { + Some(url) => url, + None => { + let msg = format!("Skipped: node '{node}' not in manifest"); + info!("Sync speed: {msg}"); + return SyncSpeedSection { + node: node.to_string(), + reference: reference.to_string(), + result: None, + min_bps, + passed: false, + detail: msg, + duration: Duration::ZERO, + }; + } + }; + let ref_url = match testnet.nodes_metadata.execution_http_url(reference) { + Some(url) => url, + None => { + let msg = format!("Skipped: reference node '{reference}' not in manifest"); + info!("Sync speed: {msg}"); + return SyncSpeedSection { + node: node.to_string(), + reference: reference.to_string(), + result: None, + min_bps, + passed: false, + detail: msg, + duration: Duration::ZERO, + }; + } + }; + + info!("Sync speed: {node} β†’ {reference} (min {min_bps:.1} blk/s, timeout {timeout_s}s, downtime {downtime_s}s)"); + + let t0 = std::time::Instant::now(); + + let http = reqwest::Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .expect("http client"); + + if arc_checks::poll_height(&http, &node_url).await.is_some() { + info!("{node} is up β€” stopping for {downtime_s}s to build a gap"); + if let Err(e) = testnet.stop(vec![node.to_string()]).await { + let msg = format!("Failed to stop {node}: {e:#}"); + return SyncSpeedSection { + node: node.to_string(), + reference: reference.to_string(), + result: None, + min_bps, + passed: false, + detail: msg, + duration: t0.elapsed(), + }; + } + tokio::time::sleep(Duration::from_secs(downtime_s)).await; + info!("Starting {node}"); + if let Err(e) = testnet.start(vec![node.to_string()], false).await { + let msg = format!("Failed to start {node}: {e:#}"); + return SyncSpeedSection { + node: node.to_string(), + reference: reference.to_string(), + result: None, + min_bps, + passed: false, + detail: msg, + duration: t0.elapsed(), + }; + } + } + + let config = arc_checks::SyncSpeedConfig { + node_name: node.to_string(), + node_url, + reference_name: reference.to_string(), + reference_url: ref_url, + max_duration: if timeout_s == 0 { + Duration::MAX + } else { + Duration::from_secs(timeout_s) + }, + }; + + match arc_checks::collect_sync_speed(config).await { + Ok(result) => { + let report = arc_checks::check_sync_speed(&result, min_bps); + let passed = report.passed(); + let detail = format!("{result}"); + let duration = t0.elapsed(); + + SyncSpeedSection { + node: node.to_string(), + reference: reference.to_string(), + result: Some(result), + min_bps, + passed, + detail, + duration, + } + } + Err(e) => { + let msg = format!("Sync speed measurement failed: {e:#}"); + SyncSpeedSection { + node: node.to_string(), + reference: reference.to_string(), + result: None, + min_bps, + passed: false, + detail: msg, + duration: t0.elapsed(), + } + } + } +} + +// ── Node version collection ───────────────────────────────────────────── + +/// Query each node's EL for arc_getVersion (Arc version) and web3_clientVersion (Reth version). +/// CL version is not queried separately β€” it is always built from the same commit as the EL. +async fn collect_node_versions(testnet: &Testnet) -> Vec { + use futures::future::join_all; + + let http = reqwest::Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .unwrap_or_default(); + + let tasks: Vec<_> = testnet + .nodes_metadata + .nodes + .iter() + .map(|(name, meta)| { + let name = name.clone(); + let el_url = meta.execution.http_url.clone(); + let http = http.clone(); + async move { + let (arc_version, reth_version) = fetch_node_versions(&http, &el_url).await; + NodeVersion { + name, + arc_version, + reth_version, + } + } + }) + .collect(); + + join_all(tasks).await +} + +/// Fetch both arc_getVersion and web3_clientVersion from a single node. +async fn fetch_node_versions(http: &reqwest::Client, url: &Url) -> (String, String) { + let arc_ver = async { + let body = serde_json::json!({ + "jsonrpc": "2.0", "method": "arc_getVersion", "params": [], "id": 1 + }); + match http.post(url.as_str()).json(&body).send().await { + Ok(resp) => match resp.json::().await { + Ok(json) => json + .get("result") + .and_then(|v| v.get("git_version")) + .and_then(|v| v.as_str()) + .unwrap_or("(error)") + .to_string(), + Err(_) => "(parse error)".to_string(), + }, + Err(_) => "(unreachable)".to_string(), + } + }; + + let reth_ver = async { + let body = serde_json::json!({ + "jsonrpc": "2.0", "method": "web3_clientVersion", "params": [], "id": 2 + }); + match http.post(url.as_str()).json(&body).send().await { + Ok(resp) => match resp.json::().await { + Ok(json) => json + .get("result") + .and_then(|v| v.as_str()) + .unwrap_or("(error)") + .to_string(), + Err(_) => "(parse error)".to_string(), + }, + Err(_) => "(unreachable)".to_string(), + } + }; + + tokio::join!(arc_ver, reth_ver) +} + +// ── Info appendices ───────────────────────────────────────────────────── + +/// Capture the text output of `quake info mesh`, `perf` (observation + cumulative), `health` +/// (cumulative), and `store`. +async fn collect_info_appendices( + testnet: &Testnet, + params: &TestParams, + raw_before: &[(String, String)], + raw_after: &[(String, String)], + observation_secs: u64, +) -> (String, String, String, String, String) { + // Mesh + let info_mesh = { + let metrics_urls = testnet.nodes_metadata.all_consensus_metrics_urls(); + let raw_metrics = crate::mesh::fetch_all_metrics(&metrics_urls).await; + let nodes_data = + crate::mesh::parse_and_classify_metrics(&raw_metrics, &testnet.manifest.nodes); + if nodes_data.is_empty() { + "No mesh metrics available.\n".to_string() + } else { + let analysis = crate::mesh::analyze(&nodes_data); + let options = crate::mesh::MeshDisplayOptions { + show_counts: true, + show_mesh: true, + show_peers: false, + show_peers_full: false, + show_duplicates: true, + }; + crate::mesh::format_report(&analysis, &options) + } + }; + + // Perf: observation window (delta β€” matches main Performance section) + let info_perf_observation = { + let nodes = crate::util::parse_perf_metrics_delta_with_groups( + raw_before, + raw_after, + &testnet.manifest.nodes, + ); + if nodes.is_empty() { + "No perf metrics available.\n".to_string() + } else { + let options = arc_checks::PerfDisplayOptions { + show_latency: true, + show_throughput: true, + show_summary: true, + }; + arc_checks::format_perf_report( + &nodes, + &options, + arc_checks::PerfReportKind::Interval { observation_secs }, + ) + } + }; + + // Perf: cumulative since process start (final scrape) + let info_perf_cumulative = { + let nodes = crate::util::parse_perf_metrics_with_groups(raw_after, &testnet.manifest.nodes); + if nodes.is_empty() { + "No perf metrics available.\n".to_string() + } else { + let options = arc_checks::PerfDisplayOptions { + show_latency: true, + show_throughput: true, + show_summary: true, + }; + arc_checks::format_perf_report( + &nodes, + &options, + arc_checks::PerfReportKind::CumulativeSinceStart, + ) + } + }; + + // Health: cumulative (final scrape). The observation-window delta view is + // already fully represented in the main Health section table, so it is not + // duplicated here. + let info_health_cumulative = { + let mut nodes_data = arc_checks::parse_all_health_metrics(raw_after); + crate::util::assign_node_groups( + nodes_data + .iter_mut() + .map(|n| (n.name.as_str(), &mut n.group)), + &testnet.manifest.nodes, + ); + arc_checks::format_health_report(&nodes_data) + }; + + // Store β€” explicit list or auto-derive from nodes with CL pruning configured + let info_store = { + let store_param = params.get_or("store_nodes", ""); + let pruned_nodes: Vec = if store_param.is_empty() { + testnet + .manifest + .nodes + .iter() + .filter(|(_, node)| node.cl_store_pruning_configured()) + .map(|(name, _)| name.clone()) + .collect() + } else { + store_param + .split(',') + .map(|s| s.trim().to_owned()) + .filter(|s| !s.is_empty()) + .collect() + }; + + let mut out = String::new(); + for node_name in &pruned_nodes { + let store_path = testnet + .dir + .join(node_name.as_str()) + .join("malachite") + .join("store.db"); + if !store_path.exists() { + continue; + } + match arc_checks::collect_store_info(&store_path) { + Ok(info) => { + let size_mb = info.size_bytes as f64 / (1024.0 * 1024.0); + let _ = writeln!(out, "### {node_name}\n"); + let _ = writeln!(out, "Size: {size_mb:.2} MB\n"); + let _ = writeln!(out, "| Table | Records | Min Key | Max Key |"); + let _ = writeln!(out, "|-------|--------:|--------:|--------:|"); + for t in &info.height_tables { + let min = t.min_height.map(|h| h.to_string()).unwrap_or("-".into()); + let max = t.max_height.map(|h| h.to_string()).unwrap_or("-".into()); + let _ = writeln!(out, "| {} | {} | {} | {} |", t.name, t.records, min, max); + } + for t in &info.composite_tables { + let _ = writeln!(out, "| {} | {} | | |", t.name, t.records); + } + let _ = writeln!(out); + } + Err(e) => { + let _ = writeln!(out, "### {node_name}\n\nerror: {e}\n"); + } + } + } + if out.is_empty() { + "No pruned nodes found.\n".to_string() + } else { + out + } + }; + + ( + info_mesh, + info_perf_observation, + info_perf_cumulative, + info_health_cumulative, + info_store, + ) +} + +// ── Load helpers (shared with sanity.rs) ──────────────────────────────── + +// ── Public entry point ────────────────────────────────────────────────── + +pub(crate) async fn run_report( + testnet: &Testnet, + params: &TestParams, + output: &std::path::Path, +) -> Result<()> { + info!("Generating network testing report..."); + + let data = collect_report_data(testnet, params, output).await?; + let markdown = render_markdown(&data); + + std::fs::write(output, &markdown)?; + info!("Report written to {}", output.display()); + + // Print one-line summary to stdout + let sanity_passed = data.sanity.as_ref().is_none_or(|s| s.passed); + let sync_passed = data.sync_speed.iter().all(|s| s.passed); + let overall = + data.mesh.passed && data.health.passed && data.perf.passed && sanity_passed && sync_passed; + let sanity_checks = data.sanity.as_ref().map_or(0, |s| s.phases.len()); + let sanity_failures = data.sanity.as_ref().map_or(0, |s| s.failures.len()); + let sync_checks = data.sync_speed.len(); + let sync_fail_count = data.sync_speed.iter().filter(|s| !s.passed).count(); + let total_checks = data.mesh.entries.len() + + data.health.nodes.len() + + data.perf.nodes.len() + + sanity_checks + + sync_checks; + let total_failures = data.mesh.failures.len() + + data.health.failures.len() + + data.perf.failures.len() + + sanity_failures + + sync_fail_count; + + if overall { + println!( + "\nβœ… All checks passed ({total_checks} checks). Report: {}", + output.display() + ); + } else { + println!( + "\n❌ {total_failures} failure(s) out of {total_checks} checks. Report: {}", + output.display() + ); + } + + Ok(()) +} diff --git a/crates/quake/src/testnet.rs b/crates/quake/src/testnet.rs index 928cb81..5c4dc28 100644 --- a/crates/quake/src/testnet.rs +++ b/crates/quake/src/testnet.rs @@ -1075,6 +1075,8 @@ impl Testnet { yes, infra_args.node_size.as_deref(), infra_args.cc_size.as_deref(), + infra_args.node_disk_gb, + infra_args.cc_disk_gb, ), RemoteSubcommand::Status => { info_mod::print_remote_infra_data(&self.infra_data); diff --git a/crates/quake/src/tests/arc_node.rs b/crates/quake/src/tests/arc_node.rs index a0e358f..71712d2 100644 --- a/crates/quake/src/tests/arc_node.rs +++ b/crates/quake/src/tests/arc_node.rs @@ -21,12 +21,12 @@ //! perimeter. //! //! ```text -//! # Using defaults (arc_node=arc-node, snapshot_provider=snapshot): +//! # Using defaults (arc_nodes=ARC_NODES group from manifest, snapshot_provider=snapshot): //! ./quake test sanity:arc_node //! -//! # With custom parameters: +//! # With custom parameters (comma-separated; accepts node names and node groups): //! ./quake test sanity:arc_node \ -//! --set arc_node=arc-node \ +//! --set arc_nodes=ARC_NODES_CONSENSUS \ //! --set snapshot_provider=snapshot //! ``` //! @@ -53,6 +53,41 @@ use super::historical_queries; use super::{quake_test, RpcClientFactory, TestParams, TestResult}; use crate::node::NodeName; use crate::testnet::Testnet; +use crate::RemoteSubcommand; + +/// Manifest node-group name used as the default when `--set arc_nodes=…` is not +/// provided. If the manifest does not define this group, the default is empty +/// and the caller should skip gracefully. +pub(crate) const DEFAULT_ARC_NODES_GROUP: &str = "ARC_NODES"; + +/// Resolve the `arc_nodes` test parameter into an explicit list of node names. +/// +/// Behavior: +/// - If `--set arc_nodes=…` is provided, split on `,` and run each token +/// through [`crate::manifest::Manifest::resolve_node_selectors`] so users can +/// mix explicit node names and node-group names (e.g. +/// `arc_nodes=ARC_NODES_CONSENSUS,arc-extra`). +/// - If the parameter is not set, return the contents of the +/// [`DEFAULT_ARC_NODES_GROUP`] node-group if it exists in the manifest, +/// otherwise an empty vector (callers should skip gracefully). +pub(crate) fn resolve_arc_nodes(testnet: &Testnet, params: &TestParams) -> Result> { + let raw = params.get_or("arc_nodes", ""); + if raw.trim().is_empty() { + return Ok(testnet + .manifest + .runtime_node_groups() + .get(DEFAULT_ARC_NODES_GROUP) + .cloned() + .unwrap_or_default()); + } + + let selectors: Vec = raw + .split(',') + .map(|s| s.trim().to_owned()) + .filter(|s| !s.is_empty()) + .collect(); + testnet.manifest.resolve_node_selectors(&selectors) +} const TARGET_HEIGHT: u64 = 120; const CATCHUP_TIMEOUT: Duration = Duration::from_secs(120); @@ -73,17 +108,20 @@ fn arc_node_test<'a>( params: &'a TestParams, ) -> TestResult<'a> { Box::pin(async move { - let arc_node_names: Vec = params - .get_or("arc_node", "arc-node") - .split(',') - .map(|s| s.trim().to_owned()) - .filter(|s| !s.is_empty()) - .collect(); + let arc_node_names = resolve_arc_nodes(testnet, params)?; + if arc_node_names.is_empty() { + info!( + "Skipping: no arc nodes resolved (set --set arc_nodes=… or define the \ + {DEFAULT_ARC_NODES_GROUP} node-group in the manifest)" + ); + return Ok(()); + } let snapshot_provider = params.get_or("snapshot_provider", "snapshot"); + let reference = params.get_or("reference", "validator1"); let addr = params.get_or("addr", arc_checks::mev::DEFAULT_ADDR); // Skip if any required node isn't in the manifest - let required_singles = ["validator1", snapshot_provider.as_str()]; + let required_singles = [reference.as_str(), snapshot_provider.as_str()]; let missing_arc: Vec<_> = arc_node_names .iter() .filter(|n| testnet.nodes_metadata.execution_http_url(n).is_none()) @@ -113,7 +151,14 @@ fn arc_node_test<'a>( .collect(); info!("[Phase 1] Snapshot recovery"); - snapshot_recovery(testnet, factory, &snapshot_provider, &arc_node_urls).await?; + snapshot_recovery( + testnet, + factory, + &reference, + &snapshot_provider, + &arc_node_urls, + ) + .await?; info!("[Phase 2] MEV protection"); mev_protection(testnet, &arc_node_names, &addr).await?; @@ -122,7 +167,7 @@ fn arc_node_test<'a>( mempool_empty(testnet, &arc_node_names, &arc_node_urls).await?; info!("[Phase 4] Transaction forwarding"); - tx_forwarding(testnet, factory, &arc_node_urls).await?; + tx_forwarding(testnet, factory, &reference, &arc_node_urls).await?; info!("[DONE] sanity:arc_node passed"); Ok(()) @@ -131,17 +176,25 @@ fn arc_node_test<'a>( /// Snapshot a provider, restore each arc-node from the snapshot, wait /// for it to catch up, and verify historical queries succeed. -async fn snapshot_recovery( +/// +/// Skipped in remote mode, currently requires local Docker Compose and filesystem access. +pub(crate) async fn snapshot_recovery( testnet: &Testnet, factory: &RpcClientFactory, + reference: &str, snapshot_provider: &str, arc_node_urls: &[(NodeName, Url)], ) -> Result<()> { - info!("Waiting for validator1 to reach height {TARGET_HEIGHT}"); + if testnet.is_remote() { + info!("[Phase 1] Snapshot recovery skipped (remote mode)"); + return Ok(()); + } + + info!("Waiting for {reference} to reach height {TARGET_HEIGHT}"); testnet - .wait(TARGET_HEIGHT, &["validator1".to_string()], WAIT_TIMEOUT) + .wait(TARGET_HEIGHT, &[reference.to_string()], WAIT_TIMEOUT) .await - .wrap_err("Validators did not reach target height")?; + .wrap_err("Reference node did not reach target height")?; info!("Waiting for {snapshot_provider} to sync to {TARGET_HEIGHT}"); testnet @@ -163,6 +216,7 @@ async fn snapshot_recovery( restore_and_verify( testnet, factory, + reference, arc_node, arc_node_url, snapshot_provider, @@ -180,6 +234,7 @@ async fn snapshot_recovery( async fn restore_and_verify( testnet: &Testnet, factory: &RpcClientFactory, + reference: &str, arc_node: &str, arc_node_url: &Url, snapshot_provider: &str, @@ -192,15 +247,15 @@ async fn restore_and_verify( tokio::time::sleep(RESTART_SETTLE).await; - let validator_url = testnet + let ref_url = testnet .nodes_metadata - .execution_http_url("validator1") - .ok_or_else(|| color_eyre::eyre::eyre!("validator1 URL not in metadata"))?; - let validator_client = factory.create(validator_url); - let current_tip = validator_client + .execution_http_url(reference) + .ok_or_else(|| color_eyre::eyre::eyre!("{reference} URL not in metadata"))?; + let ref_client = factory.create(ref_url); + let current_tip = ref_client .get_latest_block_number_with_retries(3) .await - .wrap_err("Failed to get validator1 block number")?; + .wrap_err_with(|| format!("Failed to get {reference} block number"))?; info!("Waiting for {arc_node} to catch up to block {current_tip}"); testnet @@ -261,7 +316,11 @@ fn verify_cl_store_pruning(testnet: &Testnet, snapshot_provider: &str) -> Result /// Collect relay nodes that arc-nodes connect to via `follow_endpoints` /// and verify they have MEV protection enabled. -async fn mev_protection(testnet: &Testnet, arc_node_names: &[String], addr: &str) -> Result<()> { +pub(crate) async fn mev_protection( + testnet: &Testnet, + arc_node_names: &[String], + addr: &str, +) -> Result<()> { let mut relay_names: Vec = Vec::new(); for name in arc_node_names { if let Some(node) = testnet.manifest.nodes.get(name.as_str()) { @@ -313,7 +372,7 @@ async fn mev_protection(testnet: &Testnet, arc_node_names: &[String], addr: &str /// Phase 3: Check that trusted-perimeter node mempools are empty. /// Excludes arc-nodes and their relay nodes (which have txpool disabled). -async fn mempool_empty( +pub(crate) async fn mempool_empty( testnet: &Testnet, arc_node_names: &[String], arc_node_urls: &[(NodeName, Url)], @@ -356,69 +415,105 @@ async fn mempool_empty( .join(", ") ); let report = arc_checks::check_mempool(&trusted_urls).await?; - for check in &report.checks { - ensure!( - check.passed, - "Mempool check failed on {}: {}", - check.name, - check.message - ); + if testnet.is_remote() { + for check in &report.checks { + if !check.passed { + info!( + " WARN (remote): mempool not empty on {}: {}", + check.name, check.message + ); + } + } + info!("[Phase 3] Mempool check done (warnings only in remote mode)"); + } else { + for check in &report.checks { + ensure!( + check.passed, + "Mempool check failed on {}: {}", + check.name, + check.message + ); + } + info!("[Phase 3] All trusted node mempools empty"); } - info!("[Phase 3] All trusted node mempools empty"); Ok(()) } /// Phase 4: Send transactions to each arc-node and verify they are forwarded /// to validators, included in blocks, and the arc-node mempools drain. -async fn tx_forwarding( +/// +/// In remote mode, uses `quake remote load` instead of local `testnet.load()` +/// (which requires genesis.json on the local filesystem). +pub(crate) async fn tx_forwarding( testnet: &Testnet, factory: &RpcClientFactory, + reference: &str, arc_node_urls: &[(NodeName, Url)], ) -> Result<()> { - let load_config = SpammerWrapper::parse_from([ - "test", - "-n", - &LOAD_NUM_TXS.to_string(), - "--rate", - "10", - "--mix", - "transfer=100", - ]) - .args - .to_config(true, false); + let ref_url = testnet + .nodes_metadata + .execution_http_url(reference) + .ok_or_else(|| color_eyre::eyre::eyre!("{reference} URL not in metadata"))?; + let ref_client = factory.create(ref_url); for (arc_node, arc_node_url) in arc_node_urls { let client = factory.create(arc_node_url.clone()); - let height_before = client + + let tip = ref_client .get_latest_block_number_with_retries(3) .await - .wrap_err_with(|| format!("Failed to get {arc_node} block number before load"))?; - info!("{arc_node} at height {height_before} before load"); - - info!("Sending {LOAD_NUM_TXS} transactions to {arc_node}"); + .wrap_err_with(|| format!("Failed to get {reference} height"))?; + info!("Waiting for {arc_node} to sync to reference tip ({tip})"); testnet - .load(vec![arc_node.clone()], &load_config) + .wait(tip, &[arc_node.to_string()], Duration::from_secs(60)) .await - .wrap_err_with(|| format!("Failed to send load to {arc_node}"))?; + .wrap_err_with(|| format!("{arc_node} did not sync to validator tip"))?; - info!("Waiting for new blocks after load"); - testnet - .wait( - height_before + 2, - &[arc_node.to_string()], - Duration::from_secs(30), - ) - .await - .wrap_err_with(|| format!("{arc_node} did not advance after load"))?; + info!("Sending {LOAD_NUM_TXS} transactions to {arc_node}"); + if testnet.is_remote() { + let mut args: Vec = vec!["--targets".into(), arc_node.clone()]; + args.extend(["-n".into(), LOAD_NUM_TXS.to_string()]); + args.extend(["--rate".into(), "10".into()]); + args.extend(["--mix".into(), "transfer=100".into()]); + testnet + .remote(RemoteSubcommand::Load { args }) + .await + .wrap_err_with(|| format!("Remote load to {arc_node} failed"))?; + } else { + let load_config = SpammerWrapper::parse_from([ + "test", + "-n", + &LOAD_NUM_TXS.to_string(), + "--rate", + "10", + "--mix", + "transfer=100", + ]) + .args + .to_config(true, false); + testnet + .load(vec![arc_node.clone()], &load_config) + .await + .wrap_err_with(|| format!("Failed to send load to {arc_node}"))?; + } - let height_after = client + let height_after_send = client .get_latest_block_number_with_retries(3) .await - .wrap_err_with(|| format!("Failed to get {arc_node} block number after load"))?; - info!("{arc_node} at height {height_after} after load"); - + .wrap_err_with(|| format!("Failed to get {arc_node} height after send"))?; + info!("{arc_node} at height {height_after_send} after sending txs"); + + // The send takes ~1s; some txs may already be included in the 2-3 + // blocks produced during that window. Look back slightly to cover them. + let scan_start = height_after_send.saturating_sub(2); + let scan_end = height_after_send + 6; + info!("Waiting for {arc_node} to reach height {scan_end}"); + testnet + .wait(scan_end, &[arc_node.to_string()], Duration::from_secs(30)) + .await + .wrap_err_with(|| format!("{arc_node} did not advance after load"))?; let mut total_txs = 0u64; - for h in (height_before + 1)..=height_after { + for h in scan_start..=scan_end { let block = super::historical_queries::get_block_with_txs(factory, arc_node_url, h).await?; let tx_count = block @@ -432,12 +527,32 @@ async fn tx_forwarding( total_txs += tx_count; } - ensure!( - total_txs >= LOAD_NUM_TXS, - "Expected at least {LOAD_NUM_TXS} transactions in blocks {}-{} on {arc_node}, found {total_txs}", - height_before + 1, - height_after - ); + if total_txs < LOAD_NUM_TXS { + // Diagnostic: check if txs are stuck in arc-node's mempool + let node_urls = vec![(arc_node.clone(), arc_node_url.clone())]; + match arc_checks::check_mempool(&node_urls).await { + Ok(report) => { + for check in &report.checks { + info!(" mempool {arc_node}: {}", check.message); + } + if !report.passed() { + ensure!( + false, + "Transactions stuck in {arc_node} mempool (not forwarded). \ + Expected {LOAD_NUM_TXS} in blocks {scan_start}-{scan_end}, found {total_txs}", + ); + } + } + Err(e) => { + info!(" mempool check unavailable on {arc_node}: {e:#}"); + } + } + ensure!( + false, + "Expected at least {LOAD_NUM_TXS} transactions in blocks {scan_start}-{scan_end} on {arc_node}, \ + found {total_txs} (mempool was empty β€” txs forwarded but landed outside scan window)" + ); + } info!("{arc_node}: {total_txs} transactions included in blocks"); let node_urls = vec![(arc_node.clone(), arc_node_url.clone())]; @@ -452,3 +567,63 @@ async fn tx_forwarding( info!("[Phase 4] Transaction forwarding passed"); Ok(()) } + +/// Standalone tx forwarding test. +/// +/// Sends transactions to follow-mode arc-nodes and verifies they are forwarded +/// to validators and included in blocks. Unlike `sanity:arc_node`, this does +/// NOT run snapshot recovery first β€” the arc-nodes must already be running +/// and in sync. +/// +/// # Parameters +/// +/// | Key | Default | Description | +/// |-------------|--------------------------------|------------------------------------------------------------------------| +/// | `arc_nodes` | `ARC_NODES` group | Comma-separated follow-mode nodes (names or node-group names) to test | +/// | `reference` | `validator1` | Reference node for tip height | +/// +/// # Usage +/// +/// ```text +/// quake test tx:forward # default: ARC_NODES group +/// quake test tx:forward --set arc_nodes=ARC_NODES_CONSENSUS +/// quake test tx:forward --set arc_nodes=arc-node,rpc-full +/// quake test tx:forward --set reference=validator2 +/// ``` +#[quake_test(group = "tx", name = "forward")] +fn forward_test<'a>( + testnet: &'a Testnet, + factory: &'a RpcClientFactory, + params: &'a TestParams, +) -> TestResult<'a> { + Box::pin(async move { + let arc_node_names = resolve_arc_nodes(testnet, params)?; + let reference = params.get_or("reference", "validator1"); + + let arc_node_urls: Vec<_> = testnet + .nodes_metadata + .all_execution_urls() + .into_iter() + .filter(|(name, _)| arc_node_names.contains(name)) + .collect(); + + if arc_node_urls.is_empty() { + info!( + "Skipping: no arc nodes resolved (set --set arc_nodes=… or define the \ + {DEFAULT_ARC_NODES_GROUP} node-group in the manifest)" + ); + return Ok(()); + } + + if testnet + .nodes_metadata + .execution_http_url(&reference) + .is_none() + { + info!("Skipping: reference node {reference} not in manifest"); + return Ok(()); + } + + tx_forwarding(testnet, factory, &reference, &arc_node_urls).await + }) +} diff --git a/crates/quake/src/tests/mesh.rs b/crates/quake/src/tests/mesh.rs index a00b6aa..9685985 100644 --- a/crates/quake/src/tests/mesh.rs +++ b/crates/quake/src/tests/mesh.rs @@ -72,7 +72,7 @@ use crate::testnet::Testnet; /// How the test categorizes a node based on the manifest topology. /// This determines what mesh tier is acceptable for that node. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum NodeCategory { +pub(crate) enum NodeCategory { /// Circle-operated validator (direct mesh expected). /// Must be `FullyConnected` in strict mode. CircleValidator, @@ -102,7 +102,7 @@ impl fmt::Display for NodeCategory { } /// Categorize a node based on manifest data. -pub(super) fn categorize_node( +pub(crate) fn categorize_node( node_name: &str, manifest_node: &Node, testnet: &Testnet, @@ -139,7 +139,7 @@ pub(super) fn categorize_node( /// When external validators are present, circle validators are allowed to be /// `MultiHop` because indirect paths to external validators (behind sentries) /// are expected and don't indicate a mesh problem. -pub(super) fn check_strict( +pub(crate) fn check_strict( category: NodeCategory, tier: MeshTier, has_external_validators: bool, @@ -152,13 +152,11 @@ pub(super) fn check_strict( )), _ => Err(format!("expected fully-connected, got {tier}")), }, - NodeCategory::ExternalValidator => { - if tier == MeshTier::NotConnected { - Err(format!("expected reachable (multi-hop ok), got {tier}")) - } else { - Ok(format!("{tier}")) - } - } + NodeCategory::ExternalValidator => match tier { + MeshTier::NotConnected => Err(format!("expected reachable (multi-hop ok), got {tier}")), + MeshTier::MultiHop => Ok(format!("{tier} (ok: behind sentry)")), + _ => Ok(format!("{tier}")), + }, NodeCategory::ConsensusParticipant => { if tier == MeshTier::NotConnected { Err(format!("expected connected, got {tier}")) diff --git a/crates/quake/src/tests/mod.rs b/crates/quake/src/tests/mod.rs index 49e7509..71487a5 100644 --- a/crates/quake/src/tests/mod.rs +++ b/crates/quake/src/tests/mod.rs @@ -121,15 +121,15 @@ pub(crate) mod historical_queries; pub(crate) mod snapshot; // Test modules - must come after type definitions so they can use them -mod arc_node; +pub(crate) mod arc_node; mod health; mod mempool; -mod mesh; +pub(crate) mod mesh; mod mev; mod net; mod perf; mod probe; -mod sanity; +pub(crate) mod sanity; mod sync; mod tx; diff --git a/crates/quake/src/tests/perf.rs b/crates/quake/src/tests/perf.rs index 6f4026c..b686c4f 100644 --- a/crates/quake/src/tests/perf.rs +++ b/crates/quake/src/tests/perf.rs @@ -20,11 +20,11 @@ use super::{quake_test, RpcClientFactory, TestOutcome, TestParams, TestResult}; use crate::testnet::Testnet; const DEFAULT_P50_THRESHOLD_MS: &str = "550"; -const DEFAULT_P95_THRESHOLD_MS: &str = "1000"; +const DEFAULT_P99_THRESHOLD_MS: &str = "1000"; const DEFAULT_WARMUP_S: &str = "30"; const DEFAULT_OBSERVATION_S: &str = "60"; -/// Assert that every node's block time p50 and p95 are within thresholds. +/// Assert that every node's block time p50 and p99 are within thresholds. /// /// Supports two modes: /// - `mode=interval` (default): two-scrape approach that isolates an @@ -40,7 +40,7 @@ const DEFAULT_OBSERVATION_S: &str = "60"; /// | `warmup_s` | `30` | Seconds before first scrape (`interval` mode only) | /// | `observation_s` | `60` | Observation window (between scrapes / before scrape) | /// | `block_time_p50_ms` | `550` | Fail if any node's p50 exceeds this | -/// | `block_time_p95_ms` | `1000` | Fail if any node's p95 exceeds this | +/// | `block_time_p99_ms` | `1000` | Fail if any node's p99 exceeds this | #[quake_test(group = "perf", name = "block_time")] fn block_time_test<'a>( testnet: &'a Testnet, @@ -61,8 +61,8 @@ fn block_time_test<'a>( .get_or("block_time_p50_ms", DEFAULT_P50_THRESHOLD_MS) .parse() .unwrap_or(550); - let p95_ms: u64 = params - .get_or("block_time_p95_ms", DEFAULT_P95_THRESHOLD_MS) + let p99_ms: u64 = params + .get_or("block_time_p99_ms", DEFAULT_P99_THRESHOLD_MS) .parse() .unwrap_or(1000); @@ -76,7 +76,7 @@ fn block_time_test<'a>( info!("Mode: full β€” waiting {observation_s}s for blocks to accumulate..."); tokio::time::sleep(tokio::time::Duration::from_secs(observation_s)).await; - arc_checks::check_block_time(&metrics_urls, p50_ms, p95_ms).await? + arc_checks::check_block_time(&metrics_urls, p50_ms, p99_ms).await? } else { if warmup_s > 0 { info!("Warming up for {warmup_s}s..."); @@ -92,7 +92,7 @@ fn block_time_test<'a>( debug!("Taking second scrape..."); let raw_after = arc_checks::fetch_all_metrics(&metrics_urls).await; - arc_checks::check_block_time_delta(&raw_before, &raw_after, p50_ms, p95_ms) + arc_checks::check_block_time_delta(&raw_before, &raw_after, p50_ms, p99_ms) }; let mut outcome = TestOutcome::new(); @@ -102,9 +102,9 @@ fn block_time_test<'a>( outcome .auto_summary( - &format!("All nodes within block time thresholds (p50<{p50_ms}ms, p95<{p95_ms}ms)"), + &format!("All nodes within block time thresholds (p50<{p50_ms}ms, p99<{p99_ms}ms)"), &format!( - "{{}} node(s) exceeded block time thresholds (p50<{p50_ms}ms, p95<{p95_ms}ms)" + "{{}} node(s) exceeded block time thresholds (p50<{p50_ms}ms, p99<{p99_ms}ms)" ), ) .into_result() diff --git a/crates/quake/src/tests/sanity.rs b/crates/quake/src/tests/sanity.rs index 1a36169..9517f4c 100644 --- a/crates/quake/src/tests/sanity.rs +++ b/crates/quake/src/tests/sanity.rs @@ -47,7 +47,7 @@ //! | `strict_mesh` | `true` | Enforce mesh tier expectations | //! | `mesh_verbose` | `true` | Print full `quake info mesh`-style report before mesh checks | //! | `block_time_p50_ms` | `550` | Fail if any node's p50 block time exceeds this | -//! | `block_time_p95_ms` | `1000` | Fail if any node's p95 block time exceeds this | +//! | `block_time_p99_ms` | `1000` | Fail if any node's p99 block time exceeds this | //! //! # Usage //! @@ -79,7 +79,7 @@ const DEFAULT_DURATION_S: u64 = 60; const DEFAULT_LOAD_RATE: u64 = 50; const DEFAULT_LOAD_MIX: &str = "transfer=100"; const DEFAULT_P50_MS: u64 = 550; -const DEFAULT_P95_MS: u64 = 1000; +const DEFAULT_P99_MS: u64 = 1000; const MIN_DURATION_WARNING_S: u64 = 30; // ── Load helpers ──────────────────────────────────────────────────────── @@ -87,7 +87,7 @@ const MIN_DURATION_WARNING_S: u64 = 30; /// Build a spammer::Config for local load generation. /// /// Uses SpammerArgs with CLI-matching defaults, overriding rate, time, and mix. -fn build_spammer_config( +pub(crate) fn build_spammer_config( rate: u64, duration_s: u64, mix: &str, @@ -124,7 +124,7 @@ fn build_spammer_config( } /// Build CLI args for remote load generation (passed to `quake remote load`). -fn build_remote_load_args( +pub(crate) fn build_remote_load_args( rate: u64, duration_s: u64, mix: &str, @@ -231,10 +231,10 @@ fn basic_test<'a>( .get_or("block_time_p50_ms", &DEFAULT_P50_MS.to_string()) .parse() .unwrap_or(DEFAULT_P50_MS); - let p95_ms: u64 = params - .get_or("block_time_p95_ms", &DEFAULT_P95_MS.to_string()) + let p99_ms: u64 = params + .get_or("block_time_p99_ms", &DEFAULT_P99_MS.to_string()) .parse() - .unwrap_or(DEFAULT_P95_MS); + .unwrap_or(DEFAULT_P99_MS); let load_targets: Vec = load_targets_str .split(',') @@ -266,7 +266,7 @@ fn basic_test<'a>( println!(" targets: all nodes"); } println!(" mesh: strict={strict_mesh}, full_report={mesh_verbose}"); - println!(" perf: p50 < {p50_ms}ms, p95 < {p95_ms}ms"); + println!(" perf: p50 < {p50_ms}ms, p99 < {p99_ms}ms"); println!("─────────────────────────────────────────────────────\n"); let mut health_checks: Vec = Vec::new(); @@ -363,7 +363,7 @@ fn basic_test<'a>( // ── Performance (delta between scrapes) ──────────────────── let perf_report = - arc_checks::check_block_time_delta(&raw_before, &raw_after, p50_ms, p95_ms); + arc_checks::check_block_time_delta(&raw_before, &raw_after, p50_ms, p99_ms); println!("\n── Performance check ────────────────────────────────"); for check in &perf_report.checks { let marker = if check.passed { "βœ“" } else { "βœ—" }; diff --git a/crates/quake/terraform/cc.tf b/crates/quake/terraform/cc.tf index 4b8d028..438d0cc 100644 --- a/crates/quake/terraform/cc.tf +++ b/crates/quake/terraform/cc.tf @@ -14,6 +14,15 @@ resource "aws_instance" "cc" { vpc_security_group_ids = [aws_security_group.cc_sg.id] iam_instance_profile = local.ec2_profile_name + dynamic "root_block_device" { + for_each = var.cc_volume_size != null ? [var.cc_volume_size] : [] + iterator = vol + content { + volume_size = vol.value + volume_type = "gp3" + } + } + metadata_options { http_endpoint = "enabled" http_tokens = "required" # IMDSv2 only diff --git a/crates/quake/terraform/nodes.tf b/crates/quake/terraform/nodes.tf index 9c360d8..fe8aa9a 100644 --- a/crates/quake/terraform/nodes.tf +++ b/crates/quake/terraform/nodes.tf @@ -10,6 +10,15 @@ resource "aws_instance" "node" { vpc_security_group_ids = [aws_security_group.network_sg[local.node_primary_network[var.node_names[count.index]]].id] iam_instance_profile = local.ec2_profile_name + dynamic "root_block_device" { + for_each = var.node_volume_size != null ? [var.node_volume_size] : [] + iterator = vol + content { + volume_size = vol.value + volume_type = "gp3" + } + } + metadata_options { http_endpoint = "enabled" http_tokens = "required" # IMDSv2 only diff --git a/crates/quake/terraform/variables.tf b/crates/quake/terraform/variables.tf index db909dc..ba8cde8 100644 --- a/crates/quake/terraform/variables.tf +++ b/crates/quake/terraform/variables.tf @@ -56,6 +56,22 @@ variable "cc_size" { default = "t3.xlarge" # 4 vCPUs, 16 GiB RAM } +# Root EBS volume size (GiB) for node instances. Override via `quake remote create --node-disk-gb`. +# When null (default), the AMI root volume size is unchanged. +variable "node_volume_size" { + type = number + default = null + nullable = true +} + +# Root EBS volume size (GiB) for the Control Center. Override via `quake remote create --cc-disk-gb`. +# When null (default), the AMI root volume size is unchanged. +variable "cc_volume_size" { + type = number + default = null + nullable = true +} + variable "tags" { type = list(string) default = ["arc-quake-testnet"] diff --git a/crates/spammer/Dockerfile b/crates/spammer/Dockerfile index 8e638c2..4880f41 100644 --- a/crates/spammer/Dockerfile +++ b/crates/spammer/Dockerfile @@ -52,6 +52,7 @@ RUN cargo clean # the Cargo registry, git dependencies, and the entire build target directory. RUN --mount=type=cache,sharing=private,target=/usr/local/cargo/registry \ --mount=type=cache,sharing=private,target=/usr/local/cargo/git \ + --mount=type=cache,sharing=private,target=/app/target \ # Build the Spammer binary. cargo build -p spammer --release && \ # Copy the built binary to a temporary, non-cached location to make it accessible to the next stage. diff --git a/crates/spammer/src/erc20.rs b/crates/spammer/src/erc20.rs index 826d417..1f62a67 100644 --- a/crates/spammer/src/erc20.rs +++ b/crates/spammer/src/erc20.rs @@ -24,7 +24,7 @@ use crate::generator::{TxGenerator, TESTNET_CHAIN_ID}; use crate::ws::WsClient; /// TestToken ERC-20 contract address (deterministic deployment in genesis). -pub(crate) const TEST_TOKEN_ADDRESS: Address = address!("e8e7F64D3d4eA1D5b9722A0769c3e7aC380b1423"); +pub(crate) const TEST_TOKEN_ADDRESS: Address = address!("298122B4bF05CC897662e535C18417f44C7f274b"); fn encode_transfer(to: Address, amount: U256) -> Bytes { sol! { diff --git a/crates/spammer/src/generator.rs b/crates/spammer/src/generator.rs index 87b236e..aee897c 100644 --- a/crates/spammer/src/generator.rs +++ b/crates/spammer/src/generator.rs @@ -37,7 +37,7 @@ use crate::ws::{WsClient, WsClientBuilder}; use crate::erc20::TEST_TOKEN_ADDRESS; pub(crate) const TESTNET_CHAIN_ID: u64 = 1337; -const GUZZLER_ADDRESS: Address = address!("1be052abb35D7f41609Bfec8F2fC2A684CB9984f"); +const GUZZLER_ADDRESS: Address = address!("45a834A6bB86F516D4157a8cBcc60f2F35F8398C"); /// Generates and signs transactions from a pool of pre-funded genesis accounts. /// diff --git a/crates/test/checks/src/perf.rs b/crates/test/checks/src/perf.rs index e5f2971..c3686b6 100644 --- a/crates/test/checks/src/perf.rs +++ b/crates/test/checks/src/perf.rs @@ -770,32 +770,32 @@ pub fn format_perf_report( // ── Check ──────────────────────────────────────────────────────────────── /// Fetch Prometheus metrics from all nodes and assert that every node's -/// block time p50 and p95 are within the given thresholds. +/// block time p50 and p99 are within the given thresholds. pub async fn check_block_time( metrics_urls: &[(String, Url)], p50_threshold_ms: u64, - p95_threshold_ms: u64, + p99_threshold_ms: u64, ) -> Result { let raw = fetch_all_metrics(metrics_urls).await; let nodes = parse_perf_metrics(&raw); let p50_threshold_s = p50_threshold_ms as f64 / 1000.0; - let p95_threshold_s = p95_threshold_ms as f64 / 1000.0; + let p99_threshold_s = p99_threshold_ms as f64 / 1000.0; let mut checks: Vec = nodes .iter() .map(|node| match &node.block_time { Some(stats) if stats.count > 0 => { let p50_ok = stats.p50 <= p50_threshold_s; - let p95_ok = stats.p95 <= p95_threshold_s; - let passed = p50_ok && p95_ok; + let p99_ok = stats.p99 <= p99_threshold_s; + let passed = p50_ok && p99_ok; let message = format!( - "block_time p50={:.3}s (limit {:.3}s{}) p95={:.3}s (limit {:.3}s{}) ({} blocks)", + "block_time p50={:.3}s (limit {:.3}s{}) p99={:.3}s (limit {:.3}s{}) ({} blocks)", stats.p50, p50_threshold_s, if p50_ok { "" } else { " EXCEEDED" }, - stats.p95, - p95_threshold_s, - if p95_ok { "" } else { " EXCEEDED" }, + stats.p99, + p99_threshold_s, + if p99_ok { "" } else { " EXCEEDED" }, stats.count, ); CheckResult { @@ -816,7 +816,7 @@ pub async fn check_block_time( Ok(Report { checks }) } -/// Assert block time p50/p95 thresholds using only the **delta** between two +/// Assert block time p50/p99 thresholds using only the **delta** between two /// Prometheus scrapes, isolating the observation window. /// /// This avoids cumulative histogram skew from periods before the observation @@ -825,14 +825,14 @@ pub fn check_block_time_delta( raw_before: &[(String, String)], raw_after: &[(String, String)], p50_threshold_ms: u64, - p95_threshold_ms: u64, + p99_threshold_ms: u64, ) -> Report { // Single source of truth: same intersection and subtraction as the full // perf delta report (avoids duplicate node names from Vec+HashMap pairing). let nodes = parse_perf_metrics_delta(raw_before, raw_after); let p50_threshold_s = p50_threshold_ms as f64 / 1000.0; - let p95_threshold_s = p95_threshold_ms as f64 / 1000.0; + let p99_threshold_s = p99_threshold_ms as f64 / 1000.0; let mut checks: Vec = nodes .into_iter() @@ -841,26 +841,26 @@ pub fn check_block_time_delta( match node.block_time { Some(ref s) if s.count > 0 => { let p50_ok = s.p50 <= p50_threshold_s; - let p95_ok = s.p95 <= p95_threshold_s; - let passed = p50_ok && p95_ok; + let p99_ok = s.p99 <= p99_threshold_s; + let passed = p50_ok && p99_ok; CheckResult { name, passed, message: format!( - "block_time p50={:.3}s (limit {:.3}s{}) p95={:.3}s (limit {:.3}s{}) ({} blocks)", + "block_time p50={:.3}s (limit {:.3}s{}) p99={:.3}s (limit {:.3}s{}) ({} blocks)", s.p50, p50_threshold_s, if p50_ok { "" } else { " EXCEEDED" }, - s.p95, - p95_threshold_s, - if p95_ok { "" } else { " EXCEEDED" }, + s.p99, + p99_threshold_s, + if p99_ok { "" } else { " EXCEEDED" }, s.count, ), } } _ => CheckResult { name, - passed: false, + passed: true, message: "no block_time delta data (counter reset or no observations)" .to_string(), }, @@ -1358,8 +1358,8 @@ grpc_server_handled_total{method="Propose"} 500 let report = check_block_time_delta(&raw_before, &raw_after, 550, 1000); assert_eq!(report.checks.len(), 1); assert!( - !report.checks[0].passed, - "counter reset should produce a failing check" + report.checks[0].passed, + "counter reset should pass (skip) rather than fail the threshold check" ); assert!( report.checks[0] diff --git a/crates/types/src/config.rs b/crates/types/src/config.rs index 033b04d..5167eda 100644 --- a/crates/types/src/config.rs +++ b/crates/types/src/config.rs @@ -77,6 +77,9 @@ impl Config { if self.value_sync.enabled && self.value_sync.batch_size == 0 { bail!("when value_sync is enabled, batch_size must be greater than 0"); } + if self.execution.persistence_backpressure_threshold == 0 { + bail!("execution.persistence_backpressure_threshold must be greater than 0"); + } Ok(()) } } @@ -177,8 +180,10 @@ pub struct ExecutionConfig { #[serde(default)] pub persistence_backpressure: bool, - /// Number of blocks the EL is allowed to lag behind the CL before + /// Maximum canonical-minus-persisted gap the EL may have before /// persistence backpressure is applied during startup replay. + /// + /// Backpressure begins once the gap reaches this threshold. /// Only takes effect when `persistence_backpressure` is true. #[serde(default = "ExecutionConfig::default_persistence_backpressure_threshold")] pub persistence_backpressure_threshold: u64, @@ -384,5 +389,14 @@ mod tests { config.value_sync.enabled = false; assert!(config.validate().is_ok()); } + + #[test] + fn config_rejects_zero_persistence_backpressure_threshold() { + let mut config = Config::default(); + assert!(config.validate().is_ok()); + + config.execution.persistence_backpressure_threshold = 0; + assert!(config.validate().is_err()); + } } } diff --git a/deployments/Dockerfile.consensus b/deployments/Dockerfile.consensus index 16a5652..cf1e670 100644 --- a/deployments/Dockerfile.consensus +++ b/deployments/Dockerfile.consensus @@ -34,24 +34,17 @@ FROM chef AS builder RUN printf 'arc:x:999:999::/data:/bin/false\n' > /tmp/passwd && \ printf 'arc:x:999:\n' > /tmp/group -# Define build-time arguments. +# Build-time args that are stable across commits β€” placed before dep-cook so the +# cook layer's S3 cache entry survives commits that only change git metadata. ARG BUILD_PROFILE=release ARG FEATURES="" ARG RUSTFLAGS="" -ARG GIT_COMMIT_HASH="0000000000000000000000000000000000000000" -ARG GIT_VERSION="v0.0.0-unknown" -ARG GIT_SHORT_HASH="00000000" -ARG ARC_IDEMPOTENT_BUILD=false # Set environment variables for the build process. ENV CARGO_HOME=/usr/local/cargo ENV CARGO_NET_GIT_FETCH_WITH_CLI=true ENV CARGO_HTTP_CAINFO=/etc/ssl/certs/ca-certificates.crt ENV RUSTFLAGS=$RUSTFLAGS -ENV VERGEN_GIT_SHA=$GIT_COMMIT_HASH -ENV VERGEN_GIT_DESCRIBE=$GIT_VERSION -ENV VERGEN_GIT_SHA_SHORT=$GIT_SHORT_HASH -ENV ARC_IDEMPOTENT_BUILD=$ARC_IDEMPOTENT_BUILD WORKDIR /app @@ -62,6 +55,17 @@ RUN --mount=type=cache,id=malachite-cargo-registry,sharing=locked,target=/usr/lo # Cook the recipe produced by the planner, namely compile dependencies cargo chef cook --bin arc-node-consensus --profile $BUILD_PROFILE --features "$FEATURES" --recipe-path arc-node-consensus-recipe.json +# Per-commit git version args come after dep-cook β€” moving them here prevents the +# new commit hash from busting the cook layer's S3 cache key on every push. +ARG GIT_COMMIT_HASH="0000000000000000000000000000000000000000" +ARG GIT_VERSION="v0.0.0-unknown" +ARG GIT_SHORT_HASH="00000000" +ARG ARC_IDEMPOTENT_BUILD=false +ENV VERGEN_GIT_SHA=$GIT_COMMIT_HASH +ENV VERGEN_GIT_DESCRIBE=$GIT_VERSION +ENV VERGEN_GIT_SHA_SHORT=$GIT_SHORT_HASH +ENV ARC_IDEMPOTENT_BUILD=$ARC_IDEMPOTENT_BUILD + COPY . . # Use BuildKit cache mounts for Cargo dependencies and build artifacts diff --git a/deployments/Dockerfile.engine-bench b/deployments/Dockerfile.engine-bench index 30e0ad9..100268c 100644 --- a/deployments/Dockerfile.engine-bench +++ b/deployments/Dockerfile.engine-bench @@ -31,21 +31,15 @@ FROM chef AS builder RUN printf 'arc:x:999:999::/data:/bin/false\n' > /tmp/passwd && \ printf 'arc:x:999:\n' > /tmp/group +# Build-time args that are stable across commits β€” placed before dep-cook so the +# cook layer's S3 cache entry survives commits that only change git metadata. ARG BUILD_PROFILE=release ARG RUSTFLAGS="" -ARG GIT_COMMIT_HASH="0000000000000000000000000000000000000000" -ARG GIT_VERSION="v0.0.0-unknown" -ARG GIT_SHORT_HASH="00000000" -ARG ARC_IDEMPOTENT_BUILD=false ENV CARGO_HOME=/usr/local/cargo ENV CARGO_NET_GIT_FETCH_WITH_CLI=true ENV CARGO_HTTP_CAINFO=/etc/ssl/certs/ca-certificates.crt ENV RUSTFLAGS=$RUSTFLAGS -ENV VERGEN_GIT_SHA=$GIT_COMMIT_HASH -ENV VERGEN_GIT_DESCRIBE=$GIT_VERSION -ENV VERGEN_GIT_SHA_SHORT=$GIT_SHORT_HASH -ENV ARC_IDEMPOTENT_BUILD=$ARC_IDEMPOTENT_BUILD WORKDIR /app @@ -55,6 +49,17 @@ RUN --mount=type=cache,id=bench-cargo-registry,sharing=locked,target=/usr/local/ --mount=type=cache,id=bench-cargo-target,sharing=locked,target=/app/target \ cargo chef cook --bin arc-engine-bench --profile $BUILD_PROFILE --recipe-path arc-engine-bench-recipe.json +# Per-commit git version args come after dep-cook β€” moving them here prevents the +# new commit hash from busting the cook layer's S3 cache key on every push. +ARG GIT_COMMIT_HASH="0000000000000000000000000000000000000000" +ARG GIT_VERSION="v0.0.0-unknown" +ARG GIT_SHORT_HASH="00000000" +ARG ARC_IDEMPOTENT_BUILD=false +ENV VERGEN_GIT_SHA=$GIT_COMMIT_HASH +ENV VERGEN_GIT_DESCRIBE=$GIT_VERSION +ENV VERGEN_GIT_SHA_SHORT=$GIT_SHORT_HASH +ENV ARC_IDEMPOTENT_BUILD=$ARC_IDEMPOTENT_BUILD + COPY . . RUN --mount=type=cache,id=bench-cargo-registry,sharing=locked,target=/usr/local/cargo/registry \ diff --git a/deployments/Dockerfile.execution b/deployments/Dockerfile.execution index add13a7..df68142 100644 --- a/deployments/Dockerfile.execution +++ b/deployments/Dockerfile.execution @@ -34,25 +34,18 @@ FROM chef AS builder RUN printf 'arc:x:999:999::/data:/bin/false\n' > /tmp/passwd && \ printf 'arc:x:999:\n' > /tmp/group -# Define build-time arguments. +# Build-time args that are stable across commits β€” placed before dep-cook so the +# cook layer's S3 cache entry survives commits that only change git metadata. ARG BUILD_PROFILE=release ARG FEATURES="default" ARG CHAIN="" ARG RUSTFLAGS="" -ARG GIT_COMMIT_HASH="0000000000000000000000000000000000000000" -ARG GIT_VERSION="v0.0.0-unknown" -ARG GIT_SHORT_HASH="00000000" -ARG ARC_IDEMPOTENT_BUILD=false # Set environment variables for the build process. ENV CARGO_HOME=/usr/local/cargo ENV CARGO_NET_GIT_FETCH_WITH_CLI=true ENV CARGO_HTTP_CAINFO=/etc/ssl/certs/ca-certificates.crt ENV RUSTFLAGS=$RUSTFLAGS -ENV VERGEN_GIT_SHA=$GIT_COMMIT_HASH -ENV VERGEN_GIT_DESCRIBE=$GIT_VERSION -ENV VERGEN_GIT_SHA_SHORT=$GIT_SHORT_HASH -ENV ARC_IDEMPOTENT_BUILD=$ARC_IDEMPOTENT_BUILD WORKDIR /app @@ -63,6 +56,17 @@ RUN --mount=type=cache,id=reth-cargo-registry,sharing=locked,target=/usr/local/c # Cook the recipe produced by the planner, namely compile dependencies cargo chef cook --bin arc-node-execution --profile $BUILD_PROFILE --features "$FEATURES" --recipe-path arc-node-execution-recipe.json +# Per-commit git version args come after dep-cook β€” moving them here prevents the +# new commit hash from busting the cook layer's S3 cache key on every push. +ARG GIT_COMMIT_HASH="0000000000000000000000000000000000000000" +ARG GIT_VERSION="v0.0.0-unknown" +ARG GIT_SHORT_HASH="00000000" +ARG ARC_IDEMPOTENT_BUILD=false +ENV VERGEN_GIT_SHA=$GIT_COMMIT_HASH +ENV VERGEN_GIT_DESCRIBE=$GIT_VERSION +ENV VERGEN_GIT_SHA_SHORT=$GIT_SHORT_HASH +ENV ARC_IDEMPOTENT_BUILD=$ARC_IDEMPOTENT_BUILD + COPY . . # Use BuildKit cache mounts for Cargo dependencies and build artifacts diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml index 19fbbd7..11b440f 100644 --- a/deployments/docker-compose.yml +++ b/deployments/docker-compose.yml @@ -4,7 +4,7 @@ # Stop: docker compose down # Logs: docker compose logs -f # -# Required environment variables (see docs/running-an-arc-node-docker.md): +# Required environment variables (see docs/running-an-arc-node.md#docker): # ARC_EXECUTION_IMAGE β€” EL Docker image (e.g. docker.cloudsmith.io/circle/arc-network/arc-execution:0.6.0) # ARC_CONSENSUS_IMAGE β€” CL Docker image (e.g. docker.cloudsmith.io/circle/arc-network/arc-consensus:0.6.0) # ARC_HOME β€” data directory on the host (e.g. ~/.arc) diff --git a/docs/installation.md b/docs/installation.md index ff4a5bd..5f3bba2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,13 +1,12 @@ # Install -The Arc node binaries can be installed in two ways: +The Arc node can be installed in three ways: downloading pre-built binaries via [`arcup`](#pre-built-binary), -or by building them from [source](#build-from-source). +[building from source](#build-from-source), +or using [Docker](#docker) images. -After the installation, refer to [Running an Arc Node](./running-an-arc-node.md) -for how to run an Arc node. - -> **Docker:** Container images and Docker Compose instructions are coming soon. +After installation, refer to [Running an Arc Node](./running-an-arc-node.md) +for how to start the node (binaries or Docker Compose). ## Versions @@ -113,3 +112,47 @@ arc-snapshots --version arc-node-execution --version arc-node-consensus --version ``` + +## Docker + +Running an Arc node requires two Docker images β€” one for each layer: + +| Image | Description | +|-------|-------------| +| `arc-execution` | Execution Layer (EL) β€” EVM, RPC, transaction pool | +| `arc-consensus` | Consensus Layer (CL) β€” BFT consensus, follow mode | + +You can either pull pre-built images from the public registry or build them +from source. Both approaches are described below. + +### Pre-built Docker images + +Pre-built multi-arch images (amd64 and arm64) are published to +[Cloudsmith](https://cloudsmith.io/~circle/repos/arc-network/packages/). + +Set `$ARC_VERSION` to the release from the [Versions](#versions) table, +then pull: + +```sh +docker pull docker.cloudsmith.io/circle/arc-network/arc-execution:$ARC_VERSION +docker pull docker.cloudsmith.io/circle/arc-network/arc-consensus:$ARC_VERSION +``` + +### Build Docker images + +Alternatively, build images from a release tag: + +```sh +git clone https://github.com/circlefin/arc-node.git && cd arc-node +git checkout v$ARC_VERSION +docker buildx bake \ + --set "*.args.GIT_COMMIT_HASH=$(git rev-parse v$ARC_VERSION^{commit})" \ + --set "*.args.GIT_VERSION=v$ARC_VERSION" \ + --set "*.args.GIT_SHORT_HASH=$(git rev-parse --short v$ARC_VERSION^{commit})" \ + --set "arc-execution.tags=arc-execution:$ARC_VERSION" \ + --set "arc-consensus.tags=arc-consensus:$ARC_VERSION" +``` + +After obtaining the images, see +[Running an Arc Node: Docker](./running-an-arc-node.md#docker) +for how to start the node. diff --git a/docs/running-an-arc-node-docker.md b/docs/running-an-arc-node-docker.md deleted file mode 100644 index 18e0599..0000000 --- a/docs/running-an-arc-node-docker.md +++ /dev/null @@ -1,170 +0,0 @@ -# Running an Arc Node with Docker - -As an alternative to [building from source](running-an-arc-node.md), you can -run an Arc node using Docker containers. The setup uses the same **follow mode** -as the binary guide, with IPC between the execution and consensus containers. - -## Prerequisites - -- [Docker Engine](https://docs.docker.com/engine/install/) 24+ with BuildKit -- [Docker Compose](https://docs.docker.com/compose/install/) v2 -- Meets the [system requirements](running-an-arc-node.md#system-requirements) - -## Docker images - -Running an Arc node requires two Docker images β€” one for each layer: - -| Image | Description | -|-------|-------------| -| `arc-execution` | Execution Layer (EL) β€” EVM, RPC, transaction pool | -| `arc-consensus` | Consensus Layer (CL) β€” BFT consensus, follow mode | - -You can either pull pre-built images from the public registry or build them -from source. Both approaches are described below. - -Throughout this guide, the compose file reads images from two environment -variables. Set the version once and export both before running any -`docker compose` command: - -```sh -export ARC_VERSION=0.6.0 -export ARC_HOME=~/.arc -``` - -### Public Docker images - -Pre-built multi-arch images (amd64 and arm64) are published to -[Cloudsmith](https://cloudsmith.io/~circle/repos/arc-network/packages/). - -Optionally, you can pull the images from the public repository. This step can -be skipped, as the images will be pulled automatically by `docker compose`. - -```sh -docker pull docker.cloudsmith.io/circle/arc-network/arc-execution:$ARC_VERSION -docker pull docker.cloudsmith.io/circle/arc-network/arc-consensus:$ARC_VERSION -``` - -Export the aliases `docker compose` is expecting for the Docker images. - -```sh -export ARC_EXECUTION_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-execution:$ARC_VERSION -export ARC_CONSENSUS_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-consensus:$ARC_VERSION -``` - -### Build images - -Alternatively, build images from a release tag or a commit hash: - -```sh -git clone https://github.com/circlefin/arc-node.git && cd arc-node -git checkout v$ARC_VERSION -docker buildx bake \ - --set "*.args.GIT_COMMIT_HASH=$(git rev-parse v$ARC_VERSION^{commit})" \ - --set "*.args.GIT_VERSION=v$ARC_VERSION" \ - --set "*.args.GIT_SHORT_HASH=$(git rev-parse --short v$ARC_VERSION^{commit})" \ - --set "arc-execution.tags=arc-execution:$ARC_VERSION" \ - --set "arc-consensus.tags=arc-consensus:$ARC_VERSION" -``` - -Then export the local image tags: - -```sh -export ARC_EXECUTION_IMAGE=arc-execution:$ARC_VERSION -export ARC_CONSENSUS_IMAGE=arc-consensus:$ARC_VERSION -``` - -## Prepare data directory - -Create the `$ARC_HOME` directory on the host before running Docker Compose. If it doesn't exist, Docker will create it as root and the `arc-snapshots` container will fail with permission errors: - -```sh -mkdir -p "${ARC_HOME:-$HOME/.arc}" -``` - -## Download the compose file - -Download `docker-compose.yml` into a working directory: - -```sh -curl -O https://raw.githubusercontent.com/circlefin/arc-node/v${ARC_VERSION}/deployments/docker-compose.yml -``` - -## Start - -If you have already exported `ARC_EXECUTION_IMAGE`, `ARC_CONSENSUS_IMAGE`, and -`ARC_HOME` as described above, run from the directory containing -`docker-compose.yml`: - -```sh -docker compose up -d -``` - -Or with all variables inline: - -```sh -export ARC_VERSION=0.6.0 ARC_HOME=~/.arc -export ARC_EXECUTION_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-execution:$ARC_VERSION \ - ARC_CONSENSUS_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-consensus:$ARC_VERSION -docker compose up -d -``` - -On the first run, init containers automatically: - -1. Download the latest testnet snapshots (~84 GB compressed β€” see - [download sizes](./running-an-arc-node.md#download-snapshots) for details) -2. Initialize the consensus layer private key -3. Prepare the shared IPC socket volume - -Subsequent runs detect that initialization is already complete and start -immediately. - -> The init container runs as root so it can set file ownership for the -> main services (UID 999). No manual `chown` is needed. - -## Verify - -On the first run, wait for the init containers to finish downloading snapshots -(`docker compose logs -f arc-snapshots`). Once the EL and CL containers start, -wait about 30 seconds, then check the latest block height: - -```sh -curl -s -X POST http://localhost:8545 \ - -H "Content-Type: application/json" \ - -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1}' -``` - -The `result` field should increase over time as the node catches up with the -network. Initial sync from a snapshot may take several hours depending on how -far behind the snapshot is. - -If the result remains `0x0`, check logs: - -```sh -docker compose logs -f -``` - -## Monitoring - -The containers expose Prometheus metrics on the host: - -| Endpoint | Description | -|----------|-------------| -| `localhost:9001/metrics` | Execution Layer metrics | -| `localhost:29000/metrics` | Consensus Layer metrics | - -## Stop - -```sh -docker compose down -``` - -Node data persists in `~/.arc/` (or the path set by `ARC_HOME`). To remove -all data and start fresh: - -```sh -docker compose down -v # also removes the named sockets volume -rm -rf ~/.arc -``` - -> **Warning:** This permanently deletes the consensus layer private key -> (network identity). It cannot be recovered. diff --git a/docs/running-an-arc-node.md b/docs/running-an-arc-node.md index 0a20af6..e945160 100644 --- a/docs/running-an-arc-node.md +++ b/docs/running-an-arc-node.md @@ -8,15 +8,16 @@ Arc is an open, EVM-compatible Layer-1 blockchain. Anyone can run an Arc node - **Executes every transaction** β€” Every transaction is re-executed locally through the EVM. Your node maintains its own copy of the complete blockchain state; - **Exposes a local RPC endpoint** β€” Your node provides a standard Ethereum JSON-RPC API (`http://localhost:8545`) for querying blocks, balances, and transactions, and for submitting calls directly against your own verified state. -## Quick Start - An Arc node is composed of two processes: - **Execution Layer (EL)**: executes finalized transactions and maintains the state of the blockchain; - **Consensus Layer (CL)**: fetches finalized blocks, verifies their cryptographic signatures, and passes them to the EL for execution. -Refer to the [installation](installation.md) instructions to install -`arc-node-execution` (EL) and `arc-node-consensus` (CL). +You can run a node using [binaries](#binaries) or [Docker](#docker). +Refer to the [installation](installation.md) instructions to obtain the +binaries or Docker images. + +## Binaries ### Configure paths @@ -203,12 +204,131 @@ If it remains `0x0`, check the logs of the consensus layer for errors. > If the address and port of the HTTP endpoint are configured differently than > the above example, adapt the command accordingly. -> **Docker:** For running with Docker Compose instead of binaries, see -> [Running an Arc Node with Docker](running-an-arc-node-docker.md). +## Docker + +As an alternative to running binaries directly, you can run an Arc node +using Docker containers. See [Installation: Docker](installation.md#docker) +for how to obtain the images. + +### Prerequisites + +- [Docker Engine](https://docs.docker.com/engine/install/) 24+ with BuildKit +- [Docker Compose](https://docs.docker.com/compose/install/) v2 +- Meets the [system requirements](#system-requirements) + +### Set environment variables + +The compose file reads images from environment variables. Set the version, +data directory, and image references before running any `docker compose` +command. Refer to the [Versions](installation.md#versions) table for the +current release: + +```sh +export ARC_VERSION= +export ARC_HOME=~/.arc +``` + +If you pulled pre-built images from Cloudsmith: + +```sh +export ARC_EXECUTION_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-execution:$ARC_VERSION +export ARC_CONSENSUS_IMAGE=docker.cloudsmith.io/circle/arc-network/arc-consensus:$ARC_VERSION +``` + +If you built the images locally: + +```sh +export ARC_EXECUTION_IMAGE=arc-execution:$ARC_VERSION +export ARC_CONSENSUS_IMAGE=arc-consensus:$ARC_VERSION +``` + +### Prepare data directory + +Create the `$ARC_HOME` directory on the host before running Docker Compose. +If it doesn't exist, Docker will create it as root and the `arc-snapshots` +container will fail with permission errors: + +```sh +mkdir -p "${ARC_HOME:-$HOME/.arc}" +``` + +### Download the compose file + +Download `docker-compose.yml` into a working directory: + +```sh +curl -O https://raw.githubusercontent.com/circlefin/arc-node/v${ARC_VERSION}/deployments/docker-compose.yml +``` + +### Start + +Run from the directory containing `docker-compose.yml`: + +```sh +docker compose up -d +``` + +On the first run, init containers automatically: + +1. Download the latest testnet snapshots (~84 GB compressed β€” see + [download sizes](#download-snapshots) for details) +2. Initialize the consensus layer private key +3. Prepare the shared IPC socket volume + +Subsequent runs detect that initialization is already complete and start +immediately. + +> The init container runs as root so it can set file ownership for the +> main services (UID 999). No manual `chown` is needed. + +### Verify + +On the first run, wait for the init containers to finish downloading snapshots +(`docker compose logs -f arc-snapshots`). Once the EL and CL containers start, +wait about 30 seconds, then check the latest block height: + +```sh +curl -s -X POST http://localhost:8545 \ + -H "Content-Type: application/json" \ + -d '{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1}' +``` + +The `result` field should increase over time as the node catches up with the +network. If it remains `0x0`, check logs: + +```sh +docker compose logs -f +``` + +### Docker monitoring + +The containers expose Prometheus metrics on the host: + +| Endpoint | Description | +|----------|-------------| +| `localhost:9001/metrics` | Execution Layer metrics | +| `localhost:29000/metrics` | Consensus Layer metrics | + +### Stop + +```sh +docker compose down +``` + +Node data persists in `~/.arc/` (or the path set by `ARC_HOME`). To remove +all data and start fresh: + +```sh +docker compose down -v # also removes the named sockets volume +rm -rf ~/.arc +``` + +> **Warning:** This permanently deletes the consensus layer private key +> (network identity). It cannot be recovered. ## Separated hosts -The [Quick Start](#quick-start) section describes the setup of the execution +The [Binaries](#binaries) section describes the setup of the execution (EL) and consensus (CL) layers running in the same host. The two processes interact via Inter-Process Communication (IPC), namely using local sockets to which both processes have read and write access. diff --git a/foundry.toml b/foundry.toml index d0c3ec0..7c82ac7 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,13 @@ cache_path = 'contracts/cache/forge' optimizer_runs = 200 solc_version = "0.8.29" evm_version = "prague" +# Disable auto-discovery of remappings from submodules/node_modules. Forge's auto-detected +# remappings include paths that depend on which nested submodules are initialized, which +# differs between local (recursive submodule init) and CI (non-recursive). Since +# metadata.settings.remappings is embedded in the IPFS metadata hash, any drift there +# causes bytecode divergence. Explicit remappings below are the only ones compiled code +# touches; everything else was auto-discovered cruft. +auto_detect_remappings = false remappings = [ "forge-std/=contracts/lib/forge-std/src/", "@openzeppelin/contracts-upgradeable/=contracts/lib/openzeppelin-contracts-upgradeable/contracts/", @@ -14,7 +21,7 @@ remappings = [ ] fs_permissions = [ { access = "read", path = "./assets/artifacts/" }, - { access = "read", path = "./contracts/out/hardhat/contracts/src" }, + { access = "read", path = "./contracts/out/forge" }, { access = "write", path = "./contracts/cache" }, { access = "write", path = "./assets/*/genesis.json" } ] diff --git a/scripts/engine-bench-report.py b/scripts/engine-bench-report.py new file mode 100755 index 0000000..df4b537 --- /dev/null +++ b/scripts/engine-bench-report.py @@ -0,0 +1,956 @@ +#!/usr/bin/env python3 +"""Analyze arc-engine-bench CSV output and emit JSON to stdout. + +Usage: + + engine-bench-report.py [--markdown ] + +Reads `summary.csv` and `combined_latency.csv` from `results_dir` (either +may be absent), resolves a report status (`normal`/`aggregate_only`/ +`partial`/`no_data`/`error`), computes derived stats, and prints a JSON +analysis document to stdout containing the report status, summary, +analysis, flags, and any parse errors. Callers (e.g., CI) parse the +JSON directly. + +With `--markdown `, also renders a markdown report at that path. + +CSV schemas and percentile implementation mirror +`crates/engine-bench/src/bench/output.rs`. + +Exit codes: + 0 JSON printed (and markdown written if requested) + 1 results_dir does not exist + 2 bad usage / argparse + 3 markdown write failed after JSON was emitted +""" + +import argparse +import csv +import json +import math +import os +import sys +import traceback + +WINDOW_SIZE_HIGH = 1500 # β‰₯ WINDOW_SIZE_HIGH β†’ 10 windows +WINDOW_SIZE_LOW = 600 # β‰₯ WINDOW_SIZE_LOW β†’ 5 windows; below β†’ no windowed trend +OUTLIER_MULTIPLIER = 5 # per-block outlier: total_ms > OUTLIER_MULTIPLIER Γ— median(total_ms) +TOP_OUTLIERS_LIMIT = 10 +TAIL_LATENCY_RATIO = 3 # p99/p50 threshold for a Flag +PER_BLOCK_OUTLIER_LIST_LIMIT = 3 + +REPORT_STATUS_NORMAL = "normal" +REPORT_STATUS_AGGREGATE_ONLY = "aggregate_only" +REPORT_STATUS_PARTIAL = "partial" +REPORT_STATUS_NO_DATA = "no_data" +REPORT_STATUS_ERROR = "error" + +SUMMARY_STATUS_OK = "ok" +SUMMARY_STATUS_MISSING = "missing" +SUMMARY_STATUS_ZERO_BYTES = "zero_bytes" +SUMMARY_STATUS_HEADER_ONLY = "header_only" +SUMMARY_STATUS_PARSE_FAILED = "parse_failed" + +# CombinedLatencyRow β€” crates/engine-bench/src/bench/output.rs:29-42. +EXPECTED_COMBINED_COLUMNS = [ + "block_number", "block_hash", "tx_count", "gas_used", + "new_payload_ms", "fcu_ms", "total_ms", "elapsed_ms", + "mgas_per_s", "tx_per_s", + "cumulative_mgas_per_s", "cumulative_tx_per_s", +] + +# SummaryRow β€” crates/engine-bench/src/bench/output.rs:45-66. +SUMMARY_REQUIRED_INT_COLUMNS = ["samples", "total_gas", "total_txs"] +SUMMARY_REQUIRED_FLOAT_COLUMNS = [ + "wall_clock_ms", "execution_ms", "avg_total_ms", + "avg_mgas_per_s", "avg_tx_per_s", + "p50_total_ms", "p95_total_ms", "p99_total_ms", +] +SUMMARY_OPTIONAL_COLUMNS = [ + "avg_new_payload_ms", "avg_fcu_ms", + "p50_new_payload_ms", "p95_new_payload_ms", "p99_new_payload_ms", + "p50_fcu_ms", "p95_fcu_ms", "p99_fcu_ms", +] + + +# ----- percentile + stats (pinned to Rust parity β€” do not modify) ----- + +def percentile(sorted_values, q): + n = len(sorted_values) + if n == 0: + return 0.0 + if n == 1: + return sorted_values[0] + q = max(0.0, min(1.0, q)) + rank = q * (n - 1) + lo = math.floor(rank) + hi = math.ceil(rank) + if lo == hi: + return sorted_values[lo] + return sorted_values[lo] + (sorted_values[hi] - sorted_values[lo]) * (rank - lo) + + +def stats(values): + if not values: + return None + sv = sorted(values) + return { + "n": len(values), + "avg": sum(values) / len(values), + "p50": percentile(sv, 0.5), + "p95": percentile(sv, 0.95), + "p99": percentile(sv, 0.99), + "max": sv[-1], + } + + +# ----- combined_latency.csv parsing ----- + +def _to_finite_float(s): + v = float(s) + if not math.isfinite(v): + raise ValueError(f"non-finite float: {s!r}") + return v + + +def _parse_combined_row(r): + # csv::Writer in output.rs buffers writes (flushed on Drop); a crashed + # bench can leave the final row byte-truncated. Missing cells, and + # non-finite floats from corrupt cells, signal a torn/malformed row β€” + # caller drops it so aggregates stay clean. + for col in EXPECTED_COMBINED_COLUMNS: + if r.get(col) in (None, ""): + return None + try: + return { + "block_number": int(r["block_number"]), + "tx_count": int(r["tx_count"]), + "gas_used": int(r["gas_used"]), + "new_payload_ms": _to_finite_float(r["new_payload_ms"]), + "fcu_ms": _to_finite_float(r["fcu_ms"]), + "total_ms": _to_finite_float(r["total_ms"]), + "elapsed_ms": _to_finite_float(r["elapsed_ms"]), + "mgas_per_s": _to_finite_float(r["mgas_per_s"]), + "tx_per_s": _to_finite_float(r["tx_per_s"]), + "cumulative_mgas_per_s": _to_finite_float(r["cumulative_mgas_per_s"]), + "cumulative_tx_per_s": _to_finite_float(r["cumulative_tx_per_s"]), + } + except ValueError: + return None + + +def _load_combined_rows(path): + with open(path, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) + rows = [] + try: + for row in reader: + rows.append(row) + except csv.Error as e: + raise csv.Error(f"line {reader.line_num}: {e}") from e + return rows + + +def _compute_windows(n, block_num, new_payload_ms): + if n >= WINDOW_SIZE_HIGH: + n_windows = 10 + elif n >= WINDOW_SIZE_LOW: + n_windows = 5 + else: + return 0, [] + + window_size = n // n_windows + windows = [] + for i in range(n_windows): + lo = i * window_size + hi = (i + 1) * window_size if i < n_windows - 1 else n + slice_np = new_payload_ms[lo:hi] + sv = sorted(slice_np) + windows.append({ + "first_block": block_num[lo], + "last_block": block_num[hi - 1], + "avg": sum(slice_np) / len(slice_np), + "p50": percentile(sv, 0.5), + "p95": percentile(sv, 0.95), + }) + return n_windows, windows + + +def _compute_per_block_outliers(block_num, total_ms, median_total): + if median_total <= 0: + return [] + threshold = OUTLIER_MULTIPLIER * median_total + return [ + {"block": blk, "total_ms": v, "median": median_total} + for blk, v in zip(block_num, total_ms) + if v > threshold + ] + + +def _compute_throughput(cum_mgas_last, cum_tx_last, total_gas, total_txs, last_elapsed_ms): + # cumulative_* in output.rs are denominator-normalised over run elapsed, + # so the final row's cumulative is the run-wide average. Fall back to + # totals when the bench crashed before that denominator became non-zero. + last_elapsed_s = last_elapsed_ms / 1000.0 if last_elapsed_ms > 0 else 0.0 + if cum_mgas_last <= 0.0 and last_elapsed_s > 0.0: + return ( + total_gas / last_elapsed_s / 1_000_000.0, + total_txs / last_elapsed_s, + "recomputed", + ) + return cum_mgas_last, cum_tx_last, "cumulative" + + +def analyze(raw_rows): + parsed = [_parse_combined_row(r) for r in raw_rows] + n_raw = len(parsed) + # Only the trailing row may be torn (csv::Writer was mid-flush when the + # bench crashed). Earlier Nones signal schema drift or corruption and + # must be surfaced distinctly so a systematic mismatch is not masked. + dropped_torn_rows = 1 if n_raw > 0 and parsed[-1] is None else 0 + dropped_malformed_rows = sum(1 for p in parsed[:-1] if p is None) + rows = [r for r in parsed if r is not None] + n = len(rows) + + if n == 0: + return { + "samples": 0, + "n_raw": n_raw, + "dropped_torn_rows": dropped_torn_rows, + "dropped_malformed_rows": dropped_malformed_rows, + "empty": True, + } + + cols = {k: [r[k] for r in rows] for k in rows[0]} + block_num = cols["block_number"] + tx_count = cols["tx_count"] + gas_used = cols["gas_used"] + new_payload_ms = cols["new_payload_ms"] + fcu_ms = cols["fcu_ms"] + total_ms = cols["total_ms"] + elapsed_ms = cols["elapsed_ms"] + mgas_per_s = cols["mgas_per_s"] + tx_per_s = cols["tx_per_s"] + + n_tx_gt_0 = sum(1 for c in tx_count if c > 0) + n_tx_eq_0 = n - n_tx_gt_0 + + np_tx_gt_0 = [v for v, c in zip(new_payload_ms, tx_count) if c > 0] + np_tx_eq_0 = [v for v, c in zip(new_payload_ms, tx_count) if c == 0] + + indexed = sorted(range(n), key=lambda i: (-new_payload_ms[i], block_num[i])) + top_outliers = [ + { + "block": block_num[i], + "new_payload_ms": new_payload_ms[i], + "tx": tx_count[i], + "gas": gas_used[i], + } + for i in indexed[:TOP_OUTLIERS_LIMIT] + ] + + n_windows, windows = _compute_windows(n, block_num, new_payload_ms) + + sv_total = sorted(total_ms) + median_total = percentile(sv_total, 0.5) + per_block_outliers = _compute_per_block_outliers(block_num, total_ms, median_total) + + total_gas = sum(gas_used) + total_txs = sum(tx_count) + last_elapsed_ms = elapsed_ms[-1] + avg_mgas_per_s, avg_tx_per_s, throughput_source = _compute_throughput( + cols["cumulative_mgas_per_s"][-1], cols["cumulative_tx_per_s"][-1], + total_gas, total_txs, last_elapsed_ms, + ) + + sv_np = sorted(new_payload_ms) + sv_fcu = sorted(fcu_ms) + partial_headline = { + "samples": n, + "total_gas": total_gas, + "total_txs": total_txs, + "last_sampled_elapsed_ms": last_elapsed_ms, + "avg_new_payload_ms": sum(new_payload_ms) / n, + "avg_fcu_ms": sum(fcu_ms) / n, + "avg_total_ms": sum(total_ms) / n, + "avg_mgas_per_s": avg_mgas_per_s, + "avg_tx_per_s": avg_tx_per_s, + "throughput_source": throughput_source, + "p50_new_payload_ms": percentile(sv_np, 0.5), + "p95_new_payload_ms": percentile(sv_np, 0.95), + "p99_new_payload_ms": percentile(sv_np, 0.99), + "p50_fcu_ms": percentile(sv_fcu, 0.5), + "p95_fcu_ms": percentile(sv_fcu, 0.95), + "p99_fcu_ms": percentile(sv_fcu, 0.99), + "p50_total_ms": percentile(sv_total, 0.5), + "p95_total_ms": percentile(sv_total, 0.95), + "p99_total_ms": percentile(sv_total, 0.99), + } + + throughput_tx_bearing = None + if n_tx_gt_0 > 0: + throughput_tx_bearing = { + "mgas_per_s": stats([v for v, c in zip(mgas_per_s, tx_count) if c > 0]), + "tx_per_s": stats([v for v, c in zip(tx_per_s, tx_count) if c > 0]), + } + + return { + "samples": n, + "n_raw": n_raw, + "dropped_torn_rows": dropped_torn_rows, + "dropped_malformed_rows": dropped_malformed_rows, + "n_tx_gt_0": n_tx_gt_0, + "n_tx_eq_0": n_tx_eq_0, + "per_class": { + "all": stats(new_payload_ms), + "tx_gt_0": stats(np_tx_gt_0), + "tx_eq_0": stats(np_tx_eq_0), + }, + "top_outliers": top_outliers, + "top_outliers_count": len(top_outliers), + "n_windows": n_windows, + "windows": windows, + "throughput_tx_bearing": throughput_tx_bearing, + "per_block_outliers": per_block_outliers, + "partial_headline": partial_headline, + } + + +# ----- summary.csv parsing ----- + +def _coerce(raw, cast): + """Convert `raw` via `cast`; return (value, reason). + + `reason` is None on success, "empty" for empty/None input, or + "non_numeric" when the cast raised ValueError. + """ + if raw is None or raw == "": + return None, "empty" + try: + return cast(raw), None + except ValueError: + return None, "non_numeric" + + +def parse_summary_csv(path): + """Parse summary.csv. Returns (dict | None, malformed_columns, status). + + `malformed_columns` is a list of {column, reason, raw_value} dicts. + status is one of SUMMARY_STATUS_OK, SUMMARY_STATUS_ZERO_BYTES, SUMMARY_STATUS_HEADER_ONLY. + Raises OSError / UnicodeDecodeError / csv.Error on read failure. + """ + with open(path, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) + rows = list(reader) + fieldnames = reader.fieldnames + if not rows: + return None, [], SUMMARY_STATUS_HEADER_ONLY if fieldnames else SUMMARY_STATUS_ZERO_BYTES + + r = rows[0] + malformed = [] + + def record_malformed(col, reason, raw): + malformed.append({"column": col, "reason": reason, "raw_value": raw}) + + out = {"mode": r.get("mode") or None} + if not out["mode"]: + record_malformed("mode", "empty", r.get("mode")) + + for col in SUMMARY_REQUIRED_INT_COLUMNS: + raw = r.get(col) + out[col], reason = _coerce(raw, int) + if reason is not None: + record_malformed(col, reason, raw) + for col in SUMMARY_REQUIRED_FLOAT_COLUMNS: + raw = r.get(col) + out[col], reason = _coerce(raw, float) + if reason is not None: + record_malformed(col, reason, raw) + for col in SUMMARY_OPTIONAL_COLUMNS: + raw = r.get(col) + if raw is None or raw == "": + out[col] = None + else: + out[col], reason = _coerce(raw, float) + if reason == "non_numeric": + record_malformed(col, reason, raw) + + return out, malformed, SUMMARY_STATUS_OK + + +# ----- report status resolution ----- + +def resolve_report_status(summary, latency): + """Resolve report status from parsed inputs. + + `summary` is None when summary.csv is absent or parsed to None. + `latency` is None when combined_latency.csv is absent, or the dict + returned by analyze() otherwise (which may have samples == 0). + """ + has_summary = summary is not None + has_latency = latency is not None and latency.get("samples", 0) > 0 + if has_summary and has_latency: + return REPORT_STATUS_NORMAL + if has_summary and not has_latency: + return REPORT_STATUS_AGGREGATE_ONLY + if not has_summary and has_latency: + return REPORT_STATUS_PARTIAL + return REPORT_STATUS_NO_DATA + + +# ----- flag computation ----- + +def compute_flags( + report_status, + analysis, + summary, + summary_malformed, + analyze_error, + summary_status=SUMMARY_STATUS_MISSING, + summary_error=None, +): + flags = [] + + if summary_status == SUMMARY_STATUS_ZERO_BYTES: + flags.append("⚠ summary.csv present but zero bytes") + elif summary_status == SUMMARY_STATUS_HEADER_ONLY: + flags.append( + "⚠ summary.csv has header but no data row β€” bench may have " + "failed before writing summary" + ) + + if summary_error is not None: + etype, emsg = summary_error + flags.append(f"⚠ summary.csv parse failed: {etype}: {emsg}") + + if report_status == REPORT_STATUS_PARTIAL: + flags.append( + f"⚠ partial run: summary.csv missing, N={analysis['samples']} blocks in combined_latency.csv" + ) + + if analysis is not None: + n_torn = analysis.get("dropped_torn_rows", 0) + if n_torn > 0: + s = "" if n_torn == 1 else "s" + flags.append(f"⚠ dropped {n_torn} torn trailing row{s} from combined_latency.csv") + + n_malformed = analysis.get("dropped_malformed_rows", 0) + if n_malformed > 0: + s = "" if n_malformed == 1 else "s" + flags.append( + f"⚠ dropped {n_malformed} malformed mid-file row{s} from combined_latency.csv " + "β€” possible schema drift" + ) + + if not analyze_error: + n_raw = analysis.get("n_raw", 0) + samples = analysis.get("samples", 0) + if samples == 0 and n_raw > 0: + flags.append( + f"⚠ all {n_raw} row{'s' if n_raw != 1 else ''} in " + "combined_latency.csv were malformed β€” schema drift likely" + ) + elif samples == 0 and n_raw == 0: + flags.append("⚠ combined_latency.csv had no data rows (header-only)") + + if analyze_error is not None: + etype, emsg = analyze_error + flags.append(f"⚠ combined_latency.csv parse failed: {etype}: {emsg}") + + partial_headline = (analysis or {}).get("partial_headline") or {} + throughput_recomputed = partial_headline.get("throughput_source") == "recomputed" + if report_status == REPORT_STATUS_PARTIAL and throughput_recomputed: + flags.append("⚠ cumulative throughput column was zero; recomputed from totals") + elif throughput_recomputed and report_status in (REPORT_STATUS_NORMAL, REPORT_STATUS_AGGREGATE_ONLY): + flags.append( + "⚠ combined_latency.csv cumulative throughput column was zero " + "β€” headline uses summary.csv, but latency data may be corrupt" + ) + + if analysis is not None: + samples = analysis.get("samples", 0) + if 0 < samples < 10: + flags.append( + f"⚠ only {samples} block{'s' if samples != 1 else ''} sampled " + "β€” percentile statistics are degenerate" + ) + + tail_source = partial_headline if report_status == REPORT_STATUS_PARTIAL else (summary or {}) + p50_total = tail_source.get("p50_total_ms") + p99_total = tail_source.get("p99_total_ms") + if p50_total is not None and p99_total is not None and p50_total > 0: + ratio = p99_total / p50_total + if ratio > TAIL_LATENCY_RATIO: + flags.append(f"⚠ tail-latency divergence: p99/p50={ratio:.2f}") + + if analysis is not None: + outliers = analysis.get("per_block_outliers", []) + shown = outliers[:PER_BLOCK_OUTLIER_LIST_LIMIT] + for o in shown: + flags.append( + f"⚠ per-block outlier: block {o['block']}, " + f"total_ms={o['total_ms']:.1f}, median={o['median']:.1f}" + ) + rest = len(outliers) - len(shown) + if rest > 0: + flags.append(f"…and {rest} more.") + + if report_status in (REPORT_STATUS_NORMAL, REPORT_STATUS_AGGREGATE_ONLY): + for item in summary_malformed: + col, reason, raw = item["column"], item["reason"], item["raw_value"] + if reason == "empty": + flags.append(f"⚠ malformed summary: {col} empty") + else: + flags.append( + f"⚠ malformed summary: {col} non-numeric ({raw!r})" + ) + + return flags + + +# ----- formatters ----- + +def _fmt_or_dash(fn, v): + if v is None: + return "β€”" + if isinstance(v, float) and (math.isnan(v) or math.isinf(v)): + return "β€”" + return fn(v) + + +def _fmt_int(n): + return f"{int(n)}" + + +def _fmt_int_grouped(n): + n = int(n) + return f"{n:,}" if n >= 10_000 else f"{n}" + + +def _fmt_gas_used(n): + return f"{int(n):,}" + + +def _fmt_total_gas(n): + n = float(n) + if n >= 1e9: + return f"{n / 1e9:.2f} Ggas" + if n >= 1e6: + return f"{n / 1e6:.1f} Mgas" + return f"{int(n):,} gas" + + +def _fmt_ms(v): + return f"{float(v):.1f} ms" + + +def _fmt_1dp(v): + return f"{float(v):.1f}" + + +def _fmt_mgas_per_s(v): + return f"{float(v):.1f} Mgas/s" + + +def _fmt_wall_clock_ms(ms): + seconds = float(ms) / 1000.0 + if seconds >= 10_000: + return f"{seconds:,.1f} s" + return f"{seconds:.1f} s" + + +# ----- report rendering ----- + +def _render_minimal_report(flags): + lines = [ + "# arc-engine-bench: no data", + "", + "_No benchmark output was available β€” EaaS may not have produced " + "results, the run aborted before the first block, or the CSV was " + "truncated._", + "", + ] + lines.extend(_render_flags(flags)) + return "\n".join(lines) + "\n" + + +def _render_partial_banner(analysis): + return [ + "> ⚠ **Partial results** β€” `summary.csv` not found. The bench run likely errored", + "> or was aborted. Headline numbers below are derived from `combined_latency.csv`", + f"> ({analysis['samples']} blocks); percentiles recomputed from per-block samples.", + "", + ] + + +def _render_title_and_workload(report_status, analysis, summary): + if report_status == REPORT_STATUS_PARTIAL: + ph = analysis["partial_headline"] + title_mode = "unknown (partial run)" + samples = ph["samples"] + total_gas = ph["total_gas"] + total_txs = ph["total_txs"] + elapsed_label = "last-sampled elapsed" + elapsed_value = _fmt_or_dash(_fmt_wall_clock_ms, ph["last_sampled_elapsed_ms"]) + else: + title_mode = summary.get("mode") or "unknown" + samples = summary.get("samples") + total_gas = summary.get("total_gas") + total_txs = summary.get("total_txs") + elapsed_label = "wall clock" + elapsed_value = _fmt_or_dash(_fmt_wall_clock_ms, summary.get("wall_clock_ms")) + + if analysis is not None: + classes_line = ( + f"tx-bearing: {_fmt_int_grouped(analysis['n_tx_gt_0'])} Β· " + f"empty: {_fmt_int_grouped(analysis['n_tx_eq_0'])} Β· " + ) + else: + classes_line = "" + + return [ + f"# arc-engine-bench: `{title_mode}`", + "", + f"Samples: {_fmt_or_dash(_fmt_int_grouped, samples)} blocks Β· {classes_line}" + f"total gas: {_fmt_or_dash(_fmt_total_gas, total_gas)} Β· " + f"total tx: {_fmt_or_dash(_fmt_int_grouped, total_txs)} Β· " + f"{elapsed_label}: {elapsed_value}.", + "", + ] + + +def _render_headline(report_status, analysis, summary): + if report_status == REPORT_STATUS_PARTIAL: + src = analysis["partial_headline"] + elapsed_ms = src["last_sampled_elapsed_ms"] + elapsed_label = "Last-sampled elapsed" + recomputed = src["throughput_source"] == "recomputed" + # Partial throughput is bench-elapsed averaged, not wall-clock; + # footnote keeps the semantic gap with normal mode explicit. + throughput_label = "Throughput (avg)†" + if recomputed: + footnote = ( + "† throughput recomputed from totals (cumulative column was zero); " + "basis is last-sampled elapsed, not wall clock" + ) + else: + footnote = "† throughput basis is last-sampled elapsed, not wall clock" + else: + src = summary + elapsed_ms = summary.get("wall_clock_ms") + elapsed_label = "Wall clock" + throughput_label = "Throughput (avg)" + footnote = None + + rows = [ + (elapsed_label, _fmt_wall_clock_ms, elapsed_ms), + (throughput_label, _fmt_mgas_per_s, src.get("avg_mgas_per_s")), + ("Tx/s (avg)", _fmt_1dp, src.get("avg_tx_per_s")), + ("`new_payload` avg", _fmt_ms, src.get("avg_new_payload_ms")), + ("`new_payload` p50", _fmt_ms, src.get("p50_new_payload_ms")), + ("`new_payload` p95", _fmt_ms, src.get("p95_new_payload_ms")), + ("`new_payload` p99", _fmt_ms, src.get("p99_new_payload_ms")), + ("`fcu` avg", _fmt_ms, src.get("avg_fcu_ms")), + ("`fcu` p99", _fmt_ms, src.get("p99_fcu_ms")), + ] + + lines = ["## Headline", "", "| Metric | Value |", "|---|---:|"] + for label, fmt, value in rows: + lines.append(f"| {label} | {_fmt_or_dash(fmt, value)} |") + if footnote is not None: + lines.append("") + lines.append(footnote) + lines.append("") + return lines + + +def _skip_section(heading, what): + return [ + heading, + "", + f"_combined_latency.csv not available β€” skipping {what}._", + "", + ] + + +def _render_per_window(analysis): + heading = "## Per-window trend (`new_payload_ms`)" + if analysis is None: + return _skip_section(heading, "per-window trend") + lines = [heading, ""] + if analysis.get("n_windows", 0) == 0: + lines.append("_Run too short for windowed trend._") + lines.append("") + return lines + + lines.append("| Blocks | avg | p50 | p95 |") + lines.append("|---|---:|---:|---:|") + for w in analysis["windows"]: + lines.append( + f"| {_fmt_int(w['first_block'])}–{_fmt_int(w['last_block'])} " + f"| {_fmt_1dp(w['avg'])} " + f"| {_fmt_1dp(w['p50'])} " + f"| {_fmt_1dp(w['p95'])} |" + ) + lines.append("") + return lines + + +def _render_top_outliers(analysis): + if analysis is None: + return _skip_section("## Top `new_payload` outliers", "per-block outliers") + count = analysis.get("top_outliers_count", 0) + lines = [f"## Top `new_payload` outliers (N={count})", ""] + if count == 0: + lines.append("_No samples to rank._") + lines.append("") + return lines + lines.append("| Block | new_payload_ms | tx | gas |") + lines.append("|---:|---:|---:|---:|") + for o in analysis["top_outliers"]: + lines.append( + f"| {_fmt_int(o['block'])} " + f"| {_fmt_1dp(o['new_payload_ms'])} " + f"| {_fmt_int_grouped(o['tx'])} " + f"| {_fmt_gas_used(o['gas'])} |" + ) + lines.append("") + return lines + + +def _render_per_class_entry(label, cls): + if cls is None: + return f"| {label} | β€” | β€” | β€” | β€” | β€” | β€” |" + return ( + f"| {label} | {_fmt_int_grouped(cls['n'])} " + f"| {_fmt_1dp(cls['avg'])} " + f"| {_fmt_1dp(cls['p50'])} " + f"| {_fmt_1dp(cls['p95'])} " + f"| {_fmt_1dp(cls['p99'])} " + f"| {_fmt_1dp(cls['max'])} |" + ) + + +def _render_per_class(analysis): + if analysis is None: + return _skip_section("## Per-class breakdown (`new_payload_ms`)", "per-class breakdown") + pc = analysis["per_class"] + lines = [ + "## Per-class breakdown (`new_payload_ms`)", + "", + "| Class | n | avg | p50 | p95 | p99 | max |", + "|---|---:|---:|---:|---:|---:|---:|", + _render_per_class_entry("all", pc["all"]), + _render_per_class_entry("tx > 0", pc["tx_gt_0"]), + _render_per_class_entry("tx = 0", pc["tx_eq_0"]), + "", + ] + + tb = analysis.get("throughput_tx_bearing") + lines.append("**Throughput on tx-bearing blocks**") + lines.append("") + if tb is None: + lines.append("_No tx-bearing blocks in run._") + lines.append("") + else: + lines.append("| Metric | avg | p50 | p95 |") + lines.append("|---|---:|---:|---:|") + for metric_label, key in (("`mgas_per_s`", "mgas_per_s"), ("`tx_per_s`", "tx_per_s")): + s = tb[key] + lines.append( + f"| {metric_label} | {_fmt_1dp(s['avg'])} " + f"| {_fmt_1dp(s['p50'])} " + f"| {_fmt_1dp(s['p95'])} |" + ) + lines.append("") + + lines.append( + "> The per-class `mgas_per_s` / `tx_per_s` are " + "**per-block instantaneous** (gas_used Γ· per-block latency), " + "while the Headline `Throughput (avg)` is " + "**wall-clock averaged** (total gas Γ· wall clock). " + "The two series are not directly comparable." + ) + lines.append("") + return lines + + +def _render_flags(flags): + lines = ["## Flags", ""] + if not flags: + lines.append("_No flags._") + else: + for f in flags: + lines.append(f"- {f}") + lines.append("") + return lines + + +def render_report(report_status, analysis, summary, flags): + if report_status == REPORT_STATUS_NO_DATA: + return _render_minimal_report(flags) + + lines = [] + if report_status == REPORT_STATUS_PARTIAL: + lines.extend(_render_partial_banner(analysis)) + lines.extend(_render_title_and_workload(report_status, analysis, summary)) + lines.extend(_render_headline(report_status, analysis, summary)) + lines.extend(_render_per_window(analysis)) + lines.extend(_render_top_outliers(analysis)) + lines.extend(_render_per_class(analysis)) + lines.extend(_render_flags(flags)) + return "\n".join(lines) + "\n" + + +# ----- orchestration ----- + +# Read-side errors worth surfacing as "parse failed" flags rather than +# crashing the whole run β€” callers keep whatever data did parse. +_CSV_READ_ERRORS = (OSError, UnicodeDecodeError, csv.Error) + + +def _atomic_write(path, text): + tmp = f"{path}.tmp" + with open(tmp, "w", encoding="utf-8") as f: + f.write(text) + os.replace(tmp, path) + + +def _render_error_report(exc_info): + etype, emsg = exc_info + lines = [ + "# arc-engine-bench: renderer error", + "", + "_The renderer hit an unexpected error while processing the benchmark " + "output. The CI logs contain the full traceback._", + "", + "## Error", + "", + f"`{etype}: {emsg}`", + "", + "## Flags", + "", + "- ⚠ renderer exited with an unexpected error; see CI logs for details.", + "", + ] + return "\n".join(lines) + "\n" + + +def _run_analysis(results_dir): + """Parse inputs and return the full analysis document as a dict. + + Raises FileNotFoundError if results_dir does not exist. All other + parse errors are captured into `summary_error` / `analyze_error` + fields on the returned dict so the caller can still emit JSON. + """ + if not os.path.isdir(results_dir): + raise FileNotFoundError(f"results_dir not found: {results_dir}") + + summary_path = os.path.join(results_dir, "summary.csv") + combined_path = os.path.join(results_dir, "combined_latency.csv") + + summary = None + summary_malformed = [] + summary_status = SUMMARY_STATUS_MISSING + summary_error = None + if os.path.exists(summary_path): + try: + summary, summary_malformed, summary_status = parse_summary_csv(summary_path) + except _CSV_READ_ERRORS as e: + summary_error = [type(e).__name__, str(e)] + summary_status = SUMMARY_STATUS_PARSE_FAILED + + analysis = None + analyze_error = None + if os.path.exists(combined_path): + try: + analysis = analyze(_load_combined_rows(combined_path)) + except (ValueError, KeyError) + _CSV_READ_ERRORS as e: + analyze_error = [type(e).__name__, str(e)] + + report_status = resolve_report_status(summary, analysis) + flags = compute_flags( + report_status, analysis, summary, summary_malformed, analyze_error, + summary_status=summary_status, + summary_error=summary_error, + ) + + return { + "report_status": report_status, + "summary": summary, + "summary_status": summary_status, + "summary_malformed": summary_malformed, + "summary_error": summary_error, + "analysis": analysis, + "analyze_error": analyze_error, + "flags": flags, + } + + +def _render_markdown(data): + if data["report_status"] == REPORT_STATUS_ERROR: + return _render_error_report(data["error"]) + return render_report( + data["report_status"], data["analysis"], data["summary"], data["flags"], + ) + + +def _error_document(exc): + etype, emsg = type(exc).__name__, str(exc) + return { + "report_status": REPORT_STATUS_ERROR, + "error": [etype, emsg], + "summary": None, + "summary_status": SUMMARY_STATUS_PARSE_FAILED, + "summary_malformed": [], + "summary_error": None, + "analysis": None, + "analyze_error": None, + "flags": [f"⚠ renderer error: {etype}: {emsg}"], + } + + +def main(argv): + parser = argparse.ArgumentParser( + description="Analyze arc-engine-bench CSV output; print JSON to stdout.", + ) + parser.add_argument( + "results_dir", + help="Directory containing summary.csv and/or combined_latency.csv.", + ) + parser.add_argument( + "--markdown", + metavar="PATH", + help="Also render a markdown report to PATH.", + ) + args = parser.parse_args(argv[1:]) + + try: + data = _run_analysis(args.results_dir) + except FileNotFoundError as e: + print(f"::error::{e}", file=sys.stderr) + return 1 + except Exception as e: + # Emit an error-status document so callers parsing stdout as JSON + # still get a well-formed payload. + traceback.print_exc(file=sys.stderr) + data = _error_document(e) + + json.dump(data, sys.stdout, indent=2) + sys.stdout.write("\n") + + if args.markdown: + try: + _atomic_write(args.markdown, _render_markdown(data)) + except OSError as e: + print( + f"::error::could not write markdown to {args.markdown}: {e}", + file=sys.stderr, + ) + return 3 + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/scripts/genesis/addresses.ts b/scripts/genesis/addresses.ts index 8ac869c..e580376 100644 --- a/scripts/genesis/addresses.ts +++ b/scripts/genesis/addresses.ts @@ -29,7 +29,7 @@ export const callFromAddress = '0x1800000000000000000000000000000000000003' as c // predeployed contracts export const deterministicDeployerProxyAddress = '0x4e59b44847b379578588920ca78fbf26c0b4956c' as const export const multicall3Address = '0xcA11bde05977b3631167028862bE2a173976CA11' as const -export const multicall3FromAddress = '0xEb7cc06E3D3b5F9F9a5fA2B31B477ff72bB9c8b6' as const +export const multicall3FromAddress = '0x825F535677d346626cDE45D64cf89C2a426467e0' as const // Denylist proxy address. Deterministic CREATE2-derived with prefix 0x360. // Init-code: AdminUpgradeableProxy bytecode + abi.encode(implementation, proxyAdmin, initData). @@ -37,8 +37,10 @@ export const multicall3FromAddress = '0xEb7cc06E3D3b5F9F9a5fA2B31B477ff72bB9c8b6 // To reproduce: // INIT_CODE_HASH= make mine-denylist-salt // -// Salt: 0x1ff19f9552a8ba2ba770fc38c8846b30ca47ab7b1caa6cfdfdd3021c1bbe84a4 -export const denylistAddress = '0x36082bA812806eB06C2758c412522669b5E2ac7b' as const -export const memoAddress = '0x9702466268ccF55eAB64cdf484d272Ac08d3b75b' as const -export const gasGuzzlerAddress = '0x1be052abb35D7f41609Bfec8F2fC2A684CB9984f' as const -export const testTokenAddress = '0xe8e7F64D3d4eA1D5b9722A0769c3e7aC380b1423' as const +// Salt: 0x2e8184e0b708cc70e9f829091612c4c8efef8006ee7527c73bdbbd70b64c36c8 +export const denylistAddress = '0x360Eb67EDbA456Bbe01512679f36c2717AA65121' as const +export const memoAddress = '0xe4aa7Ed3585AEf598179f873086F75Fcd6D4b755' as const +export const gasGuzzlerAddress = '0x45a834A6bB86F516D4157a8cBcc60f2F35F8398C' as const +export const testTokenAddress = '0x298122B4bF05CC897662e535C18417f44C7f274b' as const + +export const localdevFeeRecipient = '0x65E0a200006D4FF91bD59F9694220dafc49dbBC1' as const diff --git a/scripts/release-package.sh b/scripts/release-package.sh new file mode 100755 index 0000000..831bce7 --- /dev/null +++ b/scripts/release-package.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Creates a release archive with checksums from compiled binaries. +# Usage: ./scripts/release-package.sh [TARGET] +# Output: release-assets/arc-node--.tar.gz{,.sha256} + +TAG="${1:?Usage: release-package.sh [TARGET]}" +TARGET="${2:-$(rustc -vV | awk '/^host:/ {print $2}')}" + +BINARIES=(arc-node-execution arc-node-consensus arc-snapshots) +BUILD_DIR="target/release" +OUT_DIR="release-assets" +ARCHIVE_NAME="arc-node-${TAG}-${TARGET}.tar.gz" + +mkdir -p "$OUT_DIR" + +for bin in "${BINARIES[@]}"; do + if [[ ! -f "$BUILD_DIR/$bin" ]]; then + echo "error: $BUILD_DIR/$bin not found β€” run 'cargo build --release' first" >&2 + exit 1 + fi +done + +# Create archive with flat layout (no nested directories) +tar -czf "$OUT_DIR/$ARCHIVE_NAME" -C "$BUILD_DIR" "${BINARIES[@]}" + +# Generate checksum in GNU coreutils format: " " +cd "$OUT_DIR" +if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$ARCHIVE_NAME" > "${ARCHIVE_NAME}.sha256" +else + shasum -a 256 "$ARCHIVE_NAME" > "${ARCHIVE_NAME}.sha256" +fi + +echo "Packaged: $OUT_DIR/$ARCHIVE_NAME" +echo "Checksum: $OUT_DIR/${ARCHIVE_NAME}.sha256" +cat "${ARCHIVE_NAME}.sha256" diff --git a/tests/helpers/GasGuzzler.ts b/tests/helpers/GasGuzzler.ts index 969876f..3802f13 100644 --- a/tests/helpers/GasGuzzler.ts +++ b/tests/helpers/GasGuzzler.ts @@ -18,8 +18,17 @@ import hre from 'hardhat' import { Account, Address, Chain, Client, getContract, Transport } from 'viem' import { KeyedClient } from './client-extension' import { PublicClient, WalletClient } from '@nomicfoundation/hardhat-viem/types' +import { readForgeArtifactSync } from './forge-artifact' -export const gasGuzzlerArtifact = hre.artifacts.readArtifactSync('GasGuzzler') +// ABI is sourced from Hardhat to carry the full typed interface used by test +// helpers; bytecode/deployedBytecode come from Forge because genesis pins the +// Forge-compiled code at the predicted CREATE2 address. +const forge = readForgeArtifactSync('GasGuzzler') +export const gasGuzzlerArtifact = { + abi: hre.artifacts.readArtifactSync('GasGuzzler').abi, + bytecode: forge.bytecode, + deployedBytecode: forge.deployedBytecode, +} export class GasGuzzler { static deploy = async (wallet: WalletClient, client: PublicClient) => { diff --git a/tests/helpers/forge-artifact.ts b/tests/helpers/forge-artifact.ts new file mode 100644 index 0000000..e3180db --- /dev/null +++ b/tests/helpers/forge-artifact.ts @@ -0,0 +1,49 @@ +// 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. + +// Reads a Forge-compiled artifact. Use for any contract whose bytecode feeds +// into CREATE2 address computation or is compared against on-chain genesis +// code β€” Hardhat's own compile emits divergent bytecode (different source-path +// keys in metadata), so genesis-critical reads must come from Forge. + +import fs from 'fs' +import path from 'path' +import type { Abi, Hex } from 'viem' + +export type ForgeArtifact = { + abi: Abi + bytecode: Hex + deployedBytecode: Hex +} + +const FORGE_OUT_DIR = path.resolve(__dirname, '../../contracts/out/forge') + +type ForgeRawArtifact = { + abi: Abi + bytecode: { object: Hex } + deployedBytecode: { object: Hex } +} + +export function readForgeArtifactSync(contractName: string, sourceFile?: string): ForgeArtifact { + const file = sourceFile ?? `${contractName}.sol` + const artifactPath = path.join(FORGE_OUT_DIR, file, `${contractName}.json`) + const raw = JSON.parse(fs.readFileSync(artifactPath, 'utf8')) as ForgeRawArtifact + return { + abi: raw.abi, + bytecode: raw.bytecode.object, + deployedBytecode: raw.deployedBytecode.object, + } +} diff --git a/tests/helpers/index.ts b/tests/helpers/index.ts index 8dd3f80..36e49ce 100644 --- a/tests/helpers/index.ts +++ b/tests/helpers/index.ts @@ -29,3 +29,4 @@ export * from './RevertingProtocolConfig' export * from './AdminUpgradeableProxy' export * from './GasGuzzler' export * from './SystemAccounting' +export * from './forge-artifact' diff --git a/tests/helpers/networks/localdev.ts b/tests/helpers/networks/localdev.ts index fdf949c..167bb60 100644 --- a/tests/helpers/networks/localdev.ts +++ b/tests/helpers/networks/localdev.ts @@ -17,13 +17,14 @@ import hre from 'hardhat' import { createWalletClient, getChain } from '../../../scripts/hardhat/viem-helper' import { LocalDevAccountCreator } from '../../../scripts/genesis/AccountCreator' +import { localdevFeeRecipient } from '../../../scripts/genesis' import { Address } from 'viem' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { expect } from 'chai' -// Fee recipient used by all localdev validators via --suggested-fee-recipient; must match -// cl_suggested_fee_recipient in crates/quake/scenarios/localdev.toml. -export const LOCALDEV_FEE_RECIPIENT: Address = '0x65E0a200006D4FF91bD59F9694220dafc49dbBC1' +// Re-export the canonical fee recipient from scripts/genesis/addresses.ts. +// Used as genesis coinbase and cl_suggested_fee_recipient across all localdev validators. +export const LOCALDEV_FEE_RECIPIENT: Address = localdevFeeRecipient /** * Get the clients for the localdev network diff --git a/tests/localdev/NativeFiatToken.test.ts b/tests/localdev/NativeFiatToken.test.ts index 1de7722..25bbdcf 100644 --- a/tests/localdev/NativeFiatToken.test.ts +++ b/tests/localdev/NativeFiatToken.test.ts @@ -23,7 +23,6 @@ import { LOCALDEV_FEE_RECIPIENT, getClients, } from '../helpers' -import { ProtocolConfig } from '../helpers/ProtocolConfig' import { signPermit, USDC } from '../helpers/FiatToken' import { NativeCoinControl, ERR_BLOCKED_ADDRESS } from '../helpers/NativeCoinControl' import { @@ -443,17 +442,12 @@ describe('NativeFiatToken', () => { const { client, operator, sender, createRandWallet } = await clients() const receiver = await createRandWallet().then((x) => x.account) - // When rewardBeneficiary is zero, fees go to the genesis coinbase - const protocolConfig = ProtocolConfig.attach(client) - const configBeneficiary = await protocolConfig.read.rewardBeneficiary() - const beneficiary = configBeneficiary === zeroAddress ? LOCALDEV_FEE_RECIPIENT : configBeneficiary - // Blocklist the receiver address first await USDC.attach(operator).write.blacklist([receiver.address]).then(ReceiptVerifier.waitSuccess) // Setup balance tracking AFTER blocklist operation to avoid interference const balances = await balancesSnapshot(client, { - beneficiary, + beneficiary: LOCALDEV_FEE_RECIPIENT, sender: sender.account.address, receiver: receiver.address, }) diff --git a/tests/localdev/ProtocolConfig.test.ts b/tests/localdev/ProtocolConfig.test.ts index a7b2913..64ef82b 100644 --- a/tests/localdev/ProtocolConfig.test.ts +++ b/tests/localdev/ProtocolConfig.test.ts @@ -16,7 +16,6 @@ import { expect } from 'chai' import { Address, fromHex, parseEther, TransactionReceipt } from 'viem' -import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts' import { PublicClient, WalletClient } from '@nomicfoundation/hardhat-viem/types' import { balancesSnapshot, @@ -46,39 +45,6 @@ describe('ProtocolConfig Smoke Tests', function () { let mockRevertingProtocolConfigAddress: Address let gasGuzzlerAddress: Address - // Test accounts for different scenarios - const testAccounts = { - beneficiary1: privateKeyToAccount(generatePrivateKey()), - beneficiary2: privateKeyToAccount(generatePrivateKey()), - beneficiary3: privateKeyToAccount(generatePrivateKey()), - } - - // Helper to restore original state - async function restoreOriginalBeneficiary(originalBeneficiary: Address) { - const protocolConfig = ProtocolConfig.attach(publicClient) - const currentBeneficiary = await protocolConfig.read.rewardBeneficiary() - if (currentBeneficiary.toLowerCase() !== originalBeneficiary.toLowerCase()) { - await ProtocolConfig.attach(controller) - .write.updateRewardBeneficiary([originalBeneficiary]) - .then((hash) => publicClient.waitForTransactionReceipt({ hash })) - } - } - - // Helper to update beneficiary and verify - async function updateAndVerifyBeneficiary(newBeneficiary: Address): Promise
{ - const protocolConfig = ProtocolConfig.attach(publicClient) - const originalBeneficiary = await protocolConfig.read.rewardBeneficiary() - - await ProtocolConfig.attach(controller) - .write.updateRewardBeneficiary([newBeneficiary]) - .then((hash) => publicClient.waitForTransactionReceipt({ hash })) - - const updatedBeneficiary = await protocolConfig.read.rewardBeneficiary() - expectAddressEq(updatedBeneficiary, newBeneficiary, 'Beneficiary should be updated') - - return originalBeneficiary - } - // Helper to update the block gas limit async function updateBlockGasLimit(newLimit: bigint): Promise { const protocolConfigReader = ProtocolConfig.attach(publicClient) @@ -236,44 +202,6 @@ describe('ProtocolConfig Smoke Tests', function () { expectAddressEq(block.miner, LOCALDEV_FEE_RECIPIENT, 'Block miner should be LOCALDEV_FEE_RECIPIENT') }) - - it('should reflect beneficiary changes in block mining', async function () { - const originalBeneficiary = await updateAndVerifyBeneficiary(testAccounts.beneficiary1.address) - - // Verify new beneficiary is used for mining and balances - await sendTransactionAndVerifyBalances({ - beneficiary: testAccounts.beneficiary1.address, - }) - - // Restore original state - await restoreOriginalBeneficiary(originalBeneficiary) - }) - - it('should handle multiple beneficiary updates correctly', async function () { - const protocolConfig = ProtocolConfig.attach(publicClient) - const originalBeneficiary = await protocolConfig.read.rewardBeneficiary() - - // First update - await updateAndVerifyBeneficiary(testAccounts.beneficiary1.address) - await sendTransactionAndVerifyBalances({ - beneficiary: testAccounts.beneficiary1.address, - }) - - // Second update - await updateAndVerifyBeneficiary(testAccounts.beneficiary2.address) - await sendTransactionAndVerifyBalances({ - beneficiary: testAccounts.beneficiary2.address, - }) - - // Third update - await updateAndVerifyBeneficiary(testAccounts.beneficiary3.address) - await sendTransactionAndVerifyBalances({ - beneficiary: testAccounts.beneficiary3.address, - }) - - // Restore original state - await restoreOriginalBeneficiary(originalBeneficiary) - }) }) describe('Fee Distribution', function () { @@ -295,39 +223,17 @@ describe('ProtocolConfig Smoke Tests', function () { await updateBlockGasLimit(originalGasLimit) }) - it('should handle legacy transactions correctly', async function () { - const protocolConfig = ProtocolConfig.attach(publicClient) - const originalBeneficiary = await protocolConfig.read.rewardBeneficiary() - const beneficiary = testAccounts.beneficiary1.address - await updateAndVerifyBeneficiary(beneficiary) - - // Send transaction and verify both miner and balance changes - await sendTransactionAndVerifyBalances({ - beneficiary, - transferAmount: parseEther('0.1'), - transactionType: 'legacy', - }) - - // Restore original state - await restoreOriginalBeneficiary(originalBeneficiary) - }) - it('should handle EIP-1559 transactions correctly', async function () { - const protocolConfig = ProtocolConfig.attach(publicClient) - const originalBeneficiary = await protocolConfig.read.rewardBeneficiary() - const beneficiary = testAccounts.beneficiary2.address - await updateAndVerifyBeneficiary(beneficiary) - - // Send EIP-1559 transaction and verify both miner and balance changes + // Send EIP-1559 transaction and verify fee distribution to CL-provided fee recipient const transferAmount = parseEther('0.05') const { receipt, totalFee } = await sendTransactionAndVerifyBalances({ - beneficiary, + beneficiary: LOCALDEV_FEE_RECIPIENT, transferAmount, transactionType: 'eip1559', }) - // Verify Circle's custom fee distribution vs standard Ethereum + // Verify Arc's custom fee distribution vs standard Ethereum const gasUsed = BigInt(receipt.gasUsed) const effectiveGasPrice = BigInt(receipt.effectiveGasPrice || 0) // Access baseFeePerGas which exists on EIP-1559 receipts but isn't in standard type @@ -349,9 +255,6 @@ describe('ProtocolConfig Smoke Tests', function () { ) expect(totalFee > standardEthereumFee).to.be.true } - - // Restore original state - await restoreOriginalBeneficiary(originalBeneficiary) }) it('should use EMA base fee calculation in blocks', async function () { diff --git a/tests/localdev/genesis.test.ts b/tests/localdev/genesis.test.ts index 6f4727e..edb79fe 100644 --- a/tests/localdev/genesis.test.ts +++ b/tests/localdev/genesis.test.ts @@ -14,11 +14,17 @@ // See the License for the specific language governing permissions and // limitations under the License. +import fs from 'fs' +import path from 'path' import hre from 'hardhat' import { expect } from 'chai' import { + Address, + concat, createWalletClient, + encodeAbiParameters, encodeDeployData, + encodeFunctionData, Hex, http, keccak256, @@ -29,6 +35,7 @@ import { } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { + AdminUpgradeableProxy, Denylist, DeterministicDeployerProxy, expectAddressEq, @@ -36,6 +43,7 @@ import { gasGuzzlerArtifact, getClients, ProtocolConfig, + readForgeArtifactSync, } from '../helpers' import { USDC } from '../helpers/FiatToken' import { PermissionedValidatorManager, ValidatorRegistry, ValidatorStatus } from '../helpers/ValidatorManager' @@ -46,6 +54,9 @@ import { Manifest, multicall3Address, multicall3FromAddress, + permissionedManagerAddress, + protocolConfigAddress, + validatorRegistryAddress, } from '../../scripts/genesis' import { getValidators } from '../helpers/networks/localdev' import manifest from '../../assets/artifacts/manifest.json' @@ -129,6 +140,115 @@ describe('genesis', () => { expect(address).to.addressEqual(ktAddress) }) + // Regression guards: compute the CREATE2 address from current Forge-compiled bytecode + // (what genesis deploys), then assert it matches BOTH the hardcoded constant in + // scripts/genesis/addresses.ts AND the genesis placement (code present at that address + // on-chain). Guards against: + // - stale constants when bytecode shifts (compiler settings, source edits) + // - stale genesis when constants shift but genesis wasn't regenerated + describe('CREATE2 reproducibility', () => { + // Helper: read the implementation slot of an AdminUpgradeableProxy at `proxyAddress`. + // Used to verify proxies point at the CREATE2 impl address we compute from bytecode. + const implAt = async (proxyAddress: Address): Promise
=> { + const { client } = await getClients() + return AdminUpgradeableProxy.attach(client, proxyAddress).read.implementation() + } + + // The stablecoin contracts (SignatureChecker, NativeFiatTokenV2_2, FiatTokenProxy) are + // not compiled locally β€” they ship as static artifacts under + // assets/artifacts/stablecoin-contracts/. Read those directly for CREATE2 recomputation. + const loadStablecoinArtifact = (name: string) => { + const p = path.join(__dirname, '../../assets/artifacts/stablecoin-contracts', `${name}.json`) + return JSON.parse(fs.readFileSync(p, 'utf8')) as { bytecode: string; linkReferences?: unknown } + } + + it('Memo (genesis-placed)', async () => { + const { client } = await getClients() + const memoArtifact = readForgeArtifactSync('Memo') + const computed = DeterministicDeployerProxy.getDeployAddress(memoArtifact.bytecode) + + // (1) computed address matches hardcoded constant + expect(computed).to.be.addressEqual(memoAddress) + + // (2) genesis placed code at the computed address + const code = await client.getCode({ address: computed }) + expect(code?.length).to.be.greaterThan(0) + }) + + it('Multicall3From (genesis-placed)', async () => { + const { client } = await getClients() + const m3fArtifact = readForgeArtifactSync('Multicall3From') + const computed = DeterministicDeployerProxy.getDeployAddress(m3fArtifact.bytecode) + + // (1) computed address matches hardcoded constant + expect(computed).to.be.addressEqual(multicall3FromAddress) + + // (2) genesis placed code at the computed address + const code = await client.getCode({ address: computed }) + expect(code?.length).to.be.greaterThan(0) + }) + + it('ProtocolConfig implementation (salt=0)', async () => { + const { client } = await getClients() + const artifact = readForgeArtifactSync('ProtocolConfig') + const computed = DeterministicDeployerProxy.getDeployAddress(artifact.bytecode) + + // (1) on-chain proxy's IMPL_SLOT points at the CREATE2 address + expect(await implAt(protocolConfigAddress)).to.be.addressEqual(computed) + + // (2) genesis placed code at the computed address + const code = await client.getCode({ address: computed }) + expect(code?.length).to.be.greaterThan(0) + }) + + it('ValidatorRegistry implementation (salt=0)', async () => { + const { client } = await getClients() + const artifact = readForgeArtifactSync('ValidatorRegistry') + const computed = DeterministicDeployerProxy.getDeployAddress(artifact.bytecode) + + expect(await implAt(validatorRegistryAddress)).to.be.addressEqual(computed) + + const code = await client.getCode({ address: computed }) + expect(code?.length).to.be.greaterThan(0) + }) + + it('PermissionedValidatorManager implementation (salt=0, ctor arg: validatorRegistryProxy)', async () => { + const { client } = await getClients() + const artifact = readForgeArtifactSync('PermissionedValidatorManager') + const ctorArgs = encodeAbiParameters([{ type: 'address' }], [validatorRegistryAddress]) + const fullInit = concat([artifact.bytecode, ctorArgs]) + const computed = DeterministicDeployerProxy.getDeployAddress(fullInit) + + expect(await implAt(permissionedManagerAddress)).to.be.addressEqual(computed) + + const code = await client.getCode({ address: computed }) + expect(code?.length).to.be.greaterThan(0) + }) + + it('NativeFiatTokenV2_2 implementation (salt=0, linked with SignatureChecker)', async () => { + const { client, usdc } = await clients() + + // 1. SignatureChecker CREATE2 (salt=0, no args) from static stablecoin artifact + const sc = loadStablecoinArtifact('SignatureChecker') + const scAddress = DeterministicDeployerProxy.getDeployAddress(sc.bytecode as Hex) + + // 2. NativeFiatTokenV2_2 has a library placeholder for SignatureChecker; replace with + // the computed address before hashing. Placeholder format: __$<34-hex-hash>$__. + const nft = loadStablecoinArtifact('NativeFiatTokenV2_2') + const placeholder = '__$715109b5d747ea58b675c6ea3f0dba8c60$__' + const linked = nft.bytecode.split(placeholder).join(scAddress.slice(2).toLowerCase()) + + const computed = DeterministicDeployerProxy.getDeployAddress(linked as Hex) + + // (1) FiatTokenProxy's implementation points at the computed address + expect(await usdc.implementation()).to.be.addressEqual(computed) + + // (2) genesis placed code at the computed address + const code = await client.getCode({ address: computed }) + expect(code?.length).to.be.greaterThan(0) + }) + }) + describe('USDC contract setup', () => { it('implementation', async () => { const { client, usdc } = await clients() @@ -345,6 +465,59 @@ describe('genesis', () => { const contractStorageLocation = await denylistContract.read.DENYLIST_STORAGE_LOCATION() expect(contractStorageLocation).to.be.eq(expectedSlot) }) + + // Regression guard: compute the Denylist implementation CREATE2 address (salt=0) from + // current Forge bytecode, and assert it matches BOTH the on-chain proxy's IMPL_SLOT AND that + // runtime code is present at that address. Catches drift if bytecode changes without + // genesis regeneration. + it('implementation at expected CREATE2 address (salt=0)', async () => { + const { client, denylist } = await clients() + const denylistArtifact = readForgeArtifactSync('Denylist') + const computed = DeterministicDeployerProxy.getDeployAddress(denylistArtifact.bytecode) + + // (1) computed matches on-chain IMPL_SLOT (proxy points at genesis-placed impl) + const onChainImpl = await denylist.implementation() + expect(onChainImpl).to.be.addressEqual(computed) + + // (2) genesis placed runtime code at the computed address + const code = await client.getCode({ address: computed }) + expect(code?.length).to.be.greaterThan(0) + }) + + // Regression guard: compute the Denylist proxy CREATE2 address from + // AdminUpgradeableProxy bytecode + abi.encode(impl, proxyAdmin, initData) + // combined with the documented mined salt, and assert it matches BOTH the hardcoded + // denylistAddress constant AND that runtime code is placed at that address in genesis. + // Mined via `INIT_CODE_HASH= make mine-denylist-salt` β€” see scripts/genesis/addresses.ts. + it('proxy at expected CREATE2 address (mined salt)', async () => { + const { client, denylist } = await clients() + const denylistArtifact = readForgeArtifactSync('Denylist') + const proxyArtifact = readForgeArtifactSync('AdminUpgradeableProxy') + + const impl = DeterministicDeployerProxy.getDeployAddress(denylistArtifact.bytecode) + const [owner, proxyAdmin] = await Promise.all([denylist.owner(), denylist.admin()]) + + const initData = encodeFunctionData({ + abi: Denylist.abi, + functionName: 'initialize', + args: [owner], + }) + const ctorArgs = encodeAbiParameters( + [{ type: 'address' }, { type: 'address' }, { type: 'bytes' }], + [impl, proxyAdmin, initData], + ) + const fullInit = concat([proxyArtifact.bytecode, ctorArgs]) + + const MINED_SALT = 0x2e8184e0b708cc70e9f829091612c4c8efef8006ee7527c73bdbbd70b64c36c8n + const computed = DeterministicDeployerProxy.getDeployAddress(fullInit, MINED_SALT) + + // (1) computed matches hardcoded constant + expect(computed).to.be.addressEqual(denylistAddress) + + // (2) genesis placed runtime code at the computed address + const code = await client.getCode({ address: computed }) + expect(code?.length).to.be.greaterThan(0) + }) }) describe('GasGuzzler', () => { @@ -379,7 +552,7 @@ describe('genesis', () => { it('bytecode matches artifact', async () => { const { client } = await getClients() const code = await client.getCode({ address: memoAddress }) - const artifact = hre.artifacts.readArtifactSync('Memo') + const artifact = readForgeArtifactSync('Memo') expect(code).to.equal(artifact.deployedBytecode) }) }) @@ -394,7 +567,7 @@ describe('genesis', () => { it('bytecode matches artifact', async () => { const { client } = await getClients() const code = await client.getCode({ address: multicall3FromAddress }) - const artifact = hre.artifacts.readArtifactSync('Multicall3From') + const artifact = readForgeArtifactSync('Multicall3From') expect(code).to.equal(artifact.deployedBytecode) }) }) diff --git a/tests/simulation/ProtocolConfig.test.ts b/tests/simulation/ProtocolConfig.test.ts index 6a1b3f6..e2e06dc 100644 --- a/tests/simulation/ProtocolConfig.test.ts +++ b/tests/simulation/ProtocolConfig.test.ts @@ -19,7 +19,6 @@ import hre from 'hardhat' import { getChain } from '../../scripts/hardhat/viem-helper' import { ProtocolConfig, - LOCALDEV_FEE_RECIPIENT, loadGenesisConfig, type FeeParams, type ConsensusParams, @@ -67,73 +66,6 @@ describe('ProtocolConfig simulation', () => { expect(res.results[0].status).to.be.eq('success') }) - describe('ProtocolConfig Network Parameter Validation', () => { - it('should use LOCALDEV_FEE_RECIPIENT as block miner when rewardBeneficiary is zero', async () => { - const { client, randomWallet, protocolConfig } = await clients() - const protocolConfigBeneficiary = await protocolConfig.read.rewardBeneficiary() - expect(protocolConfigBeneficiary).to.addressEqual( - zeroAddress, - 'rewardBeneficiary should be zero address in localdev genesis', - ) - const controller = await protocolConfig.read.controller() - // Simulate transactions without changing state - const result = await client.simulateBlocks({ - blocks: [ - { - calls: [ - { - account: controller, - to: ProtocolConfig.address, - data: encodeFunctionData({ - abi: protocolConfig.abi, - functionName: 'updateRewardBeneficiary', - args: [randomWallet.address], // Update beneficiary to random wallet for testing - }), - }, - // Immediately query the updated beneficiary to verify the change - { - account: controller, - to: ProtocolConfig.address, - data: encodeFunctionData({ abi: protocolConfig.abi, functionName: 'rewardBeneficiary', args: [] }), - }, - ], - }, - ], - }) - - // Verify simulated blocks have the correct beneficiary as miner and all calls succeed - expect(result).to.have.length(1) - const block = result[0] - expect(block.calls.length).to.equal(2, 'Block should have 2 calls') - - // Verify all calls in this block succeeded - block.calls.forEach((call, callIndex) => { - expect(call.error).to.be.undefined - expect(call.status).to.equal('success', `call ${callIndex} should succeed`) - }) - - expect(block.miner).to.not.be.undefined - expect(block.miner).to.addressEqual( - LOCALDEV_FEE_RECIPIENT, - `Simulated block miner (${block.miner}) should use LOCALDEV_FEE_RECIPIENT`, - ) - - // verify the beneficiary read call returns the updated value - const readCall = block.calls[1] // Second call is the read - expect(readCall.data).to.not.be.undefined - const returnedBeneficiary = decodeFunctionResult({ - abi: protocolConfig.abi, - functionName: 'rewardBeneficiary', - data: schemaHex.parse(readCall.data), - }) - // Verify the contract state was actually updated - expect(returnedBeneficiary).to.addressEqual( - randomWallet.address, - 'Contract should return updated beneficiary address', - ) - }) - }) - describe('ProtocolConfig role wallet validation', () => { it('pauser wallet from genesis config can pause and unpause via simulation', async function () { const pauserWallet = protocolConfigGenesis?.pauser