diff --git a/Cargo.lock b/Cargo.lock index fa0272c6517101..dca32e8aaa72fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5528,6 +5528,7 @@ dependencies = [ "solana-tpu-client", "solana-version", "solana-vote-program", + "static_assertions", "thiserror", ] diff --git a/frozen-abi/src/abi_example.rs b/frozen-abi/src/abi_example.rs index b9d41a6b7aeb66..c0492e43f428fc 100644 --- a/frozen-abi/src/abi_example.rs +++ b/frozen-abi/src/abi_example.rs @@ -464,6 +464,12 @@ impl AbiExample for SocketAddr { } } +impl AbiExample for IpAddr { + fn example() -> Self { + IpAddr::V4(Ipv4Addr::UNSPECIFIED) + } +} + // This is a control flow indirection needed for digesting all variants of an enum pub trait AbiEnumVisitor: Serialize { fn visit_for_abi(&self, digester: &mut AbiDigester) -> DigestResult; diff --git a/gossip/Cargo.toml b/gossip/Cargo.toml index 600770e03883be..715af411189fd5 100644 --- a/gossip/Cargo.toml +++ b/gossip/Cargo.toml @@ -47,6 +47,7 @@ solana-thin-client = { path = "../thin-client", version = "=1.15.0" } solana-tpu-client = { path = "../tpu-client", version = "=1.15.0", default-features = false } solana-version = { path = "../version", version = "=1.15.0" } solana-vote-program = { path = "../programs/vote", version = "=1.15.0" } +static_assertions = "1.1.0" thiserror = "1.0" [dev-dependencies] diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index 8f926eba31d896..5befb8961438f0 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -267,7 +267,7 @@ pub fn make_accounts_hashes_message( pub(crate) type Ping = ping_pong::Ping<[u8; GOSSIP_PING_TOKEN_SIZE]>; // TODO These messages should go through the gpu pipeline for spam filtering -#[frozen_abi(digest = "Hsj6a2bmzxno1RUcSM1gzHAg2zxgw15E3feb2SimieBA")] +#[frozen_abi(digest = "F3Jpf3iK1E6hQHDkoZERHkHpVoynParBEM6u48qqifDf")] #[derive(Serialize, Deserialize, Debug, AbiEnumVisitor, AbiExample)] #[allow(clippy::large_enum_variant)] pub(crate) enum Protocol { @@ -377,6 +377,7 @@ impl Sanitize for Protocol { fn retain_staked(values: &mut Vec, stakes: &HashMap) { values.retain(|value| { match value.data { + CrdsData::ContactInfo(_) => true, CrdsData::LegacyContactInfo(_) => true, // May Impact new validators starting up without any stake yet. CrdsData::Vote(_, _) => true, diff --git a/gossip/src/cluster_info_metrics.rs b/gossip/src/cluster_info_metrics.rs index 1a94793a4f1efa..fa35fd3411ca28 100644 --- a/gossip/src/cluster_info_metrics.rs +++ b/gossip/src/cluster_info_metrics.rs @@ -641,6 +641,8 @@ pub(crate) fn submit_gossip_stats( crds_stats.pull.counts[10], i64 ), + ("ContactInfo-push", crds_stats.push.counts[11], i64), + ("ContactInfo-pull", crds_stats.pull.counts[11], i64), ( "all-push", crds_stats.push.counts.iter().sum::(), @@ -684,6 +686,8 @@ pub(crate) fn submit_gossip_stats( crds_stats.pull.fails[10], i64 ), + ("ContactInfo-push", crds_stats.push.fails[11], i64), + ("ContactInfo-pull", crds_stats.pull.fails[11], i64), ("all-push", crds_stats.push.fails.iter().sum::(), i64), ("all-pull", crds_stats.pull.fails.iter().sum::(), i64), ); diff --git a/gossip/src/contact_info.rs b/gossip/src/contact_info.rs new file mode 100644 index 00000000000000..c33610dafa6962 --- /dev/null +++ b/gossip/src/contact_info.rs @@ -0,0 +1,404 @@ +#![allow(dead_code)] +pub use crate::legacy_contact_info::LegacyContactInfo; +use { + crate::crds_value::MAX_WALLCLOCK, + serde::{Deserialize, Deserializer, Serialize}, + solana_sdk::{ + pubkey::Pubkey, + sanitize::{Sanitize, SanitizeError}, + serde_varint, short_vec, + }, + static_assertions::const_assert_eq, + std::{ + collections::HashSet, + net::{IpAddr, Ipv4Addr, SocketAddr}, + time::{SystemTime, UNIX_EPOCH}, + }, + thiserror::Error, +}; + +const SOCKET_TAG_GOSSIP: u8 = 0; +const SOCKET_TAG_REPAIR: u8 = 1; +const SOCKET_TAG_RPC: u8 = 2; +const SOCKET_TAG_RPC_PUBSUB: u8 = 3; +const SOCKET_TAG_SERVE_REPAIR: u8 = 4; +const SOCKET_TAG_TPU: u8 = 5; +const SOCKET_TAG_TPU_QUIC: u8 = 6; +const SOCKET_TAG_TPU_FORWARDS: u8 = 7; +const SOCKET_TAG_TPU_FORWARDS_QUIC: u8 = 8; +const SOCKET_TAG_TPU_VOTE: u8 = 9; +const SOCKET_TAG_TVU: u8 = 10; +const SOCKET_TAG_TVU_FORWARDS: u8 = 11; +const_assert_eq!(SOCKET_CACHE_SIZE, 12); +const SOCKET_CACHE_SIZE: usize = SOCKET_TAG_TVU_FORWARDS as usize + 1usize; + +#[derive(Debug, Error)] +enum Error { + #[error("Duplicate IP address: {0}")] + DuplicateIpAddr(IpAddr), + #[error("Duplicate socket: {0}")] + DuplicateSocket(/*key:*/ u8), + #[error("Invalid IP address index: {index}, num addrs: {num_addrs}")] + InvalidAddrIndex { index: u8, num_addrs: usize }, + #[error("Invalid port: {0}")] + InvalidPort(/*port:*/ u16), + #[error("IP addresses saturated")] + IpAddrsSaturated, + #[error("Multicast IP address: {0}")] + MulticastAddr(IpAddr), + #[error("Socket not found: {0}")] + SocketNotFound(/*key:*/ u8), + #[error("Unspecified IP address: {0}")] + UnspecifiedAddr(IpAddr), + #[error("Unused IP address: {0}")] + UnusedIpAddr(IpAddr), +} + +// XXX can't be Default if it has outset field! +#[derive(Clone, Debug, Eq, PartialEq, AbiExample, Serialize)] +pub struct ContactInfo { + pubkey: Pubkey, + #[serde(with = "serde_varint")] + wallclock: u64, + // Timestamp when the instance was first created. + // Identifies duplicate running instances. + outset: u64, + shred_version: u16, + version: solana_version::Version, + // All IP addresses are unique and referenced at least once in sockets. + #[serde(with = "short_vec")] + addrs: Vec, + // TODO: use port offset with varint? should be sorted! + // All sockets have a unique key and valid IP address index. + #[serde(with = "short_vec")] + sockets: Vec, + #[serde(skip_serializing)] + cache: [SocketAddr; SOCKET_CACHE_SIZE], +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, AbiExample, Deserialize, Serialize)] +struct SocketEntry { + key: u8, // Protocol identifier, e.g. tvu, tpu, etc + index: u8, // IpAddr index in the accompanying addrs vector. + port: u16, +} + +// Workaround since serde does not have an initializer for skipped fields. +// https://github.com/serde-rs/serde/issues/642 +#[derive(Deserialize, Serialize)] +struct ContactInfoLite { + pubkey: Pubkey, + #[serde(with = "serde_varint")] + wallclock: u64, + outset: u64, + shred_version: u16, + version: solana_version::Version, + #[serde(with = "short_vec")] + addrs: Vec, + #[serde(with = "short_vec")] + sockets: Vec, +} + +impl ContactInfo { + pub(crate) fn new(pubkey: Pubkey, wallclock: u64, shred_version: u16) -> Self { + Self { + pubkey, + wallclock, + outset: { + let now = SystemTime::now(); + let elapsed = now.duration_since(UNIX_EPOCH).unwrap(); + u64::try_from(elapsed.as_micros()).unwrap() + }, + shred_version, + version: solana_version::Version::default(), + addrs: Vec::::default(), + sockets: Vec::::default(), + cache: [socket_addr_unspecified(); SOCKET_CACHE_SIZE], + } + } + + #[inline] + pub fn pubkey(&self) -> &Pubkey { + &self.pubkey + } + + #[inline] + pub(crate) fn wallclock(&self) -> u64 { + self.wallclock + } + + #[cfg(test)] + fn get_socket(&self, key: u8) -> Option { + let &SocketEntry { index, port, .. } = + self.sockets.iter().find(|entry| entry.key == key)?; + let addr = self.addrs.get(usize::from(index))?; + let socket = SocketAddr::new(*addr, port); + sanitize_socket(&socket).ok()?; + Some(socket) + } + + // Adds given IP address to self.addrs returning respective index. + fn push_addr(&mut self, addr: IpAddr) -> Result { + match self.addrs.iter().position(|k| k == &addr) { + Some(index) => u8::try_from(index).map_err(|_| Error::IpAddrsSaturated), + None => { + let index = u8::try_from(self.addrs.len()).map_err(|_| Error::IpAddrsSaturated)?; + self.addrs.push(addr); + Ok(index) + } + } + } + + fn set_socket(&mut self, key: u8, socket: SocketAddr) -> Result<(), Error> { + sanitize_socket(&socket)?; + self.remove_socket(key); + let index = self.push_addr(socket.ip())?; + self.sockets.push(SocketEntry { + key, + index, + port: socket.port(), + }); + if let Some(entry) = self.cache.get_mut(usize::from(key)) { + *entry = socket; + } + Ok(()) + } + + // Removes the socket with the specified key. + fn remove_socket(&mut self, key: u8) { + if let Some(index) = self.sockets.iter().position(|entry| entry.key == key) { + let entry = self.sockets.remove(index); + self.maybe_remove_addr(entry.index); + if let Some(entry) = self.cache.get_mut(usize::from(key)) { + *entry = socket_addr_unspecified(); + } + } + } + + // If no socket uses the IP address at the given index, + // removes the IP address and adjusts indices. + fn maybe_remove_addr(&mut self, index: u8) { + if !self.sockets.iter().any(|entry| entry.index == index) { + self.addrs.remove(usize::from(index)); + for entry in &mut self.sockets { + if entry.index > index { + entry.index -= 1; + } + } + } + } +} + +// Workaround until feature(const_socketaddr) is stable. +fn socket_addr_unspecified() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), /*port:*/ 0u16) +} + +fn sanitize_socket(socket: &SocketAddr) -> Result<(), Error> { + if socket.port() == 0u16 { + return Err(Error::InvalidPort(socket.port())); + } + let addr = socket.ip(); + if addr.is_unspecified() { + return Err(Error::UnspecifiedAddr(addr)); + } + if addr.is_multicast() { + return Err(Error::MulticastAddr(addr)); + } + Ok(()) +} + +// Sanitizes deserialized IpAddr and socket entries. +fn sanitize_entries(addrs: &[IpAddr], sockets: &[SocketEntry]) -> Result<(), Error> { + // Verify that all IP addresses are unique. + { + let mut seen = HashSet::with_capacity(addrs.len()); + for addr in addrs { + if !seen.insert(addr) { + return Err(Error::DuplicateIpAddr(*addr)); + } + } + } + // Verify that all sockets have unique key. + { + let mut mask = [0u64; 4]; // 256-bit bitmask. + for &SocketEntry { key, .. } in sockets { + let mask = &mut mask[usize::from(key / 64u8)]; + let bit = 1u64 << (key % 64u8); + if (*mask & bit) != 0u64 { + return Err(Error::DuplicateSocket(key)); + } + *mask |= bit; + } + } + // Verify that all sockets reference a valid IP address, and + // that all IP addresses are referenced in the sockets. + { + let num_addrs = addrs.len(); + let mut hits = vec![false; num_addrs]; + for &SocketEntry { index, .. } in sockets { + *hits + .get_mut(usize::from(index)) + .ok_or(Error::InvalidAddrIndex { index, num_addrs })? = true; + } + if let Some(index) = hits.into_iter().position(|hit| !hit) { + return Err(Error::UnusedIpAddr(addrs[index])); + } + } + // TODO: verify port offsets don't overflow. + Ok(()) +} + +impl Sanitize for ContactInfo { + fn sanitize(&self) -> Result<(), SanitizeError> { + if self.wallclock >= MAX_WALLCLOCK { + return Err(SanitizeError::ValueOutOfBounds); + } + Ok(()) + } +} + +impl<'de> Deserialize<'de> for ContactInfo { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let node = ContactInfoLite::deserialize(deserializer)?; + sanitize_entries(&node.addrs, &node.sockets).map_err(serde::de::Error::custom)?; + Ok(ContactInfo::from(node)) + } +} + +impl From for ContactInfo { + fn from(node: ContactInfoLite) -> Self { + let ContactInfoLite { + pubkey, + wallclock, + outset, + shred_version, + version, + addrs, + sockets, + } = node; + let mut node = ContactInfo { + pubkey, + wallclock, + outset, + shred_version, + version, + addrs, + sockets, + cache: [socket_addr_unspecified(); SOCKET_CACHE_SIZE], + }; + // Populate node.cache. + for &SocketEntry { key, index, port } in &node.sockets { + let entry = match node.cache.get_mut(usize::from(key)) { + None => continue, + Some(entry) => entry, + }; + let addr = match node.addrs.get(usize::from(index)) { + None => continue, + Some(addr) => addr, + }; + let socket = SocketAddr::new(*addr, port); + if sanitize_socket(&socket).is_ok() { + *entry = socket; + } + } + node + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + rand::{seq::SliceRandom, Rng}, + std::{ + collections::{HashMap, HashSet}, + iter::repeat_with, + net::{Ipv4Addr, Ipv6Addr}, + ops::Range, + }, + }; + + fn new_rand_addr(rng: &mut R) -> IpAddr { + if rng.gen() { + let addr = Ipv4Addr::new(rng.gen(), rng.gen(), rng.gen(), rng.gen()); + IpAddr::V4(addr) + } else { + let addr = Ipv6Addr::new( + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + ); + IpAddr::V6(addr) + } + } + + fn new_rand_port(rng: &mut R) -> u16 { + let port = rng.gen::(); + let bits = u16::BITS - port.leading_zeros(); + let shift = rng.gen_range(0u32, bits + 1u32); + port.checked_shr(shift).unwrap_or_default() + } + + // TODO: test coverage for sanitize_entries + + #[test] + fn test_round_trip() { + const KEYS: Range = 0u8..16u8; + let mut rng = rand::thread_rng(); + let addrs: Vec = repeat_with(|| new_rand_addr(&mut rng)).take(8).collect(); + let mut node = ContactInfo { + pubkey: Pubkey::new_unique(), + wallclock: rng.gen(), + outset: rng.gen(), + shred_version: rng.gen(), + version: solana_version::Version::default(), + addrs: Vec::default(), + sockets: Vec::default(), + cache: [socket_addr_unspecified(); SOCKET_CACHE_SIZE], + }; + let mut sockets = HashMap::::new(); + for _ in 0..1 << 14 { + let addr = addrs.choose(&mut rng).unwrap(); + let socket = SocketAddr::new(*addr, new_rand_port(&mut rng)); + let key = rng.gen_range(KEYS.start, KEYS.end); + if sanitize_socket(&socket).is_ok() { + sockets.insert(key, socket); + assert_matches!(node.set_socket(key, socket), Ok(())); + assert_matches!(sanitize_entries(&node.addrs, &node.sockets), Ok(())); + } else { + assert_matches!(node.set_socket(key, socket), Err(_)); + } + for key in KEYS.clone() { + let socket = sockets.get(&key); + assert_eq!(node.get_socket(key).as_ref(), socket); + if usize::from(key) < SOCKET_CACHE_SIZE { + assert_eq!( + &node.cache[usize::from(key)], + socket.unwrap_or(&socket_addr_unspecified()) + ) + } + } + // Assert that all addresses are unique. + let num_unique_addrs = node.addrs.iter().copied().collect::>().len(); + assert_eq!(node.addrs.len(), num_unique_addrs); + // Assert that only mapped addresses are stored. + assert_eq!( + node.addrs.iter().copied().collect::>(), + sockets.values().map(SocketAddr::ip).collect::>(), + ); + // Verify that serde round trips. + let bytes = bincode::serialize(&node).unwrap(); + let other: ContactInfo = bincode::deserialize(&bytes).unwrap(); + assert_eq!(node, other); + } + } +} diff --git a/gossip/src/crds.rs b/gossip/src/crds.rs index 820a7df8ffa048..d48e54442582bc 100644 --- a/gossip/src/crds.rs +++ b/gossip/src/crds.rs @@ -94,7 +94,7 @@ pub enum GossipRoute { PushMessage, } -type CrdsCountsArray = [usize; 11]; +type CrdsCountsArray = [usize; 12]; pub(crate) struct CrdsDataStats { pub(crate) counts: CrdsCountsArray, @@ -700,6 +700,8 @@ impl CrdsDataStats { CrdsData::NodeInstance(_) => 8, CrdsData::DuplicateShred(_, _) => 9, CrdsData::IncrementalSnapshotHashes(_) => 10, + CrdsData::ContactInfo(_) => 11, + // Update CrdsCountsArray if new items are added here. } } } diff --git a/gossip/src/crds_gossip_pull.rs b/gossip/src/crds_gossip_pull.rs index 7b2b49ea33c77c..d5ec2fd4875d93 100644 --- a/gossip/src/crds_gossip_pull.rs +++ b/gossip/src/crds_gossip_pull.rs @@ -18,7 +18,7 @@ use { crds::{Crds, GossipRoute, VersionedCrdsValue}, crds_gossip, crds_gossip_error::CrdsGossipError, - crds_value::CrdsValue, + crds_value::{CrdsData, CrdsValue}, legacy_contact_info::LegacyContactInfo as ContactInfo, ping_pong::PingCache, }, @@ -519,6 +519,11 @@ impl CrdsGossipPull { let out: Vec<_> = crds .filter_bitmask(filter.mask, filter.mask_bits) .filter(pred) + .filter(|entry| { + // Exclude the new ContactInfo until the cluster has + // upgraded. + !matches!(&entry.value.data, CrdsData::ContactInfo(_)) + }) .map(|entry| entry.value.clone()) .take(output_size_limit.load(Ordering::Relaxed).max(0) as usize) .collect(); diff --git a/gossip/src/crds_gossip_push.rs b/gossip/src/crds_gossip_push.rs index c310b1e5244259..3c0690ad262acb 100644 --- a/gossip/src/crds_gossip_push.rs +++ b/gossip/src/crds_gossip_push.rs @@ -16,7 +16,7 @@ use { cluster_info::{Ping, CRDS_UNIQUE_PUBKEY_CAPACITY}, crds::{Crds, CrdsError, Cursor, GossipRoute}, crds_gossip, - crds_value::CrdsValue, + crds_value::{CrdsData, CrdsValue}, legacy_contact_info::LegacyContactInfo as ContactInfo, ping_pong::PingCache, push_active_set::PushActiveSet, @@ -193,6 +193,10 @@ impl CrdsGossipPush { let crds = crds.read().unwrap(); let entries = crds .get_entries(crds_cursor.deref_mut()) + .filter(|entry| { + // Exclude the new ContactInfo until the cluster has upgraded. + !matches!(&entry.value.data, CrdsData::ContactInfo(_)) + }) .map(|entry| &entry.value) .filter(|value| wallclock_window.contains(&value.wallclock())); for value in entries { diff --git a/gossip/src/crds_value.rs b/gossip/src/crds_value.rs index 3cdaa23abc4a5d..099c870177ad4b 100644 --- a/gossip/src/crds_value.rs +++ b/gossip/src/crds_value.rs @@ -1,6 +1,7 @@ use { crate::{ cluster_info::MAX_SNAPSHOT_HASHES, + contact_info::ContactInfo, deprecated, duplicate_shred::{DuplicateShred, DuplicateShredIndex, MAX_DUPLICATE_SHREDS}, epoch_slots::EpochSlots, @@ -92,6 +93,7 @@ pub enum CrdsData { NodeInstance(NodeInstance), DuplicateShred(DuplicateShredIndex, DuplicateShred), IncrementalSnapshotHashes(IncrementalSnapshotHashes), + ContactInfo(ContactInfo), } impl Sanitize for CrdsData { @@ -129,6 +131,7 @@ impl Sanitize for CrdsData { } } CrdsData::IncrementalSnapshotHashes(val) => val.sanitize(), + CrdsData::ContactInfo(node) => node.sanitize(), } } } @@ -492,6 +495,7 @@ pub enum CrdsValueLabel { NodeInstance(Pubkey), DuplicateShred(DuplicateShredIndex, Pubkey), IncrementalSnapshotHashes(Pubkey), + ContactInfo(Pubkey), } impl fmt::Display for CrdsValueLabel { @@ -512,6 +516,7 @@ impl fmt::Display for CrdsValueLabel { CrdsValueLabel::IncrementalSnapshotHashes(_) => { write!(f, "IncrementalSnapshotHashes({})", self.pubkey()) } + CrdsValueLabel::ContactInfo(_) => write!(f, "ContactInfo({})", self.pubkey()), } } } @@ -530,6 +535,7 @@ impl CrdsValueLabel { CrdsValueLabel::NodeInstance(p) => *p, CrdsValueLabel::DuplicateShred(_, p) => *p, CrdsValueLabel::IncrementalSnapshotHashes(p) => *p, + CrdsValueLabel::ContactInfo(pubkey) => *pubkey, } } } @@ -579,6 +585,7 @@ impl CrdsValue { CrdsData::NodeInstance(node) => node.wallclock, CrdsData::DuplicateShred(_, shred) => shred.wallclock, CrdsData::IncrementalSnapshotHashes(hash) => hash.wallclock, + CrdsData::ContactInfo(node) => node.wallclock(), } } pub fn pubkey(&self) -> Pubkey { @@ -594,6 +601,7 @@ impl CrdsValue { CrdsData::NodeInstance(node) => node.from, CrdsData::DuplicateShred(_, shred) => shred.from, CrdsData::IncrementalSnapshotHashes(hash) => hash.from, + CrdsData::ContactInfo(node) => *node.pubkey(), } } pub fn label(&self) -> CrdsValueLabel { @@ -611,6 +619,7 @@ impl CrdsValue { CrdsData::IncrementalSnapshotHashes(_) => { CrdsValueLabel::IncrementalSnapshotHashes(self.pubkey()) } + CrdsData::ContactInfo(node) => CrdsValueLabel::ContactInfo(*node.pubkey()), } } pub fn contact_info(&self) -> Option<&LegacyContactInfo> { diff --git a/gossip/src/lib.rs b/gossip/src/lib.rs index f0458328bfb0c9..81ca679080b5b9 100644 --- a/gossip/src/lib.rs +++ b/gossip/src/lib.rs @@ -3,6 +3,7 @@ pub mod cluster_info; pub mod cluster_info_metrics; +pub mod contact_info; pub mod crds; pub mod crds_entry; pub mod crds_gossip; diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index c02da723a9012d..d507f9113abc32 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -4724,6 +4724,7 @@ dependencies = [ "solana-tpu-client", "solana-version", "solana-vote-program", + "static_assertions", "thiserror", ] diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index aa4ab7f8a7c45b..d2e743e30cffc9 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -49,9 +49,9 @@ pub use solana_program::{ decode_error, ed25519_program, epoch_schedule, fee_calculator, impl_sysvar_get, incinerator, instruction, keccak, lamports, loader_instruction, loader_upgradeable_instruction, message, msg, native_token, nonce, program, program_error, program_memory, program_option, program_pack, - rent, sanitize, sdk_ids, secp256k1_program, secp256k1_recover, serialize_utils, short_vec, - slot_hashes, slot_history, stake, stake_history, syscalls, system_instruction, system_program, - sysvar, unchecked_div_by_const, vote, wasm_bindgen, + rent, sanitize, sdk_ids, secp256k1_program, secp256k1_recover, serde_varint, serialize_utils, + short_vec, slot_hashes, slot_history, stake, stake_history, syscalls, system_instruction, + system_program, sysvar, unchecked_div_by_const, vote, wasm_bindgen, }; pub mod account;