Testnet

The testnet doesn't stop teaching

Entry #47 · 2026-07-15 · Devlog

The testnet doesn't stop teaching

Last week's post ended with reward math actually paying the right people. External validators earning ASE for the blocks they signed. Bitmap and cert agreeing byte-for-byte. That felt like a clean ending.

The chain then ran for four days, hit a handful of edges nobody had stressed before, and ended the week with a full reset. This post is about what happened in between, and what the chain taught us this time.

The pattern I keep learning

Every consensus bug we've hit in the last month has followed the same shape.

Two components you designed correctly in isolation. Each one works. Each one has tests. The test suite covers the paths you thought about. Nobody ships buggy code on purpose.

Then real chain time happens. Real network jitter. Real cross-DC latency. Real validator restarts at slightly different moments. And somewhere in the interaction between the two components, you find a scenario neither component's designer considered, because the scenario doesn't exist inside either component's boundary. It only exists in the seam.

Two components. Correct interfaces. Compatible types. Wrong interaction under specific timing. Chain wedges.

I've watched this pattern play out enough times now that I stopped being surprised by it. The bugs from last week were like that. The bugs this weekend were like that. The bugs that we'll hit next week will be like that. The only way through is more chain time and more devnet coverage of the specific interaction modes we've caught.

What we hit this weekend

I won't dive deep on each edge because the fixes are unglamorous. But roughly:

  • A round timeout window that felt reasonable at 5-second block time became tight when a specific class of cross-DC jitter pushed some proposals to arrive 500ms late. The proposer's round timed out before its own proposal was seen everywhere. The next round elected a proposer that had already moved on. Different rounds got different proposals. Different nodes agreed with different proposals.
  • A snapshot download step in the one-liner installer assumed the snapshot would always exist. Between chain resets, there's a window where it doesn't. curl returns 404, the tar pipe fails, and the whole install aborts. Nobody caught this because the internal E2E test always ran with a warm snapshot present.
  • The auto-recovery cron used a tar command that assumed a specific optional file existed on the source node. On a fresh chain that file doesn't exist yet. Tar exits nonzero. The recovery script's error-handling saw a nonzero exit and bailed. The recovery that was supposed to auto-heal minor forks never fired.

Each one is embarrassing in isolation. Each one existed because the exact sequence of events we saw this weekend hadn't happened before. And each one is now covered by a fix and a devnet case.

The reset

By Sunday morning, enough of the state on-disk had accumulated from the buggy windows that starting over from a fresh genesis was the cleaner move. I posted the heads-up. Then I stopped all four baked, wiped their blocks/ and state/, kept their validator.key (which is the only thing that has to persist across resets), and started them again.

The chain came back up at h=1 within seconds. It's now at h=300+, running in tight lockstep with about 8-second blocks. Zero fork alerts since the reset.

Externals re-onboarded via the same one-liner installer that everyone else uses. Four of them running now on VPS boxes in different countries. They're bonded, they're waiting for the next epoch entry window, they'll start signing in a couple of hours. That's normal — validators bond mid-epoch, they join the active committee at the epoch boundary, they sign from then on.

The ecosystem catch-up

Every reset means every dapp that hardcodes contract addresses needs to know the new addresses.

The Asentum social app was the loud one — profile, posts, follow, votes, comments were all deployed at addresses that had code on the old chain but nothing on the new one. If you visited social.asentum.com this weekend at the wrong moment, you saw "target is not a contract" errors trying to load profiles. That's fixed now. Contracts redeployed, addresses updated, app rebuilt.

The other showcase dapps mostly didn't need contract redeploys — they either don't have singleton contracts or they use fresh ones per user session. But subs.asentum.com has a Premium contract hardcoded and that's still pointed at the old address. That's queued for the next work window. If you try to subscribe to a premium feature there right now, you'll get an error. Everything else on subs works.

Vault + WorkerRegistry — the two contracts that back the IDE Vault Explorer and the /workers panel — also need a redeploy. The deploy scripts have a small quirk that needs untangling first. So the Vault Explorer will look empty until we get to that, and any Worker daemons pointing at the old registry address are effectively idle. Not blocking anything on the chain layer.

Where the weekend leaves us

Chain: healthy. Four baked signing, five externals bonded, no forks since Sunday morning.

Tooling: the one-liner installer is more robust now. Auto-recovery works properly. Snapshot handling doesn't assume snapshots always exist. The downloads page and auto-update yaml both point at the current operator build. If you install a new node today or your existing node auto-updates tonight, you get the current code.

Social: back to normal. Profiles load, posts load, follows work.

Not yet done: subs premium, IDE Vault, Workers panel, indexer cursor cleanup (feeds will backfill as blocks flow through). The list is written down and each item has an owner and a rough time estimate.

What I'm working on next

The technical hardening we've been doing for the last month has bought us a chain that is boring most of the time and only interesting when a new edge case surfaces. That's the target state. From that state, we can start pouring energy into product.

The next block of work is the incentivized testnet layer. There's already an airdrop dashboard live at airdrop.asentum.com — every wallet accrues passive XP just by existing on-chain. What we're building next is the full interaction-capture pipeline. Every dapp use, every faucet drip, every contract deploy, every validator epoch — all crediting airdrop XP automatically through the indexer. No manual credits. No self-report forms.

The whole point of incentivizing testnet is to reward the people who showed up before it was easy. That reward needs to accurately reflect what people actually did, and the only way to make that work is to have the chain itself be the source of truth for every interaction.

That's the next few weeks.

What testnet is for, redux

Devlog #38 was about what testnet is for. That post held up. Every rough edge we've hit this month is a rough edge we won't ship on mainnet.

Every reset is a rough edge that got a fix.

Every fix is one fewer scenario where somebody eventually shows up with real value on the line and finds our chain broken in a way we already saw once.

That's the whole game. It's slower than "we shipped a chain and it just works." It's more honest than that too.

Boring is a feature. We'll get there.

— josh

Don't miss the next entry.

Join the launch list and we'll send you a note whenever there's a new devlog entry, a research drop, or a real milestone.