use cyber_hemera::hash as hemera_hash;
use tru::{
bts_scores, impulse, propose, surprise, Context, FocusingParams, Fx, Link, Report,
};
use crate::beacon::{claims_root, open_beacon, verify_beacon, BeaconArtifact, TEST_OUTER_T};
use crate::settlement::{self, Contribution};
use crate::ticket_proof::{prove_fold_tree, verify_fold_seal, FoldSeal};
use crate::tickets::{
assemble_fold_tree, easy_target, grind_settlement, self_fold, ClusterAcc,
};
#[derive(Clone)]
pub struct RewardClaim {
pub id: [u8; 32],
pub neuron: [u8; 32],
pub links: Vec<Link>,
pub belief: Fx,
pub prediction: Fx,
}
#[derive(Clone, Debug)]
pub struct SettledShare {
pub neuron: [u8; 32],
pub share: Fx,
pub amount: u64,
}
#[derive(Clone, Debug)]
pub struct SettleReceipt {
pub epoch: u64,
pub beacon: [u8; 32],
pub claims_root: [u8; 32],
pub receipt_hash: [u8; 32],
pub directed_total: Fx,
pub budget: u64,
pub shares: Vec<SettledShare>,
pub beacon_artifact: Option<BeaconArtifact>,
pub ticket_seal: Option<FoldSeal>,
pub fold_seal: Option<FoldSeal>,
pub sample_count: u64,
}
#[derive(Debug, PartialEq, Eq)]
pub enum RewardError {
EmptyClaims,
NoPositiveShare,
BudgetZero,
NoTickets,
}
#[derive(Clone, Debug)]
pub struct TicketPolicy {
pub want: usize,
pub max_attempts: u64,
pub start_nonce: u64,
pub settle_target: u64,
pub fold_target: u64,
pub miner: [u8; 32],
}
impl Default for TicketPolicy {
fn default() -> Self {
Self {
want: 8,
max_attempts: 256,
start_nonce: 0,
settle_target: easy_target(),
fold_target: easy_target(),
miner: [0u8; 32],
}
}
}
pub fn valence_to_prediction(v: i8) -> Fx {
match v {
1 => Fx::from_ratio(3, 4),
-1 => Fx::from_ratio(1, 4),
_ => Fx::from_ratio(1, 2),
}
}
pub fn claim_from_links(
id: [u8; 32],
neuron: [u8; 32],
links: Vec<Link>,
valence: i8,
) -> RewardClaim {
RewardClaim {
id,
neuron,
links,
belief: Fx::from_ratio(3, 4),
prediction: valence_to_prediction(valence),
}
}
pub fn contributions_with_rho(claims: &[RewardClaim]) -> Vec<Contribution> {
let reports: Vec<Report> = claims
.iter()
.map(|c| Report {
neuron: c.neuron,
belief: c.belief,
prediction: c.prediction,
})
.collect();
let scores = bts_scores(&reports);
let mut s_max = Fx::ONE;
for &s in &scores {
if s > s_max {
s_max = s;
}
}
if s_max <= Fx::ZERO {
s_max = Fx::ONE;
}
let n = claims.len();
let mut rhos: Vec<Fx> = if n < 2 {
vec![Fx::ONE]
} else {
scores.iter().map(|&s| surprise(s, s_max)).collect()
};
if n >= 2 && rhos.iter().all(|r| *r <= Fx::ZERO) {
rhos = vec![Fx::ONE; n];
}
claims
.iter()
.enumerate()
.map(|(i, c)| Contribution {
neuron: c.neuron,
links: c.links.clone(),
surprise: rhos[i],
})
.collect()
}
pub fn propose_claim(
base: &[Link],
claim: &RewardClaim,
ctx: &Context,
params: &FocusingParams,
) -> Fx {
propose(base, &claim.links, ctx, params)
}
pub fn settle_epoch(
epoch: u64,
prev_beacon: &[u8; 32],
base: &[Link],
claims: &[RewardClaim],
ctx: &Context,
params: &FocusingParams,
samples: u64,
budget: u64,
) -> Result<SettleReceipt, RewardError> {
if claims.is_empty() {
return Err(RewardError::EmptyClaims);
}
if budget == 0 {
return Err(RewardError::BudgetZero);
}
let ids: Vec<[u8; 32]> = claims.iter().map(|c| c.id).collect();
let cr = claims_root(&ids);
let art = open_beacon(epoch, prev_beacon, &cr, &[], TEST_OUTER_T);
if !verify_beacon(&art) {
return Err(RewardError::NoTickets); }
let b_e = art.beacon;
let contribs = contributions_with_rho(claims);
let all_links: Vec<Link> = claims.iter().flat_map(|c| c.links.clone()).collect();
let directed_total = impulse(base, &all_links, ctx, params, params.epsilon).directed;
let raw_shares = settlement::shapley(base, &contribs, ctx, params, samples, &b_e);
let shares = allocate_budget(&raw_shares, budget, directed_total)?;
let receipt = SettleReceipt {
epoch,
beacon: b_e,
claims_root: cr,
receipt_hash: receipt_hash(epoch, &b_e, &shares),
directed_total,
budget,
shares,
beacon_artifact: Some(art),
ticket_seal: None,
fold_seal: None,
sample_count: samples,
};
Ok(receipt)
}
pub fn settle_epoch_tickets(
epoch: u64,
prev_beacon: &[u8; 32],
base: &[Link],
claims: &[RewardClaim],
ctx: &Context,
params: &FocusingParams,
budget: u64,
policy: &TicketPolicy,
) -> Result<SettleReceipt, RewardError> {
settle_with_peer_accs(
epoch,
prev_beacon,
base,
claims,
ctx,
params,
budget,
policy,
&[],
)
}
pub fn settle_with_peer_accs(
epoch: u64,
prev_beacon: &[u8; 32],
base: &[Link],
claims: &[RewardClaim],
ctx: &Context,
params: &FocusingParams,
budget: u64,
policy: &TicketPolicy,
peer_accs: &[ClusterAcc],
) -> Result<SettleReceipt, RewardError> {
if claims.is_empty() {
return Err(RewardError::EmptyClaims);
}
if budget == 0 {
return Err(RewardError::BudgetZero);
}
let ids: Vec<[u8; 32]> = claims.iter().map(|c| c.id).collect();
let cr = claims_root(&ids);
let art = open_beacon(epoch, prev_beacon, &cr, &[], TEST_OUTER_T);
if !verify_beacon(&art) {
return Err(RewardError::NoTickets);
}
let b_e = art.beacon;
let contribs = contributions_with_rho(claims);
let all_links: Vec<Link> = claims.iter().flat_map(|c| c.links.clone()).collect();
let directed_total = impulse(base, &all_links, ctx, params, params.epsilon).directed;
let tickets = grind_settlement(
base,
&contribs,
ctx,
params,
&b_e,
&cr,
&policy.miner,
policy.start_nonce,
policy.max_attempts,
policy.want,
policy.settle_target,
);
if tickets.is_empty() && peer_accs.is_empty() {
return Err(RewardError::NoTickets);
}
let ticket_seal = if !tickets.is_empty() {
let seal = crate::marginal_cert::prove_replayed_batch(
base, &contribs, ctx, params, &b_e, &cr, &tickets,
)
.map_err(|_| RewardError::NoTickets)?;
if !verify_fold_seal(&seal) {
return Err(RewardError::NoTickets);
}
Some(seal)
} else {
None
};
let local = self_fold(contribs.len(), &tickets);
let mut leaves = vec![local];
leaves.extend(peer_accs.iter().cloned());
let (root, fold_seal) = match prove_fold_tree(&b_e, &cr, &leaves) {
Ok((r, seal)) if verify_fold_seal(&seal) => (r, Some(seal)),
_ => {
let r = assemble_fold_tree(&b_e, &cr, &policy.miner, &leaves, policy.fold_target);
(r, None)
}
};
if root.k == 0 {
return Err(RewardError::NoTickets);
}
let neurons: Vec<[u8; 32]> = contribs.iter().map(|c| c.neuron).collect();
let raw_shares = root.mean_shares(&neurons);
let shares = allocate_budget(&raw_shares, budget, directed_total)?;
Ok(SettleReceipt {
epoch,
beacon: b_e,
claims_root: cr,
receipt_hash: receipt_hash(epoch, &b_e, &shares),
directed_total,
budget,
shares,
beacon_artifact: Some(art),
ticket_seal,
fold_seal,
sample_count: root.k,
})
}
pub const DEFAULT_EMISSION_SCALE: u64 = 1_000_000_000;
pub fn allocate_budget_pub(
raw: &[([u8; 32], Fx)],
budget: u64,
directed_total: Fx,
) -> Result<Vec<SettledShare>, RewardError> {
allocate_budget(raw, budget, directed_total)
}
pub fn receipt_hash_pub(epoch: u64, beacon: &[u8; 32], shares: &[SettledShare]) -> [u8; 32] {
receipt_hash(epoch, beacon, shares)
}
fn allocate_budget(
raw: &[([u8; 32], Fx)],
budget: u64,
directed_total: Fx,
) -> Result<Vec<SettledShare>, RewardError> {
let conserved = tok::clip_shares(raw, directed_total).map_err(|e| match e {
tok::ConserveError::Empty => RewardError::EmptyClaims,
tok::ConserveError::NoPositiveShare => RewardError::NoPositiveShare,
tok::ConserveError::BudgetZero => RewardError::BudgetZero,
})?;
let weights: Vec<u128> = conserved.iter().map(|(_, s)| fx_weight(*s)).collect();
let sum_w: u128 = weights.iter().sum();
if sum_w == 0 {
return Err(RewardError::NoPositiveShare);
}
let mut out = Vec::with_capacity(conserved.len());
let mut allocated = 0u64;
for (i, (neuron, share)) in conserved.iter().enumerate() {
let amt = if i + 1 == conserved.len() {
budget.saturating_sub(allocated)
} else {
let a = ((weights[i] * budget as u128) / sum_w) as u64;
allocated = allocated.saturating_add(a);
a
};
out.push(SettledShare {
neuron: *neuron,
share: *share,
amount: if weights[i] == 0 { 0 } else { amt },
});
}
for (i, w) in weights.iter().enumerate() {
if *w == 0 {
out[i].amount = 0;
}
}
let paid: u64 = out.iter().map(|s| s.amount).sum();
if paid < budget {
if let Some(s) = out.iter_mut().find(|s| fx_weight(s.share) > 0) {
s.amount = s.amount.saturating_add(budget - paid);
}
}
Ok(out)
}
pub fn mint_receipt_to_ledger(
ledger: &mut tok::MintLedger,
token: tok::TokenId,
receipt: &SettleReceipt,
emission_scale: u64,
) -> Result<tok::MintReceipt, tok::MintError> {
let raw: Vec<_> = receipt
.shares
.iter()
.map(|s| (s.neuron, s.share))
.collect();
tok::execute_settle_mints(
ledger,
token,
&raw,
receipt.directed_total,
emission_scale,
receipt.budget,
&receipt.receipt_hash,
)
}
fn fx_weight(x: Fx) -> u128 {
if x <= Fx::ZERO {
return 0;
}
let f = x.to_f64();
if f <= 0.0 {
return 0;
}
let w = (f * 1_000_000_000_000.0) as u128;
w.max(1)
}
fn receipt_hash(epoch: u64, beacon: &[u8; 32], shares: &[SettledShare]) -> [u8; 32] {
let mut sorted = shares.to_vec();
sorted.sort_by(|a, b| a.neuron.cmp(&b.neuron));
let mut buf = Vec::with_capacity(8 + 32 + sorted.len() * 40);
buf.extend_from_slice(&epoch.to_le_bytes());
buf.extend_from_slice(beacon);
for s in &sorted {
buf.extend_from_slice(&s.neuron);
buf.extend_from_slice(&s.amount.to_le_bytes());
}
*hemera_hash(&buf)
.as_bytes()
.first_chunk::<32>()
.unwrap_or(&[0u8; 32])
}
pub fn verify_receipt(receipt: &SettleReceipt) -> bool {
if receipt.receipt_hash != receipt_hash(receipt.epoch, &receipt.beacon, &receipt.shares) {
return false;
}
if let Some(art) = &receipt.beacon_artifact {
if !verify_beacon(art) || art.beacon != receipt.beacon {
return false;
}
}
if let Some(seal) = &receipt.ticket_seal {
if !verify_fold_seal(seal) {
return false;
}
}
if let Some(seal) = &receipt.fold_seal {
if !verify_fold_seal(seal) {
return false;
}
}
true
}
pub fn share_of(receipt: &SettleReceipt, neuron: &[u8; 32]) -> u64 {
receipt
.shares
.iter()
.find(|s| s.neuron == *neuron)
.map(|s| s.amount)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::beacon::GENESIS_PREV;
use tru::FocusingParams;
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),
]
}
#[test]
fn solo_claim_earns_budget() {
let params = FocusingParams::default();
let claim = claim_from_links(
h(0xA1),
h(10),
vec![Link::stake(h(2), h(1), 8000)],
1,
);
let rec = settle_epoch(
1,
&GENESIS_PREV,
&base(),
&[claim],
&Context::none(),
¶ms,
16,
1000,
)
.unwrap();
assert!(verify_receipt(&rec));
assert_eq!(share_of(&rec, &h(10)), 1000);
}
#[test]
fn two_contributors_split_budget() {
let params = FocusingParams::default();
let a = claim_from_links(
h(0xA1),
h(10),
vec![Link::stake(h(2), h(1), 8000)],
1,
);
let b = claim_from_links(
h(0xB2),
h(11),
vec![Link::stake(h(3), h(1), 6000)],
1,
);
let rec = settle_epoch(
1,
&GENESIS_PREV,
&base(),
&[a, b],
&Context::none(),
¶ms,
32,
1000,
)
.unwrap();
assert!(verify_receipt(&rec));
let sa = share_of(&rec, &h(10));
let sb = share_of(&rec, &h(11));
assert_eq!(sa + sb, 1000, "conservation of budget");
assert!(sa > 0 || sb > 0);
}
#[test]
fn copy_rho_zero_earns_nothing_when_bts_scores() {
let raw = vec![(h(1), Fx::ONE), (h(2), Fx::ZERO)];
let shares = allocate_budget(&raw, 100, Fx::ONE).unwrap();
assert_eq!(shares[0].amount, 100);
assert_eq!(shares[1].amount, 0);
}
#[test]
fn empty_claims_err() {
let err = settle_epoch(
1,
&GENESIS_PREV,
&base(),
&[],
&Context::none(),
&FocusingParams::default(),
8,
100,
);
assert_eq!(err.unwrap_err(), RewardError::EmptyClaims);
}
#[test]
fn ticket_settle_matches_budget() {
let params = FocusingParams::default();
let claim = claim_from_links(
h(0xA1),
h(10),
vec![Link::stake(h(2), h(1), 8000)],
1,
);
let policy = TicketPolicy {
want: 4,
max_attempts: 64,
miner: h(10),
..TicketPolicy::default()
};
let rec = settle_epoch_tickets(
1,
&GENESIS_PREV,
&base(),
&[claim],
&Context::none(),
¶ms,
1000,
&policy,
)
.unwrap();
assert!(verify_receipt(&rec));
assert_eq!(share_of(&rec, &h(10)), 1000);
}
}