Skip to content
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

SIMD-0148: MoveStake and MoveLamports instructions #148

Merged
merged 7 commits into from
Jun 27, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions proposals/0148-stake-program-move-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
simd: '0148'
title: MoveStake and MoveLamports Instructions
authors:
- Hanako Mumei
category: Standard
type: Core
status: Draft
created: 2024-04-30
feature: (fill in with feature tracking issues once accepted)
---

## Summary

We propose introducing two new instructions to the stake program for moving
value between stake accounts with identical `Authorized` and `Lockup`:

* `MoveStake`: Move a given `amount` of active stake from one fully active
2501babe marked this conversation as resolved.
Show resolved Hide resolved
account to another fully active account, or from a fully active account to an
inactive one, turning it into an active account. In all cases, rent-exempt
balance is unaffected and minimum delegations are respected.
* `MoveLamports`: Move a given `amount` of excess lamports from one active or
inactive account to another active or inactive account, where "excess lamports"
refers to lamports that are neither delegated stake nor required for
rent-exemption.

For simplicity of implementation, we choose not to support accounts that are
activating, deactivating, or partially active. A future SIMD may choose to
extend this functionality should it be desirable.

## Motivation

Recently, a feature was activated which mandates that `Split` destinations be
prefunded with the rent-exempt reserve, because before that, `Split` could be
used to deactivate stake immediately, bypassing the cooldown period.

However, this has introduced issues for protocols that manage stake on behalf
of users without taking `Withdrawer` authority. Particularly, for one that
splits user stake across many validators and periodically redelegates between
them, every time they want to split part of a user stake to deactivate, the
protocol must fund the rent-exemption themselves. And then when that split
account is merged, those lamports cannot be reclaimed by the protocol, instead
accumulating (undelegated) in the destination merge accounts.

The purpose of the `MoveStake` instruction is to enable a flow whereby moving
stake from a user's stake accounts U1 -> U2 from validator V1 to validator V2
may proceed:

* `MoveStake` the `amount` of stake from the user stake account U1 to a
"transient" inactive account T holding sufficient lamports for rent exemption
and minimum delegation. T instantly becomes a second active stake account
delegated to V1 with `amount` stake.
* `Deactivate` T and wait an epoch.
* `DelegateStake` T to V2 and wait an epoch. T becomes an active stake account
delegated to V2 with `amount + minimum_delegation` stake.
* `MoveStake` the `amount` stake from T to U2. `Deactivate` T to return it to
its initial state. Stake has moved from U1 to U2 with no outside lamports
2501babe marked this conversation as resolved.
Show resolved Hide resolved
required and no new undelegated lamports in delegated stake accounts.

The motivation for `MoveLamports` is to enable housekeeping tasks such as
reclaiming lmaports from `Merge` destinations.
2501babe marked this conversation as resolved.
Show resolved Hide resolved

## Alternatives Considered

* There is a longstanding proposal we call Multistake, which would allow a
stake account to have two delegation amounts to the same validator, the idea
being that one serves as an onramp (and possibly offramp) for active stake on
the account. This could support other flows to accomplish the same objective,
such as allowing `DelegateStake` to accept an active stake account to begin
activating an account's excess (non-rent non-stake) lamports, or allowing a
lheeger-jump marked this conversation as resolved.
Show resolved Hide resolved
`Split` source to `Deactivate` enough stake to cover rent-exemption for the new
account. However, Multistake is a much larger design/engineering project, and
we have to solve this sooner than it would be ready.
2501babe marked this conversation as resolved.
Show resolved Hide resolved
* We discussed various proposals for allowing `Merge` to leave behind the
source account or `Split` to split into any mergeable destination. However this
confuses the presently clear distinction between these two operations and
entails additional implementation risk as they are already rather complex. A
new instruction that does one specific thing seems highly preferable.
* The original version of this SIMD proposed a `Move` that did not take an
`amount`, but this would require changes to `Split` to enable the first leg of
the proposed flow.
* Back out the changes introduced by requiring rent-exempt `Split` destinations.
This is undesirable because that restriction was added for very good reason: an
effectively unbounded amount of stake could be instantly deactivated through
repeated splitting.

## New Terminology

`MoveStake` and `MoveLamports`, two new stake program instructions.

## Detailed Design

### `MoveStake`

`MoveStake` requires 5 accounts:

* Source stake account
* Destination stake account
2501babe marked this conversation as resolved.
Show resolved Hide resolved
* Clock
* Stake history
2501babe marked this conversation as resolved.
Show resolved Hide resolved
* Stake account authority
2501babe marked this conversation as resolved.
Show resolved Hide resolved

`MoveStake` requires 1 argument:
2501babe marked this conversation as resolved.
Show resolved Hide resolved

* `amount`, a `u64` indicating the quantity of lamports to move
2501babe marked this conversation as resolved.
Show resolved Hide resolved

`MoveStake` aborts the transaction when:

* `amount` is 0
* Source and destination have the same address
* Source and destination do not have identical `Authorized` and `Lockup`
* The stake account authority is not the `Staker` on both accounts
* The stake account authority is not a signer
2501babe marked this conversation as resolved.
Show resolved Hide resolved
* Destination data length is not equal to the current version of `StakeState`
* Source is not fully active (100% of delegation is effective)
* Destination is neither fully active nor fully inactive (initialized or
deactivated)
* If destination is active, source and destination are not delegated to the same
vote account
* Moving `amount` stake would bring source below minimum delegation
* Moving `amount` stake would fail to bring destination up to minimum delegation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What error codes do these conditions produce? And what is the logger output?

Copy link
Member Author

@2501babe 2501babe May 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this really necessary to include in specifications for core program instructions?

defining all this would drastically increase the burden of writing specs and of implementing those specs. you would have to worry about the exact order errors are checked in, structure programs such that the language runtime and vm can never trigger their own error conditions for situations that would be exceptional before they're checked explicitly, and possibly rewrite specs for any nontrivial refactor or any improvement to error or logging information

even simple things like nested match expressions become a potential headache because if error A has to happen before error B, and error A is returned by the wildcard pattern but error B is in a matched block, then error B will happen second but come first by line number, and you have to start worrying "is the order here right? is the order obvious enough to someone looking at this code for the first time? should i just unwrap_or_else everything instead of writing structured code?"

if we say "you have to define the exact error codes returned for each abort condition and the precise log outputs that would result from any operation" then we effectively incentivize maintainers to use as few error codes as humanly possible and log nothing

my original version of this reply continued the first line "these are only ever consumed outside of consensus" but upon further reflection i realize that if we dont go through with porting core programs to bpf, such that there are two native versions, then error codes need to agree between them for the sake of programs that call them via cpi. but i think we shouldnt add the burden of defining error codes in spec unless we decide there will be multiple core program impls, and shouldnt define log output in spec at all

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can only realistically provide an RPC replacement if we get the error codes and log messages right.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, i dont understand how this would affect rpc if theres one bpf implementation of the stake program. wouldnt you pass through or remap any errors or log messages using the same rules as the existing rpc, without concern for the underlying program?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, i dont understand how this would affect rpc if theres one bpf implementation of the stake program.

@2501babe I assumed this SIMD changes the behavior of the stake native program. The SIMD doesn't say this change would only affect the core BPF version of the stake program. Even so, I suggest documenting error codes and log messages so API services / indexers / explorers know how to interpret the output.


If all of these conditions hold, then:

* Delegation and lamports on source are debited `amount`
* Delegation and lamports on destination are credited `amount`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this is an edge case, the credits_observed on the destination account must be updated to the weighted average of the source and destination accounts, like during merge: https://github.com/anza-xyz/agave/blob/9403ca6f0451b670d41dee1b7592daa297727ed1/programs/stake/src/stake_state.rs#L1188, where you would do (source_credits_observed * amount + destination_credits_observed * destination_amount + (amount + destination_amount) - 1) / (amount + destination_amount)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ill confess i dont understand the full implications of this and thought credits_observed was determined by, and advanced in lockstep with, epoch_credits on VoteState

i see how when two accounts are merged, the destination credits become a stake-weighted average of both accounts, but move is more complicated. checking my intuitions:

  • partial move between active accounts. eg U1 and U2 have 100 sol each, U1 has C1 credits observed, U2 has C2 credits observed, and 50 sol moves U1 -> U2. this means that 33.3% of the new C2 value is provided by C1, because 1/3 of U2's stake have observed the credits seen by U1?
  • full move between active accounts is identical to merge
  • a move from active to inactive doesnt need to fiddle with credits observed because, much like delegation, it is a garbage value when inactive and assigned a fresh value when activated that doesnt depend on its prior value
  • source credits observed never change because "credits observed" is more like an experience that a particular piece of stake has had rather than a quantity of something it carries with it

is this accurate?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's almost always advanced, except for a few edge cases, such as stakes that don't earn any rewards in an epoch.

And yep, you've correctly covered all of the situations and what should happen.

* If destination is inactive, it is set to active with the same `Stake` as
source, aside from delegation amount

### `MoveLamports`

Accounts and arguments are identical to the above.
2501babe marked this conversation as resolved.
Show resolved Hide resolved

`MoveLamports` aborts the transaction when:

* `amount` is 0
* Source and destination have the same address
* Source and destination do not have identical `Authorized` and `Lockup`
* The stake account authority is not the `Staker` on both accounts
* The stake account authority is not a signer
* Source is neither fully active nor fully inactive
* Destination is neither fully active nor fully inactive
* `amount` exceeds source `lamports - stake - rent_exempt_reserve`
2501babe marked this conversation as resolved.
Show resolved Hide resolved

If all of these conditions hold, then:

* Lamports on source are debited `amount`
* Lamports on destination are credited `amount`
lheeger-jump marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +178 to +179
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this move the two accounts in activating/deactivating?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deactivating isnt allowed for either account, and activating is only allowed for the destination for MoveLamports. tbh i describe the allowed states from first principles for the sake of letting the simd stand alone, but a lot of these are just reusing logic from Merge, eg here destination is identical to a valid merge destination, and source is a valid merge source except it cant be activating because this would require calculating whether the minimum will be upheld next epoch (which is pointless because theres no usecase)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!


## Impact

The primary utility of the proposed instructions is to support protocol
developers in moving stake without controlling the `Withdrawer`. There is no
loss of existing functionality.

## Security Considerations

Care must be taken to ensure stakes are fully active, as moving delegations
between accounts in any kind of transient state is fraught. Otherwise this
change should be fairly low impact, as it does not require changing any existing
logic, in particular avoiding making `Split` or `Merge` more permissive.
Loading