soft3/cybernode/rs/src/node.rs

//! Local HTTP adapter over Cybergraph's durable native coordinator.
//!
//! Acceptance, economics and recovery are owned by Cybergraph and BBG.
//! See `soft3/specs/native-node.md` for the HTTP and storage lifecycle contract.

pub mod chain_node;
mod chain_routes;
mod genesis;
mod http;
mod requests;
mod routes;

use std::io;
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{SystemTime, UNIX_EPOCH};

use cybergraph::native::{ImportReport, NativeNode};
use cybergraph::{NeuronId, Particle};

use crate::network::Network;

/// Runtime adapter for one local spacepussy-test node.
pub struct Node {
    moniker: String,
    network: Network,
    genesis_unix: u64,
    native: NativeNode,
    failed: bool,
}

impl Node {
    pub fn open(home: PathBuf, moniker: String) -> io::Result<Self> {
        std::fs::create_dir_all(&home)?;
        let database = home.join("bbg");
        let legacy = home.join("log");
        if legacy.try_exists()? && !database.try_exists()? {
            return Err(io::Error::other(
                "legacy log requires cyber storage import-legacy",
            ));
        }
        let genesis = genesis::load(&home, !database.try_exists()?)?;
        let native = NativeNode::open(&database, &genesis.canonical).map_err(io::Error::other)?;
        if legacy.try_exists()? {
            let log = genesis::read_bounded(&legacy, genesis::MAX_LEGACY_LOG)?;
            if !native
                .legacy_source_matches(&log)
                .map_err(io::Error::other)?
            {
                return Err(io::Error::other("legacy log differs from the imported source; explicit storage recovery is required"));
            }
        }
        Ok(Self {
            moniker,
            network: Network::SpacePussyTest,
            genesis_unix: genesis.time,
            native,
            failed: false,
        })
    }

    pub fn height(&self) -> u64 {
        self.native.graph().bbg.state.height
    }

    pub fn root_hex(&self) -> String {
        hex(&self.native.graph().bbg.state.root())
    }

    pub fn signal_count(&self) -> u64 {
        self.native.graph().bbg.state.signals.len() as u64
    }

    pub fn intent_count(&self) -> u64 {
        self.native.graph().bbg.state.intents.len() as u64
    }

    pub fn neuron_count(&self) -> u64 {
        self.native.graph().bbg.state.neurons.len() as u64
    }

    pub fn particle_count(&self) -> u64 {
        self.native.graph().bbg.state.particles.len() as u64
    }

    pub fn axon_count(&self) -> u64 {
        self.native.graph().bbg.state.axons_out.len() as u64
    }

    pub fn balance(&self, neuron: &NeuronId) -> u64 {
        self.native.balance(neuron)
    }

    pub fn pi_supply(&self) -> u64 {
        self.native.supply()
    }

    pub fn status_cybermark(&self) -> String {
        let stats = self.native.graph().bbg.statistics();
        format!(
            "---\nparticle: status\nchain: {}\nprotocol: soft3/spacepussy-test/v2\nengine: cybergraph+bbg\nversion: {}\nmoniker: {}\nheight: {}\ngenesis: {}\nbbg-root: {}\nsignals: {}\nneurons: {}\nparticles: {}\naxons: {}\nnode-count: {}\nmax-degree: {}\nrole: {}\ndenom: {}\nprefix: {}\nrpc: {}\ncatching-up: false\nstorage: bbg-fjall\nintents: {}\n---\nthe state of spacepussy-test at height {}\n",
            self.network.chain_id(), env!("CARGO_PKG_VERSION"),
            self.moniker.replace(['\n', '\r'], " "), self.height(),
            self.genesis_unix, self.root_hex(), self.signal_count(),
            self.neuron_count(), self.particle_count(), self.axon_count(),
            stats.node_count, stats.max_degree, self.network.role(),
            self.network.denom(), self.network.bech32_prefix(), self.network.rpc(),
            self.intent_count(), self.height(),
        )
    }
}

/// Import the retained legacy log into a fresh BBG directory.
/// The product caller holds the home lock for this entire operation.
pub fn import_legacy(home: &Path) -> io::Result<ImportReport> {
    let genesis = genesis::load(home, false)?;
    let log = genesis::read_bounded(&home.join("log"), genesis::MAX_LEGACY_LOG)?;
    NativeNode::import_legacy(&home.join("bbg"), &genesis.canonical, &log).map_err(io::Error::other)
}

/// Preserve the existing hex/label identity convention.
fn key32(s: &str) -> Particle {
    let candidate = s.strip_prefix("0x").unwrap_or(s);
    if !candidate.is_empty()
        && candidate.len() <= 64
        && candidate.bytes().all(|b| b.is_ascii_hexdigit())
    {
        let padded = format!("{candidate:0>64}");
        let mut out = [0u8; 32];
        for (i, byte) in out.iter_mut().enumerate() {
            // The ASCII hex check above establishes valid byte pairs.
            *byte = u8::from_str_radix(&padded[i * 2..i * 2 + 2], 16)
                .expect("validated hexadecimal pair");
        }
        out
    } else {
        hash32(s.as_bytes())
    }
}

fn hash32(bytes: &[u8]) -> Particle {
    let digest = hemera::hash(bytes);
    let mut out = [0; 32];
    out.copy_from_slice(&digest.as_bytes()[..32]);
    out
}

fn hex(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{b:02x}")).collect()
}

fn now_unix() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
}

pub fn default_home() -> PathBuf {
    let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
    PathBuf::from(home).join(".spacepussy-test")
}

/// Validate and recover durable state before binding the listener.
pub fn run(home: PathBuf, bind: &str, moniker: &str) -> io::Result<()> {
    let node = Node::open(home.clone(), moniker.to_owned())?;
    let listener = TcpListener::bind(bind)?;
    eprintln!(
        "soft3 node ยท {} ยท moniker={} ยท home={}",
        node.network.chain_id(),
        moniker,
        home.display()
    );
    eprintln!(
        "  listening {bind}; height {}; root {}",
        node.height(),
        node.root_hex()
    );
    let node = Arc::new(Mutex::new(node));
    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                let node = Arc::clone(&node);
                thread::spawn(move || {
                    if let Err(error) = http::handle_client(stream, &node) {
                        eprintln!("  request error: {error}");
                    }
                });
            }
            Err(error) => eprintln!("  accept error: {error}"),
        }
    }
    Ok(())
}

/// Run a phase-1 chain node (specs/chain.md): open the chain, start the
/// wire, serve the HTTP surface of chain-wire ยง7 on `bind`.
pub fn run_chain(opts: chain_node::ChainOpts, bind: &str) -> io::Result<()> {
    let listener = TcpListener::bind(bind)?;
    let node = chain_node::ChainNode::open(opts)?;
    let tip = node.chain.tip();
    eprintln!(
        "soft3 node ยท chain {} ยท moniker={} ยท home={}",
        node.chain_name, node.moniker, node.home.display()
    );
    eprintln!("  endpoint {}", hex(node.net.id().as_bytes()));
    for s in node.net.bound() {
        eprintln!("  radio    {s}");
    }
    eprintln!("  http     {bind}; height {}; tip {}", tip.height, hex(&tip.id));
    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                let node = Arc::clone(&node);
                thread::spawn(move || {
                    if let Err(error) = http::handle_client_with(stream, |request| chain_routes::route(&node, request)) {
                        eprintln!("  request error: {error}");
                    }
                });
            }
            Err(error) => eprintln!("  accept error: {error}"),
        }
    }
    Ok(())
}

#[cfg(test)]
mod legacy_guard_tests {
    use super::*;

    fn temp_home(label: &str) -> PathBuf {
        let mut dir = std::env::temp_dir();
        dir.push(format!(
            "soft3-node-open-test-{label}-{}-{}",
            std::process::id(),
            now_unix()
        ));
        dir
    }

    #[test]
    fn open_rejects_legacy_log_without_database() {
        let home = temp_home("legacy-only");
        std::fs::create_dir_all(&home).unwrap();
        std::fs::write(home.join("log"), b"legacy-signal-log").unwrap();

        let error = match Node::open(home.clone(), "test".into()) {
            Err(error) => error,
            Ok(_) => panic!("expected the legacy-migration guard to reject this home"),
        };
        assert_eq!(
            error.to_string(),
            "legacy log requires cyber storage import-legacy"
        );

        std::fs::remove_dir_all(&home).ok();
    }

    #[test]
    fn open_skips_legacy_guard_once_database_exists() {
        let home = temp_home("legacy-and-db");
        std::fs::create_dir_all(&home).unwrap();
        std::fs::write(home.join("log"), b"legacy-signal-log").unwrap();
        std::fs::create_dir_all(home.join("bbg")).unwrap();

        // The database directory already exists, so the migration guard must not
        // fire; the failure past it comes from genesis load, not the guard.
        let error = match Node::open(home.clone(), "test".into()) {
            Err(error) => error,
            Ok(_) => panic!("expected genesis load to fail on a database with no genesis.json"),
        };
        assert_ne!(
            error.to_string(),
            "legacy log requires cyber storage import-legacy"
        );

        std::fs::remove_dir_all(&home).ok();
    }

    #[test]
    fn open_succeeds_on_a_fresh_home_without_a_legacy_log() {
        let home = temp_home("fresh");

        let node = match Node::open(home.clone(), "test".into()) {
            Ok(node) => node,
            Err(error) => panic!("expected a fresh home to open cleanly: {error}"),
        };
        assert_eq!(node.height(), 0);

        std::fs::remove_dir_all(&home).ok();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn key32_passes_through_full_hex() {
        let hex_str = "ab".repeat(32);
        let expected: Vec<u8> = (0..32).map(|_| 0xab).collect();
        assert_eq!(key32(&hex_str).as_slice(), expected.as_slice());
    }

    #[test]
    fn key32_strips_0x_prefix() {
        let hex_str = "ab".repeat(32);
        assert_eq!(key32(&hex_str), key32(&format!("0x{hex_str}")));
    }

    #[test]
    fn key32_left_pads_short_hex() {
        assert_eq!(key32("ab"), key32(&format!("{}ab", "0".repeat(62))));
    }

    #[test]
    fn key32_accepts_uppercase_hex() {
        let hex_str = "ab".repeat(32);
        assert_eq!(key32(&hex_str), key32(&hex_str.to_uppercase()));
    }

    #[test]
    fn key32_falls_back_to_hash_for_non_hex_label() {
        assert_eq!(key32("not-hex-zz"), hash32(b"not-hex-zz"));
    }

    #[test]
    fn key32_falls_back_to_hash_when_longer_than_32_bytes() {
        let too_long = "ab".repeat(33);
        assert_eq!(key32(&too_long), hash32(too_long.as_bytes()));
    }

    #[test]
    fn key32_falls_back_to_hash_for_empty_string() {
        assert_eq!(key32(""), hash32(b""));
        assert_eq!(key32("0x"), hash32(b"0x"));
    }

    #[test]
    fn hash32_is_deterministic_and_distinct() {
        assert_eq!(hash32(b"a"), hash32(b"a"));
        assert_ne!(hash32(b"a"), hash32(b"b"));
    }
}

Homonyms

cyber/tests/node.rs
warriors/trisha/cli/node.rs
soft3/foculus/src/node.rs
soft3/crate/src/node.rs
soft3/radio/iroh-ffi/src/node.rs
cyb/evy/crates/evy_engine_dispatch/src/node.rs
soft3/zheng/rs/src/execution/node.rs
cyb/evy/forks/bevy_ecs/src/schedule/node.rs
cyb/evy/forks/bevy_core_pipeline/src/tonemapping/node.rs
cyb/evy/forks/bevy_anti_alias/src/dlss/node.rs
cyb/evy/forks/bevy_render/src/render_graph/node.rs
cyb/evy/forks/bevy_anti_alias/src/contrast_adaptive_sharpening/node.rs
cyb/evy/forks/bevy_post_process/src/motion_blur/node.rs
cyb/evy/forks/bevy_anti_alias/src/fxaa/node.rs
cyb/evy/forks/bevy_pbr/src/atmosphere/node.rs
cyb/evy/forks/bevy_core_pipeline/src/upscaling/node.rs
cyb/evy/forks/bevy_post_process/src/auto_exposure/node.rs
neural/trident/src/neural/data/tir_graph/node.rs
cyb/evy/forks/bevy_core_pipeline/src/prepass/node.rs
cyb/evy/forks/bevy_core_pipeline/src/deferred/node.rs
cyb/evy/forks/bevy_core_pipeline/src/oit/resolve/node.rs

Graph