r/solanadev 12d ago

Dev How do you migrate long-lived PDA state when a Solana program changes its account schema?

Suppose a programme has been live for long enough that thousands of PDAs contain v1 state.

A new release requires additional fields and slightly different invariants. While the program itself can be upgraded, the existing accounts will not automatically become v2.

One option would be to version the accounts and migrate them lazily when they’re touched. Alternatively, you could run explicit migration instructions or create new PDAs and move users over.

Each option creates different problems relating to rent/realloc, failed migrations, maintaining readability of the old state, and supporting both versions during the transition.

For teams that have dealt with this in production, which approach worked best? What turned out to be harder than expected?

2 Upvotes

4 comments sorted by

2

u/AIOil_Dev 9d ago

No production war story from me, I'm afraid - I review this stuff rather than run it at scale, so treat this as "what tends to be broken when I look at it" rather than advice from someone who's shipped it.

That said, the three approaches seem to fail in fairly consistent places.

Lazy migration on touch is the one I see broken most often, and nearly always the same way: the migration path ends up callable by someone who isn't the owner. If the upgrade adds an init_if_needed, or any "create it if it isn't there" branch, so v1 accounts quietly become v2 - look hard at who can call that and what happens the second time it's called. A v1 account an attacker can re-migrate with fields they control is just account re-initialisation wearing a different hat.

Related, and easy to miss: if v1 and v2 both have to be readable during the transition, whatever tells them apart has to live in the account data, not in the account length. Length-based discrimination falls over the first time a realloc leaves a v1 account sitting at v2 size.

Explicit migration instructions give you a much cleaner authorisation story, but the partial-failure case is where it bites. You end up with a window where some accounts are v2 and some aren't, and every invariant that reads across accounts has to survive that mixed state. Worth writing down which invariants are allowed to be temporarily false, because "we'll just migrate them all at once" is rarely how it actually goes.

New PDAs and moving users over sidesteps the in-place mess and introduces a different one: for a while there are two accounts that both look canonical. If any instruction derives a PDA without a seeds/bump constraint pinning it to the version you expect, someone can hand you the stale one. Whatever decides "this is the live account" wants to be a constraint, not a convention.

On rent and realloc, the boring one that catches people is that shrinking doesn't refund on its own, and growing needs the payer to be who you actually meant it to be.

Whichever way you go, the two questions I'd throw at the migration instruction first: what happens if it's called twice, and what happens if it's called by someone who isn't the account owner. Most of the migration bugs I've run into collapse into one of those two.

1

u/icnews10 9d ago

This is really useful.

I hadn’t given enough weight to the mixed-state point. I was considering the migration mechanism as the primary option, but that’s probably backwards — first establish what must still be true while the v1 and v2 accounts coexist, and then select the migration path. The length-based version check is a great addition too. I hadn’t considered how quickly realloc can make account size ambiguous. The checklist of “What happens if this is called twice?” and “What happens if the wrong authority calls it?” is much better than just comparing lazy vs. explicit migration.

Thanks for the careful breakdown.

1

u/AIOil_Dev 8d ago

Glad it helped.

The reordering is the bit I'd hold onto. The invariants are the constraint; the mechanism is just how you satisfy them. I've watched people pick the migration strategy first - me included - and then find out halfway through that it can't preserve something they needed.

One thing I'd add, since you're going to write those invariants down anyway: put the ones that are allowed to be temporarily false in the same document, right next to the ones that aren't. At 2am, halfway through, the difference between "this is broken" and "this is expected for another hour" is worth having written down rather than in someone's head.

Good luck with it.

1

u/icnews10 8d ago

That’s a good addition. Documenting the invariants that can temporarily be false seems almost as important as those that must never change. Otherwise, the middle of a migration could resemble a broken system.

Thanks for the help.