-
Notifications
You must be signed in to change notification settings - Fork 338
SIMD-0432: Loader V3: Reclaim Closed Program #432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
1b9bd68
15ac60b
2aca367
72196ed
d8b33b2
0877b2f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,255 @@ | ||
| --- | ||
| simd: '0432' | ||
| title: 'Loader V3: Reclaim Closed Program' | ||
| authors: | ||
| - Joe Caulfield (Anza) | ||
| - Dean Little (Blueshift) | ||
| category: Standard | ||
| type: Core | ||
| status: Review | ||
| created: 2025-12-14 | ||
| feature: (fill in with feature key and github tracking issues once accepted) | ||
| --- | ||
|
|
||
| ## Summary | ||
|
|
||
| This SIMD proposes changing the default behavior when closing upgradeable | ||
| programs so that program accounts are fully reclaimed and their addresses | ||
| become reusable. Tombstoning program accounts would remain supported, but only | ||
| when explicitly requested. | ||
|
|
||
| ## Motivation | ||
|
|
||
| Today, closing an upgradeable program permanently tombstones its program | ||
| account, preventing reuse of the program ID. This behavior has led to several | ||
| issues: | ||
|
|
||
| - Loss of funds: Users frequently confuse close program with close buffer in the | ||
| Solana CLI, unintentionally irreversibly disabling programs. | ||
| - Permanent account bloat: Tombstoned program accounts cannot be reclaimed and | ||
| accumulate indefinitely in the accounts database. | ||
|
Comment on lines
+29
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. surely the accounts orphaned by closing the program massively outweigh the program itself? |
||
| - RPC performance degradation: getProgramAccounts against the loader v3 program | ||
| must return all program accounts, including closed ones, increasing response | ||
| size and latency. | ||
|
Comment on lines
+29
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we know how many tombstoned program accounts there are? Is this actually a performance issue? I'm not against this proposal but I think in general we should be clear about the motivations for proposals, and keep the motivations list small - only including the most impactful ones.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just ran a scrape and found 49,210 Loader V3 program accounts on mb, 33,804 of which are tombstones. |
||
|
|
||
| These drawbacks outweigh the benefits of mandatory tombstoning. A more flexible | ||
| model allows safe address reuse by default while preserving explicit | ||
| tombstoning for users who require it. | ||
|
|
||
| ## New Terminology | ||
|
|
||
| No new terminology is introduced by this proposal. | ||
|
|
||
| ## Detailed Design | ||
|
|
||
| The `Close` instruction will be updated to include an optional boolean input. If | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: maybe mention again this is the Loader-v3 Close instruction here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| not provided, the default will be `false`. | ||
|
buffalojoec marked this conversation as resolved.
Outdated
|
||
|
|
||
| ``` | ||
| Close { tombstone: bool } | ||
| ``` | ||
|
|
||
| ``` | ||
| | 4-byte discriminator | 1-byte boolean | | ||
| ``` | ||
|
|
||
| The accounts required by the instruction are unchanged: | ||
|
|
||
| - Account 0: Programdata account (writable) | ||
| - Account 1: Recipient (writable) | ||
| - Account 2: Authority (signer) | ||
| - Account 3: Program account (writable) | ||
|
|
||
| Currently, the authority (index 2) and program (index 3) accounts are only | ||
| required for closure of initialized programs. This proposal also requires them | ||
| for reclamation of legacy tombstones. This is detailed in the Control Flow | ||
| section. | ||
|
|
||
| ### Base Workflow | ||
|
|
||
| For a value of `false`, the program will clear the program account's data, | ||
| resize it to zero, and withdraw all lamports. This will render the account no | ||
| longer rent-exempt and subject to garbage collection by the runtime at the end | ||
| of the transaction. As such, the program address can be reclaimed after the | ||
| account has been garbage collected. | ||
|
|
||
| The Close instruction MUST fail if `tombstone` is `false` and the program was | ||
| deployed in the current slot (this field is stored in the programdata account | ||
| layout). This prevents a deploy-close-reclaim loop within the same slot, which | ||
| would corrupt the program cache (see Security Considerations). Programs | ||
| deployed in the current slot can still be closed with `tombstone=true`. | ||
|
|
||
| For a value of `true`, the program will clear the program account's data, resize | ||
| it to zero, but retain the rent-exempt minimum lamports for the base account | ||
| metadata. The program account will then be assigned to itself, creating a | ||
| permanent tombstone for the program. | ||
|
|
||
| ``` | ||
| Close { tombstone } | ||
| | | ||
| +-----------+-----------+ | ||
| | | | ||
| tombstone=false tombstone=true | ||
| | | | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we add "program was deployed in the current slot" branches to this picture?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| Clear data & resize Clear data & resize | ||
| Withdraw all lamports Retain rent-exempt min | ||
| | | | ||
| Account → GC'd Owner → self (tombstone) | ||
| Address reclaimable Address permanently locked | ||
| ``` | ||
|
|
||
| In both workflows, the programdata account will be defunded and set to | ||
| `Uninitialized`, causing it to be garbage collected at end of transaction. | ||
|
|
||
| ### Control Flow | ||
|
|
||
| The entire control flow of the Loader V3 `Close` instruction is detailed below | ||
| with modifications highlighted. | ||
|
|
||
| 1. At least 2 accounts must be provided, otherwise throw | ||
| `NotEnoughAccountKeys`. | ||
| 2. Accounts at index 0 and 1 must not alias, otherwise throw | ||
| `InvalidArgument`. | ||
| 3. The "close" account (index 0) must deserialize as `UpgradeableLoaderState`, | ||
| otherwise throw `InvalidAccountData`. | ||
|
|
||
| Once the close account's state is deserialized, the remaining control flow | ||
| depends on the type of account. | ||
|
|
||
| #### `ProgramData` | ||
|
|
||
| 1. At least 4 accounts must be provided, otherwise throw | ||
| `NotEnoughAccountKeys`. | ||
| 2. The program account (index 3) must be writable, otherwise throw | ||
| `InvalidArgument`. | ||
| 3. The program account must be owned by Loader v3, otherwise throw | ||
| `IncorrectProgramId`. | ||
| 4. **[NEW]** If `tombstone` is `false`, the programdata's `slot` field must not | ||
| equal the current slot (from Clock sysvar), otherwise throw | ||
| `InvalidArgument`. | ||
| 5. The program account must deserialize as `UpgradeableLoaderState`, otherwise | ||
| throw `InvalidAccountData`. | ||
| 6. The program account must be in `Program` state, otherwise throw | ||
| `InvalidArgument`. | ||
| 7. The program account's `programdata_address` must match the close account | ||
| (index 0), otherwise throw `InvalidArgument`. | ||
| 8. If the programdata's `upgrade_authority_address` is `None` (frozen program), | ||
| throw `Immutable`. | ||
| 9. The authority (index 2) must match the programdata's | ||
| `upgrade_authority_address`, otherwise throw `IncorrectAuthority`. | ||
| 10. The authority must be a signer, otherwise throw `MissingRequiredSignature`. | ||
| 11. Transfer all lamports from the close account to the recipient (index 1). | ||
| 12. Set the close account's state to `Uninitialized` (account will be garbage | ||
| collected at end of transaction due to zero lamports). | ||
| 13. **[NEW]** Clear the program account's data and resize to zero. | ||
| 14. **[NEW]** If `tombstone` is `true`: transfer excess lamports (above | ||
| rent-exempt minimum for zero-data account) to recipient, then assign the | ||
| program account to itself. | ||
| 15. **[NEW]** If `tombstone` is `false`: transfer all lamports from the program | ||
| account to the recipient (account will be garbage collected). | ||
|
|
||
| #### `Uninitialized` | ||
|
|
||
| First determine if this is a reclaim of a legacy tombstone. | ||
|
|
||
| Programs closed before this proposal remain in a legacy tombstone state: | ||
|
|
||
| - Program account: Owned by Loader v3, `Program { programdata }` state, funded, | ||
| programdata address points to programdata account. | ||
| - Programdata account: `Uninitialized` (all-zeroes). | ||
|
|
||
| **[NEW]** This state must be infallibly evaluated. If the above program account | ||
| state is confirmed, the control flow for a legacy tombstone reclaim is as | ||
| follows: | ||
|
|
||
| 1. At least 4 accounts must be provided, otherwise throw | ||
| `NotEnoughAccountKeys`. | ||
| 2. The program account (index 3) must be writable, otherwise throw | ||
| `InvalidArgument`. | ||
| 3. The program account must be owned by Loader v3, otherwise throw | ||
| `IncorrectProgramId`. | ||
| 4. The authority (index 2) must equal the program account's pubkey (i.e., the | ||
| program keypair), otherwise throw `IncorrectAuthority`. | ||
| 5. The authority must be a signer, otherwise throw `MissingRequiredSignature`. | ||
| 6. Transfer all lamports from the close account to the recipient (index 1). | ||
| 7. Set the close account's state to `Uninitialized` (account will be garbage | ||
| collected at end of transaction due to zero lamports). | ||
| 8. Clear the program account's data and resize to zero. | ||
| 9. If `tombstone` is `true`: transfer excess lamports (above rent-exempt | ||
| minimum for zero-data account) to recipient, then assign the program | ||
| account to itself. | ||
| 10. If `tombstone` is `false`: transfer all lamports from the program account | ||
| to the recipient. | ||
|
|
||
| If this is not a reclaim of a legacy tombstone, the control flow is as follows: | ||
|
|
||
| 1. At least 1 account must be provided, otherwise throw | ||
| `NotEnoughAccountKeys`. | ||
| 2. Transfer all lamports from the close account to the recipient (index 1). | ||
|
|
||
| #### `Buffer` | ||
|
|
||
| 1. At least 3 accounts must be provided, otherwise throw | ||
| `NotEnoughAccountKeys`. | ||
| 2. If the buffer's `authority_address` is `None`, throw `Immutable`. | ||
| 3. The authority (index 2) must match the buffer's `authority_address`, | ||
| otherwise throw `IncorrectAuthority`. | ||
| 4. The authority must be a signer, otherwise throw `MissingRequiredSignature`. | ||
| 5. Transfer all lamports from the close account to the recipient (index 1). | ||
| 6. Set the close account's state to `Uninitialized` (account will be garbage | ||
| collected at end of transaction due to zero lamports). | ||
|
|
||
| #### `Program` | ||
|
|
||
| 1. Throw `InvalidArgument`. Program accounts cannot be closed directly; the | ||
| programdata account must be closed instead. | ||
|
|
||
| ### Feature Activation | ||
|
|
||
| This change will be a feature-gated behavioral change to the existing Close | ||
| instruction. After the feature is activated, the boolean value can be included | ||
| to utilize the new functionality, and legacy tombstones can be reclaimed. | ||
|
|
||
| ## Alternatives Considered | ||
|
|
||
| N/A | ||
|
|
||
| ## Impact | ||
|
|
||
| This proposal removes a harmful default behavior that has caused repeated loss | ||
| of funds and persistent state bloat, while preserving security guarantees for | ||
| users who explicitly wish to permanently disable a program ID. | ||
|
|
||
| ## Security Considerations | ||
|
|
||
| The program cache relies on two invariants: | ||
|
|
||
| 1. **One redeployment per slot**: The cache keys on program address and | ||
| deployment slot. Multiple deployments to the same address in one slot would | ||
| corrupt the cache. | ||
|
|
||
| 2. **Loader stability within a transaction**: A program's loader determines its | ||
| ABI and alignment requirements. Changing loaders mid-transaction would cause | ||
| CPI mismatches. | ||
|
|
||
| This proposal preserves both invariants: | ||
|
|
||
| - **Program account**: When closing without tombstoning, the account is drained | ||
| of lamports rather than reassigned to System. The account remains owned by | ||
| Loader v3 until garbage-collected at transaction end, preventing same-TX | ||
| redeployment. Additionally, closing without tombstoning is rejected if the | ||
| program was deployed in the current slot, preventing multiple-TX loops. | ||
|
|
||
| - **Programdata account**: Fully deallocated and reassigned to System. This is | ||
| safe because programdata is not used for cache indexing or invocation. | ||
|
|
||
| - **Tombstone**: When tombstoning, the program account is assigned to itself, | ||
| permanently locking the address. Self-owned accounts cannot be modified. | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we aren't going to discuss how this leaves any accounts owned by an untombstoned, closed program open to be drained later by the deployer or anyone who manages to acquire the program id keypair? we're talking about deployers too careless to make sure that they're performing the operation that they believe they are. why do we think they'll be careful with keys?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. imo, if you do the cost-benefit analysis on this, tombstoning programs has resulted in millions of dollars of funds being permanently locked and was a terrible choice. it really failed to protect anyone from anything and caused a whole lot of harm. my $0.02: "oh no, several cases of defi protocols and token sales with millions of dollars locked forever by our CLI having the worst UX imaginable can suddenly be recovered, a bunch of people can get their money back, a bunch of accounts might get cleaned up and people who throw their keypairs out into the wild learn why that's a bad idea. how will i ever live with the weight of this decision? 🤣 "
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why has the cli not been made more intuitive in the meantime?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with @deanmlittle that the lost and revealed keys argument is negligible. Especially since the current status is that they don't have access to these funds, thus nothing changes in these cases. |
||
| ## Backwards Compatibility | ||
|
|
||
| This change modifies the semantics of an existing Loader v3 instruction and | ||
| therefore requires a feature gate for consensus safety. | ||
|
|
||
| From a tooling perspective, the change is backwards compatible, though tooling | ||
| updates are required to access the new explicit tombstoning behavior. | ||
|
buffalojoec marked this conversation as resolved.
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whether or not the proposal addresses this is up to the deployer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but the motivation stands. Now this action is reversible.