Repository navigation
Bootstrapping the operator accounts at & after genesis #2614
Description
USDCx Faucet Launch Plan
1. Generate the AccountId of the USDCx faucet
Needed for both testnet and mainnet.
1.1 Gather the AccountIds for all the roles
Roles, with the list of notes that each role should be capable of sending to the faucet:
ADMIN(4/5 multisig)RBAC_CONFIG(grant / revoke / set-admin / renounce any of the five roles, incl.ADMINitself)FAUCET_METADATA_CONFIG(set_max_supplyonly; the other metadata fields are immutable)MIN_BURN_AMOUNT_CONFIG(change the min. burn floor)CONSTANT_FEE_POLICY_CONFIG(reprice accepted notes)
ATTEST_ADMIN(4/5 multisig)SET_ATTESTER(enable / disable a Circle attester key commitment)
DOM_PAUSER(single-sig, 5x)PAUSE_CONFIG, pause selector (halts mint and burn)
DOM_UNPAUSER(4/5 multisig)PAUSE_CONFIG, unpause selector
BLK_MANAGER(single-sig)BLOCKLIST_CONFIG(block / unblock an account)
RELAYER(single-sig, not part of the RBAC, we generate and hand to Gateway)MINT(one per Circle deposit, carrying the attestation)
We need only the account ID of each holder to generate the AccountId for the faucet. Role holders accounts are not part of the genesis state (see step 7).
Depending on the auth scheme, we need to follow different procedures to gather the (final) account ID.
1.1.a Account generation via miden-client
We encourage the participant to use the miden-client CLI (version matching the network's protocol version).
- Install the CLI:
cargo install miden-client-cli --version <CLI_VERSION> --locked. - Create a directory dedicated to this account<>network pair:
mkdir ~/.miden-<network>; cd ~/.miden-<network>, and initialise a local client home for the network the account is for:
miden-client init --local --network testnetmiden-client init --local --network <MAINNET_RPC_URL>for mainnet.
Run every command below from that directory. The endpoint <MAINNET_RPC_URL> does not need to be reachable yet.
- Create the wallet:
miden-client new-wallet --account-type public. This generates a Falcon512 key in the keystore and prints the account ID as a0xhex value. - Back up the directory. Its
.miden-<network>/keystoreholds the only copy of the signing key.
1.1.b Getting the AccountId and public key
For single signers, we need the AccountId. It would be printed at the end of the previous set of steps (as 0x hex). You can view it again: miden-client account -l.
For signers in the multisig (some signers will be single signers + multisig participants):
A multisig account is assembled from the approvers' public-key commitments. To get the pubkey:
miden-client keys --list
Send the coordinator the commitment and the scheme name (Falcon512Poseidon2).
1.1.c Account generation via Ledger
TBD
1.1.c Account generation via Bread Wallet
TBD
The coordinator assembling the multisig account, using the multisig interface (cc @Dominik1999).
1.2 Gather other data needed for seeding the account
This is only the attestation pubkey from Circle.
1.3 Derive the AccountId for the faucet itself
Done by the xusdc-genesis tool in miden-usdcx#222. It takes the five role IDs and the faucet parameters and writes usdcx-faucet.mac (nonce one, no seed), printing the ID in hex and in bech32 for each network. The ID depends only on the seed, code and storage, not on any chain state, so it can be derived and handed to Circle before any network exists.
2. Distributor account (DISTRIBUTOR)
To make the first tx on Miden, we need at least one account to hold the native token.
This could be a burner distributor account, whose only role is to have funds seeded at genesis, and send them out to the RELAYER (who submits mint attestations) and other bridge role accounts (DOM_PAUSER, etc.).
This account needs to be created pre-genesis, because after the DISTRIBUTOR is actually funded via the Ethereum transfer in the next step 3., there is one more change we need to make to the state of the faucet account before we create the genesis config TOML (see step 4).
To create this account, we need to:
- Generate a fresh account
- Amend its vault, so that it contains
init_amountof the USDCx token (using theAccountIdfrom the previous step) - Re-assemble it together to create
distributor.macaccount file - Use that file later for the funding service in Step 8
- Print the
AccountIdof the distributor, and use it for Step 3.
3. Send funds to the Ethereum/Sepolia USDCx contract
Prerequisite: Circle has registered the Miden domain (10007) on the target network's xReserve contract, and registered the USDCx faucet from step 1.3 as its remote token.
The deposit goes to the DISTRIBUTOR (from step 2) and must equal faucet.token_supply in config.json exactly:
- mainnet: 100 USDC (
100000000with 6 decimals) - testnet: 1,000,000,000 USDC (
1000000000000000)
The deposit can be made with cast (3a) or from a browser wallet (3b).
3a. cast CLI
3a.1 Import the Ethereum account into cast
The account must hold enough ETH for gas (two transactions) and the full USDC amount.
# private key from MetaMask: Account details -> Show private key
cast wallet import depositor --interactive
export FROM=$(cast wallet address --account depositor)For a Ledger-backed account, skip the import and replace --account depositor with --ledger below.
3a.2 Set the network
Configure the RPC (e.g. Infura) and onchain addresses:
# mainnet
export RPC=<mainnet rpc url> \
XRESERVE=0x8888888199b2Df864bf678259607d6D5EBb4e3Ce \
USDC=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
CIRCLE_API=https://xreserve-api.circle.com
# sepolia
export RPC=<sepolia rpc url> \
XRESERVE=0x008888878f94C0d87defdf0B07f46B93C1934442 \
USDC=0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 \
CIRCLE_API=https://xreserve-api-testnet.circle.com
export DOMAIN=10007
export AMOUNT=$(jq -r .faucet.token_supply config.json)
export RECIPIENT=<distributor "bytes32:" line printed by `xusdc-genesis new-distributor`>
export FAUCET=<faucet "bytes32:" line printed by `xusdc-genesis faucet`>3a.3 Pre-flight checks
cast call $XRESERVE "isRemoteDomainRegistered(uint32)(bool)" $DOMAIN --rpc-url $RPC # should be true
cast call $XRESERVE "getRemoteToken(uint32,address)(bytes32)" $DOMAIN $USDC --rpc-url $RPC # should == $FAUCET
cast call $XRESERVE "domainDepositsPaused(uint32)(bool)" $DOMAIN --rpc-url $RPC # should be false
cast call $USDC "balanceOf(address)(uint256)" $FROM --rpc-url $RPC # should be >= $AMOUNT
cast balance $FROM --ether --rpc-url $RPC # enough be for gas3a.4 Deposit
cast send $USDC "approve(address,uint256)" $XRESERVE $AMOUNT \
--account depositor --rpc-url $RPC
# dry run: must not revert
cast call $XRESERVE "depositToRemote(uint256,uint32,bytes32,address,uint256,bytes)" \
$AMOUNT $DOMAIN $RECIPIENT $USDC 0 0x --from $FROM --rpc-url $RPC
cast send $XRESERVE "depositToRemote(uint256,uint32,bytes32,address,uint256,bytes)" \
$AMOUNT $DOMAIN $RECIPIENT $USDC 0 0x --account depositor --rpc-url $RPC3b. Alternative: deposit from a browser wallet (MetaMask or similar)
Instead of 3a, the same calls can be made from a browser wallet through a block explorer. Both xReserve implementations are verified on Blockscout. Switch the wallet to the target network, then open the contract pages:
On each page, in the Contract tab use "Read proxy" / "Write proxy" (connect the wallet for writes). All amounts are in base units (6 decimals).
- Pre-flight, on xReserve "Read proxy" (same expected outcomes as 3a.3):
isRemoteDomainRegistered(10007)->truegetRemoteToken(10007, <USDC address>)-> the faucet's bytes32domainDepositsPaused(10007)->false
- On USDC "Write proxy", call
approve:spender: the xReserve addressvalue: the amount (100000000on mainnet,1000000000000000on testnet)
- On xReserve "Write proxy", call
depositToRemote:value: the same amountremoteDomain:10007remoteRecipient: the distributor's bytes32localToken: the USDC addressmaxFee:0hookData:0x
Before confirming each transaction, check that the wallet's simulation doesn't show a revert.
4. Genesis state: re-generate faucet account
After the deposit from Step 3. is made on Ethereum, the relayer service, once booted, will query the Circle APIs and see one pending transfer - but since the distributor is funded at genesis, we should not process this first transfer, else we double book the amount.
The relayer needs to therefore skip this (and it would be rejected by the contract anyway) by inspecting the used_nonces storage of the faucet account. But since in Step 1.3 we didn't have the nonce yet, we need to "fix" this on the faucet account.
The overall flow is:
- we generate the account without nonces as per Step 1.3
- we send the funds as in Step 3 above
- re-create the faucet account with the nonce (does not change the
AccountIdof the faucet). This is supported as of feat(genesis): record the genesis deposit nonces as consumed miden-usdcx#232 - discard the previous
usdcx-faucet.macin favor of the one generated here
4a. Get the deposit nonce
The nonce is assigned by Circle when it attests the deposit, so it only appears in Circle's attestation feed after the deposit is final on Ethereum. Re-run until the list is non-empty (set CIRCLE_API, DOMAIN and RECIPIENT from Step 3):
curl -s "$CIRCLE_API/v1/remote-domains/$DOMAIN/attestations?pageSize=100" \
| jq --arg r "${RECIPIENT#0x}" \
'{used_nonces: [.attestations[].payload | ltrimstr("0x")
| select(.[152:216] == ($r | ascii_downcase)) | "0x" + .[408:472]]}' \
| tee nonces.jsonThe filter keeps only deposits to the distributor account. For each entry, bytes 76..108 of the DepositIntent payload are the recipient and bytes 204..236 are the nonce. The resulting nonces.json is the input to xusdc-genesis record-nonces in the next step.
4b. Re-configure the faucet with the nonce
Once we have the initial nonce, regenerate the faucet account (same AccountId as step 1.3; reads the faucet from step 1.3 and nonces.json from 4a):
xusdc-genesis record-nonces --faucet usdcx-faucet.mac --nonces nonces.jsonThis writes a new usdcx-faucet.genesis.mac. Do not use the pre-deposit-generated account from before.
Now, we can include this account, together with the DISTRIBUTOR account, in the genesis config file:
native_faucet = "usdcx-faucet.genesis.mac"
[[account]]
path = "distributor.mac"This step happens ahead of launch.
5. Batch builder
Since we don't have a BATCH_FEE note, nor the collection thereof by the block builder, implemented, we don't need to worry about pre-funding the batch builder account at this point.
The batch builder's wallet account doesn't need to be pre-funded: it collects P2ID notes in the native asset, so it can pay the TX_FEE.
6. Launch
🚀
7. Post-genesis fund distribution to the operator roles
Aside from the relayer that submits mint requests to the faucet, the other operational bridge roles like ADMIN, ATTEST_ADMIN, etc. need to hold funds as well, so that they can be operational as soon as the network is up and running.
The role accounts will not exist onchain at genesis. Each holder deploys their own account with its first transaction, which consumes the P2ID note sent by the DISTRIBUTOR.
Ideally this would be scripted, so that all the required funds are distributed at once. I created https://github.com/0xMiden/infrastructure/issues/238 to track this.
USDCx
The accounts to be funded are as listed in 1.1 above.
AggLayer
The list of role accounts, and the list of notes that each role should be capable of sending to the faucet:
BRIDGE_ADMIN(4/5 multisig)RBAC_CONFIG(grant / revoke / set-admin / renounce the five bridge roles and itself)NETWORK_ACCOUNT_CONFIG(add / remove allowed note scripts, tx scripts and fee policies; select the active fee policy)PAUSE_CONFIG, unpause selector (resume a paused bridge;PAUSERcannot)
PAUSER(s) - potentially several accounts (single-sig, 5x)PAUSE_CONFIG, pause selector (halts bridge-out, claim, GER injection and faucet registration)
FAUCET_ADMIN, technically per faucet, but each faucet can set the same account as its admin. Same account asBRIDGE_ADMINby default (4/5 multisig)RBAC_CONFIG, to each faucet (rotate that faucet'sADMINandFEE_MNGR)NETWORK_ACCOUNT_CONFIG, to each faucet (allowlist and fee-policy changes)
FAUCET_MNGR(managed by Gateway)CONFIG_AGG_BRIDGE(register a faucet and its metadata hash; one per bridged token at launch)DEREGISTER_AGG_FAUCET(remove a faucet from the registry)
GER_INJECTOR(managed by Gateway)UPDATE_GER(one per L1 global exit root update)CLAIM(one per bridge-in; permissionless)
GER_REMOVER(managed by Gateway)REMOVE_GER(revoke a registered GER)
FEE_MNGR(managed by Gateway)CONSTANT_FEE_POLICY_CONFIG, to the bridge (reprice accepted notes)CONSTANT_FEE_POLICY_CONFIG, to each faucet (account-local role; each faucet has its ownFEE_MNGRmembers)
cc @mandrigin — could you confirm the list please?
Note
This initial funding step only covers the first few transactions - though we can make sure to cover a few thousand transactions e.g. with 10 USDC. Going forward, the operators need to ensure their accounts always remain funded (cc @mandrigin for AggLayer; @Keinberger for USDCx).
8. Funding service
For the partner mainnet, we want to have a funding service that sends funds to the allowlisted accounts, as described in #2561.
The funding service (node#2561) should be started with the DISTRIBUTOR account file from step 2 as --account-file.
This account can be funded in the same manner as the bridge operator roles in step 7.
Local deployment & devnet
As discussed offline with @Mirko-von-Leipzig, it might be easiest if in v0.17+, local and devnet environments also use the USDCx faucet as the native faucet, to keep things in sync.
Since there is no accounting to be tracked with Circle, we can:
- In genesis, seed with a large amount of funds.
- Set ourselves as the minting authority. This requires creating a service that submits signed
MINTattestations against the USDCx faucet, thus adding a bit of operational setup. Perhaps this could be the role of the faucet operator service itself — TBD.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Projects
- StatusShow more project fieldsTodo
Activity
bobbinth commentedon Sep 19, 2026
Thank you for writing this up! I've put all the steps up to the genesis block creation into a diagram below
Overall, I think this will work - but there is also an alternative process that would be much simpler upfront, and a bit more work post-genesis. This may be a good tradeoff though. Here is a diagram for this process:
The key difference here is that we have a "bootstrap" account that is set as the admin of the faucet at genesis, and we need only this account to create the faucet account. This means we don't need to collect inputs from other users upfront.
Once the network is up and running, the following would need to happen:
P2IDnote to the funding service account (an alternative could be to use the bootstrap account as the funding service account - but separating these two feels a bit cleaner). Funding service accepts the initial funds in the first transaction.a. At this point, we have 3 accounts in the network: the bootstrap account, the funding service account, and the USDCx faucet account.
(account_id, pub_key)tuples (should be easy to export from the wallet). These would include:a. 5 IDs/keys for the "security council" accounts.
b. Anything else?
a. For this, we probably will need to create an account offline, then add the ID to the allowlist on the node.
a. Sends funds to the multisig account, and probably additional funds to the signle-sig accounts that were created by users in step 2.
b. Assigns roles in the USDCx faucet to relevant accounts (e.g., the
ADMIN,ATTEST_ADMIN, andDOM_UNPAUSERget assigned to the multisig account) .c. Removes itself as the admin of the USDCx faucet account (i.e., transfers ownership to the multisig account).
The main work here is to write the script for step 4 - but in return we get:
Open questions/tasks
cc @Dominik1999
Mirko-von-Leipzig commentedon Sep 20, 2026
I like the simplicity of the above.
Is the idea that the sequencer will be started in some sort of special mode initially until all the required accounts are deployed? Or will we build those more manually? For example, in #2568 I add
miden-node fee-collector deploywhich appends a single block which deploys just that account.The benefit is largely that this more specialized code is separate from the normal runtime loop. For example we can't have fees enabled until all of this is settled, so the batch builder etc all need escape hatches if we want to use the normal sequencer flow.
It also lets us avoid requiring a remote tx and batch prover since we can just prove that block or two locally.
ah okay I see step (2) assumes that they can register using pin codes, so the sequencer would have to be "online" serving that. We can also just register their account IDs manually.. though I guess now the wallet flow won't let them create it without a pin code?
bobbinth commentedon Sep 20, 2026
I think the main idea is to create the minimum set of accounts needed to run the network, and then to do most of the configuration after the network is up and running. My current understanding that the absolute minimum would be:
All other accounts could be created either manually or via a script after the network is running. And then, we conceptually can split the deployment into 3 phases:
Only after these 3 steps can we consider the network to be open to users.
Mirko-von-Leipzig commentedon Sep 20, 2026
From the sequencer's perspective we need only two things deployed for normal/stead-state batch building:
Deployment of (1) is implemented in #2568 (comment) as a standalone sequencer subcommand at the moment. Otherwise we run into race conditions and need more branching/edge case handling. I think its a fairly neat solution and I would prefer similar things for this initial setup. Notably this does not require the USDCx faucet because it doesn't actually use fees or the native token.
^^ the above is essentially the same as a script but since the network isn't actually running, it needs to know about sequencer/batch/block building stuff.
Perhaps I'm just restating what you meant; I just want to clarify that I'd like to avoid reusing the steady-state sequencer runtime for this initial setup.
bobbinth commentedon Sep 20, 2026
I think it is similar though I'm imagining 3 "genesis" accounts (i.e., accounts that exist in the genesis block):
With these 3 accounts, the network can run and bootstrap everything else. I think there are two differences between how you and I think:
Mirko-von-Leipzig commentedon Sep 20, 2026
There is; genesis would be fine as well of course. The awkward thing is that it requires a private key, so probably that private key should be held only by the sequencer (?) so it should create and deploy it? If we're okay sharing that key amongst parties then we can do it genesis (it can't mutate, mostly just an audit trail problem if it gets shared out of band and abused for anonymisers).
Yes, technically the sequencer doesn't need this to create blocks so I ignored it. But deploying both it and the admin account in genesis is fine. Simplifies things if its just.. there. I do want it on all networks for conformity. As you mention, it would be great if the only difference across networks was the auth for the faucets/admin accounts.
Mirko-von-Leipzig commentedon Sep 23, 2026
The initial version has been deployed to devnet. It creates the usdcx faucet, funding service and batch builder account in genesis. The sequencer separately creates the fee collector pass through account as part of its bootstrap process - this works because it can append a dedicated block with just that deploy transaction, ignoring fees.
What's still outstanding is how to (or a script/ui) to migrate the faucet operator account from the funding service to the multisig setup.
bobbinth commentedon Sep 24, 2026
I'm assuming that this would be a either a stand-alone binary or a part of an exiting binary. At the high level, it would take
(account_id, pub_key)tuples of 5 accounts and do the following:a. We may also want to create several multisigs to assign different multisigs to different roles - but given that all people on these multisigs will probably be the same - that's probably an overkill.
ADMIN,DOM_UNPAUSERetc.).DOM_PAUSER).One open question is whether we need to do anything for AggLayer accounts.
cc @mmagician in case I missed some detail.
mmagician commentedon Sep 26, 2026
We could do this in a separate binary, but in my mind, this should all be done as part of the "bridge admin interface" that @Dominik1999 is building, so we have everything in one place:
RbacConfignotes which we are interested in here.For the roles we do want multisigs on (
ADMIN,ATTEST_ADMIN,DOM_UNPAUSER) this could work, but it's worth noting that the Rust constructors for the faucet actually reject the same account ID for some of the roles (e.g. the account assigned toADMINcannot share the role as theDOM_PAUSER).Just to be clear - this means the setting of roles in steps 2 & 3 is still done by the old, single-sig admin, i.e. the distributor itself, right? I think that makes sense, it's faster to coordinate than the multisig.
RBAC_CONFIGnote to the faucet withRbacConfig::GrantRole { role: ADMIN, account: new_admin }.RbacConfig::RevokeRole { role: ADMIN, account: old_admin }.At the protocol level, there is no prevention of first revoking access to yourself - (it's a feature actually: it makes the account completely trustless), but perhaps we could add this check as part of the "bridge admin interface" (cc Dominik)? So the tool would detect if you are trying to send a
RbacConfig::RevokeRole {...}note variant if there's only a single admin.bobbinth commentedon Sep 26, 2026
What combinations get rejected? In general, we probably don't need to have more than 1 multisig for now. We could, of course, create as many multisigs as needed, but they will all be identical (same 5 signers) - so, this probably complicates things.
I'm assuming that we'll have 6 accounts to configure the faucet: 5 signlesig accounts + 1 multisig (4-of-5). Do we have a map of how these 6 account should map to different roles?
It would be easier to do all of the configuration from the singlesig account and then, as the last step, the account removes itself from the admin role. We can also invert this - the first thing the distirbutor account does is to set the multisig as the admin, but then all configuration changes will require 4 signers to be online. Doable - just quite a bit more complicated. On the other hand, maybe a good practice run to make sure everything is working via the multisig account.
mmagician commentedon Sep 28, 2026
The rejections are mostly applied to mixing roles that are intended to be single sigs and roles that are meant to be multisigs, e.g.
ADMINandDOM_PAUSER. HavingADMINandATTEST_ADMIN, which are both multisigs, mapped to the same account ID, is ok. We don't need to worry about these combinations in our case, I just wanted to mentioned it for completeness.That's a good argument. I think if we test the rotations on testnet, it's one thing, but doing it on mainnet should be exercised independently as well - and it's better to do it before the network is fully operational.
There is one more
PAUSERaccount handled by Predicate, call itP.Then we have 5 single-sig accounts
A...E, and one multisig accountM, configured as 4/5 threshold of the pubkeys ofA..E:The accounts are mapped as follows (cc @Dominik1999 could you confirm this is correct please?):
ADMIN->MATTEST_ADMIN->MBLK_MANAGER->PDOM_PAUSER- >[A, B, C, D, E]DOM_UNPAUSER->M