Project Introduction
Portal Canary tests a Portaldot runtime upgrade on a copy of today’s mainnet before it ever touches mainnet. Each run forks mainnet at its latest finalized block (all 4,716 accounts, deposits, locks, stakes) in about 15 seconds, upgrades the copy with setCode, and checks every account and the total supply before and after, whether blocks keep coming, what changed in the runtime, and five everyday workflows on both chains. Every run’s result is recorded on the Portaldot V3 testnet and can be re-checked by anyone, in the browser or with one script, no key needed.
Problem Solved
A runtime upgrade can look perfect and still destroy money. I built a test runtime whose migration halves every reserved balance and ran it on a copy of mainnet:
- ✓
setCodeaccepted, spec_version 1002 → 1006 - ✓ blocks kept coming under the new runtime, no error anywhere
- ✗ 286.305 POT gone from 9 real mainnet accounts, while total supply still claims it exists
Every signal the chain gives said the upgrade worked. Only checking the money, account by account, showed it. Portaldot makes this harder: mainnet runs a 2021 runtime with metadata V13, which current Substrate tooling (V14+) can’t decode, and mainnet has no upgrade-testing runtime API. Every upgrade is one live setCode.
Link
Demo Video: https://youtu.be/dV6r8IFqzck
Live Web: Dashboard · Portal Canary
GitHub: GitHub - Venkat5599/portalcanary: Differential runtime-upgrade testing for Portaldot: same workflows before and after an upgrade, every difference classified, one command to reproduce. · GitHub
Pitch Deck: Portal Canary · pitch deck
Testnet details: portalcanary/docs/TESTNET.md at main · Venkat5599/portalcanary · GitHub
Technical Highlights
1. Upgrades rehearsed on real mainnet state, not an empty chain
A fresh test chain has no deposits, so a broken migration has nothing to break (my first run passed falsely for exactly this reason). Portal Canary forks mainnet’s real storage into a local chain that only it controls, freezes staking eras, keeps the supply consistent, and never writes to mainnet: the upgrade step refuses any chain that reports itself as Live.
2. Its own V13 + V14 decoder and a compatibility-risk engine
Written in Rust, it reads mainnet (V13) and V3 (V14) side by side. Every runtime difference is rated Critical / Breaking / Needs migration / Behaviour / Client update, with the reason and the before/after values. Mainnet → V3: 969 changes, 111 risks, 3 critical. Mainnet → mainnet: 0 and 0. The same engine runs in the browser as WebAssembly.
3. Proof anyone can check
Each run writes one result file that holds the sha256 of every other file, and that hash is recorded on V3 as a system.remark. The run page re-checks all three links in your browser: the files, the result, and the block. scripts/verify-runs.sh re-checks every run with no key and no Rust toolchain (it uses a prebuilt CLI checked against its sha256). Latest result: 24 of 24 recorded runs verified.
Example: run 971a7223bf7b, Critical on mainnet block #2,747,398, recorded in V3 block #237,962, tx 0x0aaa674d19e3d26c249f92a27604c5fbd005052ea5d47bece86655d7b0ab719f.
4. On-chain registry contract on the V3 EVM
CanaryRegistry at 0x62Eacc58CE0FA37F06F24074AFb7B3F22Df708FF (chain 420420777), deploy tx 0xadb58c0b7ff356a1304e8422b7f606782c91e39460bca2ab9ace133cc8656ed2. scripts/verify-contract.sh re-checks it with only curl and python3: the deployed code, the deploy tx, all 4 stored records against the committed reports, and every attest tx.
5. Every runtime pull request can run it: a GitHub Action
uses: Venkat5599/portalcanary/upgrade-check@main in any runtime repo forks mainnet, upgrades the copy to the PR’s runtime, posts the verdict and the per-account table on the PR, and fails the check when funds are lost, the chain halts, or a workflow breaks. Our CI runs it the way another repo would: the clean runtime must pass, and the broken migration must fail with “funds destroyed”.
6. Runs every day on its own
A daily check runs a clean runtime (must pass) and the broken one (must be caught) on fresh mainnet state, and opens a GitHub issue if either goes wrong. On day one it caught a real bug in our own recording step, opened issue #1, and closed it after the fix.
7. Real everything, zero mocks
Real nodes, real setCode, real V3 transactions. 117 tests (42 Rust, 33 unit, 38 browser on desktop and phone, 4 contract), and CI forks mainnet and requires the broken migration to be caught.
Team
Venkat (solo build)
- GitHub: @Venkat5599
- Email: komarivenkataramana4@gmail.com
- Track: Node Operations & Tooling
Judges & Voting Fit
For Judges: Passed check-in. The mentor feedback: “rehearsing a runtime upgrade on a fork is the value; every upgrade should pass through this; node operators would pay.” Since then I’ve added the upgrade-check Action so every runtime PR can run it, a prebuilt CLI so anyone can verify without Rust, and a keyless contract check. Everything is live on the V3 testnet and every claim can be re-checked from public data.
For Community Voters: Portaldot is moving from mainnet to V3, and every upgrade on the way is one setCode that can’t be undone. Portal Canary shows what an upgrade does to real balances before it ships. Open the dashboard, pick a runtime, keep “Mainnet state”, and press Start run. About five minutes later you get the verdict and a record you can check yourself.