From f0acf7681e1a59b377f1b3a25eada2fced08be75 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Tue, 7 Dec 2021 16:47:26 -0800 Subject: [PATCH] Add vote instructions that directly update on chain vote state (#21531) * Add vote state instructions UpdateVoteState and UpdateVoteStateSwitch * cargo tree * extract vote state version conversion to common fn --- Cargo.lock | 65 +++++++ core/src/cluster_info_vote_listener.rs | 46 ++--- core/src/consensus.rs | 26 ++- core/src/verified_vote_packets.rs | 29 +-- gossip/src/cluster_info.rs | 2 +- gossip/src/crds_value.rs | 4 +- local-cluster/tests/local_cluster.rs | 16 +- programs/bpf/Cargo.lock | 65 +++++++ programs/vote/Cargo.toml | 1 + programs/vote/src/vote_instruction.rs | 105 ++++++++++- programs/vote/src/vote_state/mod.rs | 233 +++++++++++++++++++++++-- programs/vote/src/vote_transaction.rs | 33 +++- rpc/src/rpc_pubsub.rs | 2 +- rpc/src/rpc_subscriptions.rs | 13 +- runtime/src/bank.rs | 5 +- runtime/src/bank_utils.rs | 2 +- runtime/src/vote_sender_types.rs | 8 +- transaction-status/src/parse_vote.rs | 39 +++++ 18 files changed, 591 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf2aa374d..39e03d523 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -962,6 +962,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctor" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" +dependencies = [ + "quote 1.0.10", + "syn 1.0.81", +] + [[package]] name = "ctrlc" version = "3.2.1" @@ -1277,6 +1287,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "erased-serde" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3de9ad4541d99dc22b59134e7ff8dc3d6c988c89ecd7324bf10a8362b07a2afa" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.2.8" @@ -1640,6 +1659,17 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "ghost" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479" +dependencies = [ + "proc-macro2 1.0.32", + "quote 1.0.10", + "syn 1.0.81", +] + [[package]] name = "gimli" version = "0.25.0" @@ -2012,6 +2042,16 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "inventory" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1367fed6750ff2a5bcb967a631528303bb85631f167a75eb1bf7762d57eb7678" +dependencies = [ + "ctor", + "ghost", +] + [[package]] name = "iovec" version = "0.1.4" @@ -5998,6 +6038,7 @@ dependencies = [ "solana-program-runtime", "solana-sdk", "thiserror", + "typetag", ] [[package]] @@ -6935,6 +6976,30 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +[[package]] +name = "typetag" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4080564c5b2241b5bff53ab610082234e0c57b0417f4bd10596f183001505b8a" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e60147782cc30833c05fba3bab1d9b5771b2685a2557672ac96fa5d154099c0e" +dependencies = [ + "proc-macro2 1.0.32", + "quote 1.0.10", + "syn 1.0.81", +] + [[package]] name = "ucd-trie" version = "0.1.3" diff --git a/core/src/cluster_info_vote_listener.rs b/core/src/cluster_info_vote_listener.rs index d6c20973d..bae24e80d 100644 --- a/core/src/cluster_info_vote_listener.rs +++ b/core/src/cluster_info_vote_listener.rs @@ -33,7 +33,7 @@ use { bank_forks::BankForks, commitment::VOTE_THRESHOLD_SIZE, epoch_stakes::{EpochAuthorizedVoters, EpochStakes}, - vote_sender_types::{ReplayVoteReceiver, ReplayedVote}, + vote_sender_types::ReplayVoteReceiver, }, solana_sdk::{ clock::{Epoch, Slot, DEFAULT_MS_PER_SLOT, DEFAULT_TICKS_PER_SLOT}, @@ -44,7 +44,10 @@ use { slot_hashes, transaction::Transaction, }, - solana_vote_program::{self, vote_state::Vote, vote_transaction}, + solana_vote_program::{ + vote_state::VoteTransaction, + vote_transaction::{self, ParsedVote}, + }, std::{ collections::{HashMap, HashSet}, sync::{ @@ -403,7 +406,7 @@ impl ClusterInfoVoteListener { .filter_map(|(vote_tx, packet)| { let (vote, vote_account_key) = vote_transaction::parse_vote_transaction(&vote_tx) .and_then(|(vote_account_key, vote, _)| { - if vote.slots.is_empty() { + if vote.slots().is_empty() { None } else { Some((vote, vote_account_key)) @@ -674,7 +677,7 @@ impl ClusterInfoVoteListener { #[allow(clippy::too_many_arguments)] fn track_new_votes_and_notify_confirmations( - vote: Vote, + vote: Box, vote_pubkey: &Pubkey, vote_tracker: &VoteTracker, root_bank: &Bank, @@ -687,17 +690,17 @@ impl ClusterInfoVoteListener { bank_notification_sender: &Option, cluster_confirmed_slot_sender: &Option, ) { - if vote.slots.is_empty() { + if vote.is_empty() { return; } - let last_vote_slot = *vote.slots.last().unwrap(); - let last_vote_hash = vote.hash; + let (last_vote_slot, last_vote_hash) = vote.last_voted_slot_hash().unwrap(); let root = root_bank.slot(); let mut is_new_vote = false; + let vote_slots = vote.slots(); // If slot is before the root, ignore it - for slot in vote.slots.iter().filter(|slot| **slot > root).rev() { + for slot in vote_slots.iter().filter(|slot| **slot > root).rev() { let slot = *slot; // if we don't have stake information, ignore it @@ -781,28 +784,28 @@ impl ClusterInfoVoteListener { } if is_new_vote { - subscriptions.notify_vote(&vote); - let _ = verified_vote_sender.send((*vote_pubkey, vote.slots)); + subscriptions.notify_vote(vote); + let _ = verified_vote_sender.send((*vote_pubkey, vote_slots)); } } fn filter_gossip_votes( vote_tracker: &VoteTracker, vote_pubkey: &Pubkey, - vote: &Vote, + vote: &dyn VoteTransaction, gossip_tx: &Transaction, ) -> bool { - if vote.slots.is_empty() { + if vote.is_empty() { return false; } - let last_vote_slot = vote.slots.last().unwrap(); + let last_vote_slot = vote.last_voted_slot().unwrap(); // Votes from gossip need to be verified as they have not been // verified by the replay pipeline. Determine the authorized voter // based on the last vote slot. This will drop votes from authorized // voters trying to make votes for slots earlier than the epoch for // which they are authorized let actual_authorized_voter = - vote_tracker.get_authorized_voter(vote_pubkey, *last_vote_slot); + vote_tracker.get_authorized_voter(vote_pubkey, last_vote_slot); if actual_authorized_voter.is_none() { return false; @@ -822,7 +825,7 @@ impl ClusterInfoVoteListener { fn filter_and_confirm_with_new_votes( vote_tracker: &VoteTracker, gossip_vote_txs: Vec, - replayed_votes: Vec, + replayed_votes: Vec, root_bank: &Bank, subscriptions: &RpcSubscriptions, gossip_verified_vote_hash_sender: &GossipVerifiedVoteHashSender, @@ -839,7 +842,7 @@ impl ClusterInfoVoteListener { .filter_map(|gossip_tx| { vote_transaction::parse_vote_transaction(gossip_tx) .filter(|(vote_pubkey, vote, _)| { - Self::filter_gossip_votes(vote_tracker, vote_pubkey, vote, gossip_tx) + Self::filter_gossip_votes(vote_tracker, vote_pubkey, &**vote, gossip_tx) }) .map(|v| (true, v)) }) @@ -1243,7 +1246,7 @@ mod tests { replay_votes_sender .send(( vote_keypair.pubkey(), - replay_vote.clone(), + Box::new(replay_vote.clone()), switch_proof_hash, )) .unwrap(); @@ -1490,7 +1493,8 @@ mod tests { let (votes_sender, votes_receiver) = unbounded(); let (verified_vote_sender, _verified_vote_receiver) = unbounded(); let (gossip_verified_vote_hash_sender, _gossip_verified_vote_hash_receiver) = unbounded(); - let (replay_votes_sender, replay_votes_receiver) = unbounded(); + let (replay_votes_sender, replay_votes_receiver): (ReplayVoteSender, ReplayVoteReceiver) = + unbounded(); let vote_slot = 1; let vote_bank_hash = Hash::default(); @@ -1530,7 +1534,7 @@ mod tests { replay_votes_sender .send(( vote_keypair.pubkey(), - Vote::new(vec![vote_slot], Hash::default()), + Box::new(Vote::new(vec![vote_slot], Hash::default())), switch_proof_hash, )) .unwrap(); @@ -1676,7 +1680,7 @@ mod tests { // Add gossip vote for same slot, should not affect outcome vec![( validator0_keypairs.vote_keypair.pubkey(), - Vote::new(vec![voted_slot], Hash::default()), + Box::new(Vote::new(vec![voted_slot], Hash::default())), None, )], &bank, @@ -1732,7 +1736,7 @@ mod tests { vote_txs, vec![( validator_keypairs[1].vote_keypair.pubkey(), - Vote::new(vec![first_slot_in_new_epoch], Hash::default()), + Box::new(Vote::new(vec![first_slot_in_new_epoch], Hash::default())), None, )], &new_root_bank, diff --git a/core/src/consensus.rs b/core/src/consensus.rs index d08632674..dcb5bf274 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -21,7 +21,9 @@ use { }, solana_vote_program::{ vote_instruction, - vote_state::{BlockTimestamp, Lockout, Vote, VoteState, MAX_LOCKOUT_HISTORY}, + vote_state::{ + BlockTimestamp, Lockout, Vote, VoteState, VoteTransaction, MAX_LOCKOUT_HISTORY, + }, }, std::{ cmp::Ordering, @@ -367,22 +369,16 @@ impl Tower { ) -> Vote { let vote = Vote::new(vec![slot], hash); local_vote_state.process_vote_unchecked(&vote); - let slots = if let Some(last_voted_slot_in_bank) = last_voted_slot_in_bank { + let slots = if let Some(last_voted_slot) = last_voted_slot_in_bank { local_vote_state .votes .iter() .map(|v| v.slot) - .skip_while(|s| *s <= last_voted_slot_in_bank) + .skip_while(|s| *s <= last_voted_slot) .collect() } else { local_vote_state.votes.iter().map(|v| v.slot).collect() }; - trace!( - "new vote with {:?} {:?} {:?}", - last_voted_slot_in_bank, - slots, - local_vote_state.votes - ); Vote::new(slots, hash) } @@ -415,7 +411,7 @@ impl Tower { last_voted_slot_in_bank, ); - new_vote.timestamp = self.maybe_timestamp(self.last_vote.last_voted_slot().unwrap_or(0)); + new_vote.set_timestamp(self.maybe_timestamp(self.last_vote.last_voted_slot().unwrap_or(0))); self.last_vote = new_vote; let new_root = self.root(); @@ -2252,7 +2248,7 @@ pub mod test { let mut local = VoteState::default(); let vote = Tower::apply_vote_and_generate_vote_diff(&mut local, 0, Hash::default(), None); assert_eq!(local.votes.len(), 1); - assert_eq!(vote.slots, vec![0]); + assert_eq!(vote.slots(), vec![0]); assert_eq!(local.tower(), vec![0]); } @@ -2263,7 +2259,7 @@ pub mod test { // another vote for slot 0 should return an empty vote as the diff. let vote = Tower::apply_vote_and_generate_vote_diff(&mut local, 0, Hash::default(), Some(0)); - assert!(vote.slots.is_empty()); + assert!(vote.is_empty()); } #[test] @@ -2278,7 +2274,7 @@ pub mod test { assert_eq!(local.votes.len(), 1); let vote = Tower::apply_vote_and_generate_vote_diff(&mut local, 1, Hash::default(), Some(0)); - assert_eq!(vote.slots, vec![1]); + assert_eq!(vote.slots(), vec![1]); assert_eq!(local.tower(), vec![0, 1]); } @@ -2298,7 +2294,7 @@ pub mod test { // observable in any of the results. let vote = Tower::apply_vote_and_generate_vote_diff(&mut local, 3, Hash::default(), Some(0)); - assert_eq!(vote.slots, vec![3]); + assert_eq!(vote.slots(), vec![3]); assert_eq!(local.tower(), vec![3]); } @@ -2380,7 +2376,7 @@ pub mod test { tower.record_vote(i as u64, Hash::default()); } - expected.timestamp = tower.last_vote.timestamp; + expected.timestamp = tower.last_vote.timestamp(); assert_eq!(expected, tower.last_vote) } diff --git a/core/src/verified_vote_packets.rs b/core/src/verified_vote_packets.rs index 13ffe50ec..a50cf9033 100644 --- a/core/src/verified_vote_packets.rs +++ b/core/src/verified_vote_packets.rs @@ -7,7 +7,7 @@ use { account::from_account, clock::Slot, hash::Hash, pubkey::Pubkey, signature::Signature, slot_hashes::SlotHashes, sysvar, }, - solana_vote_program::vote_state::Vote, + solana_vote_program::vote_state::VoteTransaction, std::{ collections::{BTreeMap, HashMap, HashSet}, sync::Arc, @@ -19,7 +19,7 @@ const MAX_VOTES_PER_VALIDATOR: usize = 1000; pub struct VerifiedVoteMetadata { pub vote_account_key: Pubkey, - pub vote: Vote, + pub vote: Box, pub packet: Packets, pub signature: Signature, } @@ -153,15 +153,15 @@ impl VerifiedVotePackets { packet, signature, } = verfied_vote_metadata; - if vote.slots.is_empty() { + if vote.is_empty() { error!("Empty votes should have been filtered out earlier in the pipeline"); continue; } - let slot = vote.slots.last().unwrap(); - let hash = vote.hash; + let slot = vote.last_voted_slot().unwrap(); + let hash = vote.hash(); let validator_votes = self.0.entry(vote_account_key).or_default(); - validator_votes.insert((*slot, hash), (packet, signature)); + validator_votes.insert((slot, hash), (packet, signature)); if validator_votes.len() > MAX_VOTES_PER_VALIDATOR { let smallest_key = validator_votes.keys().next().cloned().unwrap(); @@ -182,6 +182,7 @@ mod tests { crossbeam_channel::unbounded, solana_perf::packet::Packet, solana_sdk::slot_hashes::MAX_ENTRIES, + solana_vote_program::vote_state::Vote, }; #[test] @@ -198,7 +199,7 @@ mod tests { let vote = Vote::new(vec![vote_slot], vote_hash); s.send(vec![VerifiedVoteMetadata { vote_account_key, - vote: vote.clone(), + vote: Box::new(vote.clone()), packet: Packets::default(), signature: Signature::new(&[1u8; 64]), }]) @@ -218,7 +219,7 @@ mod tests { // Same slot, same hash, should not be inserted s.send(vec![VerifiedVoteMetadata { vote_account_key, - vote, + vote: Box::new(vote), packet: Packets::default(), signature: Signature::new(&[1u8; 64]), }]) @@ -240,7 +241,7 @@ mod tests { let vote = Vote::new(vec![vote_slot], new_vote_hash); s.send(vec![VerifiedVoteMetadata { vote_account_key, - vote, + vote: Box::new(vote), packet: Packets::default(), signature: Signature::new(&[1u8; 64]), }]) @@ -263,7 +264,7 @@ mod tests { let vote = Vote::new(vec![vote_slot], vote_hash); s.send(vec![VerifiedVoteMetadata { vote_account_key, - vote, + vote: Box::new(vote), packet: Packets::default(), signature: Signature::new(&[2u8; 64]), }]) @@ -302,7 +303,7 @@ mod tests { let vote = Vote::new(vec![vote_slot], vote_hash); s.send(vec![VerifiedVoteMetadata { vote_account_key, - vote, + vote: Box::new(vote), packet: Packets::default(), signature: Signature::new(&[1u8; 64]), }]) @@ -339,7 +340,7 @@ mod tests { let vote = Vote::new(vec![vote_slot], vote_hash); s.send(vec![VerifiedVoteMetadata { vote_account_key, - vote, + vote: Box::new(vote), packet: Packets::default(), signature: Signature::new_unique(), }]) @@ -393,7 +394,7 @@ mod tests { let vote = Vote::new(vec![*vote_slot], *vote_hash); s.send(vec![VerifiedVoteMetadata { vote_account_key, - vote, + vote: Box::new(vote), packet: Packets::new(vec![Packet::default(); num_packets]), signature: Signature::new_unique(), }]) @@ -457,7 +458,7 @@ mod tests { my_leader_bank.slot() + 1, )); let vote_account_key = vote_simulator.vote_pubkeys[1]; - let vote = Vote::new(vec![vote_slot], vote_hash); + let vote = Box::new(Vote::new(vec![vote_slot], vote_hash)); s.send(vec![VerifiedVoteMetadata { vote_account_key, vote, diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index edf51db19..071d31779 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -1042,7 +1042,7 @@ impl ClusterInfo { "invalid vote index: {}, switch: {}, vote slots: {:?}, tower: {:?}", vote_index, hash.is_some(), - vote.slots, + vote.slots(), tower ); } diff --git a/gossip/src/crds_value.rs b/gossip/src/crds_value.rs index 310bdf268..50b3675b8 100644 --- a/gossip/src/crds_value.rs +++ b/gossip/src/crds_value.rs @@ -306,8 +306,8 @@ impl Sanitize for Vote { impl Vote { pub fn new(from: Pubkey, transaction: Transaction, wallclock: u64) -> Self { - let slot = parse_vote_transaction(&transaction) - .and_then(|(_, vote, _)| vote.slots.last().copied()); + let slot = + parse_vote_transaction(&transaction).and_then(|(_, vote, _)| vote.last_voted_slot()); Self { from, transaction, diff --git a/local-cluster/tests/local_cluster.rs b/local-cluster/tests/local_cluster.rs index 72a8b4d2c..86f012f40 100644 --- a/local-cluster/tests/local_cluster.rs +++ b/local-cluster/tests/local_cluster.rs @@ -2572,7 +2572,7 @@ fn test_duplicate_shreds_broadcast_leader() { .map(|(_, vote, _)| vote) .unwrap(); // Filter out empty votes - if !vote.slots.is_empty() { + if !vote.is_empty() { Some((vote, leader_vote_tx)) } else { None @@ -2584,14 +2584,16 @@ fn test_duplicate_shreds_broadcast_leader() { .collect(); parsed_vote_iter.sort_by(|(vote, _), (vote2, _)| { - vote.slots.last().unwrap().cmp(vote2.slots.last().unwrap()) + vote.last_voted_slot() + .unwrap() + .cmp(&vote2.last_voted_slot().unwrap()) }); for (parsed_vote, leader_vote_tx) in &parsed_vote_iter { - if let Some(latest_vote_slot) = parsed_vote.slots.last() { + if let Some(latest_vote_slot) = parsed_vote.last_voted_slot() { info!("received vote for {}", latest_vote_slot); // Add to EpochSlots. Mark all slots frozen between slot..=max_vote_slot. - if *latest_vote_slot > max_vote_slot { + if latest_vote_slot > max_vote_slot { let new_epoch_slots: Vec = (max_vote_slot + 1..latest_vote_slot + 1).collect(); info!( @@ -2599,13 +2601,13 @@ fn test_duplicate_shreds_broadcast_leader() { new_epoch_slots ); cluster_info.push_epoch_slots(&new_epoch_slots); - max_vote_slot = *latest_vote_slot; + max_vote_slot = latest_vote_slot; } // Only vote on even slots. Note this may violate lockouts if the // validator started voting on a different fork before we could exit // it above. - let vote_hash = parsed_vote.hash; + let vote_hash = parsed_vote.hash(); if latest_vote_slot % 2 == 0 { info!( "Simulating vote from our node on slot {}, hash {}", @@ -2618,7 +2620,7 @@ fn test_duplicate_shreds_broadcast_leader() { // by this validator so it's fine. let leader_blockstore = open_blockstore(&bad_leader_ledger_path); let mut vote_slots: Vec = AncestorIterator::new_inclusive( - *latest_vote_slot, + latest_vote_slot, &leader_blockstore, ) .take(MAX_LOCKOUT_HISTORY) diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index f9003e80e..023ee5314 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -562,6 +562,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ctor" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbaabec2c953050352311293be5c6aba8e141ba19d6811862b232d6fd020484" +dependencies = [ + "quote 1.0.6", + "syn 1.0.67", +] + [[package]] name = "curve25519-dalek" version = "2.1.0" @@ -825,6 +835,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "erased-serde" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3de9ad4541d99dc22b59134e7ff8dc3d6c988c89ecd7324bf10a8362b07a2afa" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.2.8" @@ -1062,6 +1081,17 @@ dependencies = [ "wasi 0.10.1+wasi-snapshot-preview1", ] +[[package]] +name = "ghost" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.6", + "syn 1.0.67", +] + [[package]] name = "gimli" version = "0.21.0" @@ -1309,6 +1339,16 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "inventory" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1367fed6750ff2a5bcb967a631528303bb85631f167a75eb1bf7762d57eb7678" +dependencies = [ + "ctor", + "ghost", +] + [[package]] name = "ipnet" version = "2.3.0" @@ -3476,6 +3516,7 @@ dependencies = [ "solana-program-runtime", "solana-sdk", "thiserror", + "typetag", ] [[package]] @@ -3949,6 +3990,30 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +[[package]] +name = "typetag" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4080564c5b2241b5bff53ab610082234e0c57b0417f4bd10596f183001505b8a" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e60147782cc30833c05fba3bab1d9b5771b2685a2557672ac96fa5d154099c0e" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.6", + "syn 1.0.67", +] + [[package]] name = "unicode-bidi" version = "0.3.4" diff --git a/programs/vote/Cargo.toml b/programs/vote/Cargo.toml index 06ef8ca19..d16724d85 100644 --- a/programs/vote/Cargo.toml +++ b/programs/vote/Cargo.toml @@ -22,6 +22,7 @@ solana-logger = { path = "../../logger", version = "=1.10.0" } solana-metrics = { path = "../../metrics", version = "=1.10.0" } solana-program-runtime = { path = "../../program-runtime", version = "=1.10.0" } solana-sdk = { path = "../../sdk", version = "=1.10.0" } +typetag = "0.1" thiserror = "1.0" [build-dependencies] diff --git a/programs/vote/src/vote_instruction.rs b/programs/vote/src/vote_instruction.rs index f64c94d22..9b4c46dd5 100644 --- a/programs/vote/src/vote_instruction.rs +++ b/programs/vote/src/vote_instruction.rs @@ -4,7 +4,7 @@ use { crate::{ id, - vote_state::{self, Vote, VoteAuthorize, VoteInit, VoteState}, + vote_state::{self, Vote, VoteAuthorize, VoteInit, VoteState, VoteStateUpdate}, }, log::*, num_derive::{FromPrimitive, ToPrimitive}, @@ -156,6 +156,24 @@ pub enum VoteInstruction { /// 2. `[SIGNER]` Vote or withdraw authority /// 3. `[SIGNER]` New vote or withdraw authority AuthorizeChecked(VoteAuthorize), + + /// Update the onchain vote state for the signer. + /// + /// # Account references + /// 0. `[Write]` Vote account to vote with + /// 1. `[]` Slot hashes sysvar + /// 2. `[]` Clock sysvar + /// 3. `[SIGNER]` Vote authority + UpdateVoteState(VoteStateUpdate), + + /// Update the onchain vote state for the signer along with a switching proof. + /// + /// # Account references + /// 0. `[Write]` Vote account to vote with + /// 1. `[]` Slot hashes sysvar + /// 2. `[]` Clock sysvar + /// 3. `[SIGNER]` Vote authority + UpdateVoteStateSwitch(VoteStateUpdate, Hash), } fn initialize_account(vote_pubkey: &Pubkey, vote_init: &VoteInit) -> Instruction { @@ -313,6 +331,45 @@ pub fn vote_switch( ) } +pub fn update_vote_state( + vote_pubkey: &Pubkey, + authorized_voter_pubkey: &Pubkey, + vote_state_update: VoteStateUpdate, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*vote_pubkey, false), + AccountMeta::new_readonly(sysvar::slot_hashes::id(), false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*authorized_voter_pubkey, true), + ]; + + Instruction::new_with_bincode( + id(), + &VoteInstruction::UpdateVoteState(vote_state_update), + account_metas, + ) +} + +pub fn update_vote_state_switch( + vote_pubkey: &Pubkey, + authorized_voter_pubkey: &Pubkey, + vote_state_update: VoteStateUpdate, + proof_hash: Hash, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*vote_pubkey, false), + AccountMeta::new_readonly(sysvar::slot_hashes::id(), false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*authorized_voter_pubkey, true), + ]; + + Instruction::new_with_bincode( + id(), + &VoteInstruction::UpdateVoteStateSwitch(vote_state_update, proof_hash), + account_metas, + ) +} + pub fn withdraw( vote_pubkey: &Pubkey, authorized_withdrawer_pubkey: &Pubkey, @@ -406,6 +463,23 @@ pub fn process_instruction( &signers, ) } + VoteInstruction::UpdateVoteState(vote_state_update) + | VoteInstruction::UpdateVoteStateSwitch(vote_state_update, _) => { + inc_new_counter_info!("vote-state-native", 1); + vote_state::process_vote_state_update( + me, + &from_keyed_account::(keyed_account_at_index( + keyed_accounts, + first_instruction_account + 1, + )?)?, + &from_keyed_account::(keyed_account_at_index( + keyed_accounts, + first_instruction_account + 2, + )?)?, + &vote_state_update, + &signers, + ) + } VoteInstruction::Withdraw(lamports) => { let to = keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?; let rent_sysvar = if invoke_context @@ -544,6 +618,14 @@ mod tests { )), Err(InstructionError::InvalidAccountOwner), ); + assert_eq!( + process_instruction_as_one_arg(&update_vote_state( + &invalid_vote_state_pubkey(), + &Pubkey::default(), + VoteStateUpdate::default(), + )), + Err(InstructionError::InvalidAccountOwner), + ); } #[test] @@ -553,7 +635,7 @@ mod tests { &Pubkey::new_unique(), &Pubkey::new_unique(), &VoteInit::default(), - 100, + 101, ); assert_eq!( process_instruction_as_one_arg(&instructions[1]), @@ -585,6 +667,25 @@ mod tests { )), Err(InstructionError::InvalidAccountData), ); + assert_eq!( + process_instruction_as_one_arg(&update_vote_state( + &Pubkey::default(), + &Pubkey::default(), + VoteStateUpdate::default(), + )), + Err(InstructionError::InvalidAccountData), + ); + + assert_eq!( + process_instruction_as_one_arg(&update_vote_state_switch( + &Pubkey::default(), + &Pubkey::default(), + VoteStateUpdate::default(), + Hash::default(), + )), + Err(InstructionError::InvalidAccountData), + ); + assert_eq!( process_instruction_as_one_arg(&update_validator_identity( &Pubkey::new_unique(), diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index 7e1be355c..eab08f0ac 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -19,9 +19,11 @@ use { sysvar::clock::Clock, }, std::{ + any::Any, boxed::Box, cmp::Ordering, collections::{HashSet, VecDeque}, + fmt::Debug, }, }; @@ -36,9 +38,69 @@ pub const INITIAL_LOCKOUT: usize = 2; // Maximum number of credits history to keep around pub const MAX_EPOCH_CREDITS_HISTORY: usize = 64; -// Offset of VoteState::prior_voters, for determining initialization status without deserialization +// Offset of VoteState::pri : Clone + Debug {or_voters, for determining initialization status without deserialization const DEFAULT_PRIOR_VOTERS_OFFSET: usize = 82; +// VoteTransactionClone hack is done so that we can derive clone on the tower that uses the +// VoteTransaction trait object. Clone doesn't work here because it returns Self which is not +// allowed for trait objects +#[typetag::serde{tag = "type"}] +pub trait VoteTransaction: VoteTransactionClone + Debug + Send { + fn slot(&self, i: usize) -> Slot; + fn len(&self) -> usize; + fn hash(&self) -> Hash; + fn timestamp(&self) -> Option; + fn last_voted_slot(&self) -> Option; + fn last_voted_slot_hash(&self) -> Option<(Slot, Hash)>; + fn set_timestamp(&mut self, ts: Option); + + fn slots(&self) -> Vec { + (0..self.len()).map(|i| self.slot(i)).collect() + } + + fn is_empty(&self) -> bool { + self.len() == 0 + } + + // Have to manually implement because deriving PartialEq returns Self + fn eq(&self, other: &dyn VoteTransaction) -> bool; + fn as_any(&self) -> &dyn Any; +} + +pub trait VoteTransactionClone { + fn clone_box(&self) -> Box; +} + +impl VoteTransactionClone for T +where + T: VoteTransaction + Clone + 'static, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.clone_box() + } +} + +// Have to manually implement because derive returns Self +impl<'a, 'b> PartialEq for dyn VoteTransaction + 'a { + fn eq(&self, other: &(dyn VoteTransaction + 'b)) -> bool { + VoteTransaction::eq(self, other) + } +} + +// This is needed because of weirdness in the derive PartialEq macro +// See rust issue #31740 for more info +impl PartialEq<&Self> for Box { + fn eq(&self, other: &&Self) -> bool { + VoteTransaction::eq(self.as_ref(), other.as_ref()) + } +} + #[frozen_abi(digest = "Ch2vVEwos2EjAVqSHCyJjnN2MNX1yrpapZTGhMSCjWUH")] #[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] pub struct Vote { @@ -58,17 +120,51 @@ impl Vote { timestamp: None, } } +} - pub fn last_voted_slot(&self) -> Option { +#[typetag::serde] +impl VoteTransaction for Vote { + fn slot(&self, i: usize) -> Slot { + self.slots[i] + } + + fn len(&self) -> usize { + self.slots.len() + } + + fn hash(&self) -> Hash { + self.hash + } + + fn timestamp(&self) -> Option { + self.timestamp + } + + fn last_voted_slot(&self) -> Option { self.slots.last().copied() } - pub fn last_voted_slot_hash(&self) -> Option<(Slot, Hash)> { + fn last_voted_slot_hash(&self) -> Option<(Slot, Hash)> { self.slots.last().copied().map(|slot| (slot, self.hash)) } + + fn set_timestamp(&mut self, ts: Option) { + self.timestamp = ts + } + + fn eq(&self, other: &dyn VoteTransaction) -> bool { + other + .as_any() + .downcast_ref::() + .map_or(false, |x| x == self) + } + + fn as_any(&self) -> &dyn Any { + self + } } -#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] +#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Copy, Clone, AbiExample)] pub struct Lockout { pub slot: Slot, pub confirmation_count: u32, @@ -99,6 +195,74 @@ impl Lockout { } } +#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] +pub struct VoteStateUpdate { + /// The proposed tower + pub lockouts: VecDeque, + /// The proposed root + pub root: Option, + /// signature of the bank's state at the last slot + pub hash: Hash, + /// processing timestamp of last slot + pub timestamp: Option, +} + +impl VoteStateUpdate { + pub fn new(lockouts: VecDeque, root: Option, hash: Hash) -> Self { + Self { + lockouts, + root, + hash, + timestamp: None, + } + } +} + +#[typetag::serde] +impl VoteTransaction for VoteStateUpdate { + fn slot(&self, i: usize) -> Slot { + self.lockouts[i].slot + } + + fn len(&self) -> usize { + self.lockouts.len() + } + + fn hash(&self) -> Hash { + self.hash + } + + fn timestamp(&self) -> Option { + self.timestamp + } + + fn last_voted_slot(&self) -> Option { + self.lockouts.back().copied().map(|lockout| lockout.slot) + } + + fn last_voted_slot_hash(&self) -> Option<(Slot, Hash)> { + self.lockouts + .back() + .copied() + .map(|lockout| (lockout.slot, self.hash)) + } + + fn set_timestamp(&mut self, ts: Option) { + self.timestamp = ts + } + + fn eq(&self, other: &dyn VoteTransaction) -> bool { + other + .as_any() + .downcast_ref::() + .map_or(false, |x| x == self) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + #[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub struct VoteInit { pub node_pubkey: Pubkey, @@ -301,7 +465,7 @@ impl VoteState { fn check_slots_are_valid( &self, - vote: &Vote, + vote: &(impl VoteTransaction + Debug), slot_hashes: &[(Slot, Hash)], ) -> Result<(), VoteError> { // index into the vote's slots, sarting at the newest @@ -320,19 +484,19 @@ impl VoteState { // // 2) Conversely, `slot_hashes` is sorted from newest/largest vote to // the oldest/smallest vote - while i < vote.slots.len() && j > 0 { + while i < vote.len() && j > 0 { // 1) increment `i` to find the smallest slot `s` in `vote.slots` // where `s` >= `last_voted_slot` if self .last_voted_slot() - .map_or(false, |last_voted_slot| vote.slots[i] <= last_voted_slot) + .map_or(false, |last_voted_slot| vote.slot(i) <= last_voted_slot) { i += 1; continue; } // 2) Find the hash for this slot `s`. - if vote.slots[i] != slot_hashes[j - 1].0 { + if vote.slot(i) != slot_hashes[j - 1].0 { // Decrement `j` to find newer slots j -= 1; continue; @@ -354,7 +518,7 @@ impl VoteState { ); return Err(VoteError::VoteTooOld); } - if i != vote.slots.len() { + if i != vote.len() { // This means there existed some slot for which we couldn't find // a matching slot hash in step 2) info!( @@ -364,13 +528,16 @@ impl VoteState { inc_new_counter_info!("dropped-vote-slot", 1); return Err(VoteError::SlotsMismatch); } - if slot_hashes[j].1 != vote.hash { + if slot_hashes[j].1 != vote.hash() { // This means the newest vote in the slot has a match that // doesn't match the expected hash for that slot on this // fork warn!( "{} dropped vote {:?} failed to match hash {} {}", - self.node_pubkey, vote, vote.hash, slot_hashes[j].1 + self.node_pubkey, + vote, + vote.hash(), + slot_hashes[j].1 ); inc_new_counter_info!("dropped-vote-hash", 1); return Err(VoteError::SlotHashMismatch); @@ -947,13 +1114,11 @@ pub fn initialize_account( ))) } -pub fn process_vote( +fn verify_and_get_vote_state( vote_account: &KeyedAccount, - slot_hashes: &[SlotHash], clock: &Clock, - vote: &Vote, signers: &HashSet, -) -> Result<(), InstructionError> { +) -> Result { let versioned = State::::state(vote_account)?; if versioned.is_uninitialized() { @@ -964,6 +1129,18 @@ pub fn process_vote( let authorized_voter = vote_state.get_and_update_authorized_voter(clock.epoch)?; verify_authorized_signer(&authorized_voter, signers)?; + Ok(vote_state) +} + +pub fn process_vote( + vote_account: &KeyedAccount, + slot_hashes: &[SlotHash], + clock: &Clock, + vote: &Vote, + signers: &HashSet, +) -> Result<(), InstructionError> { + let mut vote_state = verify_and_get_vote_state(vote_account, clock, signers)?; + vote_state.process_vote(vote, slot_hashes, clock.epoch)?; if let Some(timestamp) = vote.timestamp { vote.slots @@ -975,6 +1152,25 @@ pub fn process_vote( vote_account.set_state(&VoteStateVersions::new_current(vote_state)) } +pub fn process_vote_state_update( + vote_account: &KeyedAccount, + slot_hashes: &[SlotHash], + clock: &Clock, + vote_state_update: &VoteStateUpdate, + signers: &HashSet, +) -> Result<(), InstructionError> { + let mut vote_state = verify_and_get_vote_state(vote_account, clock, signers)?; + + vote_state.check_slots_are_valid(vote_state_update, slot_hashes)?; + vote_state.process_new_vote_state( + vote_state_update.lockouts.clone(), + vote_state_update.root, + vote_state_update.timestamp, + clock.epoch, + )?; + vote_account.set_state(&VoteStateVersions::new_current(vote_state)) +} + pub fn create_account_with_authorized( node_pubkey: &Pubkey, authorized_voter: &Pubkey, @@ -1194,13 +1390,12 @@ mod tests { vote_state .votes .resize(MAX_LOCKOUT_HISTORY, Lockout::default()); + vote_state.root_slot = Some(1); let versioned = VoteStateVersions::new_current(vote_state); assert!(VoteState::serialize(&versioned, &mut buffer[0..4]).is_err()); VoteState::serialize(&versioned, &mut buffer).unwrap(); - assert_eq!( - VoteStateVersions::new_current(VoteState::deserialize(&buffer).unwrap()), - versioned - ); + let des = VoteState::deserialize(&buffer).unwrap(); + assert_eq!(des, versioned.convert_to_current(),); } #[test] diff --git a/programs/vote/src/vote_transaction.rs b/programs/vote/src/vote_transaction.rs index 8b5198cb1..c421fb5f4 100644 --- a/programs/vote/src/vote_transaction.rs +++ b/programs/vote/src/vote_transaction.rs @@ -1,7 +1,7 @@ use { crate::{ vote_instruction::{self, VoteInstruction}, - vote_state::Vote, + vote_state::{Vote, VoteTransaction}, }, solana_sdk::{ clock::Slot, @@ -14,14 +14,30 @@ use { }, }; -pub type ParsedVote = (Pubkey, Vote, Option); +pub type ParsedVote = (Pubkey, Box, Option); fn parse_vote(vote_ix: &CompiledInstruction, vote_key: &Pubkey) -> Option { let vote_instruction = limited_deserialize(&vote_ix.data).ok(); - vote_instruction.and_then(|vote_instruction| match vote_instruction { - VoteInstruction::Vote(vote) => Some((*vote_key, vote, None)), - VoteInstruction::VoteSwitch(vote, hash) => Some((*vote_key, vote, Some(hash))), - _ => None, + vote_instruction.and_then(|vote_instruction| { + let result: Option = match vote_instruction { + VoteInstruction::Vote(vote) => Some((*vote_key, Box::new(vote), None)), + VoteInstruction::VoteSwitch(vote, hash) => { + Some((*vote_key, Box::new(vote), Some(hash))) + } + VoteInstruction::UpdateVoteState(vote_state_update) => { + Some((*vote_key, Box::new(vote_state_update), None)) + } + VoteInstruction::UpdateVoteStateSwitch(vote_state_update, hash) => { + Some((*vote_key, Box::new(vote_state_update), Some(hash))) + } + VoteInstruction::Authorize(_, _) + | VoteInstruction::AuthorizeChecked(_) + | VoteInstruction::InitializeAccount(_) + | VoteInstruction::UpdateCommission(_) + | VoteInstruction::UpdateValidatorIdentity + | VoteInstruction::Withdraw(_) => None, + }; + result }) } @@ -123,7 +139,10 @@ mod test { ); let (key, vote, hash) = parse_vote_transaction(&vote_tx).unwrap(); assert_eq!(hash, input_hash); - assert_eq!(vote, Vote::new(vec![42], bank_hash)); + assert_eq!( + *vote.as_any().downcast_ref::().unwrap(), + Vote::new(vec![42], bank_hash) + ); assert_eq!(key, vote_keypair.pubkey()); // Test bad program id fails diff --git a/rpc/src/rpc_pubsub.rs b/rpc/src/rpc_pubsub.rs index 4075c6aac..bea5a00e4 100644 --- a/rpc/src/rpc_pubsub.rs +++ b/rpc/src/rpc_pubsub.rs @@ -1213,7 +1213,7 @@ mod tests { hash: Hash::default(), timestamp: None, }; - subscriptions.notify_vote(&vote); + subscriptions.notify_vote(Box::new(vote)); let response = receiver.recv(); assert_eq!( diff --git a/rpc/src/rpc_subscriptions.rs b/rpc/src/rpc_subscriptions.rs index 352566678..c10df6116 100644 --- a/rpc/src/rpc_subscriptions.rs +++ b/rpc/src/rpc_subscriptions.rs @@ -37,7 +37,7 @@ use { timing::timestamp, transaction, }, - solana_vote_program::vote_state::Vote, + solana_vote_program::vote_state::VoteTransaction, std::{ cell::RefCell, collections::{HashMap, VecDeque}, @@ -90,7 +90,7 @@ impl From for TimestampedNotificationEntry { pub enum NotificationEntry { Slot(SlotInfo), SlotUpdate(SlotUpdate), - Vote(Vote), + Vote(Box), Root(Slot), Bank(CommitmentSlots), Gossip(Slot), @@ -612,7 +612,7 @@ impl RpcSubscriptions { self.enqueue_notification(NotificationEntry::SignaturesReceived(slot_signatures)); } - pub fn notify_vote(&self, vote: &Vote) { + pub fn notify_vote(&self, vote: Box) { self.enqueue_notification(NotificationEntry::Vote(vote.clone())); } @@ -695,10 +695,9 @@ impl RpcSubscriptions { // in VoteState's from bank states built in ReplayStage. NotificationEntry::Vote(ref vote_info) => { let rpc_vote = RpcVote { - // TODO: Remove clones - slots: vote_info.slots.clone(), - hash: bs58::encode(vote_info.hash).into_string(), - timestamp: vote_info.timestamp, + slots: vote_info.slots(), + hash: bs58::encode(vote_info.hash()).into_string(), + timestamp: vote_info.timestamp(), }; if let Some(sub) = subscriptions .node_progress_watchers() diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index a72bdad52..22ec96492 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -6146,7 +6146,10 @@ pub fn is_simple_vote_transaction(transaction: &SanitizedTransaction) -> bool { { return matches!( vote_instruction, - VoteInstruction::Vote(_) | VoteInstruction::VoteSwitch(_, _) + VoteInstruction::Vote(_) + | VoteInstruction::VoteSwitch(_, _) + | VoteInstruction::UpdateVoteState(_) + | VoteInstruction::UpdateVoteStateSwitch(_, _) ); } } diff --git a/runtime/src/bank_utils.rs b/runtime/src/bank_utils.rs index c1021030d..ef8f467a1 100644 --- a/runtime/src/bank_utils.rs +++ b/runtime/src/bank_utils.rs @@ -48,7 +48,7 @@ pub fn find_and_send_votes( if let Some(parsed_vote) = vote_transaction::parse_sanitized_vote_transaction(tx) { - if parsed_vote.1.slots.last().is_some() { + if parsed_vote.1.last_voted_slot().is_some() { let _ = vote_sender.send(parsed_vote); } } diff --git a/runtime/src/vote_sender_types.rs b/runtime/src/vote_sender_types.rs index ef7f245cd..339ecc573 100644 --- a/runtime/src/vote_sender_types.rs +++ b/runtime/src/vote_sender_types.rs @@ -1,9 +1,7 @@ use { crossbeam_channel::{Receiver, Sender}, - solana_sdk::{hash::Hash, pubkey::Pubkey}, - solana_vote_program::vote_state::Vote, + solana_vote_program::vote_transaction::ParsedVote, }; -pub type ReplayedVote = (Pubkey, Vote, Option); -pub type ReplayVoteSender = Sender; -pub type ReplayVoteReceiver = Receiver; +pub type ReplayVoteSender = Sender; +pub type ReplayVoteReceiver = Receiver; diff --git a/transaction-status/src/parse_vote.rs b/transaction-status/src/parse_vote.rs index a7d3f3161..7f1ea4ade 100644 --- a/transaction-status/src/parse_vote.rs +++ b/transaction-status/src/parse_vote.rs @@ -70,6 +70,45 @@ pub fn parse_vote( }), }) } + VoteInstruction::UpdateVoteState(vote_state_update) => { + check_num_vote_accounts(&instruction.accounts, 4)?; + 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: "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(), + "voteStateUpdate": vote_state_update, + }), + }) + } + VoteInstruction::UpdateVoteStateSwitch(vote_state_update, hash) => { + check_num_vote_accounts(&instruction.accounts, 4)?; + 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: "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(), + "voteStateUpdate": vote_state_update, + "hash": hash.to_string(), + }), + }) + } VoteInstruction::Withdraw(lamports) => { check_num_vote_accounts(&instruction.accounts, 3)?; Ok(ParsedInstructionEnum {