Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit d815ba9

Browse files
authored
stake-pool: Add borsh support and specify size on creation (#1505)
* lending: Update JS tests to solana-test-validator * Add solana tools install * Fix oopsie on the path * Move where deployed programs go * stake-pool: Add borsh support and size on creation We can't specify the size in the instruction unfortunately, since we'd only have 10kb max for the validator list. At roughly 50 bytes per validator, that only gives us 200 validators. On the flip side, using Borsh means we can allow the validator stake list to be any size! * Add AccountType enum * Remove V1 everywhere * Add max validators as parameter and get_instance_packed_len * Add test for adding too many validators * Clippy
1 parent 34e66c6 commit d815ba9

19 files changed

+749
-600
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

stake-pool/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ repository = "https://github.com/solana-labs/solana-program-library"
99
version = "2.0.1"
1010

1111
[dependencies]
12+
borsh = "0.8"
1213
clap = "2.33.3"
1314
serde_json = "1.0.62"
1415
solana-account-decoder = "1.6.1"

stake-pool/cli/src/main.rs

Lines changed: 82 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,58 @@
11
#[macro_use]
22
extern crate lazy_static;
3-
use bincode::deserialize;
4-
use clap::{
5-
crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, AppSettings, Arg,
6-
ArgGroup, SubCommand,
7-
};
8-
use solana_account_decoder::UiAccountEncoding;
9-
use solana_clap_utils::{
10-
input_parsers::pubkey_of,
11-
input_validators::{is_amount, is_keypair, is_parsable, is_pubkey, is_url},
12-
keypair::signer_from_path,
13-
};
14-
use solana_client::{
15-
rpc_client::RpcClient,
16-
rpc_config::RpcAccountInfoConfig,
17-
rpc_config::RpcProgramAccountsConfig,
18-
rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
19-
};
20-
use solana_program::{instruction::Instruction, program_pack::Pack, pubkey::Pubkey};
21-
use solana_sdk::{
22-
account::Account,
23-
commitment_config::CommitmentConfig,
24-
native_token::{self, Sol},
25-
signature::{Keypair, Signer},
26-
system_instruction,
27-
transaction::Transaction,
28-
};
29-
use spl_stake_pool::{
30-
instruction::{
31-
add_validator_stake_account, create_validator_stake_account, deposit,
32-
initialize as initialize_pool, remove_validator_stake_account, set_owner,
33-
update_list_balance, update_pool_balance, withdraw, Fee as PoolFee,
34-
InitArgs as PoolInitArgs,
3+
4+
use {
5+
bincode::deserialize,
6+
borsh::BorshDeserialize,
7+
clap::{
8+
crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, AppSettings,
9+
Arg, ArgGroup, SubCommand,
3510
},
36-
processor::Processor as PoolProcessor,
37-
stake::authorize as authorize_stake,
38-
stake::id as stake_program_id,
39-
stake::StakeAuthorize,
40-
stake::StakeState,
41-
state::StakePool,
42-
state::ValidatorStakeList,
43-
};
44-
use spl_token::{
45-
self, instruction::approve as approve_token, instruction::initialize_account,
46-
instruction::initialize_mint, native_mint, state::Account as TokenAccount,
47-
state::Mint as TokenMint,
11+
solana_account_decoder::UiAccountEncoding,
12+
solana_clap_utils::{
13+
input_parsers::pubkey_of,
14+
input_validators::{is_amount, is_keypair, is_parsable, is_pubkey, is_url},
15+
keypair::signer_from_path,
16+
},
17+
solana_client::{
18+
rpc_client::RpcClient,
19+
rpc_config::RpcAccountInfoConfig,
20+
rpc_config::RpcProgramAccountsConfig,
21+
rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
22+
},
23+
solana_program::{
24+
borsh::get_packed_len, instruction::Instruction, program_pack::Pack, pubkey::Pubkey,
25+
},
26+
solana_sdk::{
27+
account::Account,
28+
commitment_config::CommitmentConfig,
29+
native_token::{self, Sol},
30+
signature::{Keypair, Signer},
31+
system_instruction,
32+
transaction::Transaction,
33+
},
34+
spl_stake_pool::{
35+
borsh::{get_instance_packed_len, try_from_slice_unchecked},
36+
instruction::{
37+
add_validator_stake_account, create_validator_stake_account, deposit,
38+
initialize as initialize_pool, remove_validator_stake_account, set_owner,
39+
update_list_balance, update_pool_balance, withdraw, Fee as PoolFee,
40+
},
41+
processor::Processor as PoolProcessor,
42+
stake::authorize as authorize_stake,
43+
stake::id as stake_program_id,
44+
stake::StakeAuthorize,
45+
stake::StakeState,
46+
state::StakePool,
47+
state::ValidatorStakeList,
48+
},
49+
spl_token::{
50+
self, instruction::approve as approve_token, instruction::initialize_account,
51+
instruction::initialize_mint, native_mint, state::Account as TokenAccount,
52+
state::Mint as TokenMint,
53+
},
54+
std::process::exit,
4855
};
49-
use std::process::exit;
5056

5157
struct Config {
5258
rpc_client: RpcClient,
@@ -109,7 +115,7 @@ fn get_authority_accounts(config: &Config, authority: &Pubkey) -> Vec<(Pubkey, A
109115
.unwrap()
110116
}
111117

112-
fn command_create_pool(config: &Config, fee: PoolFee) -> CommandResult {
118+
fn command_create_pool(config: &Config, fee: PoolFee, max_validators: u32) -> CommandResult {
113119
let mint_account = Keypair::new();
114120
println!("Creating mint {}", mint_account.pubkey());
115121

@@ -132,10 +138,12 @@ fn command_create_pool(config: &Config, fee: PoolFee) -> CommandResult {
132138
.get_minimum_balance_for_rent_exemption(TokenAccount::LEN)?;
133139
let pool_account_balance = config
134140
.rpc_client
135-
.get_minimum_balance_for_rent_exemption(StakePool::LEN)?;
141+
.get_minimum_balance_for_rent_exemption(get_packed_len::<StakePool>())?;
142+
let empty_validator_list = ValidatorStakeList::new_with_max_validators(max_validators);
143+
let validator_stake_list_size = get_instance_packed_len(&empty_validator_list)?;
136144
let validator_stake_list_balance = config
137145
.rpc_client
138-
.get_minimum_balance_for_rent_exemption(ValidatorStakeList::LEN)?;
146+
.get_minimum_balance_for_rent_exemption(validator_stake_list_size)?;
139147
let total_rent_free_balances = mint_account_balance
140148
+ pool_fee_account_balance
141149
+ pool_account_balance
@@ -177,15 +185,15 @@ fn command_create_pool(config: &Config, fee: PoolFee) -> CommandResult {
177185
&config.fee_payer.pubkey(),
178186
&pool_account.pubkey(),
179187
pool_account_balance,
180-
StakePool::LEN as u64,
188+
get_packed_len::<StakePool>() as u64,
181189
&spl_stake_pool::id(),
182190
),
183191
// Validator stake account list storage
184192
system_instruction::create_account(
185193
&config.fee_payer.pubkey(),
186194
&validator_stake_list.pubkey(),
187195
validator_stake_list_balance,
188-
ValidatorStakeList::LEN as u64,
196+
validator_stake_list_size as u64,
189197
&spl_stake_pool::id(),
190198
),
191199
// Initialize pool token mint account
@@ -212,7 +220,8 @@ fn command_create_pool(config: &Config, fee: PoolFee) -> CommandResult {
212220
&mint_account.pubkey(),
213221
&pool_fee_account.pubkey(),
214222
&spl_token::id(),
215-
PoolInitArgs { fee },
223+
fee,
224+
max_validators,
216225
)?,
217226
],
218227
Some(&config.fee_payer.pubkey()),
@@ -274,7 +283,7 @@ fn command_vsa_add(
274283
) -> CommandResult {
275284
// Get stake pool state
276285
let pool_data = config.rpc_client.get_account_data(&pool)?;
277-
let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap();
286+
let pool_data = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
278287

279288
let mut total_rent_free_balances: u64 = 0;
280289

@@ -365,7 +374,7 @@ fn command_vsa_remove(
365374
) -> CommandResult {
366375
// Get stake pool state
367376
let pool_data = config.rpc_client.get_account_data(&pool)?;
368-
let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap();
377+
let pool_data: StakePool = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
369378

370379
let pool_withdraw_authority: Pubkey = PoolProcessor::authority_id(
371380
&spl_stake_pool::id(),
@@ -495,7 +504,7 @@ fn command_deposit(
495504
) -> CommandResult {
496505
// Get stake pool state
497506
let pool_data = config.rpc_client.get_account_data(&pool)?;
498-
let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap();
507+
let pool_data: StakePool = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
499508

500509
// Get stake account data
501510
let stake_data = config.rpc_client.get_account_data(&stake)?;
@@ -514,7 +523,7 @@ fn command_deposit(
514523
.rpc_client
515524
.get_account_data(&pool_data.validator_stake_list)?;
516525
let validator_stake_list_data =
517-
ValidatorStakeList::deserialize(&validator_stake_list_data.as_slice())?;
526+
try_from_slice_unchecked::<ValidatorStakeList>(&validator_stake_list_data.as_slice())?;
518527
if !validator_stake_list_data.contains(&validator) {
519528
return Err("Stake account for this validator does not exist in the pool.".into());
520529
}
@@ -617,14 +626,14 @@ fn command_deposit(
617626
fn command_list(config: &Config, pool: &Pubkey) -> CommandResult {
618627
// Get stake pool state
619628
let pool_data = config.rpc_client.get_account_data(&pool)?;
620-
let pool_data = StakePool::deserialize(pool_data.as_slice()).unwrap();
629+
let pool_data = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
621630

622631
if config.verbose {
623632
let validator_list = config
624633
.rpc_client
625634
.get_account_data(&pool_data.validator_stake_list)?;
626635
let validator_stake_list_data =
627-
ValidatorStakeList::deserialize(&validator_list.as_slice())?;
636+
try_from_slice_unchecked::<ValidatorStakeList>(&validator_list.as_slice())?;
628637
println!("Current validator list");
629638
for validator in validator_stake_list_data.validators {
630639
println!(
@@ -669,12 +678,12 @@ fn command_list(config: &Config, pool: &Pubkey) -> CommandResult {
669678
fn command_update(config: &Config, pool: &Pubkey) -> CommandResult {
670679
// Get stake pool state
671680
let pool_data = config.rpc_client.get_account_data(&pool)?;
672-
let pool_data = StakePool::deserialize(pool_data.as_slice()).unwrap();
681+
let pool_data = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
673682
let validator_stake_list_data = config
674683
.rpc_client
675684
.get_account_data(&pool_data.validator_stake_list)?;
676685
let validator_stake_list_data =
677-
ValidatorStakeList::deserialize(&validator_stake_list_data.as_slice())?;
686+
try_from_slice_unchecked::<ValidatorStakeList>(&validator_stake_list_data.as_slice())?;
678687

679688
let epoch_info = config.rpc_client.get_epoch_info()?;
680689

@@ -815,7 +824,7 @@ fn command_withdraw(
815824
) -> CommandResult {
816825
// Get stake pool state
817826
let pool_data = config.rpc_client.get_account_data(&pool)?;
818-
let pool_data = StakePool::deserialize(pool_data.as_slice()).unwrap();
827+
let pool_data = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
819828

820829
let pool_mint_data = config.rpc_client.get_account_data(&pool_data.pool_mint)?;
821830
let pool_mint = TokenMint::unpack_from_slice(pool_mint_data.as_slice()).unwrap();
@@ -954,7 +963,7 @@ fn command_set_owner(
954963
new_fee_receiver: &Option<Pubkey>,
955964
) -> CommandResult {
956965
let pool_data = config.rpc_client.get_account_data(&pool)?;
957-
let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap();
966+
let pool_data: StakePool = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
958967

959968
// If new accounts are missing in the arguments use the old ones
960969
let new_owner: Pubkey = match new_owner {
@@ -1088,6 +1097,16 @@ fn main() {
10881097
.required(true)
10891098
.help("Fee denominator, fee amount is numerator divided by denominator."),
10901099
)
1100+
.arg(
1101+
Arg::with_name("max_validators")
1102+
.long("max-validators")
1103+
.short("m")
1104+
.validator(is_parsable::<u32>)
1105+
.value_name("NUMBER")
1106+
.takes_value(true)
1107+
.required(true)
1108+
.help("Max number of validators included in the stake pool"),
1109+
)
10911110
)
10921111
.subcommand(SubCommand::with_name("create-validator-stake").about("Create a new validator stake account to use with the pool")
10931112
.arg(
@@ -1344,12 +1363,14 @@ fn main() {
13441363
("create-pool", Some(arg_matches)) => {
13451364
let numerator = value_t_or_exit!(arg_matches, "fee_numerator", u64);
13461365
let denominator = value_t_or_exit!(arg_matches, "fee_denominator", u64);
1366+
let max_validators = value_t_or_exit!(arg_matches, "max_validators", u32);
13471367
command_create_pool(
13481368
&config,
13491369
PoolFee {
13501370
denominator,
13511371
numerator,
13521372
},
1373+
max_validators,
13531374
)
13541375
}
13551376
("create-validator-stake", Some(arg_matches)) => {

stake-pool/program/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ test-bpf = []
1313

1414
[dependencies]
1515
arrayref = "0.3.6"
16+
borsh = "0.8"
1617
num-derive = "0.3"
1718
num-traits = "0.2"
1819
num_enum = "0.5.1"
@@ -25,9 +26,10 @@ thiserror = "1.0"
2526
bincode = "1.3.1"
2627

2728
[dev-dependencies]
29+
proptest = "0.10"
2830
solana-program-test = "1.6.1"
2931
solana-sdk = "1.6.1"
30-
solana-vote-program = "1.5.11"
32+
solana-vote-program = "1.6.1"
3133

3234
[lib]
3335
crate-type = ["cdylib", "lib"]

stake-pool/program/src/borsh.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//! Extra borsh utils
2+
//! TODO delete once try_from_slice_unchecked has been published
3+
4+
use {
5+
borsh::{maybestd::io::Error, BorshDeserialize, BorshSerialize},
6+
std::io::{Result as IoResult, Write},
7+
};
8+
9+
/// Deserializes something and allows for incomplete reading
10+
pub fn try_from_slice_unchecked<T: BorshDeserialize>(data: &[u8]) -> Result<T, Error> {
11+
let mut data_mut = data;
12+
let result = T::deserialize(&mut data_mut)?;
13+
Ok(result)
14+
}
15+
16+
/// Helper struct which to count how much data would be written during serialization
17+
#[derive(Default)]
18+
struct WriteCounter {
19+
count: usize,
20+
}
21+
22+
impl Write for WriteCounter {
23+
fn write(&mut self, data: &[u8]) -> IoResult<usize> {
24+
let amount = data.len();
25+
self.count += amount;
26+
Ok(amount)
27+
}
28+
29+
fn flush(&mut self) -> IoResult<()> {
30+
Ok(())
31+
}
32+
}
33+
34+
/// Get the worst-case packed length for the given BorshSchema
35+
pub fn get_instance_packed_len<T: BorshSerialize>(instance: &T) -> Result<usize, Error> {
36+
let mut counter = WriteCounter::default();
37+
instance.serialize(&mut counter)?;
38+
Ok(counter.count)
39+
}

stake-pool/program/src/error.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ pub enum StakePoolError {
3737
/// Invalid validator stake list account.
3838
#[error("InvalidValidatorStakeList")]
3939
InvalidValidatorStakeList,
40-
41-
// 10.
4240
/// Invalid owner fee account.
4341
#[error("InvalidFeeAccount")]
4442
InvalidFeeAccount,
@@ -56,8 +54,6 @@ pub enum StakePoolError {
5654
/// Stake account voting for this validator already exists in the pool.
5755
#[error("ValidatorAlreadyAdded")]
5856
ValidatorAlreadyAdded,
59-
60-
// 15.
6157
/// Stake account for this validator not found in the pool.
6258
#[error("ValidatorNotFound")]
6359
ValidatorNotFound,
@@ -75,16 +71,14 @@ pub enum StakePoolError {
7571
/// Validator stake account is not found in the list storage.
7672
#[error("UnknownValidatorStakeAccount")]
7773
UnknownValidatorStakeAccount,
78-
79-
// 20.
8074
/// Wrong minting authority set for mint pool account
8175
#[error("WrongMintingAuthority")]
8276
WrongMintingAuthority,
8377

8478
// 20.
85-
/// Account is not rent-exempt
86-
#[error("AccountNotRentExempt")]
87-
AccountNotRentExempt,
79+
/// The size of the given validator stake list does match the expected amount
80+
#[error("UnexpectedValidatorListAccountSize")]
81+
UnexpectedValidatorListAccountSize,
8882
}
8983
impl From<StakePoolError> for ProgramError {
9084
fn from(e: StakePoolError) -> Self {

0 commit comments

Comments
 (0)