diff --git a/core/src/consensus.rs b/core/src/consensus.rs index d4f2345aa1..b9a6516032 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -97,6 +97,9 @@ impl SwitchForkDecision { v, )) } + (SwitchForkDecision::SameFork, VoteTransaction::TowerSync(t)) => Some( + vote_instruction::tower_sync(vote_account_pubkey, authorized_voter_pubkey, t), + ), (SwitchForkDecision::SwitchProof(switch_proof_hash), VoteTransaction::Vote(v)) => { Some(vote_instruction::vote_switch( vote_account_pubkey, @@ -114,6 +117,14 @@ impl SwitchForkDecision { v, *switch_proof_hash, )), + (SwitchForkDecision::SwitchProof(switch_proof_hash), VoteTransaction::TowerSync(t)) => { + Some(vote_instruction::tower_sync_switch( + vote_account_pubkey, + authorized_voter_pubkey, + t, + *switch_proof_hash, + )) + } (SwitchForkDecision::SameFork, VoteTransaction::CompactVoteStateUpdate(v)) => { Some(vote_instruction::compact_update_vote_state( vote_account_pubkey, @@ -221,7 +232,7 @@ pub(crate) enum BlockhashStatus { Blockhash(Hash), } -#[frozen_abi(digest = "iZi6s9BvytU3HbRsibrAD71jwMLvrqHdCjVk6qKcVvd")] +#[frozen_abi(digest = "679XkZ4upGc389SwqAsjs5tr2qB4wisqjbwtei7fGhxC")] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, AbiExample)] pub struct Tower { pub node_pubkey: Pubkey, diff --git a/core/src/consensus/tower1_14_11.rs b/core/src/consensus/tower1_14_11.rs index 22c396e097..fe9a5b40b2 100644 --- a/core/src/consensus/tower1_14_11.rs +++ b/core/src/consensus/tower1_14_11.rs @@ -6,7 +6,7 @@ use { }, }; -#[frozen_abi(digest = "F83xHQA1wxoFDy25MTKXXmFXTc9Jbp6SXRXEPcehtKbQ")] +#[frozen_abi(digest = "4LayQwoKrE2jPhbNtg3TSpKrtEtjcPiwsVPJN7aCavri")] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, AbiExample)] pub struct Tower1_14_11 { pub(crate) node_pubkey: Pubkey, diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs index 443aeb391b..72309a26ca 100644 --- a/programs/vote/src/vote_processor.rs +++ b/programs/vote/src/vote_processor.rs @@ -9,6 +9,7 @@ use { sysvar_cache::get_sysvar_with_account_check, }, solana_sdk::{ + feature_set, instruction::InstructionError, program_utils::limited_deserialize, pubkey::Pubkey, @@ -192,7 +193,17 @@ declare_process_instruction!(Entrypoint, DEFAULT_COMPUTE_UNITS, |invoke_context| &invoke_context.feature_set, ) } - + VoteInstruction::TowerSync(_tower_sync) + | VoteInstruction::TowerSyncSwitch(_tower_sync, _) => { + if !invoke_context + .feature_set + .is_active(&feature_set::enable_tower_sync_ix::id()) + { + return Err(InstructionError::InvalidInstructionData); + } + // TODO: will fill in future PR + return 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/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index f5901374d9..b600ed5528 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -28,13 +28,15 @@ use { }, }; -#[frozen_abi(digest = "2AuJFjx7SYrJ2ugCfH1jFh3Lr9UHMEPfKwwk1NcjqND1")] +#[frozen_abi(digest = "EcS3xgfomytEAQ1eVd8R76ZejwyHp2Ed8dHqQWh6zi5v")] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, AbiEnumVisitor, AbiExample)] pub enum VoteTransaction { Vote(Vote), VoteStateUpdate(VoteStateUpdate), #[serde(with = "serde_compact_vote_state_update")] CompactVoteStateUpdate(VoteStateUpdate), + #[serde(with = "serde_tower_sync")] + TowerSync(TowerSync), } impl VoteTransaction { @@ -43,6 +45,7 @@ impl VoteTransaction { VoteTransaction::Vote(vote) => vote.slots.clone(), VoteTransaction::VoteStateUpdate(vote_state_update) => vote_state_update.slots(), VoteTransaction::CompactVoteStateUpdate(vote_state_update) => vote_state_update.slots(), + VoteTransaction::TowerSync(tower_sync) => tower_sync.slots(), } } @@ -53,6 +56,7 @@ impl VoteTransaction { | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { vote_state_update.lockouts[i].slot() } + VoteTransaction::TowerSync(tower_sync) => tower_sync.lockouts[i].slot(), } } @@ -63,6 +67,7 @@ impl VoteTransaction { | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { vote_state_update.lockouts.len() } + VoteTransaction::TowerSync(tower_sync) => tower_sync.lockouts.len(), } } @@ -73,6 +78,7 @@ impl VoteTransaction { | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { vote_state_update.lockouts.is_empty() } + VoteTransaction::TowerSync(tower_sync) => tower_sync.lockouts.is_empty(), } } @@ -81,6 +87,7 @@ impl VoteTransaction { VoteTransaction::Vote(vote) => vote.hash, VoteTransaction::VoteStateUpdate(vote_state_update) => vote_state_update.hash, VoteTransaction::CompactVoteStateUpdate(vote_state_update) => vote_state_update.hash, + VoteTransaction::TowerSync(tower_sync) => tower_sync.hash, } } @@ -91,6 +98,7 @@ impl VoteTransaction { | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { vote_state_update.timestamp } + VoteTransaction::TowerSync(tower_sync) => tower_sync.timestamp, } } @@ -101,6 +109,7 @@ impl VoteTransaction { | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { vote_state_update.timestamp = ts } + VoteTransaction::TowerSync(tower_sync) => tower_sync.timestamp = ts, } } @@ -111,6 +120,7 @@ impl VoteTransaction { | VoteTransaction::CompactVoteStateUpdate(vote_state_update) => { vote_state_update.last_voted_slot() } + VoteTransaction::TowerSync(tower_sync) => tower_sync.last_voted_slot(), } } @@ -131,6 +141,12 @@ impl From for VoteTransaction { } } +impl From for VoteTransaction { + fn from(tower_sync: TowerSync) -> Self { + VoteTransaction::TowerSync(tower_sync) + } +} + // utility function, used by Stakes, tests pub fn from(account: &T) -> Option { VoteState::deserialize(account.data()).ok() diff --git a/sdk/program/src/vote/instruction.rs b/sdk/program/src/vote/instruction.rs index 568472ed53..2c4cb4157f 100644 --- a/sdk/program/src/vote/instruction.rs +++ b/sdk/program/src/vote/instruction.rs @@ -1,6 +1,7 @@ //! Vote program instructions use { + super::state::TowerSync, crate::{ clock::{Slot, UnixTimestamp}, hash::Hash, @@ -10,7 +11,7 @@ use { vote::{ program::id, state::{ - serde_compact_vote_state_update, Vote, VoteAuthorize, + serde_compact_vote_state_update, serde_tower_sync, Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, VoteAuthorizeWithSeedArgs, VoteInit, VoteStateUpdate, VoteStateVersions, }, @@ -146,6 +147,21 @@ pub enum VoteInstruction { #[serde(with = "serde_compact_vote_state_update")] VoteStateUpdate, Hash, ), + + /// Sync the onchain vote state with local tower + /// + /// # Account references + /// 0. `[Write]` Vote account to vote with + /// 1. `[SIGNER]` Vote authority + #[serde(with = "serde_tower_sync")] + TowerSync(TowerSync), + + /// Sync the onchain vote state with local tower along with a switching proof + /// + /// # Account references + /// 0. `[Write]` Vote account to vote with + /// 1. `[SIGNER]` Vote authority + TowerSyncSwitch(#[serde(with = "serde_tower_sync")] TowerSync, Hash), } impl VoteInstruction { @@ -157,7 +173,9 @@ impl VoteInstruction { | Self::UpdateVoteState(_) | Self::UpdateVoteStateSwitch(_, _) | Self::CompactUpdateVoteState(_) - | Self::CompactUpdateVoteStateSwitch(_, _), + | Self::CompactUpdateVoteStateSwitch(_, _) + | Self::TowerSync(_) + | Self::TowerSyncSwitch(_, _), ) } @@ -167,7 +185,9 @@ impl VoteInstruction { Self::UpdateVoteState(_) | Self::UpdateVoteStateSwitch(_, _) | Self::CompactUpdateVoteState(_) - | Self::CompactUpdateVoteStateSwitch(_, _), + | Self::CompactUpdateVoteStateSwitch(_, _) + | Self::TowerSync(_) + | Self::TowerSyncSwitch(_, _), ) } @@ -182,6 +202,9 @@ impl VoteInstruction { | Self::CompactUpdateVoteStateSwitch(vote_state_update, _) => { vote_state_update.last_voted_slot() } + Self::TowerSync(tower_sync) | Self::TowerSyncSwitch(tower_sync, _) => { + tower_sync.last_voted_slot() + } _ => panic!("Tried to get slot on non simple vote instruction"), } } @@ -197,6 +220,9 @@ impl VoteInstruction { | Self::CompactUpdateVoteStateSwitch(vote_state_update, _) => { vote_state_update.timestamp } + Self::TowerSync(tower_sync) | Self::TowerSyncSwitch(tower_sync, _) => { + tower_sync.timestamp + } _ => panic!("Tried to get timestamp on non simple vote instruction"), } } @@ -514,6 +540,37 @@ pub fn compact_update_vote_state_switch( ) } +pub fn tower_sync( + vote_pubkey: &Pubkey, + authorized_voter_pubkey: &Pubkey, + tower_sync: TowerSync, +) -> Instruction { + let account_metas = vec![ + AccountMeta::new(*vote_pubkey, false), + AccountMeta::new_readonly(*authorized_voter_pubkey, true), + ]; + + Instruction::new_with_bincode(id(), &VoteInstruction::TowerSync(tower_sync), account_metas) +} + +pub fn tower_sync_switch( + vote_pubkey: &Pubkey, + authorized_voter_pubkey: &Pubkey, + tower_sync: TowerSync, + 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::TowerSyncSwitch(tower_sync, proof_hash), + account_metas, + ) +} + pub fn withdraw( vote_pubkey: &Pubkey, authorized_withdrawer_pubkey: &Pubkey, diff --git a/sdk/program/src/vote/state/mod.rs b/sdk/program/src/vote/state/mod.rs index d22d5814c2..9f7bf19ead 100644 --- a/sdk/program/src/vote/state/mod.rs +++ b/sdk/program/src/vote/state/mod.rs @@ -207,6 +207,66 @@ impl VoteStateUpdate { } } +#[frozen_abi(digest = "5VUusSTenF9vZ9eHiCprVe9ABJUHCubeDNCCDxykybZY")] +#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] +pub struct TowerSync { + /// 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, + /// the unique identifier for the chain up to and + /// including this block. Does not require replaying + /// in order to compute. + pub block_id: Hash, +} + +impl From> for TowerSync { + fn from(recent_slots: Vec<(Slot, u32)>) -> Self { + let lockouts: VecDeque = recent_slots + .into_iter() + .map(|(slot, confirmation_count)| { + Lockout::new_with_confirmation_count(slot, confirmation_count) + }) + .collect(); + Self { + lockouts, + root: None, + hash: Hash::default(), + timestamp: None, + block_id: Hash::default(), + } + } +} + +impl TowerSync { + pub fn new( + lockouts: VecDeque, + root: Option, + hash: Hash, + block_id: Hash, + ) -> Self { + Self { + lockouts, + root, + hash, + timestamp: None, + block_id, + } + } + + pub fn slots(&self) -> Vec { + self.lockouts.iter().map(|lockout| lockout.slot()).collect() + } + + pub fn last_voted_slot(&self) -> Option { + self.lockouts.back().map(|l| l.slot()) + } +} + #[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] pub struct VoteInit { pub node_pubkey: Pubkey, @@ -904,6 +964,103 @@ pub mod serde_compact_vote_state_update { } } +pub mod serde_tower_sync { + use { + super::*, + crate::{ + clock::{Slot, UnixTimestamp}, + serde_varint, short_vec, + vote::state::Lockout, + }, + serde::{Deserialize, Deserializer, Serialize, Serializer}, + }; + + #[derive(Deserialize, Serialize, AbiExample)] + struct LockoutOffset { + #[serde(with = "serde_varint")] + offset: Slot, + confirmation_count: u8, + } + + #[derive(Deserialize, Serialize)] + struct CompactTowerSync { + root: Slot, + #[serde(with = "short_vec")] + lockout_offsets: Vec, + hash: Hash, + timestamp: Option, + block_id: Hash, + } + + pub fn serialize(tower_sync: &TowerSync, serializer: S) -> Result + where + S: Serializer, + { + let lockout_offsets = tower_sync.lockouts.iter().scan( + tower_sync.root.unwrap_or_default(), + |slot, lockout| { + let Some(offset) = lockout.slot().checked_sub(*slot) else { + return Some(Err(serde::ser::Error::custom("Invalid vote lockout"))); + }; + let Ok(confirmation_count) = u8::try_from(lockout.confirmation_count()) else { + return Some(Err(serde::ser::Error::custom("Invalid confirmation count"))); + }; + let lockout_offset = LockoutOffset { + offset, + confirmation_count, + }; + *slot = lockout.slot(); + Some(Ok(lockout_offset)) + }, + ); + let compact_tower_sync = CompactTowerSync { + root: tower_sync.root.unwrap_or(Slot::MAX), + lockout_offsets: lockout_offsets.collect::>()?, + hash: tower_sync.hash, + timestamp: tower_sync.timestamp, + block_id: tower_sync.block_id, + }; + compact_tower_sync.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let CompactTowerSync { + root, + lockout_offsets, + hash, + timestamp, + block_id, + } = CompactTowerSync::deserialize(deserializer)?; + let root = (root != Slot::MAX).then_some(root); + let lockouts = + lockout_offsets + .iter() + .scan(root.unwrap_or_default(), |slot, lockout_offset| { + *slot = match slot.checked_add(lockout_offset.offset) { + None => { + return Some(Err(serde::de::Error::custom("Invalid lockout offset"))) + } + Some(slot) => slot, + }; + let lockout = Lockout::new_with_confirmation_count( + *slot, + u32::from(lockout_offset.confirmation_count), + ); + Some(Ok(lockout)) + }); + Ok(TowerSync { + root, + lockouts: lockouts.collect::>()?, + hash, + timestamp, + block_id, + }) + } +} + #[cfg(test)] mod tests { use {super::*, itertools::Itertools, rand::Rng}; diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index cd60ee536e..a162ea852a 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -781,6 +781,10 @@ pub mod remove_rounding_in_fee_calculation { solana_sdk::declare_id!("BtVN7YjDzNE6Dk7kTT7YTDgMNUZTNgiSJgsdzAeTg2jF"); } +pub mod enable_tower_sync_ix { + solana_sdk::declare_id!("tSynMCspg4xFiCj1v3TDb4c7crMR5tSBhLz4sF7rrNA"); +} + pub mod deprecate_unused_legacy_vote_plumbing { solana_sdk::declare_id!("6Uf8S75PVh91MYgPQSHnjRAPQq6an5BDv9vomrCwDqLe"); } @@ -976,6 +980,7 @@ lazy_static! { (enable_chained_merkle_shreds::id(), "Enable chained Merkle shreds #34916"), (remove_rounding_in_fee_calculation::id(), "Removing unwanted rounding in fee calculation #34982"), (deprecate_unused_legacy_vote_plumbing::id(), "Deprecate unused legacy vote tx plumbing"), + (enable_tower_sync_ix::id(), "Enable tower sync vote instruction"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/transaction-status/src/parse_vote.rs b/transaction-status/src/parse_vote.rs index b84a7dab63..8416d0f279 100644 --- a/transaction-status/src/parse_vote.rs +++ b/transaction-status/src/parse_vote.rs @@ -171,6 +171,43 @@ pub fn parse_vote( }), }) } + VoteInstruction::TowerSync(tower_sync) => { + check_num_vote_accounts(&instruction.accounts, 2)?; + let tower_sync = json!({ + "lockouts": tower_sync.lockouts, + "root": tower_sync.root, + "hash": tower_sync.hash.to_string(), + "timestamp": tower_sync.timestamp, + "blockId": tower_sync.block_id, + }); + Ok(ParsedInstructionEnum { + instruction_type: "towersync".to_string(), + info: json!({ + "voteAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "voteAuthority": account_keys[instruction.accounts[1] as usize].to_string(), + "towerSync": tower_sync, + }), + }) + } + VoteInstruction::TowerSyncSwitch(tower_sync, hash) => { + check_num_vote_accounts(&instruction.accounts, 2)?; + let tower_sync = json!({ + "lockouts": tower_sync.lockouts, + "root": tower_sync.root, + "hash": tower_sync.hash.to_string(), + "timestamp": tower_sync.timestamp, + "blockId": tower_sync.block_id, + }); + Ok(ParsedInstructionEnum { + instruction_type: "towersyncswitch".to_string(), + info: json!({ + "voteAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "voteAuthority": account_keys[instruction.accounts[1] as usize].to_string(), + "towerSync": tower_sync, + "hash": hash.to_string(), + }), + }) + } VoteInstruction::Withdraw(lamports) => { check_num_vote_accounts(&instruction.accounts, 3)?; Ok(ParsedInstructionEnum { diff --git a/vote/src/vote_parser.rs b/vote/src/vote_parser.rs index 5ca00fa944..318d01564e 100644 --- a/vote/src/vote_parser.rs +++ b/vote/src/vote_parser.rs @@ -62,6 +62,10 @@ fn parse_vote_instruction_data( VoteInstruction::CompactUpdateVoteStateSwitch(vote_state_update, hash) => { Some((VoteTransaction::from(vote_state_update), Some(hash))) } + VoteInstruction::TowerSync(tower_sync) => Some((VoteTransaction::from(tower_sync), None)), + VoteInstruction::TowerSyncSwitch(tower_sync, hash) => { + Some((VoteTransaction::from(tower_sync), Some(hash))) + } VoteInstruction::Authorize(_, _) | VoteInstruction::AuthorizeChecked(_) | VoteInstruction::AuthorizeWithSeed(_) diff --git a/vote/src/vote_transaction.rs b/vote/src/vote_transaction.rs index fed2d730a0..c9ff76c7f8 100644 --- a/vote/src/vote_transaction.rs +++ b/vote/src/vote_transaction.rs @@ -3,13 +3,14 @@ use { clock::{Slot, UnixTimestamp}, hash::Hash, }, - solana_vote_program::vote_state::{Vote, VoteStateUpdate}, + solana_vote_program::vote_state::{TowerSync, Vote, VoteStateUpdate}, }; #[derive(Debug, PartialEq, Eq, Clone)] pub enum VoteTransaction { Vote(Vote), VoteStateUpdate(VoteStateUpdate), + TowerSync(TowerSync), } impl VoteTransaction { @@ -21,6 +22,7 @@ impl VoteTransaction { .iter() .map(|lockout| lockout.slot()) .collect(), + VoteTransaction::TowerSync(tower_sync) => tower_sync.slots(), } } @@ -30,6 +32,7 @@ impl VoteTransaction { VoteTransaction::VoteStateUpdate(vote_state_update) => { vote_state_update.lockouts.is_empty() } + VoteTransaction::TowerSync(tower_sync) => tower_sync.lockouts.is_empty(), } } @@ -37,6 +40,7 @@ impl VoteTransaction { match self { VoteTransaction::Vote(vote) => vote.hash, VoteTransaction::VoteStateUpdate(vote_state_update) => vote_state_update.hash, + VoteTransaction::TowerSync(tower_sync) => tower_sync.hash, } } @@ -44,6 +48,7 @@ impl VoteTransaction { match self { VoteTransaction::Vote(vote) => vote.timestamp, VoteTransaction::VoteStateUpdate(vote_state_update) => vote_state_update.timestamp, + VoteTransaction::TowerSync(tower_sync) => tower_sync.timestamp, } } @@ -53,6 +58,7 @@ impl VoteTransaction { VoteTransaction::VoteStateUpdate(vote_state_update) => { Some(vote_state_update.lockouts.back()?.slot()) } + VoteTransaction::TowerSync(tower_sync) => tower_sync.last_voted_slot(), } } @@ -72,3 +78,9 @@ impl From for VoteTransaction { VoteTransaction::VoteStateUpdate(vote_state_update) } } + +impl From for VoteTransaction { + fn from(tower_sync: TowerSync) -> Self { + VoteTransaction::TowerSync(tower_sync) + } +}