use std::collections::BTreeSet;
use cyber_hemera::hash as hemera_hash;
use tru::{Context, FocusingParams, Fx};
use crate::settlement::{self, Contribution};
const SETTLE_DOMAIN: &[u8] = b"foculus-ticket-v0";
const FOLD_DOMAIN: &[u8] = b"foculus-fold-v0";
const MARGIN_DOMAIN: &[u8] = b"foculus-marginal-v0";
const ACC_DOMAIN: &[u8] = b"foculus-acc-v0";
pub const DEFAULT_SETTLE_TARGET: u64 = 1u64 << 32;
pub fn easy_target() -> u64 {
u64::MAX
}
pub type ClusterId = [u8; 32];
#[derive(Clone, Debug)]
pub struct SettlementTicket {
pub miner: [u8; 32],
pub nonce: u64,
pub marginals: Vec<Fx>,
pub commitment: [u8; 32],
pub score: u64,
}
#[derive(Clone, Debug)]
pub struct FoldTicket {
pub miner: [u8; 32],
pub nonce: u64,
pub level: u32,
pub pair_id: [u8; 32],
pub left: ClusterAcc,
pub right: ClusterAcc,
pub result: ClusterAcc,
pub score: u64,
}
#[derive(Clone, Debug, Default)]
pub struct ClusterAcc {
pub sum_m: Vec<Fx>,
pub k: u64,
pub seen: BTreeSet<([u8; 32], u64)>,
pub commitment: [u8; 32],
}
impl ClusterAcc {
pub fn empty(n_contrib: usize) -> Self {
Self {
sum_m: vec![Fx::ZERO; n_contrib],
k: 0,
seen: BTreeSet::new(),
commitment: [0u8; 32],
}
}
pub fn mean_shares(&self, neurons: &span>; 32) -> Vec<([u8; 32], Fx)> {
if self.k == 0 || self.sum_m.len() != neurons.len() {
return neurons.iter().map(|n| (*n, Fx::ZERO)).collect();
}
let inv = Fx::ONE.div(Fx::from_int(self.k as i64));
neurons
.iter()
.zip(self.sum_m.iter())
.map(|(n, m)| (*n, *m * inv))
.collect()
}
pub fn k_min(epsilon: f64, delta: f64) -> u64 {
if epsilon <= 0.0 || delta <= 0.0 || delta >= 1.0 {
return 1;
}
let num = (2.0 / delta).ln();
let den = 2.0 * epsilon * epsilon;
(num / den).ceil().max(1.0) as u64
}
pub fn meets_precision(&self, epsilon: f64, delta: f64) -> bool {
self.k >= Self::k_min(epsilon, delta)
}
}
pub fn commit_marginals(marginals: &[Fx]) -> [u8; 32] {
let mut buf = Vec::with_capacity(MARGIN_DOMAIN.len() + 8 + marginals.len() * 8);
buf.extend_from_slice(MARGIN_DOMAIN);
buf.extend_from_slice(&(marginals.len() as u64).to_le_bytes());
for m in marginals {
buf.extend_from_slice(&m.raw().as_u64().to_le_bytes());
}
hash32(&buf)
}
pub fn settle_score(
beacon: &[u8; 32],
cluster: &ClusterId,
nonce: u64,
miner: &[u8; 32],
commitment: &[u8; 32],
) -> u64 {
let mut buf = Vec::with_capacity(SETTLE_DOMAIN.len() + 32 * 3 + 8 + 32);
buf.extend_from_slice(SETTLE_DOMAIN);
buf.extend_from_slice(beacon);
buf.extend_from_slice(cluster);
buf.extend_from_slice(&nonce.to_le_bytes());
buf.extend_from_slice(miner);
buf.extend_from_slice(commitment);
score_u64(&hash32(&buf))
}
pub fn try_settlement_ticket(
base: &[tru::Link],
contribs: &[Contribution],
ctx: &Context,
params: &FocusingParams,
beacon: &[u8; 32],
cluster: &ClusterId,
miner: &[u8; 32],
nonce: u64,
target: u64,
) -> Option<SettlementTicket> {
let n = contribs.len();
if n == 0 {
return None;
}
let order = settlement::ordering(n, beacon, nonce);
let m = settlement::marginals(base, contribs, &order, ctx, params);
let commitment = commit_marginals(&m);
let score = settle_score(beacon, cluster, nonce, miner, &commitment);
if score >= target {
return None;
}
Some(SettlementTicket {
miner: *miner,
nonce,
marginals: m,
commitment,
score,
})
}
pub fn grind_settlement(
base: &[tru::Link],
contribs: &[Contribution],
ctx: &Context,
params: &FocusingParams,
beacon: &[u8; 32],
cluster: &ClusterId,
miner: &[u8; 32],
start_nonce: u64,
max_attempts: u64,
want: usize,
target: u64,
) -> Vec<SettlementTicket> {
let mut out = Vec::with_capacity(want);
for i in 0..max_attempts {
if out.len() >= want {
break;
}
if let Some(t) = try_settlement_ticket(
base,
contribs,
ctx,
params,
beacon,
cluster,
miner,
start_nonce.saturating_add(i),
target,
) {
out.push(t);
}
}
out
}
pub fn verify_settlement_ticket(
ticket: &SettlementTicket,
beacon: &[u8; 32],
cluster: &ClusterId,
target: u64,
) -> bool {
if commit_marginals(&ticket.marginals) != ticket.commitment {
return false;
}
let s = settle_score(
beacon,
cluster,
ticket.nonce,
&ticket.miner,
&ticket.commitment,
);
s == ticket.score && s < target
}
pub fn absorb_ticket(acc: &mut ClusterAcc, ticket: &SettlementTicket) {
if !acc.seen.insert((ticket.miner, ticket.nonce)) {
return;
}
if acc.sum_m.len() != ticket.marginals.len() {
if acc.k == 0 {
acc.sum_m = vec![Fx::ZERO; ticket.marginals.len()];
} else {
return; }
}
for (s, m) in acc.sum_m.iter_mut().zip(ticket.marginals.iter()) {
*s = *s + *m;
}
acc.k = acc.k.saturating_add(1);
acc.commitment = acc_commitment(acc);
}
pub fn self_fold(n_contrib: usize, tickets: &[SettlementTicket]) -> ClusterAcc {
let mut acc = ClusterAcc::empty(n_contrib);
for t in tickets {
absorb_ticket(&mut acc, t);
}
acc
}
pub fn fold_acc(left: &ClusterAcc, right: &ClusterAcc) -> ClusterAcc {
let n = left.sum_m.len().max(right.sum_m.len());
let mut out = ClusterAcc::empty(n);
for t in left.seen.iter().chain(right.seen.iter()) {
out.seen.insert(*t);
}
if left.sum_m.len() == right.sum_m.len() && left.sum_m.len() == n {
for i in 0..n {
out.sum_m[i] = left.sum_m[i] + right.sum_m[i];
}
let inter = left.seen.intersection(&right.seen).count() as u64;
if inter == 0 {
out.k = left.k.saturating_add(right.k);
} else {
out.k = out.seen.len() as u64;
if left.k + right.k > 0 {
for i in 0..n {
out.sum_m[i] = left.sum_m[i] + right.sum_m[i];
}
}
}
} else if left.k >= right.k {
out = left.clone();
for t in &right.seen {
out.seen.insert(*t);
}
out.k = out.seen.len() as u64;
} else {
out = right.clone();
for t in &left.seen {
out.seen.insert(*t);
}
out.k = out.seen.len() as u64;
}
out.commitment = acc_commitment(&out);
out
}
pub fn pair_id(left: &ClusterAcc, right: &ClusterAcc) -> [u8; 32] {
let mut a = left.commitment;
let mut b = right.commitment;
if a > b {
std::mem::swap(&mut a, &mut b);
}
let mut buf = Vec::with_capacity(64);
buf.extend_from_slice(&a);
buf.extend_from_slice(&b);
hash32(&buf)
}
pub fn fold_score(
beacon: &[u8; 32],
cluster: &ClusterId,
level: u32,
pair: &[u8; 32],
nonce: u64,
result_commitment: &[u8; 32],
) -> u64 {
let mut buf = Vec::with_capacity(FOLD_DOMAIN.len() + 32 * 3 + 4 + 8);
buf.extend_from_slice(FOLD_DOMAIN);
buf.extend_from_slice(beacon);
buf.extend_from_slice(cluster);
buf.extend_from_slice(b"fold");
buf.extend_from_slice(&level.to_le_bytes());
buf.extend_from_slice(pair);
buf.extend_from_slice(&nonce.to_le_bytes());
buf.extend_from_slice(result_commitment);
score_u64(&hash32(&buf))
}
pub fn try_fold_ticket(
beacon: &[u8; 32],
cluster: &ClusterId,
miner: &[u8; 32],
level: u32,
left: &ClusterAcc,
right: &ClusterAcc,
nonce: u64,
target: u64,
) -> Option<FoldTicket> {
let result = fold_acc(left, right);
let pid = pair_id(left, right);
let score = fold_score(beacon, cluster, level, &pid, nonce, &result.commitment);
if score >= target {
return None;
}
Some(FoldTicket {
miner: *miner,
nonce,
level,
pair_id: pid,
left: left.clone(),
right: right.clone(),
result,
score,
})
}
pub fn grind_fold(
beacon: &[u8; 32],
cluster: &ClusterId,
miner: &[u8; 32],
level: u32,
left: &ClusterAcc,
right: &ClusterAcc,
start_nonce: u64,
max_attempts: u64,
target: u64,
) -> Option<FoldTicket> {
for i in 0..max_attempts {
if let Some(t) = try_fold_ticket(
beacon,
cluster,
miner,
level,
left,
right,
start_nonce.saturating_add(i),
target,
) {
return Some(t);
}
}
None
}
pub fn assemble_fold_tree(
beacon: &[u8; 32],
cluster: &ClusterId,
miner: &[u8; 32],
leaves: &[ClusterAcc],
fold_target: u64,
) -> ClusterAcc {
if leaves.is_empty() {
return ClusterAcc::default();
}
let mut level: Vec<ClusterAcc> = leaves.to_vec();
let mut lvl = 0u32;
while level.len() > 1 {
let mut next = Vec::new();
let mut i = 0;
while i < level.len() {
if i + 1 >= level.len() {
next.push(level[i].clone());
break;
}
let left = &level[i];
let right = &level[i + 1];
let folded = grind_fold(
beacon,
cluster,
miner,
lvl,
left,
right,
0,
64,
fold_target,
)
.map(|t| t.result)
.unwrap_or_else(|| fold_acc(left, right));
next.push(folded);
i += 2;
}
level = next;
lvl = lvl.saturating_add(1);
}
level.into_iter().next().unwrap_or_default()
}
fn acc_commitment(acc: &ClusterAcc) -> [u8; 32] {
let mut buf = Vec::with_capacity(ACC_DOMAIN.len() + 16 + acc.sum_m.len() * 8 + acc.seen.len() * 40);
buf.extend_from_slice(ACC_DOMAIN);
buf.extend_from_slice(&acc.k.to_le_bytes());
buf.extend_from_slice(&(acc.sum_m.len() as u64).to_le_bytes());
for m in &acc.sum_m {
buf.extend_from_slice(&m.raw().as_u64().to_le_bytes());
}
for (miner, nonce) in &acc.seen {
buf.extend_from_slice(miner);
buf.extend_from_slice(&nonce.to_le_bytes());
}
hash32(&buf)
}
fn hash32(buf: &[u8]) -> [u8; 32] {
*hemera_hash(buf)
.as_bytes()
.first_chunk::<32>()
.unwrap_or(&[0u8; 32])
}
fn score_u64(h: &[u8; 32]) -> u64 {
u64::from_be_bytes(h[0..8].try_into().unwrap_or([0u8; 8]))
}
#[cfg(test)]
mod tests {
use super::*;
use tru::Link;
fn h(b: u8) -> [u8; 32] {
let mut x = [0u8; 32];
x[0] = b;
x
}
fn base() -> Vec<Link> {
vec![
Link::stake(h(1), h(2), 100),
Link::stake(h(2), h(3), 100),
Link::stake(h(3), h(1), 100),
]
}
fn contribs() -> Vec<Contribution> {
vec![
Contribution {
neuron: h(10),
links: vec![Link::stake(h(2), h(1), 8000)],
surprise: Fx::ONE,
},
Contribution {
neuron: h(11),
links: vec![Link::stake(h(3), h(1), 6000)],
surprise: Fx::ONE,
},
]
}
#[test]
fn easy_grind_finds_winners() {
let c = contribs();
let cluster = h(0xC1);
let beacon = h(0xBE);
let tickets = grind_settlement(
&base(),
&c,
&Context::none(),
&FocusingParams::default(),
&beacon,
&cluster,
&h(0x91),
0,
32,
4,
easy_target(),
);
assert_eq!(tickets.len(), 4);
for t in &tickets {
assert!(verify_settlement_ticket(t, &beacon, &cluster, easy_target()));
}
}
#[test]
fn self_fold_counts_samples() {
let c = contribs();
let cluster = h(0xC1);
let beacon = h(0xBE);
let tickets = grind_settlement(
&base(),
&c,
&Context::none(),
&FocusingParams::default(),
&beacon,
&cluster,
&h(0x91),
0,
16,
3,
easy_target(),
);
let acc = self_fold(c.len(), &tickets);
assert_eq!(acc.k, 3);
assert_eq!(acc.seen.len(), 3);
let mut acc2 = acc.clone();
absorb_ticket(&mut acc2, &tickets[0]);
assert_eq!(acc2.k, 3);
}
#[test]
fn fold_acc_is_commutative() {
let c = contribs();
let cluster = h(0xC1);
let beacon = h(0xBE);
let a_tickets = grind_settlement(
&base(),
&c,
&Context::none(),
&FocusingParams::default(),
&beacon,
&cluster,
&h(0xA),
0,
8,
2,
easy_target(),
);
let b_tickets = grind_settlement(
&base(),
&c,
&Context::none(),
&FocusingParams::default(),
&beacon,
&cluster,
&h(0xB),
100,
8,
2,
easy_target(),
);
let a = self_fold(c.len(), &a_tickets);
let b = self_fold(c.len(), &b_tickets);
let ab = fold_acc(&a, &b);
let ba = fold_acc(&b, &a);
assert_eq!(ab.k, ba.k);
assert_eq!(ab.seen, ba.seen);
for i in 0..c.len() {
assert!((ab.sum_m[i].to_f64() - ba.sum_m[i].to_f64()).abs() < 1e-9);
}
}
#[test]
fn assemble_tree_merges_miners() {
let c = contribs();
let cluster = h(0xC1);
let beacon = h(0xBE);
let leaves: Vec<_> = [0xA, 0xB, 0xC]
.iter()
.map(|&m| {
let t = grind_settlement(
&base(),
&c,
&Context::none(),
&FocusingParams::default(),
&beacon,
&cluster,
&h(m),
m as u64 * 50,
8,
2,
easy_target(),
);
self_fold(c.len(), &t)
})
.collect();
let root = assemble_fold_tree(&beacon, &cluster, &h(0xFF), &leaves, easy_target());
assert!(root.k >= 6);
let neurons: Vec<_> = c.iter().map(|x| x.neuron).collect();
let shares = root.mean_shares(&neurons);
let sum: f64 = shares.iter().map(|(_, s)| s.to_f64()).sum();
assert!(sum > 0.0);
}
#[test]
fn k_min_hoeffding() {
let k = ClusterAcc::k_min(0.1, 0.01);
assert!(k > 100 && k < 400);
}
}