From 8d69e8d44772d2c2be77e99cca8b685e2ce4ba66 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Wed, 27 Jul 2022 12:23:44 -0700 Subject: [PATCH] Compact vote state updates to reduce block size (#26616) * Compact vote state updates to reduce block size * Add rpc transaction tests --- core/src/consensus.rs | 19 ++- core/src/replay_stage.rs | 13 +- programs/vote/src/vote_instruction.rs | 53 +++++- programs/vote/src/vote_processor.rs | 27 ++- runtime/src/vote_parser.rs | 12 +- sdk/src/feature_set.rs | 5 + transaction-status/src/parse_vote.rs | 229 ++++++++++++++++++++++++-- 7 files changed, 339 insertions(+), 19 deletions(-) diff --git a/core/src/consensus.rs b/core/src/consensus.rs index c0aaf2044..37c80014a 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -87,11 +87,22 @@ impl SwitchForkDecision { v, *switch_proof_hash, )), - (SwitchForkDecision::SameFork, VoteTransaction::CompactVoteStateUpdate(_v)) => None, + (SwitchForkDecision::SameFork, VoteTransaction::CompactVoteStateUpdate(v)) => { + Some(vote_instruction::compact_update_vote_state( + vote_account_pubkey, + authorized_voter_pubkey, + v, + )) + } ( - SwitchForkDecision::SwitchProof(_switch_proof_hash), - VoteTransaction::CompactVoteStateUpdate(_v), - ) => None, + SwitchForkDecision::SwitchProof(switch_proof_hash), + VoteTransaction::CompactVoteStateUpdate(v), + ) => Some(vote_instruction::compact_update_vote_state_switch( + vote_account_pubkey, + authorized_voter_pubkey, + v, + *switch_proof_hash, + )), } } diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index f4551e745..22a872265 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -56,6 +56,7 @@ use { }, solana_sdk::{ clock::{BankId, Slot, MAX_PROCESSING_AGE, NUM_CONSECUTIVE_LEADER_SLOTS}, + feature_set, genesis_config::ClusterType, hash::Hash, pubkey::Pubkey, @@ -64,7 +65,7 @@ use { timing::timestamp, transaction::Transaction, }, - solana_vote_program::vote_state::VoteTransaction, + solana_vote_program::vote_state::{CompactVoteStateUpdate, VoteTransaction}, std::{ collections::{HashMap, HashSet}, result, @@ -1980,6 +1981,16 @@ impl ReplayStage { }; // Send our last few votes along with the new one + // Compact the vote state update before sending + let should_compact = bank + .feature_set + .is_active(&feature_set::compact_vote_state_updates::id()); + let vote = match (should_compact, vote) { + (true, VoteTransaction::VoteStateUpdate(vote_state_update)) => { + VoteTransaction::from(CompactVoteStateUpdate::from(vote_state_update)) + } + (_, vote) => vote, + }; let vote_ix = switch_fork_decision .to_vote_instruction( vote, diff --git a/programs/vote/src/vote_instruction.rs b/programs/vote/src/vote_instruction.rs index e6b965de3..18b568376 100644 --- a/programs/vote/src/vote_instruction.rs +++ b/programs/vote/src/vote_instruction.rs @@ -4,8 +4,8 @@ use { crate::{ id, vote_state::{ - Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, VoteAuthorizeWithSeedArgs, - VoteInit, VoteState, VoteStateUpdate, + CompactVoteStateUpdate, Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, + VoteAuthorizeWithSeedArgs, VoteInit, VoteState, VoteStateUpdate, }, }, serde_derive::{Deserialize, Serialize}, @@ -103,6 +103,20 @@ pub enum VoteInstruction { /// 1. `[SIGNER]` Vote authority UpdateVoteStateSwitch(VoteStateUpdate, Hash), + /// Update the onchain vote state for the signer. + /// + /// # Account references + /// 0. `[Write]` Vote account to vote with + /// 1. `[SIGNER]` Vote authority + CompactUpdateVoteState(CompactVoteStateUpdate), + + /// Update the onchain vote state for the signer along with a switching proof. + /// + /// # Account references + /// 0. `[Write]` Vote account to vote with + /// 1. `[SIGNER]` Vote authority + CompactUpdateVoteStateSwitch(CompactVoteStateUpdate, Hash), + /// Given that the current Voter or Withdrawer authority is a derived key, /// this instruction allows someone who can sign for that derived key's /// base key to authorize a new Voter or Withdrawer for a vote account. @@ -370,6 +384,41 @@ pub fn update_vote_state_switch( ) } +pub fn compact_update_vote_state( + vote_pubkey: &Pubkey, + authorized_voter_pubkey: &Pubkey, + compact_vote_state_update: CompactVoteStateUpdate, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*vote_pubkey, false), + AccountMeta::new_readonly(*authorized_voter_pubkey, true), + ]; + + Instruction::new_with_bincode( + id(), + &VoteInstruction::CompactUpdateVoteState(compact_vote_state_update), + account_metas, + ) +} + +pub fn compact_update_vote_state_switch( + vote_pubkey: &Pubkey, + authorized_voter_pubkey: &Pubkey, + vote_state_update: CompactVoteStateUpdate, + proof_hash: Hash, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*vote_pubkey, false), + AccountMeta::new_readonly(*authorized_voter_pubkey, true), + ]; + + Instruction::new_with_bincode( + id(), + &VoteInstruction::CompactUpdateVoteStateSwitch(vote_state_update, proof_hash), + account_metas, + ) +} + pub fn withdraw( vote_pubkey: &Pubkey, authorized_withdrawer_pubkey: &Pubkey, diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs index dc7098e8f..dae06c4d0 100644 --- a/programs/vote/src/vote_processor.rs +++ b/programs/vote/src/vote_processor.rs @@ -4,7 +4,7 @@ use { crate::{ id, vote_instruction::VoteInstruction, - vote_state::{self, VoteAuthorize}, + vote_state::{self, VoteAuthorize, VoteStateUpdate}, }, log::*, solana_program_runtime::{ @@ -173,6 +173,31 @@ pub fn process_instruction( Err(InstructionError::InvalidInstructionData) } } + VoteInstruction::CompactUpdateVoteState(compact_vote_state_update) + | VoteInstruction::CompactUpdateVoteStateSwitch(compact_vote_state_update, _) => { + if invoke_context + .feature_set + .is_active(&feature_set::allow_votes_to_directly_update_vote_state::id()) + && invoke_context + .feature_set + .is_active(&feature_set::compact_vote_state_updates::id()) + { + let sysvar_cache = invoke_context.get_sysvar_cache(); + let slot_hashes = sysvar_cache.get_slot_hashes()?; + let clock = sysvar_cache.get_clock()?; + vote_state::process_vote_state_update( + &mut me, + slot_hashes.slot_hashes(), + &clock, + VoteStateUpdate::from(compact_vote_state_update), + &signers, + &invoke_context.feature_set, + ) + } else { + Err(InstructionError::InvalidInstructionData) + } + } + VoteInstruction::Withdraw(lamports) => { instruction_context.check_number_of_instruction_accounts(2)?; let rent_sysvar = invoke_context.get_sysvar_cache().get_rent()?; diff --git a/runtime/src/vote_parser.rs b/runtime/src/vote_parser.rs index 06c6abdbf..c67045b08 100644 --- a/runtime/src/vote_parser.rs +++ b/runtime/src/vote_parser.rs @@ -7,7 +7,7 @@ use { signature::Signature, transaction::{SanitizedTransaction, Transaction}, }, - solana_vote_program::vote_instruction::VoteInstruction, + solana_vote_program::{vote_instruction::VoteInstruction, vote_state::VoteStateUpdate}, }; pub type ParsedVote = (Pubkey, VoteTransaction, Option, Signature); @@ -29,6 +29,8 @@ pub(crate) fn is_simple_vote_transaction(transaction: &SanitizedTransaction) -> | VoteInstruction::VoteSwitch(_, _) | VoteInstruction::UpdateVoteState(_) | VoteInstruction::UpdateVoteStateSwitch(_, _) + | VoteInstruction::CompactUpdateVoteState(_) + | VoteInstruction::CompactUpdateVoteStateSwitch(..) ); } } @@ -80,6 +82,14 @@ fn parse_vote_instruction_data( VoteInstruction::UpdateVoteStateSwitch(vote_state_update, hash) => { Some((VoteTransaction::from(vote_state_update), Some(hash))) } + VoteInstruction::CompactUpdateVoteState(compact_vote_state_update) => Some(( + VoteTransaction::from(VoteStateUpdate::from(compact_vote_state_update)), + None, + )), + VoteInstruction::CompactUpdateVoteStateSwitch(compact_vote_state_update, hash) => Some(( + VoteTransaction::from(VoteStateUpdate::from(compact_vote_state_update)), + Some(hash), + )), VoteInstruction::Authorize(_, _) | VoteInstruction::AuthorizeChecked(_) | VoteInstruction::AuthorizeWithSeed(_) diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index 7be5d44bf..6708fcf41 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -476,6 +476,10 @@ pub mod use_default_units_in_fee_calculation { solana_sdk::declare_id!("8sKQrMQoUHtQSUP83SPG4ta2JDjSAiWs7t5aJ9uEd6To"); } +pub mod compact_vote_state_updates { + solana_sdk::declare_id!("86HpNqzutEZwLcPxS6EHDcMNYWk6ikhteg9un7Y2PBKE"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -589,6 +593,7 @@ lazy_static! { (cap_bpf_program_instruction_accounts::id(), "enforce max number of accounts per bpf program instruction #26628"), (loosen_cpi_size_restriction::id(), "loosen cpi size restrictions #26641"), (use_default_units_in_fee_calculation::id(), "use default units per instruction in fee calculation #26785"), + (compact_vote_state_updates::id(), "Compact vote state updates to lower block size"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/transaction-status/src/parse_vote.rs b/transaction-status/src/parse_vote.rs index 0ae00c06c..094b01ec0 100644 --- a/transaction-status/src/parse_vote.rs +++ b/transaction-status/src/parse_vote.rs @@ -5,7 +5,7 @@ use { bincode::deserialize, serde_json::json, solana_sdk::{instruction::CompiledInstruction, message::AccountKeys}, - solana_vote_program::vote_instruction::VoteInstruction, + solana_vote_program::{vote_instruction::VoteInstruction, vote_state::VoteStateUpdate}, }; pub fn parse_vote( @@ -101,7 +101,7 @@ pub fn parse_vote( }) } VoteInstruction::UpdateVoteState(vote_state_update) => { - check_num_vote_accounts(&instruction.accounts, 4)?; + check_num_vote_accounts(&instruction.accounts, 2)?; let vote_state_update = json!({ "lockouts": vote_state_update.lockouts, "root": vote_state_update.root, @@ -112,15 +112,13 @@ pub fn parse_vote( instruction_type: "updatevotestate".to_string(), info: json!({ "voteAccount": account_keys[instruction.accounts[0] as usize].to_string(), - "slotHashesSysvar": account_keys[instruction.accounts[1] as usize].to_string(), - "clockSysvar": account_keys[instruction.accounts[2] as usize].to_string(), - "voteAuthority": account_keys[instruction.accounts[3] as usize].to_string(), + "voteAuthority": account_keys[instruction.accounts[1] as usize].to_string(), "voteStateUpdate": vote_state_update, }), }) } VoteInstruction::UpdateVoteStateSwitch(vote_state_update, hash) => { - check_num_vote_accounts(&instruction.accounts, 4)?; + check_num_vote_accounts(&instruction.accounts, 2)?; let vote_state_update = json!({ "lockouts": vote_state_update.lockouts, "root": vote_state_update.root, @@ -131,9 +129,44 @@ pub fn parse_vote( instruction_type: "updatevotestateswitch".to_string(), info: json!({ "voteAccount": account_keys[instruction.accounts[0] as usize].to_string(), - "slotHashesSysvar": account_keys[instruction.accounts[1] as usize].to_string(), - "clockSysvar": account_keys[instruction.accounts[2] as usize].to_string(), - "voteAuthority": account_keys[instruction.accounts[3] as usize].to_string(), + "voteAuthority": account_keys[instruction.accounts[1] as usize].to_string(), + "voteStateUpdate": vote_state_update, + "hash": hash.to_string(), + }), + }) + } + VoteInstruction::CompactUpdateVoteState(compact_vote_state_update) => { + let vote_state_update = VoteStateUpdate::from(compact_vote_state_update); + check_num_vote_accounts(&instruction.accounts, 2)?; + let vote_state_update = json!({ + "lockouts": vote_state_update.lockouts, + "root": vote_state_update.root, + "hash": vote_state_update.hash.to_string(), + "timestamp": vote_state_update.timestamp, + }); + Ok(ParsedInstructionEnum { + instruction_type: "compactupdatevotestate".to_string(), + info: json!({ + "voteAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "voteAuthority": account_keys[instruction.accounts[1] as usize].to_string(), + "voteStateUpdate": vote_state_update, + }), + }) + } + VoteInstruction::CompactUpdateVoteStateSwitch(compact_vote_state_update, hash) => { + let vote_state_update = VoteStateUpdate::from(compact_vote_state_update); + check_num_vote_accounts(&instruction.accounts, 2)?; + let vote_state_update = json!({ + "lockouts": vote_state_update.lockouts, + "root": vote_state_update.root, + "hash": vote_state_update.hash.to_string(), + "timestamp": vote_state_update.timestamp, + }); + Ok(ParsedInstructionEnum { + instruction_type: "compactupdatevotestateswitch".to_string(), + info: json!({ + "voteAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "voteAuthority": account_keys[instruction.accounts[1] as usize].to_string(), "voteStateUpdate": vote_state_update, "hash": hash.to_string(), }), @@ -219,7 +252,7 @@ mod test { solana_sdk::{hash::Hash, message::Message, pubkey::Pubkey, sysvar}, solana_vote_program::{ vote_instruction, - vote_state::{Vote, VoteAuthorize, VoteInit}, + vote_state::{CompactVoteStateUpdate, Vote, VoteAuthorize, VoteInit}, }, }; @@ -642,4 +675,180 @@ mod test { message.instructions[0].accounts.pop(); assert!(parse_vote(&message.instructions[0], &AccountKeys::new(&keys, None)).is_err()); } + + #[test] + fn test_parse_vote_state_update_ix() { + let vote_state_update = VoteStateUpdate::from(vec![(0, 3), (1, 2), (2, 1)]); + + let vote_pubkey = Pubkey::new_unique(); + let authorized_voter_pubkey = Pubkey::new_unique(); + let instruction = vote_instruction::update_vote_state( + &vote_pubkey, + &authorized_voter_pubkey, + vote_state_update.clone(), + ); + let mut message = Message::new(&[instruction], None); + assert_eq!( + parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "updatevotestate".to_string(), + info: json!({ + "voteAccount": vote_pubkey.to_string(), + "voteAuthority": authorized_voter_pubkey.to_string(), + "voteStateUpdate": { + "lockouts": vote_state_update.lockouts, + "root": None::, + "hash": Hash::default().to_string(), + "timestamp": None::, + }, + }), + } + ); + assert!(parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..1], None) + ) + .is_err()); + let keys = message.account_keys.clone(); + message.instructions[0].accounts.pop(); + assert!(parse_vote(&message.instructions[0], &AccountKeys::new(&keys, None)).is_err()); + } + + #[test] + fn test_parse_vote_state_update_switch_ix() { + let vote_state_update = VoteStateUpdate::from(vec![(0, 3), (1, 2), (2, 1)]); + + let vote_pubkey = Pubkey::new_unique(); + let authorized_voter_pubkey = Pubkey::new_unique(); + let proof_hash = Hash::new_from_array([2; 32]); + let instruction = vote_instruction::update_vote_state_switch( + &vote_pubkey, + &authorized_voter_pubkey, + vote_state_update.clone(), + proof_hash, + ); + let mut message = Message::new(&[instruction], None); + assert_eq!( + parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "updatevotestateswitch".to_string(), + info: json!({ + "voteAccount": vote_pubkey.to_string(), + "voteAuthority": authorized_voter_pubkey.to_string(), + "voteStateUpdate": { + "lockouts": vote_state_update.lockouts, + "root": None::, + "hash": Hash::default().to_string(), + "timestamp": None::, + }, + "hash": proof_hash.to_string(), + }), + } + ); + assert!(parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..1], None) + ) + .is_err()); + let keys = message.account_keys.clone(); + message.instructions[0].accounts.pop(); + assert!(parse_vote(&message.instructions[0], &AccountKeys::new(&keys, None)).is_err()); + } + + #[test] + fn test_parse_compact_vote_state_update_ix() { + let vote_state_update = VoteStateUpdate::from(vec![(0, 3), (1, 2), (2, 1)]); + let compact_vote_state_update = CompactVoteStateUpdate::from(vote_state_update.clone()); + + let vote_pubkey = Pubkey::new_unique(); + let authorized_voter_pubkey = Pubkey::new_unique(); + let instruction = vote_instruction::compact_update_vote_state( + &vote_pubkey, + &authorized_voter_pubkey, + compact_vote_state_update, + ); + let mut message = Message::new(&[instruction], None); + assert_eq!( + parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "compactupdatevotestate".to_string(), + info: json!({ + "voteAccount": vote_pubkey.to_string(), + "voteAuthority": authorized_voter_pubkey.to_string(), + "voteStateUpdate": { + "lockouts": vote_state_update.lockouts, + "root": None::, + "hash": Hash::default().to_string(), + "timestamp": None::, + }, + }), + } + ); + assert!(parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..1], None) + ) + .is_err()); + let keys = message.account_keys.clone(); + message.instructions[0].accounts.pop(); + assert!(parse_vote(&message.instructions[0], &AccountKeys::new(&keys, None)).is_err()); + } + + #[test] + fn test_parse_compact_vote_state_update_switch_ix() { + let vote_state_update = VoteStateUpdate::from(vec![(0, 3), (1, 2), (2, 1)]); + let compact_vote_state_update = CompactVoteStateUpdate::from(vote_state_update.clone()); + + let vote_pubkey = Pubkey::new_unique(); + let authorized_voter_pubkey = Pubkey::new_unique(); + let proof_hash = Hash::new_from_array([2; 32]); + let instruction = vote_instruction::compact_update_vote_state_switch( + &vote_pubkey, + &authorized_voter_pubkey, + compact_vote_state_update, + proof_hash, + ); + let mut message = Message::new(&[instruction], None); + assert_eq!( + parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys, None) + ) + .unwrap(), + ParsedInstructionEnum { + instruction_type: "compactupdatevotestateswitch".to_string(), + info: json!({ + "voteAccount": vote_pubkey.to_string(), + "voteAuthority": authorized_voter_pubkey.to_string(), + "voteStateUpdate": { + "lockouts": vote_state_update.lockouts, + "root": None::, + "hash": Hash::default().to_string(), + "timestamp": None::, + }, + "hash": proof_hash.to_string(), + }), + } + ); + assert!(parse_vote( + &message.instructions[0], + &AccountKeys::new(&message.account_keys[0..1], None) + ) + .is_err()); + let keys = message.account_keys.clone(); + message.instructions[0].accounts.pop(); + assert!(parse_vote(&message.instructions[0], &AccountKeys::new(&keys, None)).is_err()); + } }