pub const REWARDS_MT_HEIGHT: usize = 15;
const LEAF_PREFIX: u8 = 0;
const INTERMEDIATE_PREFIX: u8 = 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LeafClaim {
pub total_rewards: u64,
pub primary_stake: u64,
pub delegated_stake: u64,
pub epoch: u64,
}
#[derive(Clone, Debug)]
pub struct ReconstructedProof {
pub claim: LeafClaim,
pub leaf_index: u16,
pub opening: [[u8; 32]; REWARDS_MT_HEIGHT],
}
fn sha256(parts: &[&[u8]]) -> [u8; 32] {
solana_program::hash::hashv(parts).to_bytes()
}
pub fn hash_leaf(claim: &LeafClaim) -> [u8; 32] {
let mut buf = [0u8; 1 + 32];
buf[0] = LEAF_PREFIX;
buf[1..9].copy_from_slice(&claim.total_rewards.to_le_bytes());
buf[9..17].copy_from_slice(&claim.primary_stake.to_le_bytes());
buf[17..25].copy_from_slice(&claim.delegated_stake.to_le_bytes());
buf[25..33].copy_from_slice(&claim.epoch.to_le_bytes());
sha256(&[&buf])
}
fn hash_intermediate(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
sha256(&[&[INTERMEDIATE_PREFIX], left, right])
}
fn default_hashes() -> [[u8; 32]; REWARDS_MT_HEIGHT] {
let mut out = [[0u8; 32]; REWARDS_MT_HEIGHT];
out[0] = sha256(&[&[LEAF_PREFIX], &[0u8; 32]]);
for i in 1..REWARDS_MT_HEIGHT {
out[i] = hash_intermediate(&out[i - 1], &out[i - 1]);
}
out
}
fn node_is_left(level: usize, leaf_index: u64) -> bool {
(leaf_index >> level) % 2 == 0
}
fn inner_node_will_still_change(level: usize, leaf_index: u64) -> bool {
if leaf_index == 0 {
return true;
}
let chunk_size: u64 = 1 << level;
(leaf_index + 1) % chunk_size != 0
}
fn advance_frontier(
nodes: &[[u8; 32]; REWARDS_MT_HEIGHT],
leaf_count: u64,
leaf: [u8; 32],
defaults: &[[u8; 32]; REWARDS_MT_HEIGHT],
) -> [[u8; 32]; REWARDS_MT_HEIGHT] {
let mut inter = *nodes;
let mut new_nodes = *nodes;
for level in 0..REWARDS_MT_HEIGHT {
if level == 0 {
inter[0] = leaf;
new_nodes[0] = leaf;
} else {
let (left, right) = if node_is_left(level - 1, leaf_count) {
(inter[level - 1], defaults[level - 1])
} else {
(nodes[level - 1], inter[level - 1])
};
inter[level] = hash_intermediate(&left, &right);
if !inner_node_will_still_change(level, leaf_count) {
new_nodes[level] = inter[level];
}
}
}
new_nodes
}
fn subtree_hash(
level: usize,
index: u64,
batch: &[[u8; 32]],
leaf_count_pre: u64,
defaults: &[[u8; 32]; REWARDS_MT_HEIGHT],
) -> [u8; 32] {
if level == 0 {
if index >= leaf_count_pre && index < leaf_count_pre + batch.len() as u64 {
batch[(index - leaf_count_pre) as usize]
} else {
defaults[0]
}
} else {
let left = subtree_hash(level - 1, 2 * index, batch, leaf_count_pre, defaults);
let right = subtree_hash(level - 1, 2 * index + 1, batch, leaf_count_pre, defaults);
hash_intermediate(&left, &right)
}
}
pub fn reconstruct_batch(
frontier: &[[u8; 32]; REWARDS_MT_HEIGHT],
leaf_count_pre: u16,
batch: &[LeafClaim],
) -> Vec<ReconstructedProof> {
let defaults = default_hashes();
let leaf_count_pre = leaf_count_pre as u64;
let leaf_hashes: Vec<[u8; 32]> = batch.iter().map(hash_leaf).collect();
let mut frontier = *frontier;
let mut out = Vec::with_capacity(batch.len());
for (k, claim) in batch.iter().enumerate() {
let j = leaf_count_pre + k as u64;
let mut opening = [[0u8; 32]; REWARDS_MT_HEIGHT];
for (level, slot) in opening.iter_mut().enumerate() {
*slot = if node_is_left(level, j) {
subtree_hash(
level,
(j >> level) + 1,
&leaf_hashes,
leaf_count_pre,
&defaults,
)
} else {
frontier[level]
};
}
out.push(ReconstructedProof {
claim: *claim,
leaf_index: j as u16,
opening,
});
frontier = advance_frontier(&frontier, j, leaf_hashes[k], &defaults);
}
out
}
pub fn empty_frontier() -> [[u8; 32]; REWARDS_MT_HEIGHT] {
default_hashes()
}
pub fn verify_proof(
claim: &LeafClaim,
leaf_index: u16,
opening: &[[u8; 32]; REWARDS_MT_HEIGHT],
root: &[u8; 32],
) -> bool {
let mut acc = hash_leaf(claim);
let li = leaf_index as u64;
for (i, sibling) in opening.iter().enumerate() {
acc = if node_is_left(i, li) {
hash_intermediate(&acc, sibling)
} else {
hash_intermediate(sibling, &acc)
};
}
acc == *root
}
#[cfg(test)]
mod tests {
use super::*;
const ON_CHAIN_DEFAULT_0: [u8; 32] = [
127, 156, 158, 49, 172, 130, 86, 202, 47, 37, 133, 131, 223, 38, 45, 188, 125, 111, 104,
242, 160, 48, 67, 213, 201, 154, 74, 229, 167, 57, 108, 233,
];
const ON_CHAIN_DEFAULT_14: [u8; 32] = [
70, 11, 7, 114, 152, 108, 34, 153, 83, 53, 232, 228, 247, 56, 169, 118, 164, 180, 150, 145,
64, 225, 2, 196, 185, 234, 128, 131, 99, 111, 99, 177,
];
fn claim(seed: u64) -> LeafClaim {
LeafClaim {
total_rewards: seed,
primary_stake: seed.wrapping_mul(3),
delegated_stake: seed.wrapping_add(7),
epoch: seed,
}
}
#[test]
fn default_hashes_match_on_chain() {
let d = default_hashes();
assert_eq!(d[0], ON_CHAIN_DEFAULT_0);
assert_eq!(d[REWARDS_MT_HEIGHT - 1], ON_CHAIN_DEFAULT_14);
}
struct RefTree {
levels: Vec<Vec<[u8; 32]>>,
defaults: [[u8; 32]; REWARDS_MT_HEIGHT],
}
impl RefTree {
fn new() -> Self {
RefTree {
levels: vec![Vec::new(); REWARDS_MT_HEIGHT],
defaults: default_hashes(),
}
}
fn node(&self, level: usize, index: usize) -> [u8; 32] {
self.levels[level]
.get(index)
.copied()
.unwrap_or(self.defaults[level])
}
fn insert(&mut self, claim: &LeafClaim) {
self.levels[0].push(hash_leaf(claim));
let mut idx = self.levels[0].len() - 1;
for level in 0..REWARDS_MT_HEIGHT - 1 {
let parent = idx / 2;
let left = self.node(level, parent * 2);
let right = self.node(level, parent * 2 + 1);
let h = hash_intermediate(&left, &right);
if parent < self.levels[level + 1].len() {
self.levels[level + 1][parent] = h;
} else {
self.levels[level + 1].push(h);
}
idx = parent;
}
}
fn root(&self) -> [u8; 32] {
let top = self.node(REWARDS_MT_HEIGHT - 1, 0);
let top_sib = self.node(REWARDS_MT_HEIGHT - 1, 1);
hash_intermediate(&top, &top_sib)
}
fn opening(&self, leaf_index: usize) -> [[u8; 32]; REWARDS_MT_HEIGHT] {
let mut opening = [[0u8; 32]; REWARDS_MT_HEIGHT];
let mut idx = leaf_index;
for (level, slot) in opening.iter_mut().enumerate() {
let sib = if idx % 2 == 0 { idx + 1 } else { idx - 1 };
*slot = self.node(level, sib);
idx /= 2;
}
opening
}
}
#[test]
fn reconstruct_from_empty_matches_reference_and_verifies() {
for n in 1..=40usize {
let batch: Vec<LeafClaim> = (0..n as u64).map(claim).collect();
let mut reference = RefTree::new();
for c in &batch {
reference.insert(c);
}
let root = reference.root();
let proofs = reconstruct_batch(&empty_frontier(), 0, &batch);
assert_eq!(proofs.len(), n);
for (i, p) in proofs.iter().enumerate() {
assert_eq!(p.leaf_index as usize, i);
assert_eq!(
p.opening,
reference.opening(i),
"opening mismatch n={n} i={i}"
);
assert!(
verify_proof(&p.claim, p.leaf_index, &p.opening, &root),
"verify failed n={n} i={i}"
);
}
}
}
#[test]
fn reconstruct_appended_batch_matches_full_tree() {
for pre in 0..20usize {
for add in 1..=12usize {
let all: Vec<LeafClaim> = (0..(pre + add) as u64).map(claim).collect();
let mut reference = RefTree::new();
for c in &all {
reference.insert(c);
}
let root = reference.root();
let defaults = default_hashes();
let mut frontier = empty_frontier();
for (i, c) in all.iter().take(pre).enumerate() {
frontier = advance_frontier(&frontier, i as u64, hash_leaf(c), &defaults);
}
let batch = &all[pre..];
let proofs = reconstruct_batch(&frontier, pre as u16, batch);
assert_eq!(proofs.len(), add);
for (k, p) in proofs.iter().enumerate() {
let global = pre + k;
assert_eq!(p.leaf_index as usize, global);
assert!(
verify_proof(&p.claim, p.leaf_index, &p.opening, &root),
"verify failed pre={pre} add={add} k={k}"
);
}
}
}
}
}