diff --git a/Cargo.lock b/Cargo.lock index 5572b447fa506b..410042b2e0631a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,7 +172,7 @@ dependencies = [ "ctrlc", "dirs-next", "indicatif 0.18.0", - "nix", + "nix 0.30.1", "reqwest 0.12.22", "scopeguard", "semver 1.0.26", @@ -411,7 +411,7 @@ dependencies = [ "console 0.16.0", "core_affinity", "crossbeam-channel", - "fd-lock", + "fd-lock 3.0.13", "indicatif 0.18.0", "itertools 0.12.1", "jsonrpc-core", @@ -1764,19 +1764,19 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.31" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", - "clap_derive 4.5.28", + "clap_derive 4.5.32", ] [[package]] name = "clap_builder" -version = "4.5.31" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -1799,9 +1799,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.28" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1824,6 +1824,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -2020,6 +2029,33 @@ dependencies = [ "cfg-if 1.0.1", ] +[[package]] +name = "crds_node" +version = "3.0.0" +dependencies = [ + "anyhow", + "borsh 1.5.7", + "clap 4.5.37", + "log", + "rand 0.8.5", + "rustyline", + "serde", + "serde_json", + "signal-hook", + "solana-gossip", + "solana-hash", + "solana-instruction", + "solana-keypair", + "solana-logger", + "solana-net-utils", + "solana-packet", + "solana-pubkey", + "solana-signer", + "solana-streamer", + "solana-time-utils", + "solana-transaction", +] + [[package]] name = "criterion" version = "0.5.1" @@ -2029,7 +2065,7 @@ dependencies = [ "anes", "cast 0.3.0", "ciborium", - "clap 4.5.31", + "clap 4.5.37", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -2173,7 +2209,7 @@ version = "3.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" dependencies = [ - "nix", + "nix 0.30.1", "windows-sys 0.59.0", ] @@ -2577,6 +2613,12 @@ dependencies = [ "cfg-if 1.0.1", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "enum-iterator" version = "1.5.0" @@ -2639,6 +2681,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "etcd-client" version = "0.11.1" @@ -2726,6 +2774,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if 1.0.1", + "rustix 1.0.2", + "windows-sys 0.59.0", +] + [[package]] name = "feature-probe" version = "0.1.1" @@ -3345,6 +3404,15 @@ dependencies = [ "hmac 0.8.1", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "0.2.12" @@ -4340,9 +4408,9 @@ checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" [[package]] name = "memchr" -version = "2.6.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" @@ -4513,6 +4581,27 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.1", + "cfg-if 1.0.1", + "cfg_aliases", + "libc", +] + [[package]] name = "nix" version = "0.30.1" @@ -5454,6 +5543,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.4.6" @@ -6080,6 +6179,28 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rustyline" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f" +dependencies = [ + "bitflags 2.9.1", + "cfg-if 1.0.1", + "clipboard-win", + "fd-lock 4.0.4", + "home", + "libc", + "log", + "memchr", + "nix 0.29.0", + "radix_trie", + "unicode-segmentation", + "unicode-width 0.2.0", + "utf8parse", + "windows-sys 0.59.0", +] + [[package]] name = "ryu" version = "1.0.5" @@ -9148,7 +9269,7 @@ dependencies = [ "hxdmp", "itertools 0.12.1", "log", - "nix", + "nix 0.30.1", "pcap-file", "rand 0.8.5", "serde", @@ -9249,7 +9370,7 @@ dependencies = [ "fnv", "libc", "log", - "nix", + "nix 0.30.1", "rand 0.8.5", "rand_chacha 0.3.1", "rayon", @@ -10722,7 +10843,7 @@ dependencies = [ "itertools 0.12.1", "libc", "log", - "nix", + "nix 0.30.1", "num_cpus", "pem", "percentage", @@ -11602,7 +11723,7 @@ dependencies = [ "assert_matches", "async-channel", "bytes", - "clap 4.5.31", + "clap 4.5.37", "crossbeam-channel", "dashmap", "futures 0.3.31", @@ -11613,7 +11734,7 @@ dependencies = [ "itertools 0.12.1", "libc", "log", - "nix", + "nix 0.30.1", "pem", "percentage", "quinn", diff --git a/Cargo.toml b/Cargo.toml index 0f3e37bcc256bb..fbcf501a7d5513 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ members = [ "geyser-plugin-interface", "geyser-plugin-manager", "gossip", + "gossip/crds_node", "install", "io-uring", "keygen", diff --git a/gossip/Cargo.toml b/gossip/Cargo.toml index 8d27b7e39ad6a5..5eb159a2e58956 100644 --- a/gossip/Cargo.toml +++ b/gossip/Cargo.toml @@ -12,10 +12,6 @@ edition = { workspace = true } [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] -[[bin]] -name = "solana-gossip" -path = "src/main.rs" - [features] frozen-abi = [ "dep:solana-frozen-abi", @@ -29,6 +25,7 @@ frozen-abi = [ "solana-vote/frozen-abi", "solana-vote-program/frozen-abi", ] +dev-context-only-utils =[] [dependencies] agave-feature-set = { workspace = true } @@ -126,5 +123,9 @@ name = "crds_shards" name = "weighted_shuffle" harness = false +[[bin]] +name = "solana-gossip" +path = "src/main.rs" + [lints] workspace = true diff --git a/gossip/crds_node/Cargo.toml b/gossip/crds_node/Cargo.toml new file mode 100644 index 00000000000000..2ceba5feff3a5b --- /dev/null +++ b/gossip/crds_node/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "crds_node" +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +version = { workspace = true } +description = "A standalone CRDS node for simulations and tests" +publish = false + +[dependencies] +anyhow = { workspace = true } +borsh = { workspace = true } +clap = { version = "4", features = ["derive", "cargo"] } +log = { workspace = true } +rand = { workspace = true } +rustyline = "15.0.0" +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +signal-hook = { workspace = true } +solana-gossip = { workspace = true, features = ["dev-context-only-utils"] } +solana-hash = { workspace = true, features = ["borsh"] } +solana-instruction = { workspace = true, features = ["bincode", "borsh"] } +solana-keypair = { workspace = true } +solana-logger = { workspace = true } +solana-net-utils = { workspace = true } +solana-packet = { workspace = true, features = ["dev-context-only-utils"] } +solana-pubkey = { workspace = true } +solana-signer = { workspace = true } +solana-streamer = { workspace = true } +solana-time-utils = { workspace = true } +solana-transaction = { workspace = true, features = ["bincode", "dev-context-only-utils"] } + +[lints] +workspace = true diff --git a/gossip/crds_node/README.md b/gossip/crds_node/README.md new file mode 100644 index 00000000000000..eda0550d48219c --- /dev/null +++ b/gossip/crds_node/README.md @@ -0,0 +1,21 @@ +# CRDS Node + +`crds_node` crate provides a lightweight, standalone implementation of a Solana +gossip/CRDS (Conflict-free Replicated Data Store) node. It offers direct control +over the messages being sent, making it ideal for testing, experimentation, and scripting. + +This tool is particularly useful for developers looking to explore or test gossip behaviors in isolation. + +## Features + +- **Standalone Operation**: Run a CRDS node independently, without requiring the full Solana validator stack. +- **Message Injection**: Supply messages via CLI arguments or pipe them through `STDIN` as JSON-serialized input. +- **Script-Friendly**: Easily integrate with testing frameworks or automation scripts. +- **Lightweight & Flexible**: Minimal dependencies and resource usage, designed for protocol experimentation at scale within environments such as mininet. + +## Caveats + +This is an internal dev tool. Use it at your own risk. + +- The external interface of this program is not stable and should not be relied upon. +- Command line arguments, set of messages and output format may change without prior notice. diff --git a/gossip/crds_node/src/commands.rs b/gossip/crds_node/src/commands.rs new file mode 100644 index 00000000000000..ee50cf33957770 --- /dev/null +++ b/gossip/crds_node/src/commands.rs @@ -0,0 +1,166 @@ +use { + crate::SHRED_VERSION, + clap::Subcommand, + log::error, + serde::{Deserialize, Serialize}, + serde_json::json, + solana_gossip::{ + cluster_info::ClusterInfo, + contact_info::ContactInfo, + crds::{Cursor, GossipRoute}, + crds_data::CrdsData, + crds_gossip::CrdsGossip, + crds_value::CrdsValue, + }, + solana_hash::Hash, + solana_keypair::Keypair, + solana_net_utils::bind_to_localhost, + solana_pubkey::Pubkey, + solana_signer::Signer, + solana_time_utils::timestamp, + solana_transaction::Transaction, + std::{net::SocketAddr, ops::ControlFlow}, +}; +#[derive(Subcommand, Debug, Serialize, Deserialize)] +pub(crate) enum Command { + Exit, + /// Send a random Vote CRDS + SendVote, + /// Send a ping and get pong back (does not actually work yet) + SendPing { + #[arg(long)] + target: Pubkey, + #[arg(long)] + target_addr: Option, + }, + /// Return list of peers currently in CRDS + Peers, + /// Returns epoch slots inserted since given slot + EpochSlots { + #[arg(short, long)] + slot: u64, + }, + /// Returns votes inserted since given slot + Votes { + #[arg(short, long)] + slot: u64, + }, + /// Insert a ContactInfo for provided peer + InsertContactInfo { + /// Keypair for the new peer to be inserted. If not provided, + /// a random identity will be generated + #[arg(short, long)] + keypair: Option, + /// Address of the new peer + #[arg(short, long)] + address: SocketAddr, + }, +} + +fn insert_contact_info(gossip: &CrdsGossip, keypair: Keypair, address: SocketAddr) { + let pubkey = keypair.pubkey(); + let mut contact_info = ContactInfo::new(pubkey, timestamp(), SHRED_VERSION); + contact_info + .set_gossip(address) + .expect("Should have valid gossip address"); + let entry = CrdsValue::new(CrdsData::ContactInfo(contact_info), &keypair); + + if let Err(err) = { + let mut gossip_crds = gossip.crds.write().unwrap(); + gossip_crds.insert(entry, timestamp(), GossipRoute::LocalMessage) + } { + error!("ClusterInfo.insert_info: {err:?}"); + } +} + +pub(crate) fn execute_command( + cluster_info: &ClusterInfo, + my_keypair: &Keypair, + command: Command, +) -> anyhow::Result> { + match command { + Command::Exit => return Ok(ControlFlow::Break(())), + Command::Peers => { + let peers = cluster_info.all_peers(); + println!("{}", json!({ "command_ok":true , "peers": &peers})); + } + Command::EpochSlots { slot } => { + let mut cursor = Cursor::new(slot); + let epoch_slots = cluster_info.get_epoch_slots(&mut cursor); + println!("{}", json!({ "command_ok":true , "slots":&epoch_slots})); + } + Command::Votes { slot } => { + let mut cursor = Cursor::new(slot); + let (labels, votes) = cluster_info.get_votes_with_labels(&mut cursor); + let senders = labels.into_iter().map(|e| e.pubkey()); + let data: Vec<_> = senders.zip(votes).collect(); + println!("{}", json!({ "command_ok":true, "votes": &data })); + } + Command::SendVote => { + let mut vote = Transaction::default(); + vote.sign(&[&my_keypair], Hash::new_unique()); + + cluster_info.push_vote(&[42], vote.clone()); + println!("{}", json!({ "command_ok":true , "vote":vote})); + } + Command::InsertContactInfo { keypair, address } => { + let keypair = keypair + .map(|v| Keypair::from_base58_string(&v)) + .unwrap_or(Keypair::new()); + insert_contact_info(&cluster_info.gossip, keypair, address); + println!("{}", json!({ "command_ok":true })); + } + Command::SendPing { + target, + target_addr, + } => { + let Some(target_addr) = target_addr.or_else(|| { + cluster_info + .lookup_contact_info(&target, |v| v.gossip()) + .flatten() + }) else { + println!( + "{}", + json!({ "command_ok":false,"status":"no address found" }) + ); + return Ok(ControlFlow::Continue(())); + }; + let (state, maybe_pkt) = cluster_info.check_ping(target, target_addr); + let ping_sent = if let Some(pkt) = maybe_pkt { + let sock = bind_to_localhost()?; + sock.send_to(pkt.data(..).unwrap(), target_addr)?; + true + } else { + false + }; + println!( + "{}", + json!({ "command_ok": true , "ping_cache_status":state, "ping_sent":ping_sent}) + ); + } + } + Ok(ControlFlow::Continue(())) +} + +/*use borsh::{BorshDeserialize, BorshSerialize}; +use solana_instruction::Instruction; +#[derive(BorshSerialize, BorshDeserialize)] +enum BankInstruction { + Initialize, + Deposit { lamports: u64 }, + Withdraw { lamports: u64 }, +} +fn fake_transaction(keypair: &Keypair) -> Transaction { + let bank_instruction = BankInstruction::Initialize; + + let instruction = Instruction::new_with_borsh(keypair.pubkey(), &bank_instruction, vec![]); + + let message = Message::new(&[instruction], Some(&keypair.pubkey())); + + let mut tx = Transaction::new_unsigned(message); + let blockhash = Hash; + tx.sign(&[keypair], blockhash); + + tx +} +*/ diff --git a/gossip/crds_node/src/main.rs b/gossip/crds_node/src/main.rs new file mode 100644 index 00000000000000..8f2c47b48af5b6 --- /dev/null +++ b/gossip/crds_node/src/main.rs @@ -0,0 +1,111 @@ +use { + crate::commands::{execute_command, Command}, + anyhow::anyhow, + clap::Parser, + serde_json::json, + solana_gossip::gossip_service::make_gossip_node, + solana_keypair::Keypair, + solana_signer::Signer, + solana_streamer::socket::SocketAddrSpace, + std::{ + net::SocketAddr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + }, +}; + +pub(crate) const SHRED_VERSION: u16 = 42; +mod commands; +#[derive(Parser, Debug)] +/// Standalone Gossip/CRDS node +/// +/// This is an internal dev tool, use this at your own risk! +/// No guarantees are given about the reliability of +/// this tool, its interface stability and fitness for a +/// particular purpose. +/// This is using Agave Gossip implementation, so should be +/// 100% compatible with Solana of corresponding release. +#[command(version, about, long_about = None)] +struct CliArgs { + /// Gossip address to bind to + #[arg(short, long, default_value = "127.0.0.1:8001")] + bind_address: SocketAddr, + /// Entrypoint for the cluster. + /// If not set, this node will become an entrypoint. + #[arg(short, long)] + entrypoint: Option, + /// Keypair for the node identity. If not provided, a random keypair will be made + #[arg(short, long)] + keypair: Option, +} + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct CliCommand { + #[command(subcommand)] + command: Command, +} + +fn main() -> anyhow::Result<()> { + let args = CliArgs::parse(); + let exit = Arc::new(AtomicBool::new(false)); + + //solana_logger::setup_with("info,solana_metrics=error"); + solana_logger::setup_with("error"); + + let keypair = Arc::new( + args.keypair + .map(|v| Keypair::from_base58_string(&v)) + .unwrap_or(Keypair::new()), + ); + let pubkey = keypair.pubkey(); + + let (gossip_svc, _ip_echo, cluster_info) = make_gossip_node( + keypair.insecure_clone(), + args.entrypoint.as_ref(), + exit.clone(), + Some(&args.bind_address), + SHRED_VERSION, + false, + SocketAddrSpace::Unspecified, + ); + println!("{}", json!({ "start_node": pubkey })); + + let mut rl = rustyline::DefaultEditor::new()?; + loop { + let readline = rl.readline(">> "); + let Ok(input_line) = readline else { break }; + rl.add_history_entry(&input_line)?; + let command = if input_line.starts_with("{") { + match serde_json::from_str::(&input_line) { + Ok(cmd) => cmd, + Err(e) => { + println!("{}", json!({"command_parse_error":e.to_string()})); + continue; + } + } + } else { + match CliCommand::try_parse_from( + std::iter::once("tool").chain(input_line.split(" ").map(|e| e.trim())), + ) { + Ok(cmd) => { + println!("{}", serde_json::to_string(&cmd.command)?); + cmd.command + } + Err(e) => { + println!("Invalid input provided, {e}"); + continue; + } + } + }; + if execute_command(&cluster_info, &keypair, command)?.is_break() { + break; + } + } + exit.store(true, Ordering::Relaxed); + println!("{}", json!({ "terminate_node": pubkey })); + gossip_svc.join().map_err(|_e| anyhow!("cannot join"))?; + Ok(()) +} diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index cd6e61ce0b0b8e..e8920fb2abcac9 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -401,6 +401,28 @@ impl ClusterInfo { self.refresh_my_gossip_contact_info(); Ok(()) } + + #[cfg(feature = "dev-context-only-utils")] + /// Checks if target node is in the ping cache, if it is returns (true, None) + /// If not in cache, returns false and, if all goes well, a Packet to send to the node + pub fn check_ping(&self, target: Pubkey, target_addr: SocketAddr) -> (bool, Option) { + let mut rng = rand::thread_rng(); + let (state, maybe_ping) = { + let keypair = self.keypair.read().unwrap(); + let mut pingcache = self.ping_cache.lock().unwrap(); + pingcache.check( + &mut rng, + &keypair, + Instant::now(), + (target, target_addr), + ) + }; + let pkt = maybe_ping.map(|ping| { + let ping = Protocol::PingMessage(ping); + make_gossip_packet(target_addr, &ping, &self.stats).unwrap() + }); + (state, pkt) + } pub fn set_tpu(&self, tpu_addr: SocketAddr) -> Result<(), ContactInfoError> { self.my_contact_info.write().unwrap().set_tpu(tpu_addr)?; @@ -1595,9 +1617,10 @@ impl ClusterInfo { R: Rng + CryptoRng, { let mut cache = HashMap::<(Pubkey, SocketAddr), bool>::new(); + let keypair = self.keypair(); let mut ping_cache = self.ping_cache.lock().unwrap(); let mut hard_check = move |node| { - let (check, ping) = ping_cache.check(rng, &self.keypair(), now, node); + let (check, ping) = ping_cache.check(rng, &keypair, now, node); if let Some(ping) = ping { let ping = Protocol::PingMessage(ping); if let Some(pkt) = make_gossip_packet(node.1, &ping, &self.stats) { diff --git a/gossip/src/crds.rs b/gossip/src/crds.rs index 0d8cbec60c8e54..0ae6e09b183dc3 100644 --- a/gossip/src/crds.rs +++ b/gossip/src/crds.rs @@ -137,6 +137,10 @@ pub struct VersionedCrdsValue { pub struct Cursor(u64); impl Cursor { + pub fn new(slot: Slot) -> Self { + Self(slot) + } + fn ordinal(&self) -> u64 { self.0 }