Devnet-only vanity program IDs
The mainnet-track program IDs — mail_program at
MaiLyqjRuHp8SSQHjiLMPmhBcuLitSta4YdoTiibXu4 and alias_program at
ALiasg6qDnwcY8HfyeC1AjXRFjyqpXxW4omtwF1i125q — also have a live devnet
deployment. But the postoffice singleton account under that ID on devnet is
permanently bricked: leftover state from an old dev iteration with no
delegate ever assigned, and the delegate-gated AdminCloseAccount reclaim
tool cannot close an account that never had a delegate (see
Closing accounts for the general
tool; it structurally cannot reach this one). Rather than add a new on-chain
“rescue” instruction, this repo carries a second, devnet-only program
identity for each program, reachable only through an opt-in devnet
Cargo feature — a clean, empty postoffice, with the mainnet-track IDs and
their deployment left completely untouched. The
domain program, born after the bricking
in the item-28 split, carries a devnet twin too — not because anything of
its own is bricked, but because the cross-program identity assertions below
mean a devnet build must swap all three IDs together or none.
Why this has to be a compile-time swap, not a config value
Each program asserts its own identity against a compiled-in constant on
every instruction (Assert::is_mail_program / Assert::is_alias_program /
Assert::is_domain_program, program_common/src/assert.rs) — this is how
alias_program and domain_program verify the postoffice account they’ve
been handed genuinely belongs to mail_program when they derive the
postoffice PDA cross-program. An SBF program is a static
binary; there’s no way for it to read an environment variable or config file
to decide “which cluster am I on.” So the devnet identity is selected with a
Cargo feature, devnet, that swaps the embedded MAIL_PROGRAM_ID /
ALIAS_PROGRAM_ID / DOMAIN_PROGRAM_ID (mail_model/src/lib.rs) and their
Address-typed twins (program_common/src/address.rs) to a second,
freshly-mined vanity set — default OFF, so every normal build stays on the
mainnet-track identity.
This mirrors the modexp-free build’s shape exactly:
a deploy-target build selected by an explicit feature flag, not a new
default. The two features are independent and compose: a real devnet deploy
of domain_program needs both, since enable_big_mod_exp_syscall is
separately inactive on devnet (see that page — since the split only the
domain program carries the proof path, so mail_program and alias_program
need just devnet).
The devnet vanity IDs
mail_program(devnet):MaiLrDyjMHm7zC5yak9jmqDHctHXAgV3C1cFHW7Yd6falias_program(devnet):ALiasqsSbBw3txjZi6EqfcxFHc4sMYKRSVzDQdpG1X6Sdomain_program(devnet):DmaiNHGvprK2op7xqZHXp8UVXXmPUtkas96Goh5sCJQn
The matching keypairs are checked into the repo at
mail_client/tests/{mail,alias,domain}_program-dev-keypair.json, beside
the other devnet test fixtures (keypair/ holds only the mainnet-track
keypairs for all three programs) — devnet-only
material, not sensitive the way a mainnet upgrade authority would be (see
CLAUDE.md’s “secrets in the tree” note covering the intentionally
checked-in test/devnet keys).
This is the second devnet-only generation: the first
(MaiLb9JN…fedMW / ALiasgDpo…fsTkX, keypairs now only in git history)
is retired — its deployments remain on devnet but nothing in this repo
targets them anymore.
The target/deploy/ footgun
cargo build-sbf and the on-chain test harness always read and write the
same fixed filename slot — target/deploy/{name}-keypair.json and
target/deploy/{name}.so — regardless of which feature you’re building
with. There is no feature-based subdirectory. Switching between a
mainnet-track build and a devnet build means copying the right keypair into
that slot every time:
# Point target/deploy/ at devnet:
cp mail_client/tests/mail_program-dev-keypair.json target/deploy/mail_program-keypair.json
cp mail_client/tests/alias_program-dev-keypair.json target/deploy/alias_program-keypair.json
cp mail_client/tests/domain_program-dev-keypair.json target/deploy/domain_program-keypair.json
# Point it back at the mainnet-track identity before any other work
# (all three mainnet-track keypairs live in keypair/):
cp keypair/mail_program-keypair.json target/deploy/mail_program-keypair.json
cp keypair/alias_program-keypair.json target/deploy/alias_program-keypair.json
cp keypair/domain_program-keypair.json target/deploy/domain_program-keypair.json
Verify which one is actually staged with solana address -k target/deploy/mail_program-keypair.json after every switch — this is a
sharper version of the existing target/deploy/ trap documented in
HANDOFF.md (a stale/wrong keypair there silently deploys or tests against
the wrong address), now with two valid destinations to mix up instead of one
valid vs. one accidental-random one.
Never run the mail_client surfpool integration suite
(cargo test -p mail-client --features devnet --test api) — don’t combine
these. tests/api/surfpool.rs deploys and address-checks
target/deploy/{name}.so against whichever IDs the test binary itself was
compiled with; running it under devnet while target/deploy/ holds the
mainnet-track .so (or vice versa) produces confusing failures that look
like an ABI break rather than a keypair mixup. The suite is not devnet-aware
and isn’t meant to be — it always exercises the mainnet-track identity
against a local surfpool validator.
Building and deploying the devnet identity
# 1. Stage the devnet keypairs (see above).
cp mail_client/tests/mail_program-dev-keypair.json target/deploy/mail_program-keypair.json
cp mail_client/tests/alias_program-dev-keypair.json target/deploy/alias_program-keypair.json
cp mail_client/tests/domain_program-dev-keypair.json target/deploy/domain_program-keypair.json
# 2. Build. domain_program composes --no-default-features (drops dnssec-proof —
# enable_big_mod_exp_syscall is still inactive on devnet, see
# modexp-free-deploy.md) with --features devnet (swaps the embedded IDs);
# mail_program and alias_program are modexp-free unconditionally and need
# just the devnet feature.
# --tools-version v1.53 is NOT optional: build-sbf defaults to
# platform-tools v1.54, whose .so deploys fine but faults with an "Access
# violation in program section" on EVERY instruction at runtime (this bit
# us the first time through — the deploy and even `postmaster init`'s
# --skip-preflight path looked like they succeeded; only a confirmed
# non-skip-preflight send or `solana confirm -v` reveals the crash).
cargo build-sbf --manifest-path mail_program/Cargo.toml \
--tools-version v1.53 --features devnet
cargo build-sbf --manifest-path alias_program/Cargo.toml \
--tools-version v1.53 --features devnet
cargo build-sbf --manifest-path domain_program/Cargo.toml \
--tools-version v1.53 --no-default-features --features devnet
# 3. Sanity-check before spending anything:
solana address -k target/deploy/mail_program-keypair.json
solana address -k target/deploy/alias_program-keypair.json
solana address -k target/deploy/domain_program-keypair.json
llvm-readelf --dyn-syms target/deploy/domain_program.so | grep -i mod_exp # expect empty
# 4. Fresh deploy under the new IDs (NOT an upgrade of the bricked deployment).
solana program deploy target/deploy/mail_program.so \
--program-id target/deploy/mail_program-keypair.json --url https://api.devnet.solana.com
solana program deploy target/deploy/alias_program.so \
--program-id target/deploy/alias_program-keypair.json --url https://api.devnet.solana.com
solana program deploy target/deploy/domain_program.so \
--program-id target/deploy/domain_program-keypair.json --url https://api.devnet.solana.com
Then build and use the CLI against the new identity — cluster targeting
(solana config / JSON_RPC_URL) and the devnet Cargo feature are
orthogonal, both need to be set:
cargo build --release -p mail-client --bin sithbit --features postmaster,devnet
# point at devnet: solana config set --url https://api.devnet.solana.com (or JSON_RPC_URL)
./target/release/sithbit postmaster init \
--seed <seed0> --seed <seed1> -k <delegate-keypair>
This lands InitPostoffice on the fresh postoffice PDA seeded off the new
devnet MAIL_PROGRAM_ID — a never-initialized account, entirely distinct
from the bricked mainnet-track-ID one.
See also
- Deploying to devnet/mainnet-beta: the modexp-free build
— the companion workaround this one composes with; a real devnet deploy of
domain_programneeds both. - Closing accounts — the
AdminCloseAccount/postmaster reclaimtool that cannot reach the bricked mainnet-track-ID devnet postoffice, which is why this identity exists. - Program upgrade authority