Heimdall v0.9.0-beta — Amoy (Zurich hardfork)
Summary
Heimdall v0.9.0-beta is a mandatory release for all Amoy node operators. It activates the Zurich hardfork at Heimdall block 37,750,000 (estimated Wednesday, June 17th 2026, ~14:00 UTC), and ships a large set of features, consensus hardening, and fixes.
All Amoy nodes must be running v0.9.0-beta before block 37,750,000. Nodes on older versions will fall out of consensus at the activation height.
Zurich hardfork (block 37,750,000)
Consensus-affecting changes, all gated by the Zurich activation height:
- Deterministic state-sync processing (
x/clerk) — event visibility moves from wall-clock-based to block-height-based. Events recorded in blockHbecome visible atH+1, with visibility heights assigned deterministically inPreBlocker. Out-of-order events at the upgrade boundary are handled explicitly, and any error on the visibility path now aborts block processing instead of being logged and skipped. New gRPC/REST queries expose record lists by height and by time. - Symmetric side-transaction caps —
PrepareProposalstops including side transactions beyond the per-block cap (50), andProcessProposaldeterministically rejects proposals that exceed it. - Wall-clock budgets for proposal and vote-extension construction —
PrepareProposaloperates under a 500 ms budget andExtendVoteunder an 800 ms budget, returning partial results instead of overrunning CometBFT timeouts (milestone proposition is skipped when the budget is exhausted). - Commit-only checkpoint signatures — checkpoint signature aggregation includes only votes flagged as
Commit. - Deterministic milestone proposition — the majority parent-hash evaluation now binds explicitly to the last end-block hash instead of iterating candidate parent hashes, guaranteeing identical results across validators.
- Producer votes restricted to the active validator set —
MsgVoteProducersrequires the voter to be in the active set, and voting-power resolution uses active-set power consistently on both sides of the majority threshold. - Bank-transfer output cap — a new ante decorator caps the aggregate number of bank-transfer outputs per transaction at 16 (
MsgMultiSendcountslen(outputs),MsgSendcounts 1), keeping per-transaction work proportional under the flat-fee model (via cosmos-sdk forkv0.2.11-polygon). MsgCheckpointaccount root hash validation — an ante decorator rejects checkpoints whoseAccountRootHashis not exactly 32 bytes.- Bor availability tolerance —
ProcessProposalandVerifyVoteExtensiontreat "block not found" responses from Bor as transient (same handling as query failures), avoiding spurious proposal rejections while a local Bor is catching up.
Features
- Full Heimdall↔Bor gRPC transport (opt-in) — every Bor-facing call now has a gRPC path, selectable per node via
bor_grpc_flaginapp.toml(withbor_grpc_urlandbor_grpc_token). Includes a startup hash-parity check across both transports and a single-round-trip batch call for milestone proposition (≈4.4× faster than the HTTP path on devnet benchmarks). HTTP remains the default; no behavior change unless enabled. Requires Borv2.8.3or newer (the full Bor gRPC server, #2194) on the connected Bor node when enabled. - Bor endpoint failover —
eth_rpc_urlandbor_grpc_urlaccept comma-separated endpoints with automatic failover, health probing, and metrics (bor_healthcheck_*). The primary endpoint is authoritative and reclaims after recovery. Failover is refused at startup on block-producing validators (safety guard). - EIP-1559 L1 transactions — bridge transactions to Ethereum use dynamic fees, configurable via
main_chain_gas_fee_cap(default 500 gwei) andmain_chain_gas_tip_cap(default 10 gwei) inapp.toml. - Bridge self-heal extensions — new recovery methods reconstruct checkpoint and state-sync data from authoritative L1/Bor sources when local records are incomplete.
tx_index-free bridge + pruning defaults — the bridge checkpoint flow no longer depends on CometBFT's tx indexer, so it works on pruned nodes; default pruning settings updated accordingly.- Targeted producer replacement —
MsgSetProducerDowntimeaccepts an optionaltarget_producer_idto designate a specific replacement producer during planned downtime (default remains round-robin). - Millisecond-precision log timestamps — Heimdall logs now match Bor's ISO-8601/ms format for cross-service correlation.
Fixes
- Producer-downtime span off-by-one and a divide-by-zero panic on empty producer sets (
x/bor). - Checkpoint transaction lookup off-by-one in the bridge after the
tx_indexremoval. - Bridge memory-safety fixes, context-aware retry sleeps, and clean shutdown of bridge goroutines.
HeimdallListenerno longer gets stuck on nodes restored from pruned snapshots.- Data race on the shared contracts-caller instance.
GetBorTxReceiptand milestone governance-parameter loading fixes.- Better gRPC client logging and a startup warning on Bor client misconfiguration;
bor_rpc_timeoutis clamped to 3 s to fit ABCI budgets. - Node home directory is no longer wiped when
/tmpis not writable. - Packaging:
postrmscript cleanup, Dockerfile and docker-compose fixes, refreshed seeds and persistent peers.
Dependencies
0xPolygon/cometbft→v0.3.8-polygon0xPolygon/cosmos-sdk→v0.2.11-polygon0xPolygon/polyproto→v0.0.8- Bor dependency rebased onto a go-ethereum
v1.17base golang.org/x/{crypto,net,sys}andquic-gobumped to clear all govulncheck advisories
Required change
In app.toml (default location ~/.heimdalld/config/app.toml, /var/lib/heimdall/config/app.toml on packaged installs), replace:
#### gas limits ####
main_chain_gas_limit = "5000000"
#### gas price ####
main_chain_max_gas_price = "400000000000"with:
#### gas price configs (EIP-1559) ####
main_chain_gas_fee_cap = "500000000000" # max fee per gas, wei (default 500 gwei)
main_chain_gas_tip_cap = "10000000000" # max priority fee per gas, wei (default 10 gwei)Apply this before restarting on the new version.
Configuration changes (app.toml)
| Field | Default | Purpose |
|---|---|---|
bor_grpc_flag
| false
| Enable gRPC transport to Bor |
bor_grpc_url
| localhost:3131
| Bor gRPC endpoint(s), comma-separated for failover |
bor_grpc_token
| empty | Bearer token for authenticated Bor gRPC |
main_chain_gas_fee_cap
| 500000000000
| Max fee per gas for L1 txs (wei) |
main_chain_gas_tip_cap
| 10000000000
| Max priority fee for L1 txs (wei) |
eth_rpc_url
| unchanged | Now accepts comma-separated endpoints (failover) |
Enabling gRPC to Bor (optional)
gRPC is opt-in; HTTP JSON-RPC stays the default, so existing operators see no change. Enable it Bor first, then Heimdall — Bor stays HTTP-compatible so bringing the server up first is safe, and Heimdall's startup parity check needs Bor already serving gRPC. Requires Bor v2.8.3 or newer (the full gRPC server #2194, plus its [grpc] loopback-default prerequisite #2078) paired with this Heimdall release (the client side).
Step 1 — Bor (config.toml): opt into the gRPC server on an address Heimdall can reach.
[grpc]
addr = "127.0.0.1:3131" # same-host validator pair; loopback is the access control, no TLS needed
# addr = "0.0.0.0:3131" # cross-host; must pair with TLS / a firewall
token = "" # bearer token; leave empty on loopback, set it for any non-loopback bindFor authenticated (non-loopback) deployments, prefer the env var over the flag/file (the flag leaks into ps/shell history):
export BOR_GRPC_TOKEN="$(openssl rand -hex 32)"
Bor's gRPC exposes only read-only public chain data, so on a same-host loopback bind a token isn't required; it matters cross-host, on shared multi-tenant hosts, and as defense-in-depth if the bind is later widened. (Equivalent flags: --grpc.addr, --grpc.token.)
Step 2 — Heimdall (app.toml): point the client at Bor and match the token.
bor_grpc_flag = "true"
bor_grpc_url = "http://127.0.0.1:3131" # same-host
# bor_grpc_url = "https://bor.example.net:3131" # cross-host (TLS)
bor_grpc_token = "<match Bor; empty if Bor's token is empty>"Restart Heimdall; at startup it runs one HeaderByNumber over both transports and logs a warning/fatal if the hashes diverge (guards against a stale Bor that doesn't populate the full proto header). Put credentials in bor_grpc_token, never in the URL; bor_grpc_url also accepts a comma-separated list for failover.
Layouts.
- Same host / same container / same pod — loopback (
http://127.0.0.1:3131), no token. Simplest; matches the defaults. - Cross-host, or separate Docker/compose containers (different network namespaces, so not loopback) — Bor on
0.0.0.0:3131+ a token; Heimdall onhttps://<host-or-service>:3131with the matching token. A remote endpoint needs an explicithttp:///https://scheme, and Heimdall refuses to send a token over plaintext to a non-loopback peer — so a cross-host token means TLS (terminate it in front of Bor's plaintext gRPC, or keep the link on a private network/firewall).
Downgrade: set bor_grpc_flag = "false" in Heimdall and restart — Bor doesn't need changing (it keeps serving both transports).
No store migration or resync is required; the upgrade is a binary swap + restart. Pre-activation behavior is unchanged, so mixed-version operation is safe only until the activation height.
Upgrade instructions
- Stop
heimdalld. - Install
v0.9.0-beta(packages attached, or build from source). - Review the new
app.tomlfields above (all optional; defaults preserve current behavior). - Restart and confirm the node resumes signing/syncing.
Deadline: before block 37,750,000 (estimated June 17th 2026, ~14:00–15:00 UTC — track the live height as the date approaches).
What's Changed
- helper(tx): add EIP-1559 dynamic gas pricing for L1 transactions by @kamuikatsurgi in #532
- backport main by @marcello33 in #536
- Updating seeds in the amoy packaging by @sanketsaagar in #515
- Switch to GCR by @adamdossa in #537
- deps: bump quic-go by @marcello33 in #538
- ci: load kurtosis images from gcr instead of docker hub by @leovct in #540
- Add Claude Code GitHub Workflow by @adamdossa in #541
- fix(ci): test-state-sync by @kamuikatsurgi in #542
- refactor: ci by @kamuikatsurgi in #543
- chore: ci improvements by @kamuikatsurgi in #544
- feat: add network diagnostics and state dump action by @kamuikatsurgi in #546
- x/bor: fix producer-downtime panic when dividing by zero by @marcello33 in #547
- helper, x/checkpoint: fix grpc client / add better logs / warn on bor client misconfig by @marcello33 in #549
- chore: remove pumba compatibility step from stateless e2e by @kamuikatsurgi in #550
- app, x/bor: address issue 58 by @marcello33 in #551
- fix: avoid mirror.gcr.io images by @kamuikatsurgi in #555
- Allow comments for claude reviews by @marcello33 in #554
- Adding workflow to publish the docker image on GHCR by @0xsajal in #559
- claude: port fixes from bor to solve claude CI issues by @marcello33 in #563
- fix: HeimdallListener stuck on pruned snapshot nodes by @marcello33 in #561
- bump deps to fix dependabot/govuln issues by @marcello33 in #564
- chore: add Claude Code security review rules for AI-assisted code review by @mt-polygon-technology in #557
- fix: bridge memory safety fixes by @marcello33 in #560
- fix docker-compose by @marcello33 in #565
- bridge: context-aware sleeps and deterministic test by @marcello33 in #569
- Fix for postrm scripts by @djpolygon in #571
- fix(logs): enable millisecond-precision timestamps in heimdall logs by @lucca30 in #570
- backport: main to develop by @kamuikatsurgi in #580
- Fixing mainnet seeds and persistent peers by @sanketsaagar in #581
- ci: fix kurtosis actions by @marcello33 in #588
- chore: pos workspace setup by @marcello33 in #585
- chore: bump comet by @marcello33 in #589
- pruning defaults + bridge tx_index-free refactor by @lucca30 in #587
- chore: bump cosmos by @marcello33 in #591
- Implement prepareProposalBudget by @marcello33 in #586
- ABCI layer tests by @avalkov in #539
- Deterministic state syncs by @marcello33 in #572
- bridge: improvements by @kamuikatsurgi in #575
- bridge: implement additional methods for self_heal by @marcello33 in #584
- app, helper, x/bor: full grpc implementation by @marcello33 in #576
- docs, .claude: add height-gated rollout review guidance by @pratikspatil024 in #590
- x/bor: add target producer id in producer planned downtime by @kamuikatsurgi in #567
- x/bor: fix off-by-one in producer downtime span by @kamuikatsurgi in #573
- bridge: fix checkpoint tx height (off-by-one) post tx_index removal by @lucca30 in #593
- chore: use Claude Opus 4.7 1M context in workflows by @kamuikatsurgi in #594
- ci: remove claude github actions by @adamdossa in #596
- fix: module api docs by @kamuikatsurgi in #600
- No more nuking default node home when /tmp is not writeable by @n8wb in #595
- CI/CD consolidation on heimdall-v2 by @sanketsaagar in #599
- backport: release 0.8.1 by @kamuikatsurgi in #603
- refactor: remove redundant variable declarations in for loops by @solunolab in #606
- chore(deps): bump cometbft to v0.3.8-polygon by @pratikspatil024 in #607
- bor, helper, bridge, metrics: add Bor endpoint failover by @pratikspatil024 in #605
- backmerge v0.8.2 by @marcello33 in #609
- deps: bump golang.org/x/{crypto,net,sys} to clear govulncheck advisories by @lucca30 in #597
New Contributors
- @adamdossa made their first contribution in #537
- @mt-polygon-technology made their first contribution in #557
- @n8wb made their first contribution in #595
- @solunolab made their first contribution in #606
Full Changelog: v0.8.2...v0.9.0-beta