Heimdall v0.9.0 is a mandatory upgrade for all mainnet node operators. This release activates the Zurich hardfork at block height 47,880,000 (estimated for Thu, June 25th, ~2 PM UTC). All mainnet nodes must upgrade before this block to remain in consensus.
Zurich Hardfork Highlights (Consensus Changes)
These changes will activate automatically at block 47,880,000:
New Features & Improvements
v2.8.3+). This offers significant performance gains, with benchmarks showing the milestone proposition call is approximately 4.4 times faster than the HTTP path.eth_rpc_url and new bor_grpc_url fields now accept comma-separated endpoints, enabling automatic failover to a healthy backup if the primary Bor node becomes unresponsive. This feature is disabled by default for block producers to prevent them from producing blocks with a remote, possibly out-of-sync Bor instance.main_chain_gas_fee_cap and main_chain_gas_tip_cap parameters.tx_index.db directory to reclaim disk space after upgrading.Important Fixes
/tmp directory was not writable.Required Configuration Change
Before restarting your node with `v0.9.0`, you must update your app.toml file to replace the old gas price settings with the new EIP-1559 parameters.
In ~/.heimdalld/config/app.toml, replace:
main_chain_gas_limit = "5000000"
main_chain_max_gas_price = "400000000000"
with:
main_chain_gas_fee_cap = "500000000000"
main_chain_gas_tip_cap = "10000000000"
# Heimdall v0.9.0 — Mainnet (Zurich hardfork)
## Summary
Heimdall `v0.9.0` is a **mandatory release for all mainnet node operators**. It activates the **Zurich hardfork** at Heimdall block **`47,880,000`** (estimated **Thu June 25th, ~2PM UTC**), and ships a large set of features, consensus hardening, and fixes.
All mainnet nodes must be running `v0.9.0` **before block `47,880,000`**. Nodes on older versions will fall out of consensus at the activation height.
## Zurich hardfork (block `47,880,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 block `H` become visible at `H+1`, with visibility heights assigned deterministically in `PreBlocker`. 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** — `PrepareProposal` stops including side transactions beyond the per-block cap (50), and `ProcessProposal` deterministically rejects proposals that exceed it.
- **Wall-clock budgets for proposal and vote-extension construction** — `PrepareProposal` operates under a 500 ms budget and `ExtendVote` under 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** — `MsgVoteProducers` requires 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 (`MsgMultiSend` counts `len(outputs)`, `MsgSend` counts 1), keeping per-transaction work proportional under the flat-fee model (via cosmos-sdk fork `v0.2.11-polygon`).
- **`MsgCheckpoint` account root hash validation** — an ante decorator rejects checkpoints whose `AccountRootHash` is not exactly 32 bytes.
- **Bor availability tolerance** — `ProcessProposal` and `VerifyVoteExtension` treat "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_flag` in `app.toml` (with `bor_grpc_url` and `bor_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 **Bor `v2.8.3` or newer** (the full Bor gRPC server, #2194) on the connected Bor node when enabled.
- **Bor endpoint failover** — `eth_rpc_url` and `bor_grpc_url` accept 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) and `main_chain_gas_tip_cap` (default 10 gwei) in `app.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** — `MsgSetProducerDowntime` accepts an optional `target_producer_id` to 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_index` removal.
- Bridge memory-safety fixes, context-aware retry sleeps, and clean shutdown of bridge goroutines.
- `HeimdallListener` no longer gets stuck on nodes restored from pruned snapshots.
- Data race on the shared contracts-caller instance.
- `GetBorTxReceipt` and milestone governance-parameter loading fixes.
- Better gRPC client logging and a startup warning on Bor client misconfiguration; `bor_rpc_timeout` is clamped to 3 s to fit ABCI budgets.
- Node home directory is no longer wiped when `/tmp` is not writable.
- Packaging: `postrm` script cleanup, Dockerfile and docker-compose fixes, refreshed seeds and persistent peers.
## Dependencies
- `0xPolygon/cometbft` → `v0.3.8-polygon`
- `0xPolygon/cosmos-sdk` → `v0.2.11-polygon`
- `0xPolygon/polyproto` → `v0.0.8`
- Bor dependency rebased onto a go-ethereum `v1.17` base
- `golang.org/x/{crypto,net,sys}` and `quic-go` bumped 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:
```toml
#### gas limits ####
main_chain_gas_limit = "5000000"
#### gas price ####
main_chain_max_gas_price = "400000000000"
```
with:
```toml
#### 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.
```toml
[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 bind
```
For 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.
```toml
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 on `https://<host-or-service>:3131` with the matching token. A remote endpoint needs an explicit `http://`/`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
1. Stop `heimdalld`.
2. Install `v0.9.0` (packages attached, or build from source).
3. Review the new `app.toml` fields above (all optional; defaults preserve current behavior).
4. 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 https://github.com/0xPolygon/heimdall-v2/pull/532
* backport main by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/536
* Updating seeds in the amoy packaging by @sanketsaagar in https://github.com/0xPolygon/heimdall-v2/pull/515
* Switch to GCR by @adamdossa in https://github.com/0xPolygon/heimdall-v2/pull/537
* deps: bump quic-go by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/538
* ci: load kurtosis images from gcr instead of docker hub by @leovct in https://github.com/0xPolygon/heimdall-v2/pull/540
* Add Claude Code GitHub Workflow by @adamdossa in https://github.com/0xPolygon/heimdall-v2/pull/541
* fix(ci): test-state-sync by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/542
* refactor: ci by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/543
* chore: ci improvements by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/544
* feat: add network diagnostics and state dump action by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/546
* x/bor: fix producer-downtime panic when dividing by zero by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/547
* helper, x/checkpoint: fix grpc client / add better logs / warn on bor client misconfig by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/549
* chore: remove pumba compatibility step from stateless e2e by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/550
* app, x/bor: address issue 58 by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/551
* fix: avoid mirror.gcr.io images by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/555
* Allow comments for claude reviews by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/554
* Adding workflow to publish the docker image on GHCR by @0xsajal in https://github.com/0xPolygon/heimdall-v2/pull/559
* claude: port fixes from bor to solve claude CI issues by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/563
* fix: HeimdallListener stuck on pruned snapshot nodes by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/561
* bump deps to fix dependabot/govuln issues by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/564
* chore: add Claude Code security review rules for AI-assisted code review by @mt-polygon-technology in https://github.com/0xPolygon/heimdall-v2/pull/557
* fix: bridge memory safety fixes by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/560
* fix docker-compose by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/565
* bridge: context-aware sleeps and deterministic test by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/569
* Fix for postrm scripts by @djpolygon in https://github.com/0xPolygon/heimdall-v2/pull/571
* fix(logs): enable millisecond-precision timestamps in heimdall logs by @lucca30 in https://github.com/0xPolygon/heimdall-v2/pull/570
* backport: main to develop by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/580
* Fixing mainnet seeds and persistent peers by @sanketsaagar in https://github.com/0xPolygon/heimdall-v2/pull/581
* ci: fix kurtosis actions by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/588
* chore: pos workspace setup by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/585
* chore: bump comet by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/589
* pruning defaults + bridge tx_index-free refactor by @lucca30 in https://github.com/0xPolygon/heimdall-v2/pull/587
* chore: bump cosmos by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/591
* Implement prepareProposalBudget by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/586
* ABCI layer tests by @avalkov in https://github.com/0xPolygon/heimdall-v2/pull/539
* Deterministic state syncs by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/572
* bridge: improvements by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/575
* bridge: implement additional methods for self_heal by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/584
* app, helper, x/bor: full grpc implementation by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/576
* docs, .claude: add height-gated rollout review guidance by @pratikspatil024 in https://github.com/0xPolygon/heimdall-v2/pull/590
* x/bor: add target producer id in producer planned downtime by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/567
* x/bor: fix off-by-one in producer downtime span by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/573
* bridge: fix checkpoint tx height (off-by-one) post tx_index removal by @lucca30 in https://github.com/0xPolygon/heimdall-v2/pull/593
* chore: use Claude Opus 4.7 1M context in workflows by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/594
* ci: remove claude github actions by @adamdossa in https://github.com/0xPolygon/heimdall-v2/pull/596
* fix: module api docs by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/600
* No more nuking default node home when /tmp is not writeable by @n8wb in https://github.com/0xPolygon/heimdall-v2/pull/595
* CI/CD consolidation on heimdall-v2 by @sanketsaagar in https://github.com/0xPolygon/heimdall-v2/pull/599
* backport: release 0.8.1 by @kamuikatsurgi in https://github.com/0xPolygon/heimdall-v2/pull/603
* refactor: remove redundant variable declarations in for loops by @solunolab in https://github.com/0xPolygon/heimdall-v2/pull/606
* chore(deps): bump cometbft to v0.3.8-polygon by @pratikspatil024 in https://github.com/0xPolygon/heimdall-v2/pull/607
* bor, helper, bridge, metrics: add Bor endpoint failover by @pratikspatil024 in https://github.com/0xPolygon/heimdall-v2/pull/605
* backmerge v0.8.2 by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/609
* deps: bump golang.org/x/{crypto,net,sys} to clear govulncheck advisories by @lucca30 in https://github.com/0xPolygon/heimdall-v2/pull/597
* x/clerk: fix archive sync around ss by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/612
* app, helper, bridge, x/*: v0.9.0 candidate (Zurich hardfork) by @marcello33 in https://github.com/0xPolygon/heimdall-v2/pull/610
## New Contributors
* @adamdossa made their first contribution in https://github.com/0xPolygon/heimdall-v2/pull/537
* @mt-polygon-technology made their first contribution in https://github.com/0xPolygon/heimdall-v2/pull/557
* @n8wb made their first contribution in https://github.com/0xPolygon/heimdall-v2/pull/595
* @solunolab made their first contribution in https://github.com/0xPolygon/heimdall-v2/pull/606
**Full Changelog**: https://github.com/0xPolygon/heimdall-v2/compare/v0.8.2...v0.9.0