Skip to content

Mainnet genesis: reproduces byte-identical from committed config; notes on role bootstrap and launch sequencing #366

Description

@osr21

Disclosure: I'm an external community contributor . I am not affiliated with Circle, have no write access to this repository, and nothing here is an official review — it's advisory only. Everything below was verified locally against the committed tree; commands are included so you can reproduce or refute any of it.

Summary

With mainnet approaching, I ran an independent pre-launch check of the committed mainnet genesis, on the reasoning that genesis is the one artifact that cannot be patched after launch.

Headline result is positive: assets/mainnet/genesis.json reproduces byte-for-byte from assets/mainnet/config.json at the pinned toolchain. No drift between the committed inputs and the committed output.

The rest of this issue is three observations about bootstrap sequencing that I could not resolve from the repo alone, plus a short list of things that look alarming under a vanilla-EVM harness but are not findings — flagged explicitly so nobody else burns time chasing them.

1. Reproducibility (verified)

git clone https://github.com/circlefin/arc-node.git && cd arc-node
git submodule update --init --recursive
foundryup -i v1.4.4          # matches .foundry-version
npm install
cp assets/mainnet/genesis.json /tmp/genesis.committed.json
npx hardhat --config hardhat.config.ts compile
npx hardhat --config hardhat.config.ts genesis --network mainnet

Result:

2d33355dcb12b47d3e36f8bceb5945643445b80c6ca763d6ee8829512f15e035  /tmp/genesis.committed.json
2d33355dcb12b47d3e36f8bceb5945643445b80c6ca763d6ee8829512f15e035  assets/mainnet/genesis.json
$ git diff --stat assets/mainnet/     # (empty)

config.json is also byte-identical after the run. Two things make this work, and both are worth keeping: the mainnet builder hardcodes every address rather than deriving from a mnemonic the way assets/testnet/genesis.config.ts does, and the genesis timestamp is a fixed constant (1778544000 = 2026-05-12T00:00:00Z) rather than currentTimestamp(). Together those make the mainnet genesis independently auditable by anyone. That's a genuinely good property and I'd suggest a CI job asserting it stays true.

Booting the committed file under anvil --init and reading state back confirms the declared config is faithfully encoded — chainId 5042, and all six feeParams plus all eight consensusParams decode to exactly the declared values (alpha 20, kRate 200, inverseElasticityMultiplier 5000, minBaseFee 20 gwei, maxBaseFee 20000 gwei, blockGasLimit 30,000,000; targetBlockTimeMs 500). 11 validators, each 2000 voting power, no duplicate public keys, each with the documented (activate=2000, remove=0) controller pair. Across the 40 distinct addresses in the config there is no key reuse between roles — every role has its own address.

2. NativeFiatToken emergency roles are zero at genesis

owner         0xf5a5658b55983E2Aa037cAC7A8431B510E8A97F4
pauser        0x0000000000000000000000000000000000000000
masterMinter  0x0000000000000000000000000000000000000000
blacklister   0x0000000000000000000000000000000000000000
rescuer       0x0000000000000000000000000000000000000000
minters       []

Testnet assigns a real account to each of these, so the difference is deliberate rather than a copy-paste slip. I checked the obvious failure mode first and it does not apply: owner is set, and all four roles are recoverable. Impersonating owner against the booted genesis, updateMasterMinter / updatePauser / updateBlacklister / updateRescuer all succeed, and the guards hold under negative control (a non-owner gets Ownable: caller is not the owner; pause() from a random caller gets Pausable: caller is not the pauser). So this is not a bricking bug.

The part I'd still raise: until owner sends those transactions, the chain has no pause capability, no blacklist capability, and no minting capability, and total genesis supply is exactly 10,000 USDC to a single prefund account. If that interval is intended to be closed in the first block or two by a launch runbook, it would be worth saying so somewhere in-repo, because reading genesis alone the natural conclusion is that these were left unset by mistake.

3. Every privileged account is unfunded at genesis

This is the one I'd most like a maintainer's read on. Cross-referencing each role address against alloc:

Role Balance Code
NativeFiatToken.owner 0 no
NativeFiatToken.proxy.admin 0 no
ProtocolConfig.owner / .controller / .pauser 0 no
PermissionedValidatorManager.owner / .pauser 0 no
Denylist.owner 0 no
validatorRegisterer[0] / [1] 0 no
prefund[0] 0x50A2…bE57 10,000 USDC no

Ten privileged accounts, all zero balance and no code; one funded EOA. Since gas on Arc is paid in the native token and minBaseFee is 20 gwei — I searched crates/ for a fee-exemption, gasless, or sponsored-transaction path and found none, and executor.rs carries a hardcoded GENESIS_MIN_BASE_FEE = 20_000_000_000 fallback — none of those ten accounts can transact until 0x50A2…bE57 funds them. Every administrative capability of the chain, including the role recovery in §2, therefore depends on that one key being available at launch. A 21,000-gas transfer at the floor costs ~0.00042 USDC, so the amounts are trivial; the dependency is structural rather than economic.

Also worth noting that none of these are contracts at genesis, so if any are intended to be multisigs they'd need deploying and the roles re-pointed afterwards.

Is the funding of these accounts part of a launch runbook that lives outside this repo? If so I think it's worth a line in docs/, since the genesis file currently reads as if the admin set is inert.

4. USDC is 6 decimals as an ERC-20 and 18 decimals as the native unit

Verified on the booted chain, for the single prefunded account:

eth_getBalance   10000000000000000000000   (1e22, 18dp)
balanceOf()      10000000000               (1e10, 6dp)
decimals()       6

Both correctly represent 10,000 USDC, and the ethereum-lists/chains entry for 5042 declaring nativeCurrency.decimals: 18 is right for the native unit — I'd initially suspected a mismatch here and it turned out to be consistent, so I want to be clear this is not a bug. But integrators reading decimals() == 6 and then scaling a native-value transfer by 1e6 will be off by 1e12. Given USDC is both the gas token and the ERC-20 here, a short note in the docs would prevent a predictable class of integration error.

Not findings — vanilla-EVM harness artifacts

Because anvil has none of the Arc precompiles (0x1800…000x1800…04 are 1-byte 0xef markers in genesis, with the real implementations in the node), these calls revert under the harness and are expected, not defects: totalSupply(), ProtocolConfig.blockGasLimit()/minBaseFee()/maxBaseFee(), Denylist.paused(), and the ValidatorRegistry getters. The struct-returning feeParams() / consensusParams() accessors do work, which is how §1 was checked. Anyone repeating this should read those reverts as harness limits.

Minor note: the 0xef stub prefix is EIP-3541-forbidden for newly deployed code, so those markers can only ever originate from genesis. That looks intentional and I mention it only because it's a nice invariant — nothing can later deploy a lookalike at a precompile-adjacent address.

Suggestions

  1. A CI job asserting the mainnet genesis reproduces byte-for-byte from its committed config, so the property in §1 can't silently regress before launch.
  2. Document the genesis→operational bootstrap sequence (§2, §3), or point at where it lives.
  3. A docs note on the 6-vs-18 decimal duality (§4).

Happy to open PRs for the CI check or the docs note if that's useful. Related but distinct: #344 covers validator sets with no positive voting power — the mainnet set is fine on that front (11 × 2000).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions