use std::collections::BTreeMap;
use std::io::{self, IsTerminal};
use anyhow::{bail, Context as _, Result};
use clap::builder::styling::{AnsiColor, Styles};
use clap::{Parser, Subcommand};
use tru::{compute_focusing, Context, FocusingGraph, FocusingParams, Fx, Link};
use crate::chain::{CyberlinkRecord, Signal, SELF_NETWORK};
use crate::finality::{certified, crosses_threshold, finalizes, Domain, Finality};
use crate::focus::Focus;
use crate::fork::{ForkChoice, MinHash, Serialize};
use crate::reconcile::Reconciler;
use crate::settlement::{self, Contribution};
fn tty() -> bool {
io::stdout().is_terminal()
}
fn paint(code: &str, s: &str) -> String {
if tty() {
format!("\x1b[{code}m{s}\x1b[0m")
} else {
s.to_string()
}
}
fn dim(s: &str) -> String {
paint("90", s)
}
fn cyan(s: &str) -> String {
paint("36", s)
}
fn green(s: &str) -> String {
paint("32", s)
}
fn yellow(s: &str) -> String {
paint("33", s)
}
fn red(s: &str) -> String {
paint("31", s)
}
fn bold(s: &str) -> String {
paint("1", s)
}
fn kv(key: &str, val: &str) -> String {
format!("{} {}", dim(key), val)
}
const STYLES: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().bold())
.usage(AnsiColor::Green.on_default().bold())
.literal(AnsiColor::Cyan.on_default())
.placeholder(AnsiColor::BrightBlack.on_default());
const LOGO: &str = "\
\x1b[31m███████╗ ██████╗ ██████╗██╗ ██╗██╗ ██╗ ██╗███████╗\x1b[0m
\x1b[33m██╔════╝██╔═══██╗██╔════╝██║ ██║██║ ██║ ██║██╔════╝\x1b[0m
\x1b[32m█████╗ ██║ ██║██║ ██║ ██║██║ ██║ ██║███████╗\x1b[0m
\x1b[36m██╔══╝ ██║ ██║██║ ██║ ██║██║ ██║ ██║╚════██║\x1b[0m
\x1b[34m██║ ╚██████╔╝╚██████╗╚██████╔╝███████╗╚██████╔╝███████║\x1b[0m
\x1b[35m╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝\x1b[0m";
fn banner() -> String {
if !tty() {
return String::new();
}
format!(
"{LOGO}\n{tag}\n{params}\n",
tag = paint("37", " consensus by convergence"),
params = dim(
"\n φ* · fork-choice · finality · settlement · sync\n \
Goldilocks field · p = 2⁶⁴ − 2³² + 1 · fixed-point\n"
),
)
}
#[derive(Parser)]
#[command(
name = "foculus",
about = "consensus by convergence — φ*, fork-choice, finality, settlement, sync",
version,
styles = STYLES
)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Focus {
#[arg(long = "link", value_name = "FROM:TO:AMOUNT", required = true)]
links: Vec<String>,
},
Reconcile {
#[arg(long, default_value = "minhash")]
strategy: String,
#[arg(long = "signal", value_name = "NEURON:STEP:FROM:TO", required = true)]
signals: Vec<String>,
},
Conflicts {
#[arg(long = "signal", value_name = "NEURON:STEP:FROM:TO", required = true)]
signals: Vec<String>,
},
Finality {
#[arg(long = "phi", value_name = "VALUE", required = true)]
phi: Vec<String>,
#[arg(long)]
target: String,
#[arg(long = "kappa-prime", default_value = "1.5")]
kappa_prime: String,
#[arg(long, default_value = "0")]
uncert: String,
#[arg(long, default_value = "0.085")]
gap: String,
#[arg(long = "kappa-d", default_value = "0.74")]
kappa_d: String,
#[arg(long, default_value = "2.25")]
c: String,
},
Settle {
#[arg(long = "link", value_name = "FROM:TO:AMOUNT", required = true)]
base: Vec<String>,
#[arg(long = "contrib", value_name = "NEURON:FROM:TO:AMOUNT", required = true)]
contribs: Vec<String>,
#[arg(long, default_value = "64")]
samples: u64,
#[arg(long, default_value = "beacon")]
beacon: String,
},
Chain {
#[arg(long = "signal", value_name = "NEURON:STEP:FROM:TO", required = true)]
signals: Vec<String>,
},
#[cfg(feature = "net")]
Node(NodeArgs),
}
pub fn run() -> Result<()> {
let cli = Cli::parse();
let Some(command) = cli.command else {
print!("{}", banner());
let mut cmd = <Cli as clap::CommandFactory>::command();
cmd.print_help().ok();
return Ok(());
};
match command {
Command::Focus { links } => cmd_focus(&links),
Command::Reconcile { strategy, signals } => cmd_reconcile(&strategy, &signals),
Command::Conflicts { signals } => cmd_conflicts(&signals),
Command::Finality {
phi,
target,
kappa_prime,
uncert,
gap,
kappa_d,
c,
} => cmd_finality(&phi, &target, &kappa_prime, &uncert, &gap, &kappa_d, &c),
Command::Settle {
base,
contribs,
samples,
beacon,
} => cmd_settle(&base, &contribs, samples, &beacon),
Command::Chain { signals } => cmd_chain(&signals),
#[cfg(feature = "net")]
Command::Node(args) => node::run(args),
}
}
#[derive(Default)]
struct Labels {
to_id: BTreeMap<String, [u8; 32]>,
to_label: BTreeMap<[u8; 32], String>,
}
impl Labels {
fn id(&mut self, label: &str) -> [u8; 32] {
if let Some(id) = self.to_id.get(label) {
return *id;
}
let id = *cyber_hemera::hash(label.as_bytes())
.as_bytes()
.first_chunk::<32>()
.unwrap_or(&[0u8; 32]);
self.to_id.insert(label.to_string(), id);
self.to_label.insert(id, label.to_string());
id
}
fn label(&self, id: &[u8; 32]) -> String {
self.to_label
.get(id)
.cloned()
.unwrap_or_else(|| short_hex(id))
}
}
fn short_hex(id: &[u8; 32]) -> String {
let mut s = String::with_capacity(12);
for b in &id[..6] {
s.push_str(&format!("{:02x}", b));
}
s
}
fn parse_fx(s: &str) -> Result<Fx> {
let s = s.trim();
if let Some((int, frac)) = s.split_once('.') {
let den: i64 = 10i64
.checked_pow(frac.len() as u32)
.context("fractional part too long")?;
let int_v: i64 = if int.is_empty() || int == "-" {
0
} else {
int.parse().context("bad integer part")?
};
let frac_v: i64 = if frac.is_empty() {
0
} else {
frac.parse().context("bad fractional part")?
};
let sign = if s.starts_with('-') { -1 } else { 1 };
let num = int_v.abs() * den + frac_v;
Ok(Fx::from_int(sign * num).div(Fx::from_int(den)))
} else {
Ok(Fx::from_int(s.parse().context("bad integer")?))
}
}
fn parse_link(spec: &str, labels: &mut Labels) -> Result<CyberlinkRecord> {
let parts: Vec<&str> = spec.split(':').collect();
if parts.len() != 3 {
bail!("link must be from:to:amount, got `{spec}`");
}
let amount: u64 = parts[2].parse().context("bad amount")?;
let from = labels.id(parts[0]);
let to = labels.id(parts[1]);
Ok(CyberlinkRecord {
neuron: from,
from,
to,
token: [0u8; 32],
amount,
valence: 1,
height: 0,
})
}
fn parse_signal(spec: &str, labels: &mut Labels) -> Result<Signal> {
let parts: Vec<&str> = spec.split(':').collect();
if parts.len() != 4 {
bail!("signal must be neuron:step:from:to, got `{spec}`");
}
let neuron = labels.id(parts[0]);
let step: u64 = parts[1].parse().context("bad step")?;
let from = labels.id(parts[2]);
let to = labels.id(parts[3]);
Ok(Signal {
neuron,
network: SELF_NETWORK,
links: vec![CyberlinkRecord {
neuron,
from,
to,
token: [0u8; 32],
amount: 1,
valence: 1,
height: 0,
}],
delta_pi: vec![],
prev: [0u8; 32],
step,
height: 0,
proof: None,
})
}
fn cmd_focus(link_specs: &[String]) -> Result<()> {
let mut labels = Labels::default();
let mut links = Vec::new();
for s in link_specs {
let l = parse_link(s, &mut labels)?;
links.push(Link::stake(l.from, l.to, l.amount as u128));
}
let ctx = Context::none();
let graph = FocusingGraph::build(links, &ctx);
let params = FocusingParams::default();
let result = compute_focusing(&graph, ¶ms);
let ids = graph.node_ids();
let mut rows: Vec<(String, Fx)> = ids
.iter()
.enumerate()
.map(|(i, id)| (labels.label(id), result.focus[i]))
.collect();
rows.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
println!(
"{} {}",
green("focus"),
dim(&format!("φ* over {} particles", rows.len()))
);
let max = rows.first().map(|(_, p)| p.to_f64()).unwrap_or(1.0).max(1e-9);
for (label, phi) in &rows {
let f = phi.to_f64();
let bar = "▮".repeat(((f / max) * 16.0).round() as usize);
println!(
" {} {} {}",
cyan(&format!("{label:<14}")),
yellow(&format!("{f:.6}")),
green(&bar)
);
}
println!(
" {}",
kv("syntropy", &yellow(&format!("{:.6}", result.syntropy.to_f64())))
);
Ok(())
}
fn cmd_reconcile(strategy: &str, signal_specs: &[String]) -> Result<()> {
let mut labels = Labels::default();
let signals: Vec<Signal> = signal_specs
.iter()
.map(|s| parse_signal(s, &mut labels))
.collect::<Result<_>>()?;
let resolved = match strategy {
"serialize" => run_reconcile(Serialize, &signals),
"minhash" => run_reconcile(MinHash, &signals),
"focus" => run_reconcile(Focus::new(), &signals),
other => bail!("unknown strategy `{other}` (serialize | minhash | focus)"),
}?;
if resolved.is_empty() {
println!(
"{} {}",
green("reconcile"),
dim(&format!(
"no conflicts among {} signals — nothing to resolve",
signals.len()
))
);
return Ok(());
}
println!(
"{} {}",
green("reconcile"),
dim(&format!(
"{} conflict(s) · strategy {}",
resolved.len(),
bold(strategy)
))
);
for r in &resolved {
let who = signals
.iter()
.find(|s| s.content_id() == r.winner)
.map(|s| format!("{}:{}", labels.label(&s.neuron), s.step))
.unwrap_or_else(|| short_hex(&r.winner));
println!(
" {} {} {}",
dim(&short_hex(&r.key)),
dim("→"),
green(&format!("✓ {who}"))
);
}
Ok(())
}
fn run_reconcile<F: ForkChoice>(
fork: F,
signals: &[Signal],
) -> Result<Vec<crate::reconcile::Resolved>> {
let mut rec = Reconciler::new(fork);
let mut out = Vec::new();
for s in signals {
match rec.observe(s) {
Ok(mut r) => out.append(&mut r),
Err(e) => bail!("reconcile failed: {e:?}"),
}
}
Ok(out)
}
fn cmd_conflicts(signal_specs: &[String]) -> Result<()> {
let mut labels = Labels::default();
let mut index = crate::conflict::ConflictIndex::new();
let mut signals = Vec::new();
for s in signal_specs {
let sig = parse_signal(s, &mut labels)?;
index.observe(&sig);
signals.push(sig);
}
let conflicts = index.conflicts();
if conflicts.is_empty() {
println!(
"{} {}",
green("conflicts"),
dim(&format!("none among {} signals", signals.len()))
);
return Ok(());
}
println!(
"{} {}",
green("conflicts"),
dim(&format!("{} group(s)", conflicts.len()))
);
for key in &conflicts {
let group = index.group(key).unwrap();
let members: Vec<String> = group
.members()
.iter()
.map(|s| yellow(&format!("{}:{}", labels.label(&s.neuron), s.step)))
.collect();
println!(
" {} {}",
dim(&short_hex(key)),
members.join(&dim(" ⟂ "))
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn cmd_finality(
phi: &[String],
target: &str,
kappa_prime: &str,
uncert: &str,
gap: &str,
kappa_d: &str,
c: &str,
) -> Result<()> {
let focus: Vec<Fx> = phi.iter().map(|s| parse_fx(s)).collect::<Result<_>>()?;
let particles: Vec<[u8; 32]> = (0..focus.len() as u8).map(|b| [b; 32]).collect();
let domain = Domain::from_focus(particles, focus);
let target = parse_fx(target)?;
let kp = parse_fx(kappa_prime)?;
let uncert = parse_fx(uncert)?;
let gap = parse_fx(gap)?;
let kd = parse_fx(kappa_d)?;
let c = parse_fx(c)?;
let crosses = crosses_threshold(target, &domain, kp);
let cert = certified(uncert, gap, kd, c, kp);
let verdict = finalizes(target, &domain, uncert, gap, kd, c, kp);
let flag = |b: bool| {
if b {
green("yes")
} else {
red("no")
}
};
let num = |x: f64| yellow(&format!("{x:.6}"));
println!(
"{} {}",
green("finality"),
dim(&format!("domain of {} particles", domain.len()))
);
println!(" {}", kv("μ_D ", &num(domain.mean().to_f64())));
println!(" {}", kv("var_D ", &num(domain.variance().to_f64())));
println!(" {}", kv("φ*_target ", &num(target.to_f64())));
println!(" {}", kv("crosses τ_D ", &flag(crosses)));
println!(" {}", kv("certified P2 ", &flag(cert)));
println!(
" {} {}",
dim("verdict "),
match verdict {
Finality::Final => bold(&green("● FINAL")),
Finality::Pending => yellow("○ pending"),
}
);
Ok(())
}
fn cmd_settle(base_specs: &[String], contrib_specs: &[String], samples: u64, beacon: &str) -> Result<()> {
let mut labels = Labels::default();
let mut base = Vec::new();
for s in base_specs {
let l = parse_link(s, &mut labels)?;
base.push(Link::stake(l.from, l.to, l.amount as u128));
}
let mut contribs = Vec::new();
for s in contrib_specs {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 4 {
bail!("contrib must be neuron:from:to:amount, got `{s}`");
}
let neuron = labels.id(parts[0]);
let from = labels.id(parts[1]);
let to = labels.id(parts[2]);
let amount: u64 = parts[3].parse().context("bad amount")?;
contribs.push(Contribution {
neuron,
links: vec![Link::stake(from, to, amount as u128)],
surprise: Fx::ONE,
});
}
let beacon_id = labels.id(beacon);
let ctx = Context::none();
let params = FocusingParams::default();
let shares = settlement::shapley(&base, &contribs, &ctx, ¶ms, samples, &beacon_id);
println!(
"{} {}",
green("settle"),
dim(&format!("Shapley lottery · {samples} beacon-seeded samples"))
);
let total: f64 = shares.iter().map(|(_, s)| s.to_f64()).sum();
for (neuron, share) in &shares {
println!(
" {} {}",
cyan(&format!("{:<14}", labels.label(neuron))),
yellow(&format!("{:.6}", share.to_f64()))
);
}
println!(" {}", kv("Σ shares", &yellow(&format!("{total:.6}"))));
Ok(())
}
fn cmd_chain(signal_specs: &[String]) -> Result<()> {
use crate::chain::{ChainError, SignalChain};
let mut labels = Labels::default();
let mut chains: BTreeMap<[u8; 32], SignalChain> = BTreeMap::new();
let mut equivocations = 0usize;
let mut ok = 0usize;
for spec in signal_specs {
let sig = parse_signal(spec, &mut labels)?;
let neuron = sig.neuron;
let chain = chains.entry(neuron).or_default();
match chain.append(sig) {
Ok(()) => ok += 1,
Err(ChainError::Equivocation) => {
equivocations += 1;
println!(
" {} {} {}",
red("⚠"),
yellow("equivocation"),
dim(&short_hex(&neuron))
);
}
Err(e) => println!(
" {} {} {}",
red("✗"),
dim(&short_hex(&neuron)),
dim(&format!("{e:?}"))
),
}
}
let equiv_str = if equivocations == 0 {
green("0 equivocations")
} else {
yellow(&format!("{equivocations} equivocation(s)"))
};
println!(
"{} {}",
green("chain"),
dim(&format!(
"{} chain(s) · {ok} appended · ",
chains.len()
)) + &equiv_str
);
Ok(())
}
#[cfg(feature = "net")]
mod node {
use std::path::PathBuf;
use anyhow::Result;
use clap::{Args, Subcommand};
use crate::node::SyncNode;
#[derive(Args)]
pub struct NodeArgs {
#[arg(short, long, default_value = "~/.foculus")]
dir: String,
#[arg(short, long, default_value = "4200")]
port: u16,
#[arg(short, long, default_value = "2")]
k: usize,
#[arg(short, long, default_value = "4")]
n: usize,
#[command(subcommand)]
cmd: NodeCommand,
}
#[derive(Subcommand)]
enum NodeCommand {
Daemon {
#[arg(short, long, default_value = "30")]
interval: u64,
},
Put {
file: PathBuf,
#[arg(short, long)]
name: Option<String>,
},
Get {
name: String,
#[arg(short, long)]
output: Option<PathBuf>,
},
Rm { name: String },
Ls,
Status,
AddPeer {
node_id: String,
#[arg(short, long, default_value = "0")]
capacity: String,
},
Sync { peer: Option<String> },
Gc,
Audit,
}
pub fn run(args: NodeArgs) -> Result<()> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async move { run_async(args).await })
}
async fn run_async(args: NodeArgs) -> Result<()> {
tracing_subscriber::fmt::init();
let dir = expand_tilde(&args.dir);
let node = SyncNode::start(&dir, args.k, args.n, args.port).await?;
match args.cmd {
NodeCommand::Daemon { interval } => node.run_daemon(interval).await?,
NodeCommand::Put { file, name } => {
let data = std::fs::read(&file)?;
let name = name.unwrap_or_else(|| {
file.file_name().unwrap_or_default().to_string_lossy().to_string()
});
node.put_file(&name, &data).await?;
}
NodeCommand::Get { name, output } => {
let data = node.get_file(&name).await?;
if let Some(path) = output {
std::fs::write(&path, &data)?;
println!("wrote {} bytes to {}", data.len(), path.display());
} else {
use std::io::Write;
std::io::stdout().write_all(&data)?;
}
}
NodeCommand::Rm { name } => node.delete_file(&name).await?,
NodeCommand::Ls => {
let files = node.list_files().await;
if files.is_empty() {
println!("(no files)");
} else {
for f in &files {
println!("{f}");
}
}
}
NodeCommand::Status => {
let (files, peers, chunks, _) = node.status().await;
println!("node: {}", node.node_id());
println!("files: {files}");
println!("peers: {peers}");
println!("chunks: {chunks}");
println!("erasure: k={}, n={}", args.k, args.n);
}
NodeCommand::AddPeer { node_id, capacity } => {
let cap = parse_capacity(&capacity)?;
node.add_peer(&node_id, cap).await?;
}
NodeCommand::Sync { peer } => {
if let Some(p) = peer {
node.sync_with(&p).await?;
} else {
node.sync_all().await?;
}
}
NodeCommand::Gc => {
node.gc().await?;
}
NodeCommand::Audit => {
node.audit().await?;
}
}
node.save().await?;
Ok(())
}
fn expand_tilde(path: &str) -> PathBuf {
if let Some(rest) = path.strip_prefix("~/") {
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home).join(rest);
}
}
PathBuf::from(path)
}
fn parse_capacity(s: &str) -> Result<u64> {
let s = s.trim().to_uppercase();
if s == "0" || s.is_empty() {
return Ok(0);
}
let (num_str, mult) = if let Some(n) = s.strip_suffix("GB") {
(n, 1_000_000_000u64)
} else if let Some(n) = s.strip_suffix("MB") {
(n, 1_000_000u64)
} else if let Some(n) = s.strip_suffix("KB") {
(n, 1_000u64)
} else if let Some(n) = s.strip_suffix('B') {
(n, 1u64)
} else {
(s.as_str(), 1u64)
};
let num: u64 = num_str.trim().parse()?;
Ok(num * mult)
}
}
#[cfg(feature = "net")]
pub use node::NodeArgs;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_fx_reads_decimals_exactly() {
assert!((parse_fx("0.85").unwrap().to_f64() - 0.85).abs() < 1e-9);
assert!((parse_fx("1.5").unwrap().to_f64() - 1.5).abs() < 1e-9);
assert!((parse_fx("2").unwrap().to_f64() - 2.0).abs() < 1e-9);
assert!((parse_fx("0.003").unwrap().to_f64() - 0.003).abs() < 1e-9);
}
#[test]
fn labels_are_stable_and_reversible() {
let mut l = Labels::default();
let a1 = l.id("alice");
let a2 = l.id("alice");
assert_eq!(a1, a2, "same label → same id");
assert_eq!(l.label(&a1), "alice", "id reverses to label");
assert_ne!(l.id("bob"), a1, "distinct labels → distinct ids");
}
#[test]
fn parse_link_and_signal_shapes() {
let mut l = Labels::default();
let link = parse_link("alice:bob:100", &mut l).unwrap();
assert_eq!(link.amount, 100);
assert_eq!(link.from, l.id("alice"));
let sig = parse_signal("alice:0:x:y", &mut l).unwrap();
assert_eq!(sig.step, 0);
assert_eq!(sig.links.len(), 1);
assert!(parse_link("bad", &mut l).is_err());
assert!(parse_signal("a:b:c", &mut l).is_err());
}
}