pub mod mining; use std::error::Error; use std::fs; use std::path::Path; use clap::{arg, command, ArgAction, Parser}; use libp2p::identity::Keypair; use libp2p::multiaddr::Multiaddr; use libp2p::{allow_block_list, connection_limits, memory_connection_limits, PeerId}; use nockapp::driver::Operation; use nockapp::kernel::boot; use nockapp::wire::Wire; use nockapp::{one_punch_driver, NockApp, NounExt}; use nockchain_bitcoin_sync::{bitcoin_watcher_driver, BitcoinRPCConnection, GenesisNodeType}; use nockchain_libp2p_io::p2p::{ MAX_ESTABLISHED_CONNECTIONS, MAX_ESTABLISHED_CONNECTIONS_PER_PEER, MAX_ESTABLISHED_INCOMING_CONNECTIONS, MAX_ESTABLISHED_OUTGOING_CONNECTIONS, MAX_PENDING_INCOMING_CONNECTIONS, MAX_PENDING_OUTGOING_CONNECTIONS, }; use termcolor::{ColorChoice, StandardStream}; use tokio::net::UnixListener; pub mod colors; use std::path::PathBuf; use clap::value_parser; use colors::*; use nockapp::noun::slab::NounSlab; use nockvm::jets::hot::HotEntry; use nockvm::noun::{D, T}; use nockvm_macros::tas; use tracing::{debug, info, instrument}; use crate::mining::MiningKeyConfig; /// Module for handling driver initialization signals pub mod driver_init { use nockapp::driver::{make_driver, IODriverFn, PokeResult}; use nockapp::noun::slab::NounSlab; use nockapp::wire::{SystemWire, Wire}; use nockvm::noun::{D, T}; use nockvm_macros::tas; use tokio::sync::oneshot; use tracing::{debug, error, info}; /// A collection of initialization signals for drivers #[derive(Default)] pub struct DriverInitSignals { /// Sender for the born signal pub born_tx: Option>, /// Receiver for the born signal pub born_rx: Option>, /// Map of driver names to their initialization signal senders pub driver_signals: std::collections::HashMap>, } impl DriverInitSignals { /// Create a new DriverInitSignals instance pub fn new() -> Self { let (born_tx, born_rx) = oneshot::channel(); Self { born_tx: Some(born_tx), born_rx: Some(born_rx), driver_signals: std::collections::HashMap::new(), } } /// Register a driver with an initialization signal pub fn register_driver(&mut self, name: &str) -> oneshot::Sender<()> { let (tx, rx) = oneshot::channel(); self.driver_signals.insert(name.to_string(), rx); tx } /// Get the initialization signal sender for a driver pub fn get_signal_sender(&self, name: &str) -> Option<&oneshot::Receiver<()>> { self.driver_signals.get(name) } /// Create a task that waits for all registered drivers to initialize pub fn create_born_task(&mut self) -> tokio::task::JoinHandle<()> { let born_tx = self.born_tx.take().expect("Born signal already used"); let driver_signals = std::mem::take(&mut self.driver_signals); tokio::spawn(async move { // Wait for all registered drivers to initialize concurrently let mut join_set = tokio::task::JoinSet::new(); for (name, rx) in driver_signals { let name = name.clone(); join_set.spawn(async move { let _ = rx.await; info!("driver '{}' initialized", name); }); } // Wait for all tasks to complete while let Some(result) = join_set.join_next().await { result.expect("Task panicked"); } // Send the born poke signal let _ = born_tx.send(()); info!("all drivers initialized, born poke sent"); }) } /// Create the born driver that waits for the born signal pub fn create_born_driver(&mut self) -> IODriverFn { let born_rx = self.born_rx.take().expect("born signal already used"); make_driver(move |handle| { Box::pin(async move { // Wait for the born signal let _ = born_rx.await; // Send the born poke let mut born_slab = NounSlab::new(); let born = T( &mut born_slab, &[D(tas!(b"command")), D(tas!(b"born")), D(0)], ); born_slab.set_root(born); let wire = SystemWire.to_wire(); let result = handle.poke(wire, born_slab).await?; match result { PokeResult::Ack => debug!("born poke acknowledged"), PokeResult::Nack => error!("Born poke nacked"), } Ok(()) }) }) } } } // TODO: command-line/configure /** Path to read current node's identity from */ pub const IDENTITY_PATH: &str = ".nockchain_identity"; /** Path to read current node's peer ID from */ pub const PEER_ID_EXTENSION: &str = ".peer_id"; // TODO: command-line/configure /** Extension for peer ID files */ const PEER_ID_FILE_EXTENSION: &str = "peerid"; // Libp2p multiaddrs don't support const construction, so we have to put strings literals and parse them at startup /** Backbone nodes for our testnet */ const TESTNET_BACKBONE_NODES: &[&str] = &[]; // Libp2p multiaddrs don't support const construction, so we have to put strings literals and parse them at startup // TODO: feature flag testnet/realnet /** Backbone nodes for our realnet */ #[allow(dead_code)] const REALNET_BACKBONE_NODES: &[&str] = &[ "/dnsaddr/nockchain-backbone.zorp.io", "/ip4/34.35.75.234/udp/30000/quic-v1", "/ip4/34.176.41.23/udp/30000/quic-v1", "/ip4/34.16.237.144/udp/30000/quic-v1", "/ip4/34.85.34.153/udp/30000/quic-v1", "/ip4/34.95.155.151/udp/30000/quic-v1", "/ip4/34.97.242.48/udp/30000/quic-v1", "/ip4/34.162.206.28/udp/30000/quic-v1", "/ip4/34.174.22.166/udp/30000/quic-v1", "/ip4/34.129.248.106/udp/30000/quic-v1", "/ip4/34.18.98.38/udp/30000/quic-v1" ]; /** How often we should affirmatively ask other nodes for their heaviest chain */ const CHAIN_INTERVAL_SECS: u64 = 20; /// The height of the bitcoin block that we want to sync our genesis block to /// Currently, this is the height of an existing block for testing. It will be /// switched to a future block for launch. const GENESIS_HEIGHT: u64 = 897767; /// Command line arguments #[derive(Parser, Debug, Clone)] #[command(name = "nockchain")] pub struct NockchainCli { #[command(flatten)] pub nockapp_cli: nockapp::kernel::boot::Cli, #[arg( long, help = "npc socket path", default_value = ".socket/nockchain_npc.sock" )] pub npc_socket: String, #[arg(long, help = "Mine in-kernel", default_value = "false")] pub mine: bool, #[arg( long, help = "Pubkey to mine to (mutually exclusive with --mining-key-adv)" )] pub mining_pubkey: Option, #[arg( long, help = "Advanced mining key configuration (mutually exclusive with --mining-pubkey). Format: share,m:key1,key2,key3", value_parser = value_parser!(MiningKeyConfig), num_args = 1.., value_delimiter = ',', )] pub mining_key_adv: Option>, #[arg(long, help = "Watch for genesis block", default_value = "false")] pub genesis_watcher: bool, #[arg(long, help = "Mine genesis block", default_value = "false")] pub genesis_leader: bool, #[arg(long, help = "use fake genesis block", default_value = "false")] pub fakenet: bool, #[arg(long, help = "Genesis block message", default_value = "Hail Zorp")] pub genesis_message: String, #[arg( long, help = "URL for Bitcoin Core RPC", default_value = "http://100.98.183.39:8332" )] pub btc_node_url: String, #[arg(long, help = "Username for Bitcoin Core RPC")] pub btc_username: Option, #[arg(long, help = "Password for Bitcoin Core RPC")] pub btc_password: Option, #[arg(long, help = "Auth cookie path for Bitcoin Core RPC")] pub btc_auth_cookie: Option, #[arg(long, short, help = "Initial peer", action = ArgAction::Append)] pub peer: Vec, #[arg(long, help = "Allowed peer IDs file")] pub allowed_peers_path: Option, #[arg(long, help = "Don't dial default peers")] pub no_default_peers: bool, #[arg(long, help = "Bind address", action = ArgAction::Append)] pub bind: Vec, #[arg( long, help = "Generate a new peer ID, discarding the existing one", default_value = "false" )] pub new_peer_id: bool, #[arg(long, help = "Maximum established incoming connections")] pub max_established_incoming: Option, #[arg(long, help = "Maximum established outgoing connections")] pub max_established_outgoing: Option, #[arg(long, help = "Maximum pending incoming connections")] pub max_pending_incoming: Option, #[arg(long, help = "Maximum pending outgoing connections")] pub max_pending_outgoing: Option, #[arg(long, help = "Maximum established connections")] pub max_established: Option, #[arg(long, help = "Maximum established connections per peer")] pub max_established_per_peer: Option, #[arg(long, help = "Maximum system memory percentage for connection limits")] pub max_system_memory_fraction: Option, #[arg(long, help = "Maximum process memory for connection limits (bytes)")] pub max_system_memory_bytes: Option, } impl NockchainCli { pub fn validate(&self) -> Result<(), String> { if self.mine && !(self.mining_pubkey.is_some() || self.mining_key_adv.is_some()) { return Err( "Cannot specify mine without either mining_pubkey or mining_key_adv".to_string(), ); } if self.mining_pubkey.is_some() && self.mining_key_adv.is_some() { return Err( "Cannot specify both mining_pubkey and mining_key_adv at the same time".to_string(), ); } if self.genesis_leader && self.genesis_watcher { return Err( "Cannot specify both genesis_leader and genesis_watcher at the same time" .to_string(), ); } if !self.fakenet && (self.genesis_watcher || self.genesis_leader) { if self.btc_node_url.is_empty() { return Err( "Must specify --btc-node-url when using genesis_watcher or genesis_leader" .to_string(), ); } if self.btc_auth_cookie.is_none() { if self.btc_username.is_none() && self.btc_password.is_none() { return Err("Must specify either --btc-username or --btc-password when using genesis_watcher or genesis_leader on livenet".to_string()); } } } Ok(()) } /// Helper function to create a BitcoinRPCConnection from CLI arguments fn create_bitcoin_connection(&self) -> BitcoinRPCConnection { let url = self.btc_node_url.clone(); let height = GENESIS_HEIGHT; let auth = if let Some(username) = self.btc_username.clone() { let password = self.btc_password.clone().unwrap_or_else(|| { panic!( "Panicked at {}:{} (git sha: {:?})", file!(), line!(), option_env!("GIT_SHA") ) }); bitcoincore_rpc::Auth::UserPass(username, password) } else { let cookie_path_str = self.btc_auth_cookie.clone().unwrap_or_else(|| { panic!( "Panicked at {}:{} (git sha: {:?})", file!(), line!(), option_env!("GIT_SHA") ) }); let cookie_path = PathBuf::from(cookie_path_str); bitcoincore_rpc::Auth::CookieFile(cookie_path) }; BitcoinRPCConnection::new(url, auth, height) } } /// # Load a keypair from a file or create a new one if the file doesn't exist /// /// This function attempts to read a keypair from a specified file. If the file exists, it reads the keypair from the file. /// If the file does not exist, it generates a new keypair, writes it to the file, and returns it. /// /// # Arguments /// * `keypair_path` - A reference to a Path object representing the file path where the keypair should be stored /// * `force_new` - If true, generate a new keypair even if one already exists /// /// # Returns /// A Result containing the Keypair or an error if any operation fails pub fn gen_keypair(keypair_path: &Path) -> Result> { let new_keypair = libp2p::identity::Keypair::generate_ed25519(); let new_keypair_bytes = new_keypair.to_protobuf_encoding()?; std::fs::write(keypair_path, new_keypair_bytes)?; let peer_id = new_keypair.public().to_peer_id(); // write the peer_id encoded as base58 to a file std::fs::write( keypair_path.with_extension(PEER_ID_FILE_EXTENSION), peer_id.to_base58(), )?; info!("Generated new identity as peer {peer_id}"); Ok(new_keypair) } fn load_keypair(keypair_path: &Path, force_new: bool) -> Result> { if keypair_path.try_exists()? && !force_new { let keypair_bytes = std::fs::read(keypair_path)?; let keypair = libp2p::identity::Keypair::from_protobuf_encoding(&keypair_bytes[..])?; let peer_id = keypair.public().to_peer_id(); info!("Loaded identity as peer {peer_id}"); Ok(keypair) } else { if force_new && keypair_path.try_exists()? { info!("Discarding existing peer ID and generating a new one"); std::fs::remove_file(keypair_path)?; } gen_keypair(keypair_path) } } #[instrument(skip(kernel_jam, hot_state))] pub async fn init_with_kernel( cli: Option, kernel_jam: &[u8], hot_state: &[HotEntry], ) -> Result> { welcome(); if let Some(cli) = &cli { cli.validate()?; } let mut nockapp = boot::setup( kernel_jam, cli.as_ref().map(|c| c.nockapp_cli.clone()), hot_state, "nockchain", None, ) .await?; let keypair = { let keypair_path = Path::new(IDENTITY_PATH); load_keypair( keypair_path, cli.as_ref().map(|c| c.new_peer_id).unwrap_or(false), )? }; eprintln!( "allowed_peers_path: {:?}", cli.as_ref().unwrap().allowed_peers_path ); let allowed = cli.as_ref().and_then(|c| { c.allowed_peers_path.as_ref().map(|path| { let contents = fs::read_to_string(path).expect("failed to read allowed peers file: {}"); let peer_ids: Vec = contents .lines() .map(|line| { let peer_id_bytes = bs58::decode(line) .into_vec() .expect("failed to decode peer ID bytes from base58"); PeerId::from_bytes(&peer_id_bytes).expect("failed to decode peer ID from bytes") }) .collect(); let mut allow_behavior = allow_block_list::Behaviour::::default(); for peer_id in peer_ids { allow_behavior.allow_peer(peer_id); } allow_behavior }) }); let bind_multiaddrs = cli .as_ref() .map_or(vec!["/ip4/0.0.0.0/udp/0/quic-v1".parse()?], |c| { c.bind .clone() .into_iter() .map(|addr_str| addr_str.parse().expect("could not parse bind multiaddr")) .collect() }); let limits = connection_limits::ConnectionLimits::default() .with_max_established_incoming( cli.as_ref() .and_then(|c| c.max_established_incoming) .and(Some(MAX_ESTABLISHED_INCOMING_CONNECTIONS)), ) .with_max_established_outgoing( cli.as_ref() .and_then(|c| c.max_established_outgoing) .and(Some(MAX_ESTABLISHED_OUTGOING_CONNECTIONS)), ) .with_max_pending_incoming( cli.as_ref() .and_then(|c| c.max_pending_incoming) .and(Some(MAX_PENDING_INCOMING_CONNECTIONS)), ) .with_max_pending_outgoing( cli.as_ref() .and_then(|c| c.max_pending_outgoing) .and(Some(MAX_PENDING_OUTGOING_CONNECTIONS)), ) .with_max_established( cli.as_ref() .and_then(|c| c.max_established) .and(Some(MAX_ESTABLISHED_CONNECTIONS)), ) .with_max_established_per_peer( cli.as_ref() .and_then(|c| c.max_established_per_peer) .and(Some(MAX_ESTABLISHED_CONNECTIONS_PER_PEER)), ); let memory_limits = cli.as_ref().and_then(|c| { if c.max_system_memory_bytes.is_some() && c.max_system_memory_fraction.is_some() { panic!( "Must provide neither or one of --max-system-memory_bytes or --max-system-memory_percentage" )}; if let Some(max_bytes) = c.max_system_memory_bytes { Some(memory_connection_limits::Behaviour::with_max_bytes(max_bytes)) } else { c.max_system_memory_fraction.map(memory_connection_limits::Behaviour::with_max_percentage) } }); let default_backbone_peers = if cli.as_ref().map(|c| c.fakenet).unwrap_or(false) { TESTNET_BACKBONE_NODES } else { REALNET_BACKBONE_NODES }; let backbone_peers = default_backbone_peers .iter() .map(|multiaddr_str| { multiaddr_str .parse() .expect("could not parse multiaddr from built-in string") }) .collect(); // Set up initial peer addresses to connect to let mut peer_multiaddrs: Vec = if cli.as_ref().is_some_and(|c| c.no_default_peers) { Vec::new() } else { backbone_peers }; if let Some(c) = cli.as_ref() { let v: Vec = c .peer .clone() .into_iter() .map(|multiaddr_str| { multiaddr_str .parse() .expect("could not parse multiaddr from string") }) .collect(); peer_multiaddrs.extend(v); } debug!("peer_multiaddrs: {:?}", peer_multiaddrs); let equix_builder = equix::EquiXBuilder::new(); // Create driver initialization signals. the idea here is that we want to wait for // drivers that emit init pokes to complete before we send the born poke. let mut driver_signals = driver_init::DriverInitSignals::new(); // Register drivers that need initialization signals let mining_init_tx = driver_signals.register_driver("mining"); let libp2p_init_tx = driver_signals.register_driver("libp2p"); let watcher_init_tx = driver_signals.register_driver("bitcoin_watcher"); // Create the born task that waits for all drivers to initialize let _born_task = driver_signals.create_born_task(); if cli.as_ref().map(|c| c.fakenet).unwrap_or(false) { let message = cli .as_ref() .map(|c| c.genesis_message.clone()) .unwrap_or("".to_string()); let node_type = if cli.as_ref().map(|c| c.genesis_leader).unwrap_or(false) { GenesisNodeType::Leader } else { GenesisNodeType::Watcher }; let watcher_driver = bitcoin_watcher_driver(None, node_type, message, Some(watcher_init_tx)); nockapp.add_io_driver(watcher_driver).await; } else if cli .as_ref() .map(|c| c.genesis_watcher || c.genesis_leader) .unwrap_or(false) { let message = cli .as_ref() .map(|c| c.genesis_message.clone()) .unwrap_or("".to_string()); let connection = cli.as_ref().unwrap().create_bitcoin_connection(); let node_type = if cli.as_ref().map(|c| c.genesis_leader).unwrap_or(false) { GenesisNodeType::Leader } else { GenesisNodeType::Watcher }; let watcher_driver = bitcoin_watcher_driver(Some(connection), node_type, message, Some(watcher_init_tx)); nockapp.add_io_driver(watcher_driver).await; } else { // Realnet with no BTC node let mut poke_slab = NounSlab::new(); let poke_noun = T( &mut poke_slab, &[D(tas!(b"command")), D(tas!(b"btc-data")), D(0)], ); poke_slab.set_root(poke_noun); nockapp .poke(nockapp::wire::SystemWire.to_wire(), poke_slab) .await .expect("Failed to poke for no BTC hash"); } let mining_config = cli.as_ref().and_then(|c| { if let Some(pubkey) = &c.mining_pubkey { Some(vec![MiningKeyConfig { share: 1, m: 1, keys: vec![pubkey.clone()], }]) } else if let Some(mining_key_adv) = &c.mining_key_adv { Some(mining_key_adv.clone()) } else { None } }); let mine = cli.as_ref().map_or(false, |c| c.mine); let mining_driver = crate::mining::create_mining_driver(mining_config, mine, Some(mining_init_tx)); nockapp.add_io_driver(mining_driver).await; let libp2p_driver = nockchain_libp2p_io::nc::make_libp2p_driver( keypair, bind_multiaddrs, allowed, limits, memory_limits, &peer_multiaddrs, equix_builder, Some(libp2p_init_tx), ); nockapp.add_io_driver(libp2p_driver).await; // Create the born driver that waits for the born signal let born_driver = driver_signals.create_born_driver(); // Add the born driver to the nockapp nockapp.add_io_driver(born_driver).await; // set up socket let socket_path = Path::new( &cli.as_ref() .unwrap_or_else(|| { panic!( "Panicked at {}:{} (git sha: {:?})", file!(), line!(), option_env!("GIT_SHA") ) }) .npc_socket, ); nockapp.npc_socket_path = Some(socket_path.to_path_buf()); if let Some(parent) = socket_path.parent() { fs::create_dir_all(parent)?; } let listener = UnixListener::bind(socket_path)?; nockapp .add_io_driver(nockapp::npc_listener_driver(listener)) .await; // set up timer let mut timer_slab = NounSlab::new(); let timer_noun = T( &mut timer_slab, &[D(tas!(b"command")), D(tas!(b"timer")), D(0)], ); timer_slab.set_root(timer_noun); nockapp .add_io_driver(nockapp::timer_driver(CHAIN_INTERVAL_SECS, timer_slab)) .await; nockapp.add_io_driver(nockapp::exit_driver()).await; Ok(nockapp) } fn welcome() { let mut stdout = StandardStream::stdout(ColorChoice::Auto); let banner = " _ _ _ _ _ | \\ | | ___ ___| | _____| |__ __ _(_)_ __ | \\| |/ _ \\ / __| |/ / __| '_ \\ / _` | | '_ \\ | |\\ | (_) | (__| < (__| | | | (_| | | | | | |_| \\_|\\___/ \\___|_|\\_\\___|_| |_|\\__,_|_|_| |_| "; print_banner(&mut stdout, banner); let info = [ ("Build label", env!("BUILD_EMBED_LABEL")), ("Build host", env!("BUILD_HOST")), ("Build user", env!("BUILD_USER")), ("Build timestamp", env!("BUILD_TIMESTAMP")), ("Build date", env!("FORMATTED_DATE")), // ("Git commit", env!("BAZEL_GIT_COMMIT")), // ("Build timestamp", env!("VERGEN_BUILD_TIMESTAMP")), // ("Cargo debug", env!("VERGEN_CARGO_DEBUG")), // ("Cargo features", env!("VERGEN_CARGO_FEATURES")), // ("Cargo opt level", env!("VERGEN_CARGO_OPT_LEVEL")), // ("Cargo target", env!("VERGEN_CARGO_TARGET_TRIPLE")), // ("Git branch", env!("VERGEN_GIT_BRANCH")), // ("Git commit date", env!("VERGEN_GIT_COMMIT_DATE")), // ("Git commit author", env!("VERGEN_GIT_COMMIT_AUTHOR_NAME")), // ("Git commit message", env!("VERGEN_GIT_COMMIT_MESSAGE")), // ("Git commit timestamp", env!("VERGEN_GIT_COMMIT_TIMESTAMP")), // ("Git commit SHA", env!("VERGEN_GIT_SHA")), // ("Rustc channel", env!("VERGEN_RUSTC_CHANNEL")), // ("Rustc host", env!("VERGEN_RUSTC_HOST_TRIPLE")), // ("Rustc LLVM version", env!("VERGEN_RUSTC_LLVM_VERSION")), ]; print_version_info(&mut stdout, &info); }