pub(crate) mod error;
mod hasher;
use error::{Error, Result};
use hasher::Sha256Hasher;
use sn_protocol::messages::{Hash, MerkleTreeNodesType};
use merkletree::{
hash::Algorithm,
merkle::{next_pow2, MerkleTree},
proof::Proof,
store::VecStore,
};
use std::collections::BTreeMap;
use typenum::{UInt, UTerm, B0, B1};
use xor_name::XorName;
pub type PaymentProofsTrailInfoMap = BTreeMap<XorName, (Vec<MerkleTreeNodesType>, Vec<usize>)>;
type BinaryMerkletreeProofType = Proof<MerkleTreeNodesType, UInt<UInt<UTerm, B1>, B0>>;
pub fn build_payment_proofs<'a>(
content_addrs: impl Iterator<Item = &'a XorName>,
) -> Result<(Hash, PaymentProofsTrailInfoMap)> {
let mut addrs: Vec<_> = content_addrs
.map(|addr| {
let mut arr = MerkleTreeNodesType::default();
arr.copy_from_slice(addr);
arr
})
.collect();
if addrs.is_empty() {
return Err(Error::ProofTree(
"Cannot build payment proofs with an empty list of addresses".to_string(),
));
}
let num_of_leaves = usize::max(2, next_pow2(addrs.len()));
let num_of_addrs = addrs.len();
for _ in num_of_addrs..num_of_leaves {
addrs.push(MerkleTreeNodesType::default());
}
let merkletree = MerkleTree::<MerkleTreeNodesType, Sha256Hasher, VecStore<_>>::new(
addrs.clone().into_iter(),
)
.map_err(|err| Error::ProofTree(err.to_string()))?;
let root_hash = merkletree.root().into();
let mut payment_proofs = BTreeMap::new();
for (index, addr) in addrs.into_iter().take(num_of_addrs).enumerate() {
let proof = merkletree
.gen_proof(index)
.map_err(|err| Error::GenAuditTrail {
index,
reason: err.to_string(),
})?;
payment_proofs.insert(
XorName(addr),
(proof.lemma().to_vec(), proof.path().to_vec()),
);
}
Ok((root_hash, payment_proofs))
}
pub fn validate_payment_proof(
addr_name: XorName,
root_hash: &Hash,
audit_trail: &[MerkleTreeNodesType],
path: &[usize],
) -> Result<usize> {
trace!("Verifying payment proof for chunk store {addr_name:?} ...");
let proof =
BinaryMerkletreeProofType::new::<UTerm, UTerm>(None, audit_trail.to_vec(), path.to_vec())
.map_err(|err| Error::InvalidAuditTrail(err.to_string()))?;
let mut hasher = Sha256Hasher::default();
let leaf_to_validate = hasher.leaf(addr_name.0);
if leaf_to_validate != proof.item() {
return Err(Error::AuditTrailItemMismatch(addr_name));
}
if *root_hash != proof.root().into() {
return Err(Error::RootHashMismatch(*root_hash));
}
if !proof
.validate::<Sha256Hasher>()
.map_err(|err| Error::AuditTrailSelfValidation(err.to_string()))?
{
return Err(Error::AuditTrailSelfValidation(
"inclusion proof validation failed".to_string(),
));
}
let leaf_index = leaf_index(path);
Ok(leaf_index)
}
fn leaf_index(path: &[usize]) -> usize {
let mut leaf_index = 0;
path.iter().rev().for_each(|p| {
leaf_index = (leaf_index << 1) + p;
});
leaf_index
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use eyre::{eyre, Result};
use tiny_keccak::{Hasher, Sha3};
use xor_name::XorName;
fn hash(prefix: u8, bytes_l: &[u8], bytes_r: &[u8]) -> MerkleTreeNodesType {
let mut sha3 = Sha3::v256();
sha3.update(&[prefix]);
sha3.update(bytes_l);
sha3.update(bytes_r);
let mut hash = MerkleTreeNodesType::default();
sha3.finalize(&mut hash);
hash
}
#[test]
fn test_payment_proof_basic() -> Result<()> {
assert!(
matches!(build_payment_proofs(vec![].iter()), Err(Error::ProofTree(err)) if err == "Cannot build payment proofs with an empty list of addresses")
);
let name0 = XorName([11; 32]);
let name1 = XorName([22; 32]);
let name2 = XorName([33; 32]);
let leaf0 = hash(0, &name0.0, &[]);
let leaf1 = hash(0, &name1.0, &[]);
let leaf2 = hash(0, &name2.0, &[]);
let leaf3 = hash(0, &MerkleTreeNodesType::default(), &[]);
let node0 = hash(1, &leaf0, &leaf1);
let node1 = hash(1, &leaf2, &leaf3);
let root = hash(1, &node0, &node1);
let addrs = [name0, name1, name2];
let (root_hash, payment_proofs) = build_payment_proofs(addrs.iter())?;
assert_eq!(payment_proofs.len(), addrs.len());
assert_eq!(root_hash, root.into());
Ok(())
}
#[test]
fn test_payment_proof_validation() -> Result<()> {
let name0 = XorName([11; 32]);
let name1 = XorName([22; 32]);
let name2 = XorName([33; 32]);
let addrs = [name0, name1, name2];
let (root_hash, payment_proofs) = build_payment_proofs(addrs.iter())?;
assert_eq!(payment_proofs.len(), addrs.len());
assert!(
matches!(payment_proofs.get(&name0), Some((audit_trail, path))
if matches!(validate_payment_proof(name0, &root_hash, audit_trail, path), Ok(0))
)
);
assert!(
matches!(payment_proofs.get(&name1), Some((audit_trail, path))
if matches!(validate_payment_proof(name1, &root_hash, audit_trail, path), Ok(1))
)
);
assert!(
matches!(payment_proofs.get(&name2), Some(( audit_trail, path))
if matches!(validate_payment_proof(name2, &root_hash, audit_trail, path), Ok(2))
)
);
let (audit_trail, path) = payment_proofs
.get(&name2)
.cloned()
.ok_or_else(|| eyre!("Failed to obtain valid payment proof"))?;
let invalid_name = XorName([99; 32]);
assert!(matches!(
validate_payment_proof(invalid_name, &root_hash, &audit_trail, &path),
Err(Error::AuditTrailItemMismatch(name)) if name == invalid_name
));
let mut corrupted_audit_trail = audit_trail.clone();
corrupted_audit_trail[1][0] = 0; assert!(matches!(
validate_payment_proof(name2, &root_hash, &corrupted_audit_trail, &path),
Err(Error::AuditTrailSelfValidation(_))
));
let mut corrupted_audit_trail = audit_trail.clone();
corrupted_audit_trail.push(name0.0); assert!(matches!(
validate_payment_proof(name2, &root_hash, &corrupted_audit_trail, &path),
Err(Error::InvalidAuditTrail(_))
));
let invalid_root_hash: Hash = [66; 32].into();
assert!(matches!(
validate_payment_proof(name2, &invalid_root_hash, &audit_trail, &path),
Err(Error::RootHashMismatch(root_hash)) if root_hash == invalid_root_hash
));
Ok(())
}
proptest! {
#[test]
fn test_payment_proof_non_power_of_2_input(num_of_addrs in 1..1000) {
let mut rng = rand::thread_rng();
let random_names = (0..num_of_addrs).map(|_| XorName::random(&mut rng)).collect::<Vec<_>>();
let (root_hash, payment_proofs) = build_payment_proofs(random_names.iter())?;
assert_eq!(payment_proofs.len(), num_of_addrs as usize);
for (index, xorname) in random_names.into_iter().enumerate() {
let (audit_trail, path) = payment_proofs.get(&xorname).expect("Missing payment proof for addr: {addr:?}");
assert_eq!(leaf_index(path), index);
matches!(validate_payment_proof(xorname, &root_hash, audit_trail, path), Ok(leaf_index) if leaf_index == index);
}
}
}
}