Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ homepage = "https://maidsafe.net"
license = "GPL-3.0"
name = "evmlib"
repository = "https://github.com/WithAutonomi/evmlib"
version = "0.7.0"
version = "0.8.0"

[features]
external-signer = []
Expand Down
186 changes: 2 additions & 184 deletions src/merkle_batch_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,16 @@
// KIND, either express or implied. Please review the Licences for the specific language governing
// permissions and limitations relating to use of the SAFE Network Software.

//! Merkle batch payment types and disk-based mock smart contract
//! Merkle batch payment types
//!
//! This module contains the minimal types needed for Merkle batch payments and a disk-based
//! mock implementation of the smart contract. When the real smart contract is ready, the
//! disk contract will be replaced with actual on-chain calls.
//! This module contains the minimal types needed for Merkle batch payments.

use crate::common::{Address as RewardsAddress, Amount};

#[cfg(test)]
use crate::common::U256;
use serde::{Deserialize, Serialize};

#[cfg(test)]
use std::path::PathBuf;

#[cfg(test)]
use thiserror::Error;

/// Pool hash type (32 bytes) - compatible with XorName without the dependency
pub type PoolHash = [u8; 32];

Expand Down Expand Up @@ -81,180 +73,6 @@ pub struct OnChainPaymentInfo {
pub paid_node_addresses: Vec<(RewardsAddress, usize, Amount)>,
}

#[cfg(test)]
/// Errors that can occur during smart contract operations
#[derive(Debug, Error)]
pub enum SmartContractError {
#[error("Wrong number of candidate nodes: expected {expected}, got {got}")]
WrongCandidateCount { expected: usize, got: usize },

#[error("Wrong number of candidate pools: expected {expected}, got {got}")]
WrongPoolCount { expected: usize, got: usize },

#[error("Depth {depth} exceeds maximum supported depth {max}")]
DepthTooLarge { depth: u8, max: u8 },

#[error("Payment not found for winner pool hash: {0}")]
PaymentNotFound(String),

#[error("IO error: {0}")]
IoError(#[from] std::io::Error),

#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
}

#[cfg(test)]
/// Disk-based Merkle payment contract (mock for testing)
///
/// This simulates smart contract behavior by storing payment data to disk.
/// Only available for testing.
pub struct DiskMerklePaymentContract {
storage_path: PathBuf, // ~/.autonomi/merkle_payments/
}

#[cfg(test)]
impl DiskMerklePaymentContract {
/// Create a new contract with a specific storage path
pub fn new_with_path(storage_path: PathBuf) -> Result<Self, SmartContractError> {
std::fs::create_dir_all(&storage_path)?;
Ok(Self { storage_path })
}

/// Create a new contract with the default storage path
/// Uses: DATA_DIR/autonomi/merkle_payments/
pub fn new() -> Result<Self, SmartContractError> {
let storage_path = if let Some(data_dir) = dirs_next::data_dir() {
data_dir.join("autonomi").join("merkle_payments")
} else {
// Fallback to current directory if data_dir is not available
PathBuf::from(".autonomi").join("merkle_payments")
};
Self::new_with_path(storage_path)
}

/// Submit batch payment (simulates smart contract logic)
///
/// # Arguments
/// * `depth` - Tree depth
/// * `pool_commitments` - Minimal pool commitments (2^ceil(depth/2) pools with hashes + addresses)
/// * `merkle_payment_timestamp` - Client-defined timestamp committed to by all nodes in their quotes
///
/// # Returns
/// * `winner_pool_hash` - Hash of winner pool (storage key for verification)
/// * `amount` - Amount paid for the Merkle tree
pub fn pay_for_merkle_tree(
&self,
depth: u8,
pool_commitments: Vec<PoolCommitment>,
merkle_payment_timestamp: u64,
) -> Result<(PoolHash, Amount), SmartContractError> {
// Validate: depth is within supported range
if depth > MAX_MERKLE_DEPTH {
return Err(SmartContractError::DepthTooLarge {
depth,
max: MAX_MERKLE_DEPTH,
});
}

// Validate: correct number of pools (2^ceil(depth/2))
let expected_pools = expected_reward_pools(depth);
if pool_commitments.len() != expected_pools {
return Err(SmartContractError::WrongPoolCount {
expected: expected_pools,
got: pool_commitments.len(),
});
}

// Validate: each pool has exactly CANDIDATES_PER_POOL candidates
for pool in &pool_commitments {
if pool.candidates.len() != CANDIDATES_PER_POOL {
return Err(SmartContractError::WrongCandidateCount {
expected: CANDIDATES_PER_POOL,
got: pool.candidates.len(),
});
}
}

// Select winner pool using random selection
let winner_pool_idx = rand::random::<usize>() % pool_commitments.len();

let winner_pool = &pool_commitments[winner_pool_idx];
let winner_pool_hash = winner_pool.pool_hash;

println!("\n=== MERKLE BATCH PAYMENT ===");
println!("Depth: {depth}");
println!("Total pools: {}", pool_commitments.len());
println!("Nodes per pool: {CANDIDATES_PER_POOL}");
println!("Winner pool index: {winner_pool_idx}");
println!("Winner pool hash: {}", hex::encode(winner_pool_hash));

// Select 'depth' unique winner nodes within the winner pool
use std::collections::HashSet;
let mut winner_node_indices = HashSet::new();
while winner_node_indices.len() < depth as usize {
let idx = rand::random::<usize>() % winner_pool.candidates.len();
winner_node_indices.insert(idx);
}
let winner_node_indices: Vec<usize> = winner_node_indices.into_iter().collect();

println!(
"\nSelected {} winner nodes from pool:",
winner_node_indices.len()
);

// Calculate total amount from winner node prices
let mut total_amount = Amount::ZERO;

// Extract paid node addresses, along with their indices
let mut paid_node_addresses = Vec::new();
for (i, &node_idx) in winner_node_indices.iter().enumerate() {
let candidate = &winner_pool.candidates[node_idx];
let addr = candidate.rewards_address;
paid_node_addresses.push((addr, node_idx, candidate.price));
total_amount += candidate.price;
println!(" Node {}: {addr} (price: {})", i + 1, candidate.price);
}

println!(
"\nSimulating payment to {} nodes, total: {total_amount}...",
paid_node_addresses.len()
);
println!("=========================\n");

// Store payment info on 'blockchain' (indexed by winner_pool_hash)
let info = OnChainPaymentInfo {
depth,
merkle_payment_timestamp,
paid_node_addresses,
};

let file_path = self
.storage_path
.join(format!("{}.json", hex::encode(winner_pool_hash)));
let json = serde_json::to_string_pretty(&info)?;
std::fs::write(&file_path, json)?;

println!("✓ Stored payment info to: {}", file_path.display());

Ok((winner_pool_hash, total_amount))
}

/// Get payment info by winner pool hash
pub fn get_payment_info(
&self,
winner_pool_hash: PoolHash,
) -> Result<OnChainPaymentInfo, SmartContractError> {
let file_path = self
.storage_path
.join(format!("{}.json", hex::encode(winner_pool_hash)));
let json = std::fs::read_to_string(&file_path)
.map_err(|_| SmartContractError::PaymentNotFound(hex::encode(winner_pool_hash)))?;
let info = serde_json::from_str(&json)?;
Ok(info)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
3 changes: 0 additions & 3 deletions src/merkle_payments/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ pub use crate::merkle_batch_payment::{
expected_reward_pools,
};

#[cfg(test)]
pub use crate::merkle_batch_payment::SmartContractError;

// Export payment types (nodes, pools, proofs)
pub use merkle_payment::{
MerklePaymentCandidateNode, MerklePaymentCandidatePool, MerklePaymentProof,
Expand Down
50 changes: 29 additions & 21 deletions tests/payment_vault.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,26 +347,34 @@ async fn test_pay_for_merkle_tree_duplicate_rejected() {

payment_vault.set_provider(network_token.contract.provider().clone());

// First payment should succeed
let _ = payment_vault
.pay_for_merkle_tree(
depth,
pool_commitments.clone(),
merkle_payment_timestamp,
&transaction_config,
)
.await
.expect("first payment should succeed");

// Second payment with same pools and timestamp should fail (PaymentAlreadyExists)
let result = payment_vault
.pay_for_merkle_tree(
depth,
pool_commitments,
merkle_payment_timestamp,
&transaction_config,
)
.await;
// With 2 pools, each payment randomly picks a winner pool via on-chain entropy
// (block.prevrandao, block.timestamp). By pigeonhole, 3 payments guarantee at least
// one duplicate winner pool hash, which the contract rejects.
let max_attempts = num_pools + 1;
let mut saw_duplicate_rejection = false;

for i in 0..max_attempts {
let result = payment_vault
.pay_for_merkle_tree(
depth,
pool_commitments.clone(),
merkle_payment_timestamp,
&transaction_config,
)
.await;

if result.is_err() {
saw_duplicate_rejection = true;
break;
}
assert!(
i < num_pools,
"Payment {i} succeeded but all {num_pools} pool slots should be filled"
);
}

assert!(result.is_err(), "Duplicate payment should be rejected");
assert!(
saw_duplicate_rejection,
"Expected at least one duplicate rejection in {max_attempts} attempts"
);
}
Loading