Diverging rails and a padlock breaking apart at the fork ← All research
Security

Stealing a shielded note across a fork

Upgrading how spends are authorised is one of the most dangerous things a private chain can do. For a window, TSN's fork boundary let an attacker spend a shielded note they never owned. Here is the mechanism, the fix, and the invariant we should have started with.

9 min TSN security Closed · pre-mainnet

In a shielded chain, a "note" is a private coin. Two properties keep it safe: no one can see who owns it, and only its owner can spend it. The second property is enforced by a spend-authorization scheme — the rule that decides whether a given spend is allowed to consume a given note. Change that rule mid-life, and you are performing surgery on the one invariant that makes the money yours.

The change

TSN introduced a hardened spend-authorization at a hardfork — call the new binding AuthBound. Notes created after the fork are bound to it: a valid spend must satisfy the new, stronger authorization. The problem was never the new notes. It was the old ones.

The window

Notes minted before the fork were authorised under the legacy scheme. When the new consensus activated, it needed a policy for those legacy notes — and the gap was that a legacy note could still be consumed across the boundary without being re-checked under AuthBound. A note's public, on-chain data was enough for an attacker to construct a spend that the post-fork rules accepted. The owner's secret was never required. In effect, at the fork line, unmigrated notes were briefly ownerless — spendable by whoever moved first.

Threat, stated plainly

Anyone watching the chain could take a pre-fork shielded note that wasn't theirs and spend it after activation, because the authorization the new rules demanded of new notes was not demanded of old ones. Confidentiality was intact; ownership was not.

activation height H PRE-FORK legacy authorization legacy notes theft window spend without AuthBound ✗ POST-FORK AuthBound required AuthBound notes MigrateToAuth before H →
Fig 1. The fix moves every legacy note through MigrateToAuth before height H. Anything not migrated by H is abandoned — unspendable — so the theft window closes to zero.

The fix: migrate, then abandon

The correct shape for a spend-auth upgrade is that no note survives the boundary unbound. TSN's answer is MigrateToAuth: before the activation height, an owner re-binds each legacy note to AuthBound using the note's real secret — proving ownership in the act of migrating. At the height, the door shuts. Any note not migrated is abandoned: permanently unspendable under the new rules. The fork is crossed cleanly — after it, the only spendable notes are AuthBound notes whose owners proved themselves.

Abandoning unmigrated value sounds harsh, and it is a real cost. But the alternative — carrying legacy notes forward under a rule that a stranger can satisfy — is not a chain you can call private money. A deadline that only the true owner can beat is exactly the property you want.

The invariant we should have had

The lesson generalises past this one bug:

Invariant

Any change to the spend-authorization scheme must, at the fork boundary, atomically re-bind or abandon every pre-existing note. There is no valid third state in which an old note remains spendable under the new rules without satisfying them. If your migration has a window where "old note, new rules, old authorization" type-checks, that window is a theft window.

We found and closed this before mainnet, which is the entire reason it is a lab note and not an incident report. The uncomfortable part — worth stating in a research log rather than hiding — is that the safe design was knowable up front: the invariant above is not a discovery, it is a checklist item we should have written before touching the authorization scheme. We are publishing it so it is on the checklist next time.

Filed under security · closed before mainnet genesis. MigrateToAuth is the mechanism; the invariant is the takeaway.