vote.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. #![allow(clippy::arithmetic_side_effects)]
  2. use {
  3. solana_account::ReadableAccount,
  4. solana_cli::{
  5. check_balance,
  6. cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig},
  7. spend_utils::SpendAmount,
  8. },
  9. solana_cli_output::{parse_sign_only_reply_string, OutputFormat},
  10. solana_commitment_config::CommitmentConfig,
  11. solana_faucet::faucet::run_local_faucet_with_unique_port_for_tests,
  12. solana_keypair::Keypair,
  13. solana_net_utils::SocketAddrSpace,
  14. solana_rpc_client::rpc_client::RpcClient,
  15. solana_rpc_client_nonce_utils::blockhash_query::{self, BlockhashQuery},
  16. solana_signer::{null_signer::NullSigner, Signer},
  17. solana_test_validator::TestValidator,
  18. solana_vote_program::vote_state::{VoteAuthorize, VoteStateV4},
  19. test_case::test_case,
  20. };
  21. #[test_case(None; "base")]
  22. #[test_case(Some(1_000_000); "with_compute_unit_price")]
  23. fn test_vote_authorize_and_withdraw(compute_unit_price: Option<u64>) {
  24. let mint_keypair = Keypair::new();
  25. let mint_pubkey = mint_keypair.pubkey();
  26. let faucet_addr = run_local_faucet_with_unique_port_for_tests(mint_keypair);
  27. let test_validator =
  28. TestValidator::with_no_fees(mint_pubkey, Some(faucet_addr), SocketAddrSpace::Unspecified);
  29. let rpc_client =
  30. RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed());
  31. let default_signer = Keypair::new();
  32. let mut config = CliConfig::recent_for_tests();
  33. config.json_rpc_url = test_validator.rpc_url();
  34. config.signers = vec![&default_signer];
  35. request_and_confirm_airdrop(&rpc_client, &config, &config.signers[0].pubkey(), 100_000)
  36. .unwrap();
  37. // Create vote account
  38. let vote_account_keypair = Keypair::new();
  39. let vote_account_pubkey = vote_account_keypair.pubkey();
  40. config.signers = vec![&default_signer, &vote_account_keypair];
  41. config.command = CliCommand::CreateVoteAccount {
  42. vote_account: 1,
  43. seed: None,
  44. identity_account: 0,
  45. authorized_voter: None,
  46. authorized_withdrawer: config.signers[0].pubkey(),
  47. commission: 0,
  48. sign_only: false,
  49. dump_transaction_message: false,
  50. blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
  51. nonce_account: None,
  52. nonce_authority: 0,
  53. memo: None,
  54. fee_payer: 0,
  55. compute_unit_price,
  56. };
  57. process_command(&config).unwrap();
  58. let vote_account = rpc_client
  59. .get_account(&vote_account_keypair.pubkey())
  60. .unwrap();
  61. let vote_state =
  62. VoteStateV4::deserialize(vote_account.data(), &vote_account_keypair.pubkey()).unwrap();
  63. let authorized_withdrawer = vote_state.authorized_withdrawer;
  64. assert_eq!(authorized_withdrawer, config.signers[0].pubkey());
  65. let expected_balance = rpc_client
  66. .get_minimum_balance_for_rent_exemption(VoteStateV4::size_of())
  67. .unwrap()
  68. .max(1);
  69. check_balance!(expected_balance, &rpc_client, &vote_account_pubkey);
  70. // Transfer in some more SOL
  71. config.signers = vec![&default_signer];
  72. config.command = CliCommand::Transfer {
  73. amount: SpendAmount::Some(10_000),
  74. to: vote_account_pubkey,
  75. from: 0,
  76. sign_only: false,
  77. dump_transaction_message: false,
  78. allow_unfunded_recipient: true,
  79. no_wait: false,
  80. blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
  81. nonce_account: None,
  82. nonce_authority: 0,
  83. memo: None,
  84. fee_payer: 0,
  85. derived_address_seed: None,
  86. derived_address_program_id: None,
  87. compute_unit_price,
  88. };
  89. process_command(&config).unwrap();
  90. let expected_balance = expected_balance + 10_000;
  91. check_balance!(expected_balance, &rpc_client, &vote_account_pubkey);
  92. // Authorize vote account withdrawal to another signer
  93. let first_withdraw_authority = Keypair::new();
  94. config.signers = vec![&default_signer];
  95. config.command = CliCommand::VoteAuthorize {
  96. vote_account_pubkey,
  97. new_authorized_pubkey: first_withdraw_authority.pubkey(),
  98. vote_authorize: VoteAuthorize::Withdrawer,
  99. sign_only: false,
  100. dump_transaction_message: false,
  101. blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
  102. nonce_account: None,
  103. nonce_authority: 0,
  104. memo: None,
  105. fee_payer: 0,
  106. authorized: 0,
  107. new_authorized: None,
  108. compute_unit_price,
  109. };
  110. process_command(&config).unwrap();
  111. let vote_account = rpc_client
  112. .get_account(&vote_account_keypair.pubkey())
  113. .unwrap();
  114. let vote_state =
  115. VoteStateV4::deserialize(vote_account.data(), &vote_account_keypair.pubkey()).unwrap();
  116. let authorized_withdrawer = vote_state.authorized_withdrawer;
  117. assert_eq!(authorized_withdrawer, first_withdraw_authority.pubkey());
  118. // Authorize vote account withdrawal to another signer with checked instruction
  119. let withdraw_authority = Keypair::new();
  120. config.signers = vec![&default_signer, &first_withdraw_authority];
  121. config.command = CliCommand::VoteAuthorize {
  122. vote_account_pubkey,
  123. new_authorized_pubkey: withdraw_authority.pubkey(),
  124. vote_authorize: VoteAuthorize::Withdrawer,
  125. sign_only: false,
  126. dump_transaction_message: false,
  127. blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
  128. nonce_account: None,
  129. nonce_authority: 0,
  130. memo: None,
  131. fee_payer: 0,
  132. authorized: 1,
  133. new_authorized: Some(1),
  134. compute_unit_price,
  135. };
  136. process_command(&config).unwrap_err(); // unsigned by new authority should fail
  137. config.signers = vec![
  138. &default_signer,
  139. &first_withdraw_authority,
  140. &withdraw_authority,
  141. ];
  142. config.command = CliCommand::VoteAuthorize {
  143. vote_account_pubkey,
  144. new_authorized_pubkey: withdraw_authority.pubkey(),
  145. vote_authorize: VoteAuthorize::Withdrawer,
  146. sign_only: false,
  147. dump_transaction_message: false,
  148. blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
  149. nonce_account: None,
  150. nonce_authority: 0,
  151. memo: None,
  152. fee_payer: 0,
  153. authorized: 1,
  154. new_authorized: Some(2),
  155. compute_unit_price,
  156. };
  157. process_command(&config).unwrap();
  158. let vote_account = rpc_client
  159. .get_account(&vote_account_keypair.pubkey())
  160. .unwrap();
  161. let vote_state =
  162. VoteStateV4::deserialize(vote_account.data(), &vote_account_keypair.pubkey()).unwrap();
  163. let authorized_withdrawer = vote_state.authorized_withdrawer;
  164. assert_eq!(authorized_withdrawer, withdraw_authority.pubkey());
  165. // Withdraw from vote account
  166. let destination_account = solana_pubkey::new_rand(); // Send withdrawal to new account to make balance check easy
  167. config.signers = vec![&default_signer, &withdraw_authority];
  168. config.command = CliCommand::WithdrawFromVoteAccount {
  169. vote_account_pubkey,
  170. withdraw_authority: 1,
  171. withdraw_amount: SpendAmount::Some(1_000),
  172. destination_account_pubkey: destination_account,
  173. sign_only: false,
  174. dump_transaction_message: false,
  175. blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
  176. nonce_account: None,
  177. nonce_authority: 0,
  178. memo: None,
  179. fee_payer: 0,
  180. compute_unit_price,
  181. };
  182. process_command(&config).unwrap();
  183. let expected_balance = expected_balance - 1_000;
  184. check_balance!(expected_balance, &rpc_client, &vote_account_pubkey);
  185. check_balance!(1_000, &rpc_client, &destination_account);
  186. // Re-assign validator identity
  187. let new_identity_keypair = Keypair::new();
  188. config.signers.push(&new_identity_keypair);
  189. config.command = CliCommand::VoteUpdateValidator {
  190. vote_account_pubkey,
  191. new_identity_account: 2,
  192. withdraw_authority: 1,
  193. sign_only: false,
  194. dump_transaction_message: false,
  195. blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
  196. nonce_account: None,
  197. nonce_authority: 0,
  198. memo: None,
  199. fee_payer: 0,
  200. compute_unit_price,
  201. };
  202. process_command(&config).unwrap();
  203. // Close vote account
  204. let destination_account = solana_pubkey::new_rand(); // Send withdrawal to new account to make balance check easy
  205. config.signers = vec![&default_signer, &withdraw_authority];
  206. config.command = CliCommand::CloseVoteAccount {
  207. vote_account_pubkey,
  208. withdraw_authority: 1,
  209. destination_account_pubkey: destination_account,
  210. memo: None,
  211. fee_payer: 0,
  212. compute_unit_price,
  213. };
  214. process_command(&config).unwrap();
  215. check_balance!(0, &rpc_client, &vote_account_pubkey);
  216. check_balance!(expected_balance, &rpc_client, &destination_account);
  217. }
  218. #[test_case(None; "base")]
  219. #[test_case(Some(1_000_000); "with_compute_unit_price")]
  220. fn test_offline_vote_authorize_and_withdraw(compute_unit_price: Option<u64>) {
  221. let mint_keypair = Keypair::new();
  222. let mint_pubkey = mint_keypair.pubkey();
  223. let faucet_addr = run_local_faucet_with_unique_port_for_tests(mint_keypair);
  224. let test_validator =
  225. TestValidator::with_no_fees(mint_pubkey, Some(faucet_addr), SocketAddrSpace::Unspecified);
  226. let rpc_client =
  227. RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed());
  228. let default_signer = Keypair::new();
  229. let mut config_payer = CliConfig::recent_for_tests();
  230. config_payer.json_rpc_url = test_validator.rpc_url();
  231. config_payer.signers = vec![&default_signer];
  232. let mut config_offline = CliConfig::recent_for_tests();
  233. config_offline.json_rpc_url = String::default();
  234. config_offline.command = CliCommand::ClusterVersion;
  235. let offline_keypair = Keypair::new();
  236. config_offline.signers = vec![&offline_keypair];
  237. // Verify that we cannot reach the cluster
  238. process_command(&config_offline).unwrap_err();
  239. request_and_confirm_airdrop(
  240. &rpc_client,
  241. &config_payer,
  242. &config_payer.signers[0].pubkey(),
  243. 100_000,
  244. )
  245. .unwrap();
  246. check_balance!(100_000, &rpc_client, &config_payer.signers[0].pubkey());
  247. request_and_confirm_airdrop(
  248. &rpc_client,
  249. &config_offline,
  250. &config_offline.signers[0].pubkey(),
  251. 100_000,
  252. )
  253. .unwrap();
  254. check_balance!(100_000, &rpc_client, &config_offline.signers[0].pubkey());
  255. // Create vote account with specific withdrawer
  256. let vote_account_keypair = Keypair::new();
  257. let vote_account_pubkey = vote_account_keypair.pubkey();
  258. config_payer.signers = vec![&default_signer, &vote_account_keypair];
  259. config_payer.command = CliCommand::CreateVoteAccount {
  260. vote_account: 1,
  261. seed: None,
  262. identity_account: 0,
  263. authorized_voter: None,
  264. authorized_withdrawer: offline_keypair.pubkey(),
  265. commission: 0,
  266. sign_only: false,
  267. dump_transaction_message: false,
  268. blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
  269. nonce_account: None,
  270. nonce_authority: 0,
  271. memo: None,
  272. fee_payer: 0,
  273. compute_unit_price,
  274. };
  275. process_command(&config_payer).unwrap();
  276. let vote_account = rpc_client
  277. .get_account(&vote_account_keypair.pubkey())
  278. .unwrap();
  279. let vote_state =
  280. VoteStateV4::deserialize(vote_account.data(), &vote_account_keypair.pubkey()).unwrap();
  281. let authorized_withdrawer = vote_state.authorized_withdrawer;
  282. assert_eq!(authorized_withdrawer, offline_keypair.pubkey());
  283. let expected_balance = rpc_client
  284. .get_minimum_balance_for_rent_exemption(VoteStateV4::size_of())
  285. .unwrap()
  286. .max(1);
  287. check_balance!(expected_balance, &rpc_client, &vote_account_pubkey);
  288. // Transfer in some more SOL
  289. config_payer.signers = vec![&default_signer];
  290. config_payer.command = CliCommand::Transfer {
  291. amount: SpendAmount::Some(10_000),
  292. to: vote_account_pubkey,
  293. from: 0,
  294. sign_only: false,
  295. dump_transaction_message: false,
  296. allow_unfunded_recipient: true,
  297. no_wait: false,
  298. blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
  299. nonce_account: None,
  300. nonce_authority: 0,
  301. memo: None,
  302. fee_payer: 0,
  303. derived_address_seed: None,
  304. derived_address_program_id: None,
  305. compute_unit_price,
  306. };
  307. process_command(&config_payer).unwrap();
  308. let expected_balance = expected_balance + 10_000;
  309. check_balance!(expected_balance, &rpc_client, &vote_account_pubkey);
  310. // Authorize vote account withdrawal to another signer, offline
  311. let withdraw_authority = Keypair::new();
  312. let blockhash = rpc_client.get_latest_blockhash().unwrap();
  313. config_offline.command = CliCommand::VoteAuthorize {
  314. vote_account_pubkey,
  315. new_authorized_pubkey: withdraw_authority.pubkey(),
  316. vote_authorize: VoteAuthorize::Withdrawer,
  317. sign_only: true,
  318. dump_transaction_message: false,
  319. blockhash_query: BlockhashQuery::None(blockhash),
  320. nonce_account: None,
  321. nonce_authority: 0,
  322. memo: None,
  323. fee_payer: 0,
  324. authorized: 0,
  325. new_authorized: None,
  326. compute_unit_price,
  327. };
  328. config_offline.output_format = OutputFormat::JsonCompact;
  329. let sig_response = process_command(&config_offline).unwrap();
  330. let sign_only = parse_sign_only_reply_string(&sig_response);
  331. assert!(sign_only.has_all_signers());
  332. let offline_presigner = sign_only
  333. .presigner_of(&config_offline.signers[0].pubkey())
  334. .unwrap();
  335. config_payer.signers = vec![&offline_presigner];
  336. config_payer.command = CliCommand::VoteAuthorize {
  337. vote_account_pubkey,
  338. new_authorized_pubkey: withdraw_authority.pubkey(),
  339. vote_authorize: VoteAuthorize::Withdrawer,
  340. sign_only: false,
  341. dump_transaction_message: false,
  342. blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
  343. nonce_account: None,
  344. nonce_authority: 0,
  345. memo: None,
  346. fee_payer: 0,
  347. authorized: 0,
  348. new_authorized: None,
  349. compute_unit_price,
  350. };
  351. process_command(&config_payer).unwrap();
  352. let vote_account = rpc_client
  353. .get_account(&vote_account_keypair.pubkey())
  354. .unwrap();
  355. let vote_state =
  356. VoteStateV4::deserialize(vote_account.data(), &vote_account_keypair.pubkey()).unwrap();
  357. let authorized_withdrawer = vote_state.authorized_withdrawer;
  358. assert_eq!(authorized_withdrawer, withdraw_authority.pubkey());
  359. // Withdraw from vote account offline
  360. let destination_account = solana_pubkey::new_rand(); // Send withdrawal to new account to make balance check easy
  361. let blockhash = rpc_client.get_latest_blockhash().unwrap();
  362. let fee_payer_null_signer = NullSigner::new(&default_signer.pubkey());
  363. config_offline.signers = vec![&fee_payer_null_signer, &withdraw_authority];
  364. config_offline.command = CliCommand::WithdrawFromVoteAccount {
  365. vote_account_pubkey,
  366. withdraw_authority: 1,
  367. withdraw_amount: SpendAmount::Some(1_000),
  368. destination_account_pubkey: destination_account,
  369. sign_only: true,
  370. dump_transaction_message: false,
  371. blockhash_query: BlockhashQuery::None(blockhash),
  372. nonce_account: None,
  373. nonce_authority: 0,
  374. memo: None,
  375. fee_payer: 0,
  376. compute_unit_price,
  377. };
  378. config_offline.output_format = OutputFormat::JsonCompact;
  379. let sig_response = process_command(&config_offline).unwrap();
  380. let sign_only = parse_sign_only_reply_string(&sig_response);
  381. let offline_presigner = sign_only
  382. .presigner_of(&config_offline.signers[1].pubkey())
  383. .unwrap();
  384. config_payer.signers = vec![&default_signer, &offline_presigner];
  385. config_payer.command = CliCommand::WithdrawFromVoteAccount {
  386. vote_account_pubkey,
  387. withdraw_authority: 1,
  388. withdraw_amount: SpendAmount::Some(1_000),
  389. destination_account_pubkey: destination_account,
  390. sign_only: false,
  391. dump_transaction_message: false,
  392. blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
  393. nonce_account: None,
  394. nonce_authority: 0,
  395. memo: None,
  396. fee_payer: 0,
  397. compute_unit_price,
  398. };
  399. process_command(&config_payer).unwrap();
  400. let expected_balance = expected_balance - 1_000;
  401. check_balance!(expected_balance, &rpc_client, &vote_account_pubkey);
  402. check_balance!(1_000, &rpc_client, &destination_account);
  403. // Re-assign validator identity offline
  404. let blockhash = rpc_client.get_latest_blockhash().unwrap();
  405. let new_identity_keypair = Keypair::new();
  406. let new_identity_null_signer = NullSigner::new(&new_identity_keypair.pubkey());
  407. config_offline.signers = vec![
  408. &fee_payer_null_signer,
  409. &withdraw_authority,
  410. &new_identity_null_signer,
  411. ];
  412. config_offline.command = CliCommand::VoteUpdateValidator {
  413. vote_account_pubkey,
  414. new_identity_account: 2,
  415. withdraw_authority: 1,
  416. sign_only: true,
  417. dump_transaction_message: false,
  418. blockhash_query: BlockhashQuery::None(blockhash),
  419. nonce_account: None,
  420. nonce_authority: 0,
  421. memo: None,
  422. fee_payer: 0,
  423. compute_unit_price,
  424. };
  425. process_command(&config_offline).unwrap();
  426. config_offline.output_format = OutputFormat::JsonCompact;
  427. let sig_response = process_command(&config_offline).unwrap();
  428. let sign_only = parse_sign_only_reply_string(&sig_response);
  429. let offline_presigner = sign_only
  430. .presigner_of(&config_offline.signers[1].pubkey())
  431. .unwrap();
  432. config_payer.signers = vec![&default_signer, &offline_presigner, &new_identity_keypair];
  433. config_payer.command = CliCommand::VoteUpdateValidator {
  434. vote_account_pubkey,
  435. new_identity_account: 2,
  436. withdraw_authority: 1,
  437. sign_only: false,
  438. dump_transaction_message: false,
  439. blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
  440. nonce_account: None,
  441. nonce_authority: 0,
  442. memo: None,
  443. fee_payer: 0,
  444. compute_unit_price,
  445. };
  446. process_command(&config_payer).unwrap();
  447. // Close vote account offline. Must use WithdrawFromVoteAccount and specify amount, since
  448. // CloseVoteAccount requires RpcClient
  449. let destination_account = solana_pubkey::new_rand(); // Send withdrawal to new account to make balance check easy
  450. config_offline.signers = vec![&fee_payer_null_signer, &withdraw_authority];
  451. config_offline.command = CliCommand::WithdrawFromVoteAccount {
  452. vote_account_pubkey,
  453. withdraw_authority: 1,
  454. withdraw_amount: SpendAmount::Some(expected_balance),
  455. destination_account_pubkey: destination_account,
  456. sign_only: true,
  457. dump_transaction_message: false,
  458. blockhash_query: BlockhashQuery::None(blockhash),
  459. nonce_account: None,
  460. nonce_authority: 0,
  461. memo: None,
  462. fee_payer: 0,
  463. compute_unit_price,
  464. };
  465. process_command(&config_offline).unwrap();
  466. config_offline.output_format = OutputFormat::JsonCompact;
  467. let sig_response = process_command(&config_offline).unwrap();
  468. let sign_only = parse_sign_only_reply_string(&sig_response);
  469. let offline_presigner = sign_only
  470. .presigner_of(&config_offline.signers[1].pubkey())
  471. .unwrap();
  472. config_payer.signers = vec![&default_signer, &offline_presigner];
  473. config_payer.command = CliCommand::WithdrawFromVoteAccount {
  474. vote_account_pubkey,
  475. withdraw_authority: 1,
  476. withdraw_amount: SpendAmount::Some(expected_balance),
  477. destination_account_pubkey: destination_account,
  478. sign_only: false,
  479. dump_transaction_message: false,
  480. blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
  481. nonce_account: None,
  482. nonce_authority: 0,
  483. memo: None,
  484. fee_payer: 0,
  485. compute_unit_price,
  486. };
  487. process_command(&config_payer).unwrap();
  488. check_balance!(0, &rpc_client, &vote_account_pubkey);
  489. check_balance!(expected_balance, &rpc_client, &destination_account);
  490. }