diff --git a/stake-pool/py/stake_pool/actions.py b/stake-pool/py/stake_pool/actions.py index a7eff4d9547..e44afba4d80 100644 --- a/stake-pool/py/stake_pool/actions.py +++ b/stake-pool/py/stake_pool/actions.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Optional, Tuple from solana.keypair import Keypair from solana.publickey import PublicKey @@ -22,7 +22,8 @@ find_stake_program_address, \ find_transient_stake_program_address, \ find_withdraw_authority_program_address, \ - find_metadata_account + find_metadata_account, \ + find_ephemeral_stake_program_address from stake_pool.state import STAKE_POOL_LAYOUT, ValidatorList, Fee, StakePool import stake_pool.instructions as sp @@ -480,8 +481,13 @@ async def update_stake_pool(client: AsyncClient, payer: Keypair, stake_pool_addr async def increase_validator_stake( - client: AsyncClient, payer: Keypair, staker: Keypair, stake_pool_address: PublicKey, - validator_vote: PublicKey, lamports: int + client: AsyncClient, + payer: Keypair, + staker: Keypair, + stake_pool_address: PublicKey, + validator_vote: PublicKey, + lamports: int, + ephemeral_stake_seed: Optional[int] = None ): resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) data = resp['result']['value']['data'] @@ -493,7 +499,13 @@ async def increase_validator_stake( (withdraw_authority, seed) = find_withdraw_authority_program_address(STAKE_POOL_PROGRAM_ID, stake_pool_address) validator_info = next(x for x in validator_list.validators if x.vote_account_address == validator_vote) - transient_stake_seed = validator_info.transient_seed_suffix + 1 # bump up by one to avoid reuse + + if ephemeral_stake_seed is None: + transient_stake_seed = validator_info.transient_seed_suffix + 1 # bump up by one to avoid reuse + else: + # we are updating an existing transient stake account, so we must use the same seed + transient_stake_seed = validator_info.transient_seed_suffix + validator_stake_seed = validator_info.validator_seed_suffix or None (transient_stake, _) = find_transient_stake_program_address( STAKE_POOL_PROGRAM_ID, @@ -509,29 +521,64 @@ async def increase_validator_stake( ) txn = Transaction() - txn.add( - sp.increase_validator_stake( - sp.IncreaseValidatorStakeParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - staker=staker.public_key, - withdraw_authority=withdraw_authority, - validator_list=stake_pool.validator_list, - reserve_stake=stake_pool.reserve_stake, - transient_stake=transient_stake, - validator_stake=validator_stake, - validator_vote=validator_vote, - clock_sysvar=SYSVAR_CLOCK_PUBKEY, - rent_sysvar=SYSVAR_RENT_PUBKEY, - stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, - stake_config_sysvar=SYSVAR_STAKE_CONFIG_ID, - system_program_id=sys.SYS_PROGRAM_ID, - stake_program_id=STAKE_PROGRAM_ID, - lamports=lamports, - transient_stake_seed=transient_stake_seed, + if ephemeral_stake_seed is not None: + + # We assume there is an existing transient account that we will update + (ephemeral_stake, _) = find_ephemeral_stake_program_address( + STAKE_POOL_PROGRAM_ID, + stake_pool_address, + ephemeral_stake_seed) + + txn.add( + sp.increase_additional_validator_stake( + sp.IncreaseAdditionalValidatorStakeParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + staker=staker.public_key, + withdraw_authority=withdraw_authority, + validator_list=stake_pool.validator_list, + reserve_stake=stake_pool.reserve_stake, + transient_stake=transient_stake, + validator_stake=validator_stake, + validator_vote=validator_vote, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + rent_sysvar=SYSVAR_RENT_PUBKEY, + stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, + stake_config_sysvar=SYSVAR_STAKE_CONFIG_ID, + system_program_id=sys.SYS_PROGRAM_ID, + stake_program_id=STAKE_PROGRAM_ID, + lamports=lamports, + transient_stake_seed=transient_stake_seed, + ephemeral_stake=ephemeral_stake, + ephemeral_stake_seed=ephemeral_stake_seed + ) + ) + ) + + else: + txn.add( + sp.increase_validator_stake( + sp.IncreaseValidatorStakeParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + staker=staker.public_key, + withdraw_authority=withdraw_authority, + validator_list=stake_pool.validator_list, + reserve_stake=stake_pool.reserve_stake, + transient_stake=transient_stake, + validator_stake=validator_stake, + validator_vote=validator_vote, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + rent_sysvar=SYSVAR_RENT_PUBKEY, + stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, + stake_config_sysvar=SYSVAR_STAKE_CONFIG_ID, + system_program_id=sys.SYS_PROGRAM_ID, + stake_program_id=STAKE_PROGRAM_ID, + lamports=lamports, + transient_stake_seed=transient_stake_seed, + ) ) ) - ) signers = [payer, staker] if payer != staker else [payer] await client.send_transaction( @@ -539,8 +586,13 @@ async def increase_validator_stake( async def decrease_validator_stake( - client: AsyncClient, payer: Keypair, staker: Keypair, stake_pool_address: PublicKey, - validator_vote: PublicKey, lamports: int + client: AsyncClient, + payer: Keypair, + staker: Keypair, + stake_pool_address: PublicKey, + validator_vote: PublicKey, + lamports: int, + ephemeral_stake_seed: Optional[int] = None ): resp = await client.get_account_info(stake_pool_address, commitment=Confirmed) data = resp['result']['value']['data'] @@ -559,7 +611,13 @@ async def decrease_validator_stake( stake_pool_address, validator_stake_seed, ) - transient_stake_seed = validator_info.transient_seed_suffix + 1 # bump up by one to avoid reuse + + if ephemeral_stake_seed is None: + transient_stake_seed = validator_info.transient_seed_suffix + 1 # bump up by one to avoid reuse + else: + # we are updating an existing transient stake account, so we must use the same seed + transient_stake_seed = validator_info.transient_seed_suffix + (transient_stake, _) = find_transient_stake_program_address( STAKE_POOL_PROGRAM_ID, validator_info.vote_account_address, @@ -568,26 +626,61 @@ async def decrease_validator_stake( ) txn = Transaction() - txn.add( - sp.decrease_validator_stake_with_reserve( - sp.DecreaseValidatorStakeWithReserveParams( - program_id=STAKE_POOL_PROGRAM_ID, - stake_pool=stake_pool_address, - staker=staker.public_key, - withdraw_authority=withdraw_authority, - validator_list=stake_pool.validator_list, - reserve_stake=stake_pool.reserve_stake, - validator_stake=validator_stake, - transient_stake=transient_stake, - clock_sysvar=SYSVAR_CLOCK_PUBKEY, - stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, - system_program_id=sys.SYS_PROGRAM_ID, - stake_program_id=STAKE_PROGRAM_ID, - lamports=lamports, - transient_stake_seed=transient_stake_seed, + + if ephemeral_stake_seed is not None: + + # We assume there is an existing transient account that we will update + (ephemeral_stake, _) = find_ephemeral_stake_program_address( + STAKE_POOL_PROGRAM_ID, + stake_pool_address, + ephemeral_stake_seed) + + txn.add( + sp.decrease_additional_validator_stake( + sp.DecreaseAdditionalValidatorStakeParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + staker=staker.public_key, + withdraw_authority=withdraw_authority, + validator_list=stake_pool.validator_list, + reserve_stake=stake_pool.reserve_stake, + validator_stake=validator_stake, + transient_stake=transient_stake, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + rent_sysvar=SYSVAR_RENT_PUBKEY, + stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, + system_program_id=sys.SYS_PROGRAM_ID, + stake_program_id=STAKE_PROGRAM_ID, + lamports=lamports, + transient_stake_seed=transient_stake_seed, + ephemeral_stake=ephemeral_stake, + ephemeral_stake_seed=ephemeral_stake_seed + ) + ) + ) + + else: + + txn.add( + sp.decrease_validator_stake_with_reserve( + sp.DecreaseValidatorStakeWithReserveParams( + program_id=STAKE_POOL_PROGRAM_ID, + stake_pool=stake_pool_address, + staker=staker.public_key, + withdraw_authority=withdraw_authority, + validator_list=stake_pool.validator_list, + reserve_stake=stake_pool.reserve_stake, + validator_stake=validator_stake, + transient_stake=transient_stake, + clock_sysvar=SYSVAR_CLOCK_PUBKEY, + stake_history_sysvar=SYSVAR_STAKE_HISTORY_PUBKEY, + system_program_id=sys.SYS_PROGRAM_ID, + stake_program_id=STAKE_PROGRAM_ID, + lamports=lamports, + transient_stake_seed=transient_stake_seed, + ) ) ) - ) signers = [payer, staker] if payer != staker else [payer] await client.send_transaction( diff --git a/stake-pool/py/stake_pool/constants.py b/stake-pool/py/stake_pool/constants.py index f6c278e2625..ff1f1dbe2f3 100644 --- a/stake-pool/py/stake_pool/constants.py +++ b/stake-pool/py/stake_pool/constants.py @@ -78,6 +78,23 @@ def find_transient_stake_program_address( ) +def find_ephemeral_stake_program_address( + program_id: PublicKey, + stake_pool_address: PublicKey, + seed: int +) -> Tuple[PublicKey, int]: + + """Generates the ephemeral program address for stake pool redelegation""" + return PublicKey.find_program_address( + [ + EPHEMERAL_STAKE_SEED_PREFIX, + bytes(stake_pool_address), + seed.to_bytes(8, 'little'), + ], + program_id, + ) + + def find_metadata_account( mint_key: PublicKey ) -> Tuple[PublicKey, int]: @@ -100,3 +117,5 @@ def find_metadata_account( """Seed used to derive transient stake accounts.""" METADATA_SEED_PREFIX = b"metadata" """Seed used to avoid certain collision attacks.""" +EPHEMERAL_STAKE_SEED_PREFIX = b'ephemeral' +"""Seed for ephemeral stake account""" diff --git a/stake-pool/py/stake_pool/instructions.py b/stake-pool/py/stake_pool/instructions.py index 4a773a2d7f1..8a3ae4cf5f3 100644 --- a/stake-pool/py/stake_pool/instructions.py +++ b/stake-pool/py/stake_pool/instructions.py @@ -543,6 +543,94 @@ class UpdateTokenMetadataParams(NamedTuple): """URI of the uploaded metadata of the spl-token.""" +class IncreaseAdditionalValidatorStakeParams(NamedTuple): + """(Staker only) Increase stake on a validator from the reserve account.""" + + # Accounts + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[]` Stake pool.""" + staker: PublicKey + """`[s]` Staker.""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority.""" + validator_list: PublicKey + """`[w]` Validator stake list storage account.""" + reserve_stake: PublicKey + """`[w]` Stake pool's reserve.""" + ephemeral_stake: PublicKey + """The ephemeral stake account used during the operation.""" + transient_stake: PublicKey + """`[w]` Transient stake account to receive split.""" + validator_stake: PublicKey + """`[]` Canonical stake account to check.""" + validator_vote: PublicKey + """`[]` Validator vote account to delegate to.""" + clock_sysvar: PublicKey + """`[]` Clock sysvar.""" + rent_sysvar: PublicKey + """`[]` Rent sysvar.""" + stake_history_sysvar: PublicKey + """'[]' Stake history sysvar.""" + stake_config_sysvar: PublicKey + """'[]' Stake config sysvar.""" + system_program_id: PublicKey + """`[]` System program.""" + stake_program_id: PublicKey + """`[]` Stake program.""" + + # Params + lamports: int + """Amount of lamports to increase on the given validator.""" + transient_stake_seed: int + """Seed to used to create the transient stake account.""" + ephemeral_stake_seed: int + """The seed used to generate the ephemeral stake account""" + + +class DecreaseAdditionalValidatorStakeParams(NamedTuple): + """(Staker only) Decrease active stake on a validator, eventually moving it to the reserve""" + + # Accounts + program_id: PublicKey + """SPL Stake Pool program account.""" + stake_pool: PublicKey + """`[]` Stake pool.""" + staker: PublicKey + """`[s]` Staker.""" + withdraw_authority: PublicKey + """`[]` Stake pool withdraw authority.""" + validator_list: PublicKey + """`[w]` Validator stake list storage account.""" + reserve_stake: PublicKey + """The reserve stake account to move the stake to.""" + validator_stake: PublicKey + """`[w]` Canonical stake to split from.""" + ephemeral_stake: PublicKey + """The ephemeral stake account used during the operation.""" + transient_stake: PublicKey + """`[w]` Transient stake account to receive split.""" + clock_sysvar: PublicKey + """`[]` Clock sysvar.""" + rent_sysvar: PublicKey + """`[]` Rent sysvar.""" + stake_history_sysvar: PublicKey + """'[]' Stake history sysvar.""" + system_program_id: PublicKey + """`[]` System program.""" + stake_program_id: PublicKey + """`[]` Stake program.""" + + # Params + lamports: int + """Amount of lamports to split into the transient stake account.""" + transient_stake_seed: int + """Seed to used to create the transient stake account.""" + ephemeral_stake_seed: int + """The seed used to generate the ephemeral stake account""" + + class InstructionType(IntEnum): """Stake Pool Instruction Types.""" @@ -1009,6 +1097,42 @@ def increase_validator_stake(params: IncreaseValidatorStakeParams) -> Transactio ) +def increase_additional_validator_stake( + params: IncreaseAdditionalValidatorStakeParams, + ) -> TransactionInstruction: + + """Creates `IncreaseAdditionalValidatorStake` instruction (rebalance from reserve account to transient account)""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.ephemeral_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.transient_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.validator_vote, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_config_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), + ], + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.INCREASE_ADDITIONAL_VALIDATOR_STAKE, + args={ + 'lamports': params.lamports, + 'transient_stake_seed': params.transient_stake_seed, + 'ephemeral_stake_seed': params.ephemeral_stake_seed + } + ) + ) + ) + + def decrease_validator_stake(params: DecreaseValidatorStakeParams) -> TransactionInstruction: """Creates instruction to decrease the stake on a validator.""" return TransactionInstruction( @@ -1037,6 +1161,38 @@ def decrease_validator_stake(params: DecreaseValidatorStakeParams) -> Transactio ) +def decrease_additional_validator_stake(params: DecreaseAdditionalValidatorStakeParams) -> TransactionInstruction: + """ Creates `DecreaseAdditionalValidatorStake` instruction (rebalance from validator account to + transient account).""" + return TransactionInstruction( + keys=[ + AccountMeta(pubkey=params.stake_pool, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.staker, is_signer=True, is_writable=False), + AccountMeta(pubkey=params.withdraw_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.validator_list, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.reserve_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.validator_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.ephemeral_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.transient_stake, is_signer=False, is_writable=True), + AccountMeta(pubkey=params.clock_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_history_sysvar, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.system_program_id, is_signer=False, is_writable=False), + AccountMeta(pubkey=params.stake_program_id, is_signer=False, is_writable=False), + ], + program_id=params.program_id, + data=INSTRUCTIONS_LAYOUT.build( + dict( + instruction_type=InstructionType.DECREASE_ADDITIONAL_VALIDATOR_STAKE, + args={ + 'lamports': params.lamports, + 'transient_stake_seed': params.transient_stake_seed, + 'ephemeral_stake_seed': params.ephemeral_stake_seed + } + ) + ) + ) + + def decrease_validator_stake_with_reserve(params: DecreaseValidatorStakeWithReserveParams) -> TransactionInstruction: """Creates instruction to decrease the stake on a validator.""" return TransactionInstruction( diff --git a/stake-pool/py/tests/test_a_time_sensitive.py b/stake-pool/py/tests/test_a_time_sensitive.py index 78abbf47d31..682d1202bea 100644 --- a/stake-pool/py/tests/test_a_time_sensitive.py +++ b/stake-pool/py/tests/test_a_time_sensitive.py @@ -29,16 +29,33 @@ async def test_increase_decrease_this_is_very_slow(async_client, validators, pay # increase to all futures = [ - increase_validator_stake(async_client, payer, payer, stake_pool_address, validator, increase_amount) + increase_validator_stake(async_client, payer, payer, stake_pool_address, validator, increase_amount // 2) for validator in validators ] await asyncio.gather(*futures) + # validate the increase is now on the transient account resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) data = resp['result']['value']['data'] validator_list = ValidatorList.decode(data[0], data[1]) for validator in validator_list.validators: - assert validator.transient_stake_lamports == increase_amount + stake_rent_exemption + assert validator.transient_stake_lamports == increase_amount // 2 + stake_rent_exemption + assert validator.active_stake_lamports == minimum_amount + + # increase the same amount to test the increase additional instruction + futures = [ + increase_validator_stake(async_client, payer, payer, stake_pool_address, validator, increase_amount // 2, + ephemeral_stake_seed=0) + for validator in validators + ] + await asyncio.gather(*futures) + + # validate the additional increase is now on the transient account + resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) + data = resp['result']['value']['data'] + validator_list = ValidatorList.decode(data[0], data[1]) + for validator in validator_list.validators: + assert validator.transient_stake_lamports == increase_amount + stake_rent_exemption * 2 assert validator.active_stake_lamports == minimum_amount print("Waiting for epoch to roll over") @@ -51,7 +68,7 @@ async def test_increase_decrease_this_is_very_slow(async_client, validators, pay for validator in validator_list.validators: assert validator.last_update_epoch != 0 assert validator.transient_stake_lamports == 0 - assert validator.active_stake_lamports == increase_amount + minimum_amount + assert validator.active_stake_lamports == increase_amount + minimum_amount + stake_rent_exemption # decrease from all futures = [ @@ -60,12 +77,19 @@ async def test_increase_decrease_this_is_very_slow(async_client, validators, pay ] await asyncio.gather(*futures) + # validate the decrease is now on the transient account resp = await async_client.get_account_info(validator_list_address, commitment=Confirmed) data = resp['result']['value']['data'] validator_list = ValidatorList.decode(data[0], data[1]) for validator in validator_list.validators: assert validator.transient_stake_lamports == decrease_amount + stake_rent_exemption - assert validator.active_stake_lamports == increase_amount - decrease_amount + minimum_amount + assert validator.active_stake_lamports == increase_amount - decrease_amount + minimum_amount + \ + stake_rent_exemption + + # DO NOT test decrese additional instruction as it is confirmed NOT to be working as advertised + + # roll over one epoch and verify we have the balances that we expect + expected_active_stake_lamports = increase_amount - decrease_amount + minimum_amount + stake_rent_exemption print("Waiting for epoch to roll over") await waiter.wait_for_next_epoch(async_client) @@ -76,4 +100,4 @@ async def test_increase_decrease_this_is_very_slow(async_client, validators, pay validator_list = ValidatorList.decode(data[0], data[1]) for validator in validator_list.validators: assert validator.transient_stake_lamports == 0 - assert validator.active_stake_lamports == increase_amount - decrease_amount + minimum_amount + assert validator.active_stake_lamports == expected_active_stake_lamports