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
@@ -1,6 +1,6 @@
[package]
name = "ant-node"
version = "0.6.0"
version = "0.7.0"
edition = "2021"
authors = ["David Irvine <david.irvine@maidsafe.net>"]
description = "Pure quantum-proof network node for the Autonomi decentralized network"
Expand Down
11 changes: 11 additions & 0 deletions src/ant_protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@

pub mod chunk;

/// Number of nodes in a Kademlia close group.
///
/// Clients fetch quotes from the `CLOSE_GROUP_SIZE` closest nodes to a target
/// address and select the median-priced quote for payment.
pub const CLOSE_GROUP_SIZE: usize = 5;

/// Minimum number of close group members that must agree for a decision to be valid.
///
/// This is a simple majority: `(CLOSE_GROUP_SIZE / 2) + 1`.
pub const CLOSE_GROUP_MAJORITY: usize = (CLOSE_GROUP_SIZE / 2) + 1;

// Re-export chunk types for convenience
pub use chunk::{
ChunkGetRequest, ChunkGetResponse, ChunkMessage, ChunkMessageBody, ChunkPutRequest,
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ pub mod upgrade;

pub use ant_protocol::{
ChunkGetRequest, ChunkGetResponse, ChunkMessage, ChunkMessageBody, ChunkPutRequest,
ChunkPutResponse, ChunkQuoteRequest, ChunkQuoteResponse, CHUNK_PROTOCOL_ID, MAX_CHUNK_SIZE,
ChunkPutResponse, ChunkQuoteRequest, ChunkQuoteResponse, CHUNK_PROTOCOL_ID,
CLOSE_GROUP_MAJORITY, CLOSE_GROUP_SIZE, MAX_CHUNK_SIZE,
};
pub use client::{
compute_address, hex_node_id_to_encoded_peer_id, peer_id_to_xor_name, xor_distance, DataChunk,
Expand Down
62 changes: 31 additions & 31 deletions src/payment/single_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,14 @@
//! Total cost is the same as Standard mode (3x), but with one actual payment.
//! This saves gas fees while maintaining the same total payment amount.

use crate::ant_protocol::CLOSE_GROUP_SIZE;
use crate::error::{Error, Result};
use ant_evm::{Amount, PaymentQuote, QuoteHash, QuotingMetrics, RewardsAddress};
use evmlib::contract::payment_vault;
use evmlib::wallet::Wallet;
use evmlib::Network as EvmNetwork;
use tracing::info;

/// Required number of quotes for `SingleNode` payment (matches `CLOSE_GROUP_SIZE`)
pub const REQUIRED_QUOTES: usize = 5;

/// Create zero-valued `QuotingMetrics` for payment verification.
///
/// The contract doesn't validate metric values, so we use zeroes.
Expand All @@ -37,20 +35,20 @@ fn zero_quoting_metrics() -> QuotingMetrics {
}
}

/// Index of the median-priced node after sorting
const MEDIAN_INDEX: usize = 2;
/// Index of the median-priced node after sorting, derived from `CLOSE_GROUP_SIZE`.
const MEDIAN_INDEX: usize = CLOSE_GROUP_SIZE / 2;

/// Single node payment structure for a chunk.
///
/// Contains exactly 5 quotes where only the median-priced one receives payment (3x),
/// and the other 4 have `Amount::ZERO`.
/// Contains exactly `CLOSE_GROUP_SIZE` quotes where only the median-priced one
/// receives payment (3x), and the remaining quotes have `Amount::ZERO`.
///
/// The fixed-size array ensures compile-time enforcement of the 5-quote requirement,
/// making the median index (2) always valid.
/// The fixed-size array ensures compile-time enforcement of the quote count,
/// making the median index always valid.
#[derive(Debug, Clone)]
pub struct SingleNodePayment {
/// All 5 quotes (sorted by price) - fixed size ensures median index is always valid
pub quotes: [QuotePaymentInfo; REQUIRED_QUOTES],
/// All quotes (sorted by price) - fixed size ensures median index is always valid
pub quotes: [QuotePaymentInfo; CLOSE_GROUP_SIZE],
}

/// Information about a single quote payment
Expand Down Expand Up @@ -82,9 +80,9 @@ impl SingleNodePayment {
/// Returns error if not exactly 5 quotes are provided.
pub fn from_quotes(mut quotes_with_prices: Vec<(PaymentQuote, Amount)>) -> Result<Self> {
let len = quotes_with_prices.len();
if len != REQUIRED_QUOTES {
if len != CLOSE_GROUP_SIZE {
return Err(Error::Payment(format!(
"SingleNode payment requires exactly {REQUIRED_QUOTES} quotes, got {len}"
"SingleNode payment requires exactly {CLOSE_GROUP_SIZE} quotes, got {len}"
)));
}

Expand All @@ -96,7 +94,7 @@ impl SingleNodePayment {
.get(MEDIAN_INDEX)
.ok_or_else(|| {
Error::Payment(format!(
"Missing median quote at index {MEDIAN_INDEX}: expected {REQUIRED_QUOTES} quotes but get() failed"
"Missing median quote at index {MEDIAN_INDEX}: expected {CLOSE_GROUP_SIZE} quotes but get() failed"
))
})?
.1;
Expand All @@ -123,8 +121,8 @@ impl SingleNodePayment {
})
.collect();

// Convert Vec to array - we already validated length is REQUIRED_QUOTES
let quotes: [QuotePaymentInfo; REQUIRED_QUOTES] = quotes_vec
// Convert Vec to array - we already validated length is CLOSE_GROUP_SIZE
let quotes: [QuotePaymentInfo; CLOSE_GROUP_SIZE] = quotes_vec
.try_into()
.map_err(|_| Error::Payment("Failed to convert quotes to fixed array".to_string()))?;

Expand All @@ -140,7 +138,7 @@ impl SingleNodePayment {
/// Get the median quote that receives payment.
///
/// Returns `None` only if the internal array is somehow shorter than `MEDIAN_INDEX`,
/// which should never happen since the array is fixed-size `[_; REQUIRED_QUOTES]`.
/// which should never happen since the array is fixed-size `[_; CLOSE_GROUP_SIZE]`.
#[must_use]
pub fn paid_quote(&self) -> Option<&QuotePaymentInfo> {
self.quotes.get(MEDIAN_INDEX)
Expand All @@ -163,9 +161,9 @@ impl SingleNodePayment {

info!(
"Paying for {} quotes: 1 real ({} atto) + {} with 0 atto",
REQUIRED_QUOTES,
CLOSE_GROUP_SIZE,
self.total_amount(),
REQUIRED_QUOTES - 1
CLOSE_GROUP_SIZE - 1
);

let (tx_hashes, _gas_info) = wallet.pay_for_quotes(quote_payments).await.map_err(
Expand Down Expand Up @@ -338,9 +336,9 @@ mod tests {

let transaction_config = TransactionConfig::default();

// Create 5 random quote payments (autonomi pattern)
// Create CLOSE_GROUP_SIZE random quote payments (autonomi pattern)
let mut quote_payments = vec![];
for _ in 0..5 {
for _ in 0..CLOSE_GROUP_SIZE {
let quote_hash = dummy_hash();
let reward_address = dummy_address();
let amount = Amount::from(1u64);
Expand Down Expand Up @@ -412,8 +410,8 @@ mod tests {

let mut quote_payments = vec![(real_quote_hash, real_reward_address, real_amount)];

// Add 4 dummy payments with 0 amount
for _ in 0..4 {
// Add dummy payments with 0 amount for remaining close group members
for _ in 0..CLOSE_GROUP_SIZE - 1 {
let dummy_quote_hash = dummy_hash();
let dummy_reward_address = dummy_address();
let dummy_amount = Amount::from(0u64); // 0 amount
Expand Down Expand Up @@ -559,8 +557,9 @@ mod tests {
#[test]
#[allow(clippy::unwrap_used)]
fn test_paid_quote_returns_median() {
let quotes: Vec<_> = (0..5u8)
.map(|i| (make_test_quote(i + 1), Amount::from(u64::from(i + 1) * 10)))
let quotes: Vec<_> = (1u8..)
.take(CLOSE_GROUP_SIZE)
.map(|i| (make_test_quote(i), Amount::from(u64::from(i) * 10)))
.collect();

let payment = SingleNodePayment::from_quotes(quotes).unwrap();
Expand All @@ -576,17 +575,18 @@ mod tests {
#[test]
#[allow(clippy::unwrap_used)]
fn test_all_quotes_have_distinct_addresses() {
let quotes: Vec<_> = (0..5u8)
.map(|i| (make_test_quote(i + 1), Amount::from(u64::from(i + 1) * 10)))
let quotes: Vec<_> = (1u8..)
.take(CLOSE_GROUP_SIZE)
.map(|i| (make_test_quote(i), Amount::from(u64::from(i) * 10)))
.collect();

let payment = SingleNodePayment::from_quotes(quotes).unwrap();

// Verify all 5 quotes are present (sorting doesn't lose data)
// Verify all quotes are present (sorting doesn't lose data)
let mut addresses: Vec<_> = payment.quotes.iter().map(|q| q.rewards_address).collect();
addresses.sort();
addresses.dedup();
assert_eq!(addresses.len(), 5);
assert_eq!(addresses.len(), CLOSE_GROUP_SIZE);
}

#[test]
Expand Down Expand Up @@ -629,7 +629,7 @@ mod tests {
let chunk_size = 1024usize;

let mut quotes_with_prices = Vec::new();
for i in 0..REQUIRED_QUOTES {
for i in 0..CLOSE_GROUP_SIZE {
let quoting_metrics = QuotingMetrics {
data_size: chunk_size,
data_type: 0,
Expand Down Expand Up @@ -687,7 +687,7 @@ mod tests {
.ok_or_else(|| Error::Payment("Failed to calculate median price".to_string()))?;
println!("✓ Sorted and selected median price: {median_price} atto");

assert_eq!(payment.quotes.len(), REQUIRED_QUOTES);
assert_eq!(payment.quotes.len(), CLOSE_GROUP_SIZE);
let median_amount = payment
.quotes
.get(MEDIAN_INDEX)
Expand Down
26 changes: 13 additions & 13 deletions src/payment/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
//! This is the core payment verification logic for ant-node.
//! All new data requires EVM payment on Arbitrum (no free tier).

use crate::ant_protocol::CLOSE_GROUP_SIZE;
use crate::error::{Error, Result};
use crate::payment::cache::{CacheStats, VerifiedCache, XorName};
use crate::payment::proof::{
deserialize_merkle_proof, deserialize_proof, detect_proof_type, ProofType,
};
use crate::payment::quote::{verify_quote_content, verify_quote_signature};
use crate::payment::single_node::REQUIRED_QUOTES;
use ant_evm::merkle_payments::OnChainPaymentInfo;
use ant_evm::{ProofOfPayment, RewardsAddress};
use evmlib::contract::merkle_payment_vault;
Expand Down Expand Up @@ -410,9 +410,9 @@ impl PaymentVerifier {
}

let quote_count = payment.peer_quotes.len();
if quote_count != REQUIRED_QUOTES {
if quote_count != CLOSE_GROUP_SIZE {
return Err(Error::Payment(format!(
"Payment must have exactly {REQUIRED_QUOTES} quotes, got {quote_count}"
"Payment must have exactly {CLOSE_GROUP_SIZE} quotes, got {quote_count}"
)));
}

Expand Down Expand Up @@ -1039,9 +1039,9 @@ mod tests {
signature: vec![0u8; 64],
};

// Build 5 quotes with distinct peer IDs (required by REQUIRED_QUOTES enforcement)
// Build CLOSE_GROUP_SIZE quotes with distinct peer IDs
let mut peer_quotes = Vec::new();
for _ in 0..5 {
for _ in 0..CLOSE_GROUP_SIZE {
let keypair = Keypair::generate_ed25519();
let peer_id = PeerId::from_public_key(&keypair.public());
peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
Expand Down Expand Up @@ -1121,7 +1121,7 @@ mod tests {
let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);

let mut peer_quotes = Vec::new();
for _ in 0..5 {
for _ in 0..CLOSE_GROUP_SIZE {
let keypair = libp2p::identity::Keypair::generate_ed25519();
let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
Expand Down Expand Up @@ -1152,7 +1152,7 @@ mod tests {
let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);

let mut peer_quotes = Vec::new();
for _ in 0..5 {
for _ in 0..CLOSE_GROUP_SIZE {
let keypair = libp2p::identity::Keypair::generate_ed25519();
let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
Expand Down Expand Up @@ -1183,7 +1183,7 @@ mod tests {
let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);

let mut peer_quotes = Vec::new();
for _ in 0..5 {
for _ in 0..CLOSE_GROUP_SIZE {
let keypair = libp2p::identity::Keypair::generate_ed25519();
let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
Expand Down Expand Up @@ -1214,7 +1214,7 @@ mod tests {
let quote = make_fake_quote(xorname, future_timestamp, rewards_addr);

let mut peer_quotes = Vec::new();
for _ in 0..5 {
for _ in 0..CLOSE_GROUP_SIZE {
let keypair = libp2p::identity::Keypair::generate_ed25519();
let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
Expand Down Expand Up @@ -1248,7 +1248,7 @@ mod tests {
let quote = make_fake_quote(xorname, old_timestamp, rewards_addr);

let mut peer_quotes = Vec::new();
for _ in 0..5 {
for _ in 0..CLOSE_GROUP_SIZE {
let keypair = libp2p::identity::Keypair::generate_ed25519();
let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
Expand Down Expand Up @@ -1304,7 +1304,7 @@ mod tests {
// Use real ML-DSA keys so the pub_key→peer_id binding check passes
let ml_dsa = MlDsa65::new();
let mut peer_quotes = Vec::new();
for _ in 0..5 {
for _ in 0..CLOSE_GROUP_SIZE {
let (public_key, _secret_key) = ml_dsa.generate_keypair().expect("keygen");
let pub_key_bytes = public_key.as_bytes().to_vec();
let encoded = encoded_peer_id_for_pub_key(&pub_key_bytes);
Expand Down Expand Up @@ -1348,7 +1348,7 @@ mod tests {

// Use random ed25519 peer IDs — they won't match BLAKE3(pub_key)
let mut peer_quotes = Vec::new();
for _ in 0..5 {
for _ in 0..CLOSE_GROUP_SIZE {
let keypair = libp2p::identity::Keypair::generate_ed25519();
let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
Expand Down Expand Up @@ -1409,7 +1409,7 @@ mod tests {
// Build a valid tagged single-node proof
let quote = make_fake_quote(xorname, SystemTime::now(), rewards_addr);
let mut peer_quotes = Vec::new();
for _ in 0..5 {
for _ in 0..CLOSE_GROUP_SIZE {
let keypair = libp2p::identity::Keypair::generate_ed25519();
let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
peer_quotes.push((EncodedPeerId::from(peer_id), quote.clone()));
Expand Down
Loading