use std::collections::HashSet;
use super::merkle_tree::{BadMerkleProof, MerkleBranch, MidpointProof};
use crate::RewardsAddress;
use evmlib::merkle_batch_payment::{
CandidateNode, CostUnitOverflow, PoolCommitment, PoolCommitmentPacked, PoolHash,
calculate_total_cost_unit, encode_data_type_and_cost,
};
use evmlib::quoting_metrics::QuotingMetrics;
use libp2p::{
PeerId,
identity::{Keypair, PublicKey},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tiny_keccak::{Hasher, Sha3};
use xor_name::XorName;
pub use evmlib::merkle_batch_payment::CANDIDATES_PER_POOL;
#[derive(Debug, Error)]
pub enum MerklePaymentVerificationError {
#[error("Winner pool hash mismatch: expected {expected:?}, got {got:?}")]
WinnerPoolHashMismatch { expected: PoolHash, got: PoolHash },
#[error("Merkle proof verification failed: {0}")]
MerkleProofFailed(#[from] BadMerkleProof),
#[error(
"Paid addresses not subset of candidate pool. Paid: {smart_contract_paid_node_addresses:?}, Candidates: {candidate_addresses:?}"
)]
PaidAddressesNotSubset {
smart_contract_paid_node_addresses: Vec<RewardsAddress>,
candidate_addresses: Vec<RewardsAddress>,
},
#[error("Wrong number of paid addresses: expected {expected}, got {got}")]
WrongPaidAddressCount { expected: usize, got: usize },
#[error("Invalid node signature for address {address}")]
InvalidNodeSignature { address: RewardsAddress },
#[error("Timestamp mismatch for node {address}: expected {expected}, got {got}")]
TimestampMismatch {
address: RewardsAddress,
expected: u64,
got: u64,
},
#[error("Pool commitment does not match the pool")]
CommitmentDoesNotMatchPool,
#[error("Paid node index {index} is out of bounds for pool size {pool_size}")]
PaidNodeIndexOutOfBounds { index: usize, pool_size: usize },
#[error("Address mismatch at index {index}: expected {expected}, got {actual}")]
PaidAddressMismatch {
index: usize,
expected: RewardsAddress,
actual: RewardsAddress,
},
#[error("Invalid node peer id for address {address} at index {index}")]
InvalidNodePeerId {
index: usize,
address: RewardsAddress,
},
#[error("Data type mismatch: expected {expected}, got {got} at node {address}")]
DataTypeMismatch {
address: RewardsAddress,
expected: u32,
got: u32,
},
#[error("Data size mismatch: expected {expected}, got {got} at node {address}")]
DataSizeMismatch {
address: RewardsAddress,
expected: usize,
got: usize,
},
#[error(
"Cost unit mismatch at candidate index {index}: on-chain packed={on_chain_packed}, expected packed={expected_packed}"
)]
CostUnitMismatch {
index: usize,
on_chain_packed: String,
expected_packed: String,
},
#[error("Winner pool hash not found in on-chain packed commitments")]
WinnerPoolNotInCommitments,
#[error("Cost unit overflow during packing: {0}")]
CostUnitOverflow(#[from] CostUnitOverflow),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct MerklePaymentCandidateNode {
pub pub_key: Vec<u8>,
pub quoting_metrics: QuotingMetrics,
pub reward_address: RewardsAddress,
pub merkle_payment_timestamp: u64,
pub signature: Vec<u8>,
}
impl MerklePaymentCandidateNode {
pub fn new(
keypair: &Keypair,
quoting_metrics: QuotingMetrics,
reward_address: RewardsAddress,
merkle_payment_timestamp: u64,
) -> Result<Self, libp2p::identity::SigningError> {
let pub_key = keypair.public().encode_protobuf();
let msg = Self::bytes_to_sign("ing_metrics, &reward_address, merkle_payment_timestamp);
let signature = keypair.sign(&msg)?;
Ok(Self {
pub_key,
quoting_metrics,
reward_address,
merkle_payment_timestamp,
signature,
})
}
pub fn bytes_to_sign(
quoting_metrics: &QuotingMetrics,
reward_address: &RewardsAddress,
timestamp: u64,
) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice("ing_metrics.to_bytes());
bytes.extend_from_slice(reward_address.as_slice());
bytes.extend_from_slice(×tamp.to_le_bytes());
bytes
}
fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.pub_key);
bytes.extend_from_slice(&self.quoting_metrics.to_bytes());
bytes.extend_from_slice(self.reward_address.as_slice());
bytes.extend_from_slice(&self.merkle_payment_timestamp.to_le_bytes());
bytes.extend_from_slice(&self.signature);
bytes
}
pub fn peer_id(&self) -> Result<PeerId, libp2p::identity::DecodingError> {
PublicKey::try_decode_protobuf(&self.pub_key).map(|pk| pk.to_peer_id())
}
pub fn verify_signature(&self) -> bool {
let pub_key = match PublicKey::try_decode_protobuf(&self.pub_key) {
Ok(pk) => pk,
Err(_) => return false,
};
let msg = Self::bytes_to_sign(
&self.quoting_metrics,
&self.reward_address,
self.merkle_payment_timestamp,
);
pub_key.verify(&msg, &self.signature)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct MerklePaymentCandidatePool {
pub midpoint_proof: MidpointProof,
pub candidate_nodes: [MerklePaymentCandidateNode; CANDIDATES_PER_POOL],
}
pub(crate) fn sha3_256(input: &[u8]) -> [u8; 32] {
let mut sha3 = Sha3::v256();
let mut output = [0u8; 32];
sha3.update(input);
sha3.finalize(&mut output);
output
}
impl MerklePaymentCandidatePool {
pub fn hash(&self) -> PoolHash {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.midpoint_proof.hash());
bytes.extend_from_slice(&(self.candidate_nodes.len() as u32).to_le_bytes());
for node in &self.candidate_nodes {
bytes.extend_from_slice(&node.to_bytes());
}
sha3_256(&bytes)
}
pub fn to_commitment(&self) -> PoolCommitment {
let candidates: [CandidateNode; CANDIDATES_PER_POOL] =
self.candidate_nodes.clone().map(|node| CandidateNode {
rewards_address: node.reward_address,
metrics: node.quoting_metrics.clone(),
});
PoolCommitment {
pool_hash: self.hash(),
candidates,
}
}
pub fn to_commitment_packed(&self) -> Result<PoolCommitmentPacked, CostUnitOverflow> {
self.to_commitment().to_packed()
}
pub fn verify_cost_units(
&self,
on_chain_commitments: &[PoolCommitmentPacked],
winner_pool_hash: &PoolHash,
) -> Result<(), MerklePaymentVerificationError> {
let on_chain_winner = on_chain_commitments
.iter()
.find(|pc| pc.pool_hash == *winner_pool_hash)
.ok_or(MerklePaymentVerificationError::WinnerPoolNotInCommitments)?;
for (i, (on_chain_candidate, signed_node)) in on_chain_winner
.candidates
.iter()
.zip(self.candidate_nodes.iter())
.enumerate()
{
let expected_data_type =
evmlib::contract::data_type_conversion(signed_node.quoting_metrics.data_type);
let expected_cost_unit = calculate_total_cost_unit(&signed_node.quoting_metrics);
let expected_packed =
encode_data_type_and_cost(expected_data_type, expected_cost_unit)?;
if on_chain_candidate.data_type_and_total_cost_unit != expected_packed {
return Err(MerklePaymentVerificationError::CostUnitMismatch {
index: i,
on_chain_packed: on_chain_candidate.data_type_and_total_cost_unit.to_string(),
expected_packed: expected_packed.to_string(),
});
}
}
Ok(())
}
pub fn verify_commitment(
&self,
commitment: &PoolCommitment,
merkle_payment_timestamp: u64,
) -> Result<(), MerklePaymentVerificationError> {
self.verify_signatures(merkle_payment_timestamp)?;
let expected_commitment = self.to_commitment();
if commitment != &expected_commitment {
return Err(MerklePaymentVerificationError::CommitmentDoesNotMatchPool);
}
Ok(())
}
pub fn candidate_nodes_addresses(&self) -> HashSet<RewardsAddress> {
self.candidate_nodes
.iter()
.map(|node| node.reward_address)
.collect()
}
pub fn verify_signatures(
&self,
merkle_payment_timestamp: u64,
) -> Result<(), MerklePaymentVerificationError> {
for node in &self.candidate_nodes {
if !node.verify_signature() {
return Err(MerklePaymentVerificationError::InvalidNodeSignature {
address: node.reward_address,
});
}
}
for node in &self.candidate_nodes {
if node.merkle_payment_timestamp != merkle_payment_timestamp {
return Err(MerklePaymentVerificationError::TimestampMismatch {
address: node.reward_address,
expected: merkle_payment_timestamp,
got: node.merkle_payment_timestamp,
});
}
}
if let Some(first_node) = self.candidate_nodes.first() {
let expected_data_type = first_node.quoting_metrics.data_type;
let expected_data_size = first_node.quoting_metrics.data_size;
for node in &self.candidate_nodes[..] {
if node.quoting_metrics.data_type != expected_data_type {
return Err(MerklePaymentVerificationError::DataTypeMismatch {
address: node.reward_address,
expected: expected_data_type,
got: node.quoting_metrics.data_type,
});
}
if node.quoting_metrics.data_size != expected_data_size {
return Err(MerklePaymentVerificationError::DataSizeMismatch {
address: node.reward_address,
expected: expected_data_size,
got: node.quoting_metrics.data_size,
});
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct MerklePaymentProof {
pub address: XorName,
pub data_proof: MerkleBranch,
pub winner_pool: MerklePaymentCandidatePool,
}
impl MerklePaymentProof {
pub fn new(
address: XorName,
data_proof: MerkleBranch,
winner_pool: MerklePaymentCandidatePool,
) -> Self {
Self {
address,
data_proof,
winner_pool,
}
}
pub fn winner_pool_hash(&self) -> PoolHash {
self.winner_pool.hash()
}
pub fn corresponding_peer_ids(
&self,
paid_nodes: &[(RewardsAddress, usize)],
) -> Result<Vec<PeerId>, MerklePaymentVerificationError> {
let mut peer_ids = Vec::with_capacity(paid_nodes.len());
for (expected_address, index) in paid_nodes {
let node = self.winner_pool.candidate_nodes.get(*index).ok_or(
MerklePaymentVerificationError::PaidNodeIndexOutOfBounds {
index: *index,
pool_size: self.winner_pool.candidate_nodes.len(),
},
)?;
if node.reward_address != *expected_address {
return Err(MerklePaymentVerificationError::PaidAddressMismatch {
index: *index,
expected: *expected_address,
actual: node.reward_address,
});
}
let peer_id =
node.peer_id()
.map_err(|_| MerklePaymentVerificationError::InvalidNodePeerId {
address: node.reward_address,
index: *index,
})?;
peer_ids.push(peer_id);
}
Ok(peer_ids)
}
pub fn verify(
&self,
smart_contract_depth: u8,
smart_contract_timestamp: u64,
smart_contract_pool_hash: &PoolHash,
smart_contract_paid_nodes: &[(RewardsAddress, usize)],
) -> Result<(), MerklePaymentVerificationError> {
self.winner_pool
.verify_signatures(smart_contract_timestamp)?;
let actual_hash = self.winner_pool.hash();
if actual_hash != *smart_contract_pool_hash {
return Err(MerklePaymentVerificationError::WinnerPoolHashMismatch {
expected: *smart_contract_pool_hash,
got: actual_hash,
});
}
let smart_contract_root = self.winner_pool.midpoint_proof.root();
crate::merkle_payments::verify_merkle_proof(
&self.address,
&self.data_proof,
&self.winner_pool.midpoint_proof,
smart_contract_depth,
smart_contract_root,
smart_contract_timestamp,
)?;
if smart_contract_paid_nodes.len() != smart_contract_depth as usize {
return Err(MerklePaymentVerificationError::WrongPaidAddressCount {
expected: smart_contract_depth as usize,
got: smart_contract_paid_nodes.len(),
});
}
self.corresponding_peer_ids(smart_contract_paid_nodes)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::merkle_payments::merkle_tree::MerkleTree;
use evmlib::quoting_metrics::QuotingMetrics;
use std::time::{SystemTime, UNIX_EPOCH};
use tempfile::TempDir;
fn make_test_addresses(count: usize) -> Vec<XorName> {
(0..count)
.map(|i| XorName::from_content(&i.to_le_bytes()))
.collect()
}
fn create_mock_quoting_metrics(node_id: usize) -> QuotingMetrics {
QuotingMetrics {
data_type: 0,
data_size: 4 * 1024 * 1024, close_records_stored: node_id * 100,
records_per_type: vec![],
max_records: 1000,
received_payment_count: node_id * 10,
live_time: 3600 + (node_id as u64),
network_density: Some([node_id as u8; 32]),
network_size: Some(1000),
}
}
fn create_test_candidate_nodes(
timestamp: u64,
) -> [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] {
std::array::from_fn(|i| {
let keypair = Keypair::generate_ed25519();
MerklePaymentCandidateNode::new(
&keypair,
create_mock_quoting_metrics(i),
RewardsAddress::from([i as u8; 20]),
timestamp,
)
.expect("Failed to create candidate node")
})
}
fn create_test_candidate_nodes_with_peer_ids(
timestamp: u64,
) -> (
[MerklePaymentCandidateNode; CANDIDATES_PER_POOL],
Vec<PeerId>,
) {
let mut peer_ids = Vec::with_capacity(CANDIDATES_PER_POOL);
let nodes = std::array::from_fn(|i| {
let keypair = Keypair::generate_ed25519();
let peer_id = keypair.public().to_peer_id();
let node = MerklePaymentCandidateNode::new(
&keypair,
create_mock_quoting_metrics(i),
RewardsAddress::from([i as u8; 20]),
timestamp,
)
.expect("Failed to create candidate node");
peer_ids.push(peer_id);
node
});
(nodes, peer_ids)
}
#[test]
fn test_candidate_node_constructor_and_signature() {
let keypair = Keypair::generate_ed25519();
let peer_id = keypair.public().to_peer_id();
let quoting_metrics = create_mock_quoting_metrics(42);
let reward_address = RewardsAddress::from([0x42; 20]);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let node = MerklePaymentCandidateNode::new(
&keypair,
quoting_metrics.clone(),
reward_address,
timestamp,
)
.expect("Failed to create candidate node");
assert_eq!(
node.peer_id().expect("Failed to derive peer_id"),
peer_id,
"PeerId should match keypair"
);
assert!(
node.verify_signature(),
"Signature should be valid for the signed data"
);
assert_eq!(node.reward_address, reward_address);
assert_eq!(node.merkle_payment_timestamp, timestamp);
assert_eq!(
node.quoting_metrics.close_records_stored,
quoting_metrics.close_records_stored
);
}
#[test]
fn test_signature_verification_with_tampering() {
let keypair = Keypair::generate_ed25519();
let quoting_metrics = create_mock_quoting_metrics(1);
let reward_address = RewardsAddress::from([0x11; 20]);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let mut node = MerklePaymentCandidateNode::new(
&keypair,
quoting_metrics.clone(),
reward_address,
timestamp,
)
.expect("Failed to create candidate node");
assert!(
node.verify_signature(),
"Original signature should be valid"
);
node.reward_address = RewardsAddress::from([0x22; 20]);
assert!(
!node.verify_signature(),
"Signature should fail after tampering with reward_address"
);
node.reward_address = reward_address;
node.quoting_metrics.close_records_stored = 999;
assert!(
!node.verify_signature(),
"Signature should fail after tampering with quoting_metrics"
);
node.quoting_metrics = quoting_metrics;
node.merkle_payment_timestamp = timestamp + 3600; assert!(
!node.verify_signature(),
"Signature should fail after tampering with timestamp"
);
let wrong_keypair = Keypair::generate_ed25519();
let wrong_node = MerklePaymentCandidateNode::new(
&wrong_keypair,
create_mock_quoting_metrics(2),
RewardsAddress::from([0x33; 20]),
timestamp,
)
.expect("Failed to create node with wrong keypair");
let original_signature = node.signature.clone();
node.signature = wrong_node.signature.clone();
assert!(
!node.verify_signature(),
"Signature from different keypair should fail"
);
node.signature = original_signature;
node.reward_address = reward_address;
node.merkle_payment_timestamp = timestamp;
assert!(
node.verify_signature(),
"Original signature should work after restoration"
);
}
#[test]
fn test_pool_commitment_verification() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let addresses = make_test_addresses(10);
let tree = MerkleTree::from_xornames(addresses).unwrap();
let reward_candidates = tree.reward_candidates(timestamp).unwrap();
let reward_pool = &reward_candidates[0];
let candidate_nodes = create_test_candidate_nodes(timestamp);
let pool = MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes,
};
let commitment = pool.to_commitment();
assert!(
pool.verify_commitment(&commitment, timestamp).is_ok(),
"Commitment should verify against original pool"
);
assert_eq!(
commitment.pool_hash,
pool.hash(),
"Commitment pool_hash should match pool.hash()"
);
let expected_candidates: [CandidateNode; CANDIDATES_PER_POOL] =
pool.candidate_nodes.clone().map(|node| CandidateNode {
rewards_address: node.reward_address,
metrics: node.quoting_metrics.clone(),
});
assert_eq!(
commitment.candidates, expected_candidates,
"Commitment candidates should match pool nodes"
);
assert_eq!(
commitment.candidates.len(),
CANDIDATES_PER_POOL,
"Should have exactly {CANDIDATES_PER_POOL} candidates",
);
let mut tampered_pool = pool.clone();
tampered_pool.candidate_nodes[0].reward_address = RewardsAddress::from([0xFF; 20]);
assert!(
tampered_pool
.verify_commitment(&commitment, timestamp)
.is_err(),
"Commitment should not verify against tampered pool"
);
let tampered_commitment = tampered_pool.to_commitment();
assert_ne!(
commitment.pool_hash, tampered_commitment.pool_hash,
"Tampered pool should have different hash"
);
assert_ne!(
commitment.candidates[0].rewards_address,
tampered_commitment.candidates[0].rewards_address,
"Tampered pool should have different addresses"
);
let commitment2 = pool.to_commitment();
assert_eq!(
commitment.pool_hash, commitment2.pool_hash,
"Same pool should generate same commitment hash"
);
assert_eq!(
commitment.candidates, commitment2.candidates,
"Same pool should generate same candidates"
);
}
#[test]
fn test_pool_verify_method() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let addresses = make_test_addresses(10);
let tree = MerkleTree::from_xornames(addresses).unwrap();
let reward_candidates = tree.reward_candidates(timestamp).unwrap();
let reward_pool = &reward_candidates[0];
let candidate_nodes = create_test_candidate_nodes(timestamp);
let pool = MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes,
};
assert!(
pool.verify_signatures(timestamp).is_ok(),
"Valid pool should verify successfully"
);
let mut invalid_sig_pool = pool.clone();
invalid_sig_pool.candidate_nodes[0].signature = vec![0xFF; 64]; assert!(
invalid_sig_pool.verify_signatures(timestamp).is_err(),
"Pool with invalid signature should fail verification"
);
let mut tampered_pool = pool.clone();
tampered_pool.candidate_nodes[0].reward_address = RewardsAddress::from([0xFF; 20]);
assert!(
tampered_pool.verify_signatures(timestamp).is_err(),
"Pool with tampered node data should fail verification (signature mismatch)"
);
}
#[test]
fn test_pool_verify_timestamp_consistency() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let addresses = make_test_addresses(10);
let tree = MerkleTree::from_xornames(addresses).unwrap();
let reward_candidates = tree.reward_candidates(timestamp).unwrap();
let reward_pool = &reward_candidates[0];
let candidate_nodes = create_test_candidate_nodes(timestamp);
let pool = MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes,
};
assert!(
pool.verify_signatures(timestamp).is_ok(),
"Pool with identical timestamps should verify"
);
let mut mismatched_pool = pool.clone();
let different_keypair = Keypair::generate_ed25519();
let different_timestamp = timestamp + 3600; mismatched_pool.candidate_nodes[5] = MerklePaymentCandidateNode::new(
&different_keypair,
create_mock_quoting_metrics(5),
RewardsAddress::from([5u8; 20]),
different_timestamp,
)
.expect("Failed to create node with different timestamp");
assert!(
mismatched_pool.verify_signatures(timestamp).is_err(),
"Pool with mismatched timestamps should fail verification"
);
}
#[test]
fn test_invalid_public_key_error() {
let keypair = Keypair::generate_ed25519();
let mut node = MerklePaymentCandidateNode::new(
&keypair,
create_mock_quoting_metrics(1),
RewardsAddress::from([0x11; 20]),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
)
.expect("Failed to create candidate node");
node.pub_key = vec![0xFF; 10];
assert!(
node.peer_id().is_err(),
"Should fail to derive peer_id from invalid pub_key"
);
assert!(
!node.verify_signature(),
"Signature verification should fail with invalid pub_key"
);
}
#[test]
fn test_node_hash_determinism() {
let keypair = Keypair::generate_ed25519();
let quoting_metrics = create_mock_quoting_metrics(42);
let reward_address = RewardsAddress::from([0x42; 20]);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let node1 = MerklePaymentCandidateNode::new(
&keypair,
quoting_metrics.clone(),
reward_address,
timestamp,
)
.expect("Failed to create first node");
let node2 =
MerklePaymentCandidateNode::new(&keypair, quoting_metrics, reward_address, timestamp)
.expect("Failed to create second node");
assert_eq!(
node1.to_bytes(),
node2.to_bytes(),
"Same inputs should produce same byte representation"
);
let addresses = make_test_addresses(10);
let tree = MerkleTree::from_xornames(addresses).unwrap();
let reward_candidates = tree.reward_candidates(timestamp).unwrap();
let reward_pool = &reward_candidates[0];
let pool1 = MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes: std::array::from_fn(|_| node1.clone()),
};
let pool2 = MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes: std::array::from_fn(|_| node2.clone()),
};
assert_eq!(
pool1.hash(),
pool2.hash(),
"Identical pools should produce identical hashes"
);
}
#[test]
fn test_corresponding_peer_ids_happy_path() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let addresses = make_test_addresses(10);
let tree = MerkleTree::from_xornames(addresses.clone()).unwrap();
let reward_candidates = tree.reward_candidates(timestamp).unwrap();
let reward_pool = &reward_candidates[0];
let (candidate_nodes, peer_ids) = create_test_candidate_nodes_with_peer_ids(timestamp);
let expected_peer_ids: Vec<_> = peer_ids
.iter()
.enumerate()
.map(|(i, pid)| (RewardsAddress::from([i as u8; 20]), *pid))
.collect();
let proof = MerklePaymentProof::new(
addresses[0],
tree.generate_address_proof(0, addresses[0]).unwrap(),
MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes,
},
);
let paid_nodes = vec![
(RewardsAddress::from([0; 20]), 0),
(RewardsAddress::from([1; 20]), 1),
(RewardsAddress::from([2; 20]), 2),
];
let result = proof.corresponding_peer_ids(&paid_nodes);
assert!(
result.is_ok(),
"Should succeed when indices and addresses match"
);
let peer_ids = result.unwrap();
assert_eq!(peer_ids.len(), 3, "Should return 3 PeerIds");
for (i, (_addr, expected_peer_id)) in expected_peer_ids.iter().take(3).enumerate() {
assert_eq!(
peer_ids[i], *expected_peer_id,
"Should return PeerId at index {i} in correct order"
);
}
}
#[test]
fn test_corresponding_peer_ids_index_out_of_bounds() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let addresses = make_test_addresses(10);
let tree = MerkleTree::from_xornames(addresses.clone()).unwrap();
let reward_candidates = tree.reward_candidates(timestamp).unwrap();
let reward_pool = &reward_candidates[0];
let candidate_nodes = create_test_candidate_nodes(timestamp);
let proof = MerklePaymentProof::new(
addresses[0],
tree.generate_address_proof(0, addresses[0]).unwrap(),
MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes,
},
);
let paid_nodes = vec![
(RewardsAddress::from([0; 20]), 0), (RewardsAddress::from([99; 20]), 99), ];
let result = proof.corresponding_peer_ids(&paid_nodes);
assert!(result.is_err(), "Should fail when index is out of bounds");
match result {
Err(MerklePaymentVerificationError::PaidNodeIndexOutOfBounds { index, pool_size }) => {
assert_eq!(index, 99);
assert_eq!(pool_size, CANDIDATES_PER_POOL);
}
_ => panic!("Expected PaidNodeIndexOutOfBounds error"),
}
}
#[test]
fn test_corresponding_peer_ids_address_mismatch() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let addresses = make_test_addresses(10);
let tree = MerkleTree::from_xornames(addresses.clone()).unwrap();
let reward_candidates = tree.reward_candidates(timestamp).unwrap();
let reward_pool = &reward_candidates[0];
let candidate_nodes = create_test_candidate_nodes(timestamp);
let proof = MerklePaymentProof::new(
addresses[0],
tree.generate_address_proof(0, addresses[0]).unwrap(),
MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes,
},
);
let paid_nodes = vec![
(RewardsAddress::from([99; 20]), 0), ];
let result = proof.corresponding_peer_ids(&paid_nodes);
assert!(
result.is_err(),
"Should fail when address doesn't match index"
);
match result {
Err(MerklePaymentVerificationError::PaidAddressMismatch {
index,
expected,
actual,
}) => {
assert_eq!(index, 0);
assert_eq!(expected, RewardsAddress::from([99; 20]));
assert_eq!(actual, RewardsAddress::from([0; 20]));
}
_ => panic!("Expected PaidAddressMismatch error"),
}
}
#[test]
fn test_corresponding_peer_ids_multiple_nodes_same_address() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let addresses = make_test_addresses(10);
let tree = MerkleTree::from_xornames(addresses.clone()).unwrap();
let reward_candidates = tree.reward_candidates(timestamp).unwrap();
let reward_pool = &reward_candidates[0];
let shared_address = RewardsAddress::from([42; 20]);
let mut candidate_nodes = Vec::new();
let mut peer_ids_for_shared_address = Vec::new();
for i in 0..3 {
let keypair = Keypair::generate_ed25519();
let peer_id = keypair.public().to_peer_id();
let node = MerklePaymentCandidateNode::new(
&keypair,
create_mock_quoting_metrics(i),
shared_address, timestamp,
)
.expect("Failed to create candidate node");
candidate_nodes.push(node);
peer_ids_for_shared_address.push(peer_id);
}
for i in 3..CANDIDATES_PER_POOL {
let keypair = Keypair::generate_ed25519();
let node = MerklePaymentCandidateNode::new(
&keypair,
create_mock_quoting_metrics(i),
RewardsAddress::from([i as u8; 20]),
timestamp,
)
.expect("Failed to create candidate node");
candidate_nodes.push(node);
}
let proof = MerklePaymentProof::new(
addresses[0],
tree.generate_address_proof(0, addresses[0]).unwrap(),
MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes: candidate_nodes
.try_into()
.expect("Should have exactly CANDIDATES_PER_POOL nodes"),
},
);
let paid_nodes = vec![
(shared_address, 0), (shared_address, 1), ];
let result = proof.corresponding_peer_ids(&paid_nodes);
assert!(
result.is_ok(),
"Should succeed when indices and addresses match"
);
let peer_ids = result.unwrap();
assert_eq!(
peer_ids.len(),
2,
"Should return exactly 2 PeerIds for the 2 paid indices"
);
assert_eq!(peer_ids[0], peer_ids_for_shared_address[0]);
assert_eq!(peer_ids[1], peer_ids_for_shared_address[1]);
let paid_nodes = vec![(shared_address, 2)];
let result = proof.corresponding_peer_ids(&paid_nodes);
assert!(result.is_ok());
let peer_ids = result.unwrap();
assert_eq!(peer_ids[0], peer_ids_for_shared_address[2]);
}
#[test]
fn test_corresponding_peer_ids_invalid_public_key() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let addresses = make_test_addresses(10);
let tree = MerkleTree::from_xornames(addresses.clone()).unwrap();
let reward_candidates = tree.reward_candidates(timestamp).unwrap();
let reward_pool = &reward_candidates[0];
let mut candidate_nodes = Vec::new();
for i in 0..CANDIDATES_PER_POOL {
let keypair = Keypair::generate_ed25519();
let node = MerklePaymentCandidateNode::new(
&keypair,
create_mock_quoting_metrics(i),
RewardsAddress::from([i as u8; 20]),
timestamp,
)
.expect("Failed to create candidate node");
candidate_nodes.push(node);
}
candidate_nodes[5].pub_key = vec![0xFF; 10];
let proof = MerklePaymentProof::new(
addresses[0],
tree.generate_address_proof(0, addresses[0]).unwrap(),
MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes: candidate_nodes
.try_into()
.expect("Should have exactly CANDIDATES_PER_POOL nodes"),
},
);
let paid_nodes = vec![(RewardsAddress::from([5; 20]), 5)];
let result = proof.corresponding_peer_ids(&paid_nodes);
assert!(
result.is_err(),
"Should fail when candidate has invalid pub_key"
);
match result {
Err(MerklePaymentVerificationError::InvalidNodePeerId { address, index }) => {
assert_eq!(address, RewardsAddress::from([5; 20]));
assert_eq!(index, 5);
}
_ => panic!("Expected InvalidNodePeerId error"),
}
}
#[test]
fn test_corresponding_peer_ids_empty_input() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let addresses = make_test_addresses(10);
let tree = MerkleTree::from_xornames(addresses.clone()).unwrap();
let reward_candidates = tree.reward_candidates(timestamp).unwrap();
let reward_pool = &reward_candidates[0];
let mut candidate_nodes = Vec::new();
for i in 0..CANDIDATES_PER_POOL {
let keypair = Keypair::generate_ed25519();
let node = MerklePaymentCandidateNode::new(
&keypair,
create_mock_quoting_metrics(i),
RewardsAddress::from([i as u8; 20]),
timestamp,
)
.expect("Failed to create candidate node");
candidate_nodes.push(node);
}
let proof = MerklePaymentProof::new(
addresses[0],
tree.generate_address_proof(0, addresses[0]).unwrap(),
MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes: candidate_nodes
.try_into()
.expect("Should have exactly CANDIDATES_PER_POOL nodes"),
},
);
let paid_nodes: Vec<(RewardsAddress, usize)> = vec![];
let result = proof.corresponding_peer_ids(&paid_nodes);
assert!(result.is_ok(), "Should succeed with empty input");
let peer_ids = result.unwrap();
assert_eq!(peer_ids.len(), 0, "Should return empty Vec for empty input");
}
#[test]
fn test_corresponding_peer_ids_partial_payment() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let addresses = make_test_addresses(10);
let tree = MerkleTree::from_xornames(addresses.clone()).unwrap();
let reward_candidates = tree.reward_candidates(timestamp).unwrap();
let reward_pool = &reward_candidates[0];
let mut candidate_nodes = Vec::new();
let mut all_peer_ids = Vec::new();
for i in 0..CANDIDATES_PER_POOL {
let keypair = Keypair::generate_ed25519();
let peer_id = keypair.public().to_peer_id();
let node = MerklePaymentCandidateNode::new(
&keypair,
create_mock_quoting_metrics(i),
RewardsAddress::from([i as u8; 20]),
timestamp,
)
.expect("Failed to create candidate node");
candidate_nodes.push(node);
all_peer_ids.push(peer_id);
}
let proof = MerklePaymentProof::new(
addresses[0],
tree.generate_address_proof(0, addresses[0]).unwrap(),
MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes: candidate_nodes
.try_into()
.expect("Should have exactly CANDIDATES_PER_POOL nodes"),
},
);
let paid_nodes = vec![
(RewardsAddress::from([0; 20]), 0),
(RewardsAddress::from([1; 20]), 1),
(RewardsAddress::from([2; 20]), 2),
(RewardsAddress::from([3; 20]), 3),
(RewardsAddress::from([4; 20]), 4),
(RewardsAddress::from([5; 20]), 5),
(RewardsAddress::from([6; 20]), 6),
];
let result = proof.corresponding_peer_ids(&paid_nodes);
assert!(
result.is_ok(),
"Should succeed when indices and addresses match"
);
let peer_ids = result.unwrap();
assert_eq!(
peer_ids.len(),
7,
"Should return only 7 PeerIds for the 7 paid nodes"
);
for i in 0..7 {
assert_eq!(
peer_ids[i], all_peer_ids[i],
"Should return PeerId at index {i} in correct order"
);
}
}
#[test]
fn test_complete_merkle_batch_payment_flow() {
let address_count = 100;
let addresses = make_test_addresses(address_count);
let tree = MerkleTree::from_xornames(addresses.clone()).unwrap();
let _root = tree.root();
let depth = tree.depth();
assert_eq!(depth, 7);
let merkle_payment_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let reward_candidates = tree.reward_candidates(merkle_payment_timestamp).unwrap();
let expected_pools = crate::merkle_payments::expected_reward_pools(depth);
assert_eq!(reward_candidates.len(), expected_pools);
let mut all_candidate_pools = Vec::new();
for (pool_idx, reward_pool) in reward_candidates.iter().enumerate() {
let mut candidate_nodes = Vec::new();
for node_id in 0..CANDIDATES_PER_POOL {
let keypair = Keypair::generate_ed25519();
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
assert!(
merkle_payment_timestamp <= current_time,
"Timestamp should not be in the future"
);
assert!(
current_time - merkle_payment_timestamp
< crate::merkle_payments::merkle_tree::MERKLE_PAYMENT_EXPIRATION,
"Timestamp should not be expired"
);
let node = MerklePaymentCandidateNode::new(
&keypair,
create_mock_quoting_metrics(node_id),
RewardsAddress::from([(pool_idx * CANDIDATES_PER_POOL + node_id) as u8; 20]),
merkle_payment_timestamp,
)
.expect("Failed to create candidate node");
assert!(node.verify_signature());
assert_eq!(
node.merkle_payment_timestamp, merkle_payment_timestamp,
"Honest nodes should return the same merkle payment timestamp"
);
candidate_nodes.push(node);
}
let pool = MerklePaymentCandidatePool {
midpoint_proof: reward_pool.clone(),
candidate_nodes: candidate_nodes
.try_into()
.expect("Should have exactly CANDIDATES_PER_POOL nodes"),
};
all_candidate_pools.push(pool);
}
let pool_commitments: Vec<PoolCommitment> = all_candidate_pools
.iter()
.map(|pool| pool.to_commitment())
.collect();
let temp_dir = TempDir::new().unwrap();
let contract = evmlib::merkle_batch_payment::DiskMerklePaymentContract::new_with_path(
temp_dir.path().to_path_buf(),
)
.unwrap();
let (winner_pool_hash, _amount) = contract
.pay_for_merkle_tree(depth, pool_commitments.clone(), merkle_payment_timestamp)
.unwrap();
let payment_info = contract.get_payment_info(winner_pool_hash).unwrap();
assert_eq!(payment_info.depth, depth);
assert_eq!(
payment_info.merkle_payment_timestamp,
merkle_payment_timestamp
);
assert_eq!(payment_info.paid_node_addresses.len(), depth as usize);
let (winner_pool, winner_commitment) = all_candidate_pools
.iter()
.zip(pool_commitments.iter())
.find(|(pool, _)| pool.hash() == winner_pool_hash)
.expect("Winner pool should be found");
assert!(
winner_pool
.verify_commitment(winner_commitment, merkle_payment_timestamp)
.is_ok(),
"Winner commitment should verify against full pool data"
);
let payment_proofs: Vec<MerklePaymentProof> = addresses
.iter()
.enumerate()
.map(|(i, address_hash)| {
let address_proof = tree.generate_address_proof(i, *address_hash).unwrap();
MerklePaymentProof::new(*address_hash, address_proof, winner_pool.clone())
})
.collect();
for payment_proof in &payment_proofs {
let winner_to_fetch = payment_proof.winner_pool_hash();
let payment = contract
.get_payment_info(winner_to_fetch)
.expect("Payment should be found");
let verification_result = payment_proof.verify(
payment.depth,
payment.merkle_payment_timestamp,
&winner_to_fetch,
&payment.paid_node_addresses,
);
if let Err(e) = &verification_result {
eprintln!("Verification failed: {e:?}");
}
assert!(
verification_result.is_ok(),
"Payment proof should verify against smart contract data: {verification_result:?}"
);
}
}
}