use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::net::SocketAddrV4;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use iroh::address_lookup::MdnsAddressLookup;
use iroh::address_lookup::memory::MemoryLookup;
use iroh::endpoint::Connection;
use iroh::protocol::{AcceptError, ProtocolHandler, Router};
use iroh::{Endpoint, EndpointAddr, EndpointId, RelayMode, SecretKey};
use tokio::sync::RwLock;
use crate::gossip::{SettleMsg, Topic};
use crate::tickets::ClusterAcc;
use crate::wire::{decode_settle_msg, encode_frame, encode_settle_msg, split_frame};
pub const SETTLE_ALPN: &[u8] = b"foculus/settle/1";
#[derive(Default)]
struct RadioState {
inbox: VecDeque<SettleMsg>,
seen: BTreeSet<[u8; 32]>,
subs: BTreeSet<Topic>,
peers: BTreeMap<EndpointId, EndpointAddr>,
}
#[derive(Clone)]
struct SettleProtocol {
state: Arc<RwLock<RadioState>>,
}
impl std::fmt::Debug for SettleProtocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("SettleProtocol")
}
}
impl ProtocolHandler for SettleProtocol {
async fn accept(&self, conn: Connection) -> Result<(), AcceptError> {
let _ = handle_inbound(conn, self.state.clone()).await;
Ok(())
}
}
async fn handle_inbound(conn: Connection, state: Arc<RwLock<RadioState>>) -> Result<()> {
let mut stream = conn.accept_uni().await?;
let mut buf = Vec::new();
loop {
let mut chunk = [0u8; 8192];
let n = match stream.read(&mut chunk).await? {
None | Some(0) => break,
Some(n) => n,
};
buf.extend_from_slice(&chunk[..n]);
while let Some((frame, rest)) = split_frame(&buf) {
buf = rest.to_vec();
if let Some(msg) = decode_settle_msg(&frame) {
ingest(&state, msg).await;
}
}
}
Ok(())
}
async fn ingest(state: &Arc<RwLock<RadioState>>, msg: SettleMsg) {
let cid = msg.content_id();
let topic = msg.topic();
let mut st = state.write().await;
if !st.subs.is_empty() && !st.subs.contains(&topic) {
return;
}
if st.seen.insert(cid) {
st.inbox.push_back(msg);
}
}
pub struct SettleRadio {
endpoint: Endpoint,
state: Arc<RwLock<RadioState>>,
_router: Router,
memory_lookup: Option<MemoryLookup>,
}
impl SettleRadio {
pub async fn start(data_dir: &Path, port: u16) -> Result<Self> {
std::fs::create_dir_all(data_dir)?;
let key = load_or_create_key(&data_dir.join("settle_secret.key"))?;
let bind = SocketAddrV4::new(std::net::Ipv4Addr::UNSPECIFIED, port);
let endpoint = Endpoint::builder()
.relay_mode(RelayMode::Disabled)
.secret_key(key)
.address_lookup(MdnsAddressLookup::builder())
.bind_addr(bind)
.context("bind addr")?
.bind()
.await
.context("bind endpoint")?;
Self::from_endpoint(endpoint, None).await
}
pub async fn start_memory(lookup: MemoryLookup, secret: SecretKey) -> Result<Self> {
let endpoint = Endpoint::builder()
.relay_mode(RelayMode::Disabled)
.secret_key(secret)
.address_lookup(lookup.clone())
.bind()
.await
.context("bind memory endpoint")?;
lookup.add_endpoint_info(endpoint.addr());
Self::from_endpoint(endpoint, Some(lookup)).await
}
async fn from_endpoint(endpoint: Endpoint, memory_lookup: Option<MemoryLookup>) -> Result<Self> {
let state = Arc::new(RwLock::new(RadioState::default()));
let proto = SettleProtocol {
state: state.clone(),
};
let router = Router::builder(endpoint.clone())
.accept(SETTLE_ALPN, proto)
.spawn();
Ok(Self {
endpoint,
state,
_router: router,
memory_lookup,
})
}
pub fn endpoint_id(&self) -> EndpointId {
self.endpoint.id()
}
pub fn endpoint_addr(&self) -> EndpointAddr {
self.endpoint.addr()
}
pub async fn add_peer_addr(&self, addr: EndpointAddr) {
if let Some(lookup) = &self.memory_lookup {
lookup.add_endpoint_info(addr.clone());
}
self.state.write().await.peers.insert(addr.id, addr);
}
pub async fn add_peer_id(&self, id: EndpointId) {
self.state
.write()
.await
.peers
.entry(id)
.or_insert_with(|| EndpointAddr::new(id));
}
pub async fn subscribe(&self, topic: Topic) {
self.state.write().await.subs.insert(topic);
}
pub async fn publish(&self, msg: SettleMsg) -> Result<usize> {
let body = encode_settle_msg(&msg);
let frame = encode_frame(&body);
ingest(&self.state, msg).await;
let peers: Vec<EndpointAddr> = self.state.read().await.peers.values().cloned().collect();
let me = self.endpoint.id();
let mut sent = 0usize;
for peer in peers {
if peer.id == me {
continue;
}
match self.push_to_peer(peer, &frame).await {
Ok(()) => sent += 1,
Err(_e) => {
}
}
}
Ok(sent)
}
async fn push_to_peer(&self, peer: EndpointAddr, frame: &[u8]) -> Result<()> {
let conn = self
.endpoint
.connect(peer.clone(), SETTLE_ALPN)
.await
.with_context(|| format!("connect {}", peer.id))?;
let mut send = conn.open_uni().await.context("open uni")?;
send.write_all(frame).await.context("write")?;
send.finish().context("finish")?;
tokio::time::sleep(Duration::from_millis(20)).await;
Ok(())
}
pub async fn drain(&self) -> Vec<SettleMsg> {
let mut st = self.state.write().await;
st.inbox.drain(..).collect()
}
pub async fn collect_self_accs(&self, topic: &Topic) -> Vec<ClusterAcc> {
let msgs = self.drain().await;
let mut accs = Vec::new();
let mut rest = Vec::new();
for m in msgs {
match &m {
SettleMsg::SelfAcc { topic: t, acc, .. } if t == topic => {
accs.push(acc.clone());
}
_ => rest.push(m),
}
}
if !rest.is_empty() {
let mut st = self.state.write().await;
for m in rest {
st.inbox.push_back(m);
}
}
accs
}
pub async fn collect_claims(&self, topic: &Topic) -> Vec<crate::rewards::RewardClaim> {
let msgs = self.drain().await;
let mut claims = Vec::new();
let mut rest = Vec::new();
for m in msgs {
match m {
SettleMsg::ClaimAnnounce {
topic: t, claim, ..
} if t == *topic => claims.push(claim),
other => rest.push(other),
}
}
if !rest.is_empty() {
let mut st = self.state.write().await;
for m in rest {
st.inbox.push_back(m);
}
}
claims
}
pub async fn wait_self_accs(
&self,
topic: &Topic,
want: usize,
timeout: Duration,
) -> Vec<ClusterAcc> {
let start = std::time::Instant::now();
let mut accs = Vec::new();
while start.elapsed() < timeout {
let batch = self.collect_self_accs(topic).await;
accs.extend(batch);
let mut seen = BTreeSet::new();
accs.retain(|a| seen.insert(a.commitment));
if accs.len() >= want {
break;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
accs
}
pub fn id_string(&self) -> String {
self.endpoint.id().to_string()
}
pub fn addr_json(&self) -> String {
serde_json::to_string(&self.endpoint.addr()).unwrap_or_default()
}
pub async fn shutdown(self) -> Result<()> {
self._router.shutdown().await.ok();
Ok(())
}
}
fn load_or_create_key(path: &Path) -> Result<SecretKey> {
if path.exists() {
let bytes = std::fs::read(path)?;
let arr: [u8; 32] = bytes
.try_into()
.map_err(|_| anyhow::anyhow!("bad settle secret key"))?;
Ok(SecretKey::from(arr))
} else {
let key = SecretKey::generate(&mut rand::rng());
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, key.to_bytes())?;
Ok(key)
}
}
pub struct RadioSettleSession {
pub radio: Arc<SettleRadio>,
pub topic: Topic,
pub miner: [u8; 32],
}
impl RadioSettleSession {
pub fn new(radio: Arc<SettleRadio>, topic: Topic, miner: [u8; 32]) -> Self {
Self {
radio,
topic,
miner,
}
}
pub async fn join_topic(&self) {
self.radio.subscribe(self.topic).await;
}
pub async fn announce_claim(&self, claim: crate::rewards::RewardClaim) -> Result<usize> {
self.radio
.publish(crate::wire::claim_announce(self.topic, claim))
.await
}
pub async fn publish_self_acc(&self, acc: ClusterAcc) -> Result<usize> {
self.radio
.publish(SettleMsg::SelfAcc {
topic: self.topic,
miner: self.miner,
acc,
})
.await
}
pub async fn announce_receipt(&self, receipt_hash: [u8; 32], epoch: u64) -> Result<usize> {
self.radio
.publish(SettleMsg::ReceiptHash {
topic: self.topic,
receipt_hash,
epoch,
})
.await
}
pub async fn peer_accs(&self, want: usize, timeout: Duration) -> Vec<ClusterAcc> {
self.radio
.wait_self_accs(&self.topic, want, timeout)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::beacon::{claims_root, open_beacon, GENESIS_PREV, TEST_OUTER_T};
use crate::rewards::{
claim_from_links, contributions_with_rho, settle_with_peer_accs, verify_receipt,
TicketPolicy,
};
use crate::tickets::{grind_settlement, self_fold, easy_target};
use tru::{Context, FocusingParams, Link};
fn h(b: u8) -> [u8; 32] {
let mut x = [0u8; 32];
x[0] = b;
x
}
#[tokio::test]
async fn two_endpoints_exchange_self_acc() {
let lookup = MemoryLookup::new();
let k1 = SecretKey::generate(&mut rand::rng());
let k2 = SecretKey::generate(&mut rand::rng());
let a = SettleRadio::start_memory(lookup.clone(), k1)
.await
.expect("a");
let b = SettleRadio::start_memory(lookup.clone(), k2)
.await
.expect("b");
a.add_peer_addr(b.endpoint_addr()).await;
b.add_peer_addr(a.endpoint_addr()).await;
let topic = h(0xC1);
a.subscribe(topic).await;
b.subscribe(topic).await;
let base = vec![
Link::stake(h(1), h(2), 100),
Link::stake(h(2), h(3), 100),
Link::stake(h(3), h(1), 100),
];
let contribs = vec![crate::settlement::Contribution {
neuron: h(10),
links: vec![Link::stake(h(2), h(1), 8000)],
surprise: tru::Fx::ONE,
}];
let tickets = grind_settlement(
&base,
&contribs,
&Context::none(),
&FocusingParams::default(),
&h(0xBE),
&topic,
&h(0x91),
0,
16,
2,
easy_target(),
);
let acc = self_fold(contribs.len(), &tickets);
let sent = a
.publish(SettleMsg::SelfAcc {
topic,
miner: h(0x91),
acc: acc.clone(),
})
.await
.expect("publish");
assert!(sent >= 1, "should push to peer b");
let mut got = Vec::new();
for _ in 0..40 {
got = b.collect_self_accs(&topic).await;
if !got.is_empty() {
break;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
assert_eq!(got.len(), 1);
assert_eq!(got[0].commitment, acc.commitment);
assert_eq!(got[0].k, acc.k);
a.shutdown().await.ok();
b.shutdown().await.ok();
}
#[tokio::test]
async fn radio_multi_miner_settle_e2e() {
let lookup = MemoryLookup::new();
let miner_a = SettleRadio::start_memory(
lookup.clone(),
SecretKey::generate(&mut rand::rng()),
)
.await
.unwrap();
let miner_b = SettleRadio::start_memory(
lookup.clone(),
SecretKey::generate(&mut rand::rng()),
)
.await
.unwrap();
let settler = SettleRadio::start_memory(
lookup.clone(),
SecretKey::generate(&mut rand::rng()),
)
.await
.unwrap();
for (x, y) in [
(&miner_a, &miner_b),
(&miner_a, &settler),
(&miner_b, &settler),
(&miner_b, &miner_a),
(&settler, &miner_a),
(&settler, &miner_b),
] {
x.add_peer_addr(y.endpoint_addr()).await;
}
let claims = vec![
claim_from_links(h(0xA1), h(10), vec![Link::stake(h(2), h(1), 8000)], 1),
claim_from_links(h(0xB2), h(11), vec![Link::stake(h(3), h(1), 6000)], 1),
];
let ids: Vec<_> = claims.iter().map(|c| c.id).collect();
let topic = claims_root(&ids);
miner_a.subscribe(topic).await;
miner_b.subscribe(topic).await;
settler.subscribe(topic).await;
let base = vec![
Link::stake(h(1), h(2), 100),
Link::stake(h(2), h(3), 100),
Link::stake(h(3), h(1), 100),
];
let art = open_beacon(1, &GENESIS_PREV, &topic, &[], TEST_OUTER_T);
let b_e = art.beacon;
let contribs = contributions_with_rho(&claims);
for (radio, miner_id, start) in [
(&miner_a, h(0xA), 0u64),
(&miner_b, h(0xB), 40u64),
] {
let t = grind_settlement(
&base,
&contribs,
&Context::none(),
&FocusingParams::default(),
&b_e,
&topic,
&miner_id,
start,
32,
2,
easy_target(),
);
let acc = self_fold(contribs.len(), &t);
radio
.publish(SettleMsg::SelfAcc {
topic,
miner: miner_id,
acc,
})
.await
.unwrap();
}
let peer_accs = settler
.wait_self_accs(&topic, 2, Duration::from_secs(5))
.await;
assert!(
peer_accs.len() >= 2,
"settler should receive 2 SelfAccs, got {}",
peer_accs.len()
);
let rec = settle_with_peer_accs(
1,
&GENESIS_PREV,
&base,
&claims,
&Context::none(),
&FocusingParams::default(),
1000,
&TicketPolicy {
want: 2,
max_attempts: 32,
miner: h(0xCC),
start_nonce: 200,
..TicketPolicy::default()
},
&peer_accs,
)
.unwrap();
assert!(verify_receipt(&rec));
let paid: u64 = rec.shares.iter().map(|s| s.amount).sum();
assert_eq!(paid, 1000);
miner_a.shutdown().await.ok();
miner_b.shutdown().await.ok();
settler.shutdown().await.ok();
}
}