From 20f3f42154547904781d25bb843d7094640e9bbe Mon Sep 17 00:00:00 2001 From: Warm Beer Date: Fri, 3 Apr 2026 10:25:08 +0200 Subject: [PATCH 1/4] refactor: remove unused DiskMerklePaymentContract and related types Dead code: DiskMerklePaymentContract, OnChainPaymentInfo, and SmartContractError were defined but never referenced outside their own module. Removes associated imports and re-exports. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/merkle_batch_payment.rs | 200 +----------------------------------- src/merkle_payments/mod.rs | 6 +- 2 files changed, 3 insertions(+), 203 deletions(-) diff --git a/src/merkle_batch_payment.rs b/src/merkle_batch_payment.rs index 205ead2..4b76ecd 100644 --- a/src/merkle_batch_payment.rs +++ b/src/merkle_batch_payment.rs @@ -6,11 +6,9 @@ // 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}; @@ -18,12 +16,6 @@ use crate::common::{Address as RewardsAddress, Amount}; 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]; @@ -67,194 +59,6 @@ pub struct CandidateNode { pub price: Amount, } -/// What's stored on-chain (or disk) - indexed by winner_pool_hash -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct OnChainPaymentInfo { - /// Tree depth - pub depth: u8, - - /// Merkle payment timestamp provided by client (unix seconds) - /// This is the timestamp that all nodes in the pool used for their quotes - pub merkle_payment_timestamp: u64, - - /// Addresses of the 'depth' nodes that were paid, with their pool index and paid amount - 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 { - 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 { - 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, - 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::() % 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::() % winner_pool.candidates.len(); - winner_node_indices.insert(idx); - } - let winner_node_indices: Vec = 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 { - 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::*; diff --git a/src/merkle_payments/mod.rs b/src/merkle_payments/mod.rs index ef6d1b4..532f15d 100644 --- a/src/merkle_payments/mod.rs +++ b/src/merkle_payments/mod.rs @@ -7,13 +7,9 @@ mod merkle_tree; // Re-export types from the merkle_batch_payment module (already in evmlib) pub use crate::merkle_batch_payment::{ - CANDIDATES_PER_POOL, MAX_MERKLE_DEPTH, OnChainPaymentInfo, PoolCommitment, - expected_reward_pools, + CANDIDATES_PER_POOL, MAX_MERKLE_DEPTH, PoolCommitment, 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, From 7924cbd3de0b3fd554c8e410a7d3719c1efedc3a Mon Sep 17 00:00:00 2001 From: Warm Beer Date: Fri, 3 Apr 2026 10:31:34 +0200 Subject: [PATCH 2/4] fix: restore OnChainPaymentInfo which is used externally Co-Authored-By: Claude Opus 4.6 (1M context) --- src/merkle_batch_payment.rs | 14 ++++++++++++++ src/merkle_payments/mod.rs | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/merkle_batch_payment.rs b/src/merkle_batch_payment.rs index 4b76ecd..52cee67 100644 --- a/src/merkle_batch_payment.rs +++ b/src/merkle_batch_payment.rs @@ -59,6 +59,20 @@ pub struct CandidateNode { pub price: Amount, } +/// What's stored on-chain (or disk) - indexed by winner_pool_hash +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct OnChainPaymentInfo { + /// Tree depth + pub depth: u8, + + /// Merkle payment timestamp provided by client (unix seconds) + /// This is the timestamp that all nodes in the pool used for their quotes + pub merkle_payment_timestamp: u64, + + /// Addresses of the 'depth' nodes that were paid, with their pool index and paid amount + pub paid_node_addresses: Vec<(RewardsAddress, usize, Amount)>, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/merkle_payments/mod.rs b/src/merkle_payments/mod.rs index 532f15d..8e2d60b 100644 --- a/src/merkle_payments/mod.rs +++ b/src/merkle_payments/mod.rs @@ -7,7 +7,8 @@ mod merkle_tree; // Re-export types from the merkle_batch_payment module (already in evmlib) pub use crate::merkle_batch_payment::{ - CANDIDATES_PER_POOL, MAX_MERKLE_DEPTH, PoolCommitment, expected_reward_pools, + CANDIDATES_PER_POOL, MAX_MERKLE_DEPTH, OnChainPaymentInfo, PoolCommitment, + expected_reward_pools, }; // Export payment types (nodes, pools, proofs) From 1c0998da57f35348fbec9916a64458cfa8c75151 Mon Sep 17 00:00:00 2001 From: Warm Beer Date: Fri, 3 Apr 2026 10:47:13 +0200 Subject: [PATCH 3/4] fix(test): make duplicate merkle payment test deterministic selectWinnerPool uses block.prevrandao and block.timestamp as entropy, so with 2 pools the second call had ~50% chance of picking the other pool and succeeding. Use pigeonhole: with N pools, N+1 attempts guarantee at least one duplicate winner pool hash rejection. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/payment_vault.rs | 50 ++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/tests/payment_vault.rs b/tests/payment_vault.rs index 90b2883..66b3420 100644 --- a/tests/payment_vault.rs +++ b/tests/payment_vault.rs @@ -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" + ); } From 46029ff783b94d2803abb0ac4fab6a9f1e29adf2 Mon Sep 17 00:00:00 2001 From: Warm Beer Date: Fri, 3 Apr 2026 10:54:58 +0200 Subject: [PATCH 4/4] chore: bump version to 0.8.0 Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e00dcbd..f814301 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = []