Engineer. Investor. Writer.
Shipping a Fork Without Shipping a Binary
How Base schedules a hardfork by writing it to a contract on Ethereum instead of baking a timestamp into every node, why the reads fail closed, and why the proof has to agree on the same schedule.
Every hardfork has a single instant when the whole network must start enforcing new rules at once — a new fee formula, a new opcode, a fix everyone signed off on months ago. Miss that instant on some fraction of nodes and the network splits: the laggards keep validating by the old rules while everyone else moves on, and now two chains disagree about what is true. Hitting that instant together is the hard part, and for years the standard way of doing it made it harder than it needed to be.
This post is about how Base schedules those instants without asking every operator to ship a new binary first, and why the proof has to agree on the schedule too.
The old way: a timestamp in the binary
The traditional approach bakes the activation time into the software. Pick a future timestamp, hard-code it, cut a release, and get every operator to upgrade before it lands. If enough of them do, the network flips together at the appointed second. If they don't, the stragglers fork off. So every upgrade became a coordination scramble — cut the release, chase operators, watch dashboards, hope — and moving the date meant running the whole dance again from the top.
Put the schedule where every node already looks
The schedule doesn't belong in the binary. It belongs somewhere every node already watches: Ethereum.
Base keeps a contract on L1, ProtocolVersions, whose only job is to hold the upgrade schedule — which upgrades are coming and the exact time each activates. The schedule is now data on L1, not code in a release. Every Base node already follows Ethereum, so reading the schedule from a contract is just one more thing it already does. Change the contract and every node picks up the change on its own, no release required.
How a node consumes it
A node reads the contract, gets back a list of (upgrade, activation time), and has to decide how far to trust it. Base exposes three levels, which double as a rollout ladder.
Watch-only reads the schedule, logs what it saw, and changes nothing — you can watch real schedules flow through production before they touch anything. Apply-at-startup reads once at boot and pins those activation times into config for the run. Apply-live keeps reading after boot and updates the rules on the fly when the on-chain schedule changes, no restart, with a manual refresh available.
The thing being updated is a small table of when each upgrade activates. A real timestamp switches an upgrade on at that time. Zero does not mean "now" — it means off, recorded as an explicit "never active" override:
// One entry from the schedule read off L1.
match activation_time {
// A real timestamp: activate once we reach it.
Some(ts) if ts > 0 => registry.set_activation(upgrade, ts),
// Zero (or absent) does NOT mean "now". It means this upgrade is
// switched off — record an explicit "never active" override.
Some(0) | None => registry.set_never_active(upgrade),
_ => {}
}Don't trust the contract too much
Reading your own consensus rules off a live contract should make you nervous, and the safeguards reflect that. Reads are bounded: a hard deadline per request, a cap on response size, and a limit on the number of scheduled upgrades, so a slow, oversized, or absurd answer can't tie a node up. Startup reads get a few retries with backoff; live reads are dropped if the node is shutting down.
The rule that matters is what happens when a read looks wrong: the node fails closed. Empty, stale, or describing an upgrade the node can't support — it refuses the change and keeps running on what it already knew. A missing answer is a reason to do nothing, never a reason to flip a switch. It's the same instinct as reading boot info in the proof: absent has to be handled as a deliberate decision, not a happy accident.
The proof has to agree
Here is where this ties back to how Base proves it ran correctly. The proof re-executes history, and it has to use the exact rules that were live at the time. If the chain activated an upgrade at noon, a proof about a 1pm block must activate it too. Follow even slightly different rules and the proof computes a different result and gets thrown out.
So the schedule can't be allowed to drift freely in the background. The proof is pinned to the same schedule the chain used, so the rules the proof follows and the rules the chain followed are guaranteed to be the same set. A contract on Ethereum and a sealed proving program, forced to agree on one shared fact.
Why it's worth it
Scheduling an upgrade stops being a release-and-coordinate scramble and becomes a change to a contract every node already watches. Rescheduling is another small change to the same contract. Rollout is staged — watch-only, then apply-at-startup, then apply-live — so you build confidence instead of betting everything on a flag day. And because the bounded, fail-closed reads and the proof pinning are there from the start, none of that convenience costs a guarantee: a bad read does nothing, and a drifting schedule can't pull the proof out of step with the chain.
Change the rules by changing a contract, let every node find out on its own, and make the proof follow the very same rules. That's the trick.
✎ Test Yourself
11 questions · each scores the instant you answer.
The coordination problem
Q1
What is the concrete failure when only part of the network activates a hardfork at the scheduled instant?
The old way
Q2
In the timestamp-in-the-binary approach, what does moving an activation date require?
The schedule on L1
Q3
What is the ProtocolVersions contract's role, and why L1 specifically?
Consuming the schedule
Q4
The three consumption modes double as a rollout ladder. What is the most conservative rung?
Q5
In apply-live mode, what happens when the on-chain schedule changes while the node is running?
Q6
An entry's activation time is 0. How is it interpreted?
Reading safely
Q7
Why bound each contract read with a deadline, a response-size cap, and a limit on the number of scheduled upgrades?
Q8
What does 'fail closed' mean when a read comes back empty, stale, or unsupportable?
The proof must agree
Q9
Why must a proof re-execute a block using the exact upgrade set that was live at that block's time?
Q10
How is the proof kept from drifting out of step with the chain's schedule?
Payoff
Q11
What is the net result of moving the schedule on-chain with fail-closed reads and proof pinning?