diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 98143e97e0..cd279a4102 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -131,6 +131,13 @@ pub enum StakeInstruction { /// 3. [] Stake history sysvar that carries stake warmup/cooldown history /// 4. [SIGNER] Stake authority Merge, + + /// Authorize a key to manage stake or withdrawal with a derived key + /// + /// # Account references + /// 0. [WRITE] Stake account to be updated + /// 1. [SIGNER] Base key of stake or withdraw authority + AuthorizeWithSeed(AuthorizeWithSeedArgs), } #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] @@ -140,6 +147,14 @@ pub struct LockupArgs { pub custodian: Option, } +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct AuthorizeWithSeedArgs { + pub new_authorized_pubkey: Pubkey, + pub stake_authorize: StakeAuthorize, + pub authority_seed: String, + pub authority_owner: Pubkey, +} + fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Lockup) -> Instruction { Instruction::new( id(), @@ -341,6 +356,33 @@ pub fn authorize( ) } +pub fn authorize_with_seed( + stake_pubkey: &Pubkey, + authority_base: &Pubkey, + authority_seed: String, + authority_owner: &Pubkey, + new_authorized_pubkey: &Pubkey, + stake_authorize: StakeAuthorize, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*stake_pubkey, false), + AccountMeta::new_readonly(*authority_base, true), + ]; + + let args = AuthorizeWithSeedArgs { + new_authorized_pubkey: *new_authorized_pubkey, + stake_authorize, + authority_seed, + authority_owner: *authority_owner, + }; + + Instruction::new( + id(), + &StakeInstruction::AuthorizeWithSeed(args), + account_metas, + ) +} + pub fn delegate_stake( stake_pubkey: &Pubkey, authorized_pubkey: &Pubkey, @@ -420,7 +462,17 @@ pub fn process_instruction( &Rent::from_keyed_account(next_keyed_account(keyed_accounts)?)?, ), StakeInstruction::Authorize(authorized_pubkey, stake_authorize) => { - me.authorize(&authorized_pubkey, stake_authorize, &signers) + me.authorize(&signers, &authorized_pubkey, stake_authorize) + } + StakeInstruction::AuthorizeWithSeed(args) => { + let authority_base = next_keyed_account(keyed_accounts)?; + me.authorize_with_seed( + &authority_base, + &args.authority_seed, + &args.authority_owner, + &args.new_authorized_pubkey, + args.stake_authorize, + ) } StakeInstruction::DelegateStake => { let vote = next_keyed_account(keyed_accounts)?; diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 8905f5f4f2..d4d23b8d50 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -556,9 +556,17 @@ pub trait StakeAccount { ) -> Result<(), InstructionError>; fn authorize( &self, - authority: &Pubkey, - stake_authorize: StakeAuthorize, signers: &HashSet, + new_authority: &Pubkey, + stake_authorize: StakeAuthorize, + ) -> Result<(), InstructionError>; + fn authorize_with_seed( + &self, + authority_base: &KeyedAccount, + authority_seed: &str, + authority_owner: &Pubkey, + new_authority: &Pubkey, + stake_authorize: StakeAuthorize, ) -> Result<(), InstructionError>; fn delegate( &self, @@ -627,24 +635,42 @@ impl<'a> StakeAccount for KeyedAccount<'a> { /// staker. The default staker is the owner of the stake account's pubkey. fn authorize( &self, - authority: &Pubkey, - stake_authorize: StakeAuthorize, signers: &HashSet, + new_authority: &Pubkey, + stake_authorize: StakeAuthorize, ) -> Result<(), InstructionError> { match self.state()? { StakeState::Stake(mut meta, stake) => { meta.authorized - .authorize(signers, authority, stake_authorize)?; + .authorize(signers, new_authority, stake_authorize)?; self.set_state(&StakeState::Stake(meta, stake)) } StakeState::Initialized(mut meta) => { meta.authorized - .authorize(signers, authority, stake_authorize)?; + .authorize(signers, new_authority, stake_authorize)?; self.set_state(&StakeState::Initialized(meta)) } _ => Err(InstructionError::InvalidAccountData), } } + fn authorize_with_seed( + &self, + authority_base: &KeyedAccount, + authority_seed: &str, + authority_owner: &Pubkey, + new_authority: &Pubkey, + stake_authorize: StakeAuthorize, + ) -> Result<(), InstructionError> { + let mut signers = HashSet::default(); + if let Some(base_pubkey) = authority_base.signer_key() { + signers.insert(Pubkey::create_with_seed( + base_pubkey, + authority_seed, + authority_owner, + )?); + } + self.authorize(&signers, &new_authority, stake_authorize) + } fn delegate( &self, vote_account: &KeyedAccount, @@ -2560,7 +2586,7 @@ mod tests { #[test] fn test_authorize_uninit() { - let stake_pubkey = Pubkey::new_rand(); + let new_authority = Pubkey::new_rand(); let stake_lamports = 42; let stake_account = Account::new_ref_data_with_space( stake_lamports, @@ -2570,21 +2596,21 @@ mod tests { ) .expect("stake_account"); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); - let signers = vec![stake_pubkey].into_iter().collect(); + let stake_keyed_account = KeyedAccount::new(&new_authority, true, &stake_account); + let signers = vec![new_authority].into_iter().collect(); assert_eq!( - stake_keyed_account.authorize(&stake_pubkey, StakeAuthorize::Staker, &signers), + stake_keyed_account.authorize(&signers, &new_authority, StakeAuthorize::Staker), Err(InstructionError::InvalidAccountData) ); } #[test] fn test_authorize_lockup() { - let stake_pubkey = Pubkey::new_rand(); + let stake_authority = Pubkey::new_rand(); let stake_lamports = 42; let stake_account = Account::new_ref_data_with_space( stake_lamports, - &StakeState::Initialized(Meta::auto(&stake_pubkey)), + &StakeState::Initialized(Meta::auto(&stake_authority)), std::mem::size_of::(), &id(), ) @@ -2595,16 +2621,16 @@ mod tests { let to_keyed_account = KeyedAccount::new(&to, false, &to_account); let clock = Clock::default(); - let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account); + let stake_keyed_account = KeyedAccount::new(&stake_authority, true, &stake_account); let stake_pubkey0 = Pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); + let signers = vec![stake_authority].into_iter().collect(); assert_eq!( - stake_keyed_account.authorize(&stake_pubkey0, StakeAuthorize::Staker, &signers), + stake_keyed_account.authorize(&signers, &stake_pubkey0, StakeAuthorize::Staker), Ok(()) ); assert_eq!( - stake_keyed_account.authorize(&stake_pubkey0, StakeAuthorize::Withdrawer, &signers), + stake_keyed_account.authorize(&signers, &stake_pubkey0, StakeAuthorize::Withdrawer), Ok(()) ); if let StakeState::Initialized(Meta { authorized, .. }) = @@ -2619,7 +2645,7 @@ mod tests { // A second authorization signed by the stake_keyed_account should fail let stake_pubkey1 = Pubkey::new_rand(); assert_eq!( - stake_keyed_account.authorize(&stake_pubkey1, StakeAuthorize::Staker, &signers), + stake_keyed_account.authorize(&signers, &stake_pubkey1, StakeAuthorize::Staker), Err(InstructionError::MissingRequiredSignature) ); @@ -2628,7 +2654,7 @@ mod tests { // Test a second authorization by the newly authorized pubkey let stake_pubkey2 = Pubkey::new_rand(); assert_eq!( - stake_keyed_account.authorize(&stake_pubkey2, StakeAuthorize::Staker, &signers0), + stake_keyed_account.authorize(&signers0, &stake_pubkey2, StakeAuthorize::Staker), Ok(()) ); if let StakeState::Initialized(Meta { authorized, .. }) = @@ -2638,7 +2664,7 @@ mod tests { } assert_eq!( - stake_keyed_account.authorize(&stake_pubkey2, StakeAuthorize::Withdrawer, &signers0), + stake_keyed_account.authorize(&signers0, &stake_pubkey2, StakeAuthorize::Withdrawer), Ok(()) ); if let StakeState::Initialized(Meta { authorized, .. }) = @@ -2677,6 +2703,88 @@ mod tests { ); } + #[test] + fn test_authorize_with_seed() { + let base_pubkey = Pubkey::new_rand(); + let seed = "42"; + let withdrawer_pubkey = Pubkey::create_with_seed(&base_pubkey, &seed, &id()).unwrap(); + let stake_lamports = 42; + let stake_account = Account::new_ref_data_with_space( + stake_lamports, + &StakeState::Initialized(Meta::auto(&withdrawer_pubkey)), + std::mem::size_of::(), + &id(), + ) + .expect("stake_account"); + + let base_account = Account::new_ref(1, 0, &id()); + let base_keyed_account = KeyedAccount::new(&base_pubkey, true, &base_account); + + let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account); + + let new_authority = Pubkey::new_rand(); + + // Wrong seed + assert_eq!( + stake_keyed_account.authorize_with_seed( + &base_keyed_account, + &"", + &id(), + &new_authority, + StakeAuthorize::Staker, + ), + Err(InstructionError::MissingRequiredSignature) + ); + + // Wrong base + assert_eq!( + stake_keyed_account.authorize_with_seed( + &stake_keyed_account, + &seed, + &id(), + &new_authority, + StakeAuthorize::Staker, + ), + Err(InstructionError::MissingRequiredSignature) + ); + + // Set stake authority + assert_eq!( + stake_keyed_account.authorize_with_seed( + &base_keyed_account, + &seed, + &id(), + &new_authority, + StakeAuthorize::Staker, + ), + Ok(()) + ); + + // Set withdraw authority + assert_eq!( + stake_keyed_account.authorize_with_seed( + &base_keyed_account, + &seed, + &id(), + &new_authority, + StakeAuthorize::Withdrawer, + ), + Ok(()) + ); + + // No longer withdraw authority + assert_eq!( + stake_keyed_account.authorize_with_seed( + &stake_keyed_account, + &seed, + &id(), + &new_authority, + StakeAuthorize::Withdrawer, + ), + Err(InstructionError::MissingRequiredSignature) + ); + } + #[test] fn test_authorize_override() { let withdrawer_pubkey = Pubkey::new_rand(); @@ -2692,39 +2800,39 @@ mod tests { let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account); // Authorize a staker pubkey and move the withdrawer key into cold storage. - let stake_pubkey = Pubkey::new_rand(); + let new_authority = Pubkey::new_rand(); let signers = vec![withdrawer_pubkey].into_iter().collect(); assert_eq!( - stake_keyed_account.authorize(&stake_pubkey, StakeAuthorize::Staker, &signers), + stake_keyed_account.authorize(&signers, &new_authority, StakeAuthorize::Staker), Ok(()) ); // Attack! The stake key (a hot key) is stolen and used to authorize a new staker. let mallory_pubkey = Pubkey::new_rand(); - let signers = vec![stake_pubkey].into_iter().collect(); + let signers = vec![new_authority].into_iter().collect(); assert_eq!( - stake_keyed_account.authorize(&mallory_pubkey, StakeAuthorize::Staker, &signers), + stake_keyed_account.authorize(&signers, &mallory_pubkey, StakeAuthorize::Staker), Ok(()) ); // Verify the original staker no longer has access. let new_stake_pubkey = Pubkey::new_rand(); assert_eq!( - stake_keyed_account.authorize(&new_stake_pubkey, StakeAuthorize::Staker, &signers), + stake_keyed_account.authorize(&signers, &new_stake_pubkey, StakeAuthorize::Staker), Err(InstructionError::MissingRequiredSignature) ); // Verify the withdrawer (pulled from cold storage) can save the day. let signers = vec![withdrawer_pubkey].into_iter().collect(); assert_eq!( - stake_keyed_account.authorize(&new_stake_pubkey, StakeAuthorize::Withdrawer, &signers), + stake_keyed_account.authorize(&signers, &new_stake_pubkey, StakeAuthorize::Withdrawer), Ok(()) ); // Attack! Verify the staker cannot be used to authorize a withdraw. let signers = vec![new_stake_pubkey].into_iter().collect(); assert_eq!( - stake_keyed_account.authorize(&mallory_pubkey, StakeAuthorize::Withdrawer, &signers), + stake_keyed_account.authorize(&signers, &mallory_pubkey, StakeAuthorize::Withdrawer), Ok(()) ); } @@ -3517,7 +3625,7 @@ mod tests { let new_staker_pubkey = Pubkey::new_rand(); assert_eq!( - stake_keyed_account.authorize(&new_staker_pubkey, StakeAuthorize::Staker, &signers), + stake_keyed_account.authorize(&signers, &new_staker_pubkey, StakeAuthorize::Staker), Ok(()) ); let authorized = diff --git a/runtime/src/system_instruction_processor.rs b/runtime/src/system_instruction_processor.rs index de7faac6b8..8189f097fa 100644 --- a/runtime/src/system_instruction_processor.rs +++ b/runtime/src/system_instruction_processor.rs @@ -145,16 +145,11 @@ fn create_account( transfer(from, to, lamports) } -fn transfer(from: &KeyedAccount, to: &KeyedAccount, lamports: u64) -> Result<(), InstructionError> { - if lamports == 0 { - return Ok(()); - } - - if from.signer_key().is_none() { - debug!("Transfer: from must sign"); - return Err(InstructionError::MissingRequiredSignature); - } - +fn transfer_verified( + from: &KeyedAccount, + to: &KeyedAccount, + lamports: u64, +) -> Result<(), InstructionError> { if !from.data_is_empty()? { debug!("Transfer: `from` must not carry data"); return Err(InstructionError::InvalidArgument); @@ -173,6 +168,45 @@ fn transfer(from: &KeyedAccount, to: &KeyedAccount, lamports: u64) -> Result<(), Ok(()) } +fn transfer(from: &KeyedAccount, to: &KeyedAccount, lamports: u64) -> Result<(), InstructionError> { + if lamports == 0 { + return Ok(()); + } + + if from.signer_key().is_none() { + debug!("Transfer: from must sign"); + return Err(InstructionError::MissingRequiredSignature); + } + + transfer_verified(from, to, lamports) +} + +fn transfer_with_seed( + from: &KeyedAccount, + from_base: &KeyedAccount, + from_seed: &str, + from_owner: &Pubkey, + to: &KeyedAccount, + lamports: u64, +) -> Result<(), InstructionError> { + if lamports == 0 { + return Ok(()); + } + + if from_base.signer_key().is_none() { + debug!("Transfer: from must sign"); + return Err(InstructionError::MissingRequiredSignature); + } + + if *from.unsigned_key() + != Pubkey::create_with_seed(from_base.unsigned_key(), from_seed, from_owner)? + { + return Err(SystemError::AddressWithSeedMismatch.into()); + } + + transfer_verified(from, to, lamports) +} + pub fn process_instruction( _owner: &Pubkey, keyed_accounts: &[KeyedAccount], @@ -220,6 +254,16 @@ pub fn process_instruction( let to = next_keyed_account(keyed_accounts_iter)?; transfer(from, to, lamports) } + SystemInstruction::TransferWithSeed { + lamports, + from_seed, + from_owner, + } => { + let from = next_keyed_account(keyed_accounts_iter)?; + let base = next_keyed_account(keyed_accounts_iter)?; + let to = next_keyed_account(keyed_accounts_iter)?; + transfer_with_seed(from, base, &from_seed, &from_owner, to, lamports) + } SystemInstruction::AdvanceNonceAccount => { let me = &mut next_keyed_account(keyed_accounts_iter)?; me.advance_nonce_account( @@ -864,6 +908,62 @@ mod tests { assert_eq!(to_keyed_account.account.borrow().lamports, 51); } + #[test] + fn test_transfer_with_seed() { + let base = Pubkey::new_rand(); + let base_account = Account::new_ref(100, 0, &Pubkey::new(&[2; 32])); // account owner should not matter + let from_base_keyed_account = KeyedAccount::new(&base, true, &base_account); + let from_seed = "42"; + let from_owner = system_program::id(); + let from = Pubkey::create_with_seed(&base, from_seed, &from_owner).unwrap(); + let from_account = Account::new_ref(100, 0, &Pubkey::new(&[2; 32])); // account owner should not matter + let to = Pubkey::new(&[3; 32]); + let to_account = Account::new_ref(1, 0, &to); // account owner should not matter + let from_keyed_account = KeyedAccount::new(&from, true, &from_account); + let to_keyed_account = KeyedAccount::new(&to, false, &to_account); + transfer_with_seed( + &from_keyed_account, + &from_base_keyed_account, + &from_seed, + &from_owner, + &to_keyed_account, + 50, + ) + .unwrap(); + let from_lamports = from_keyed_account.account.borrow().lamports; + let to_lamports = to_keyed_account.account.borrow().lamports; + assert_eq!(from_lamports, 50); + assert_eq!(to_lamports, 51); + + // Attempt to move more lamports than remaining in from_account + let from_keyed_account = KeyedAccount::new(&from, true, &from_account); + let result = transfer_with_seed( + &from_keyed_account, + &from_base_keyed_account, + &from_seed, + &from_owner, + &to_keyed_account, + 100, + ); + assert_eq!(result, Err(SystemError::ResultWithNegativeLamports.into())); + assert_eq!(from_keyed_account.account.borrow().lamports, 50); + assert_eq!(to_keyed_account.account.borrow().lamports, 51); + + // test unsigned transfer of zero + let from_keyed_account = KeyedAccount::new(&from, false, &from_account); + assert!(transfer_with_seed( + &from_keyed_account, + &from_base_keyed_account, + &from_seed, + &from_owner, + &to_keyed_account, + 0, + ) + .is_ok(),); + assert_eq!(from_keyed_account.account.borrow().lamports, 50); + assert_eq!(to_keyed_account.account.borrow().lamports, 51); + } + #[test] fn test_transfer_lamports_from_nonce_account_fail() { let from = Pubkey::new_rand(); diff --git a/sdk/src/system_instruction.rs b/sdk/src/system_instruction.rs index b737deaa80..ab524a784e 100644 --- a/sdk/src/system_instruction.rs +++ b/sdk/src/system_instruction.rs @@ -52,7 +52,7 @@ impl DecodeError for NonceError { /// maximum permitted size of data: 10 MB pub const MAX_PERMITTED_DATA_LENGTH: u64 = 10 * 1024 * 1024; -#[frozen_abi(digest = "E343asdJPd3aEbHSsmeeGzvztc9X2maaHhWxVS4P6hvW")] +#[frozen_abi(digest = "EpsptsKTYzMoQGSdoWRfPbwT3odGNfK3imEUTrxpLF1i")] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, AbiExample, AbiEnumVisitor)] pub enum SystemInstruction { /// Create a new account @@ -197,6 +197,23 @@ pub enum SystemInstruction { /// Owner program account owner: Pubkey, }, + + /// Transfer lamports from a derived address + /// + /// # Account references + /// 0. [WRITE] Funding account + /// 1. [SIGNER] Base for funding account + /// 2. [WRITE] Recipient account + TransferWithSeed { + /// Amount to transfer + lamports: u64, + + /// Seed to use to derive the funding account address + from_seed: String, + + /// Owner to use to derive the funding account address + from_owner: Pubkey, + }, } pub fn create_account( @@ -293,6 +310,30 @@ pub fn transfer(from_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64) -> Inst ) } +pub fn transfer_with_seed( + from_pubkey: &Pubkey, // must match create_address_with_seed(base, seed, owner) + from_base: &Pubkey, + from_seed: String, + from_owner: &Pubkey, + to_pubkey: &Pubkey, + lamports: u64, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*from_pubkey, false), + AccountMeta::new_readonly(*from_base, true), + AccountMeta::new(*to_pubkey, false), + ]; + Instruction::new( + system_program::id(), + &SystemInstruction::TransferWithSeed { + lamports, + from_seed, + from_owner: *from_owner, + }, + account_metas, + ) +} + pub fn allocate(pubkey: &Pubkey, space: u64) -> Instruction { let account_metas = vec![AccountMeta::new(*pubkey, true)]; Instruction::new( diff --git a/stake-monitor/src/lib.rs b/stake-monitor/src/lib.rs index d4ee6a6fb2..e3b954e2d5 100644 --- a/stake-monitor/src/lib.rs +++ b/stake-monitor/src/lib.rs @@ -133,6 +133,7 @@ fn process_transaction( ); } StakeInstruction::Authorize(_, _) + | StakeInstruction::AuthorizeWithSeed(_) | StakeInstruction::DelegateStake | StakeInstruction::Deactivate => { // These instructions are always permitted diff --git a/web3.js/module.d.ts b/web3.js/module.d.ts index 7a132825ff..765709e66d 100644 --- a/web3.js/module.d.ts +++ b/web3.js/module.d.ts @@ -689,6 +689,15 @@ declare module '@solana/web3.js' { stakeAuthorizationType: StakeAuthorizationType; }; + export type AuthorizeWithSeedStakeParams = { + stakePubkey: PublicKey; + authorityBase: PublicKey; + authoritySeed: string; + authorityOwner: PublicKey; + newAuthorizedPubkey: PublicKey; + stakeAuthorizationType: StakeAuthorizationType; + }; + export type SplitStakeParams = { stakePubkey: PublicKey; authorizedPubkey: PublicKey; @@ -725,6 +734,7 @@ declare module '@solana/web3.js' { export type StakeInstructionType = | 'Initialize' | 'Authorize' + | 'AuthorizeWithSeed' | 'Delegate' | 'Split' | 'Withdraw' diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index 79302e6057..cc2f6a7b66 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -704,6 +704,15 @@ declare module '@solana/web3.js' { stakeAuthorizationType: StakeAuthorizationType, |}; + declare export type AuthorizeWithSeedStakeParams = {| + stakePubkey: PublicKey, + authorityBase: PublicKey, + authoritySeed: string, + authorityOwner: PublicKey; + newAuthorizedPubkey: PublicKey, + stakeAuthorizationType: StakeAuthorizationType, + |}; + declare export type SplitStakeParams = {| stakePubkey: PublicKey, authorizedPubkey: PublicKey, @@ -740,6 +749,7 @@ declare module '@solana/web3.js' { declare export type StakeInstructionType = | 'Initialize' | 'Authorize' + | 'AuthorizeWithSeed' | 'Delegate' | 'Split' | 'Withdraw' diff --git a/web3.js/src/stake-program.js b/web3.js/src/stake-program.js index 77f0220c56..c2b4a369a5 100644 --- a/web3.js/src/stake-program.js +++ b/web3.js/src/stake-program.js @@ -124,6 +124,25 @@ export type AuthorizeStakeParams = {| stakeAuthorizationType: StakeAuthorizationType, |}; +/** + * Authorize stake instruction params using a derived key + * @typedef {Object} AuthorizeWithSeedStakeParams + * @property {PublicKey} stakePubkey + * @property {PublicKey} authorityBase + * @property {string} authoritySeed + * @property {PublicKey} authorityOwner + * @property {PublicKey} newAuthorizedPubkey + * @property {StakeAuthorizationType} stakeAuthorizationType + */ +export type AuthorizeWithSeedStakeParams = {| + stakePubkey: PublicKey, + authorityBase: PublicKey, + authoritySeed: string, + authorityOwner: PublicKey, + newAuthorizedPubkey: PublicKey, + stakeAuthorizationType: StakeAuthorizationType, +|}; + /** * Split stake instruction params * @typedef {Object} SplitStakeParams @@ -262,6 +281,31 @@ export class StakeInstruction { }; } + /** + * Decode an authorize-with-seed stake instruction and retrieve the instruction params. + */ + static decodeAuthorizeWithSeed( + instruction: TransactionInstruction, + ): AuthorizeWithSeedStakeParams { + this.checkProgramId(instruction.programId); + this.checkKeyLength(instruction.keys, 2); + const {newAuthorized, stakeAuthorizationType, authoritySeed, authorityOwner} = decodeData( + STAKE_INSTRUCTION_LAYOUTS.AuthorizeWithSeed, + instruction.data, + ); + + return { + stakePubkey: instruction.keys[0].pubkey, + authorityBase: instruction.keys[1].pubkey, + authoritySeed: authoritySeed, + authorityOwner: new PublicKey(authorityOwner), + newAuthorizedPubkey: new PublicKey(newAuthorized), + stakeAuthorizationType: { + index: stakeAuthorizationType, + }, + }; + } + /** * Decode a split stake instruction and retrieve the instruction params. */ @@ -341,7 +385,7 @@ export class StakeInstruction { /** * An enumeration of valid StakeInstructionType's - * @typedef { 'Initialize' | 'Authorize' | 'Delegate' | 'Split' | 'Withdraw' + * @typedef { 'Initialize' | 'Authorize' | 'AuthorizeWithSeed' | 'Delegate' | 'Split' | 'Withdraw' | 'Deactivate' } StakeInstructionType */ export type StakeInstructionType = $Keys; @@ -388,6 +432,16 @@ export const STAKE_INSTRUCTION_LAYOUTS = Object.freeze({ index: 5, layout: BufferLayout.struct([BufferLayout.u32('instruction')]), }, + AuthorizeWithSeed: { + index: 8, + layout: BufferLayout.struct([ + BufferLayout.u32('instruction'), + Layout.publicKey('newAuthorized'), + BufferLayout.u32('stakeAuthorizationType'), + Layout.rustString('authoritySeed'), + Layout.publicKey('authorityOwner'), + ]), + }, }); /** @@ -551,6 +605,38 @@ export class StakeProgram { }); } + /** + * Generate a Transaction that authorizes a new PublicKey as Staker + * or Withdrawer on the Stake account. + */ + static authorizeWithSeed(params: AuthorizeWithSeedStakeParams): Transaction { + const { + stakePubkey, + authorityBase, + authoritySeed, + authorityOwner, + newAuthorizedPubkey, + stakeAuthorizationType, + } = params; + + const type = STAKE_INSTRUCTION_LAYOUTS.AuthorizeWithSeed; + const data = encodeData(type, { + newAuthorized: newAuthorizedPubkey.toBuffer(), + stakeAuthorizationType: stakeAuthorizationType.index, + authoritySeed: authoritySeed, + authorityOwner: authorityOwner.toBuffer(), + }); + + return new Transaction().add({ + keys: [ + {pubkey: stakePubkey, isSigner: false, isWritable: true}, + {pubkey: authorityBase, isSigner: true, isWritable: false}, + ], + programId: this.programId, + data, + }); + } + /** * Generate a Transaction that splits Stake tokens into another stake account */ diff --git a/web3.js/test/stake-program.test.js b/web3.js/test/stake-program.test.js index 9a887fc438..170d17ed64 100644 --- a/web3.js/test/stake-program.test.js +++ b/web3.js/test/stake-program.test.js @@ -128,6 +128,27 @@ test('authorize', () => { expect(params).toEqual(StakeInstruction.decodeAuthorize(stakeInstruction)); }); +test('authorizeWithSeed', () => { + const stakePubkey = new Account().publicKey; + const authorityBase = new Account().publicKey; + const authoritySeed = 'test string'; + const authorityOwner = new Account().publicKey; + const newAuthorizedPubkey = new Account().publicKey; + const stakeAuthorizationType = StakeAuthorizationLayout.Staker; + const params = { + stakePubkey, + authorityBase, + authoritySeed, + authorityOwner, + newAuthorizedPubkey, + stakeAuthorizationType, + }; + const transaction = StakeProgram.authorizeWithSeed(params); + expect(transaction.instructions).toHaveLength(1); + const [stakeInstruction] = transaction.instructions; + expect(params).toEqual(StakeInstruction.decodeAuthorizeWithSeed(stakeInstruction)); +}); + test('split', () => { const stakePubkey = new Account().publicKey; const authorizedPubkey = new Account().publicKey;