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
183 changes: 95 additions & 88 deletions Cargo.lock

Large diffs are not rendered by default.

18 changes: 4 additions & 14 deletions ant-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,24 +218,14 @@ fn resolve_evm_network(
.payment_token_address
.parse()
.map_err(|e| anyhow::anyhow!("Invalid token address: {e}"))?;
let payments_addr: EvmAddress = evm
.data_payments_address
let vault_addr: EvmAddress = evm
.payment_vault_address
.parse()
.map_err(|e| anyhow::anyhow!("Invalid payments address: {e}"))?;
let merkle_addr: Option<EvmAddress> = evm
.merkle_payments_address
.as_ref()
.map(|s| {
s.parse().map_err(|e| {
anyhow::anyhow!("Invalid merkle payments address: {e}")
})
})
.transpose()?;
.map_err(|e| anyhow::anyhow!("Invalid payment vault address: {e}"))?;
return Ok(EvmNetwork::Custom(CustomNetwork {
rpc_url_http: rpc_url,
payment_token_address: token_addr,
data_payments_address: payments_addr,
merkle_payments_address: merkle_addr,
payment_vault_address: vault_addr,
}));
}
}
Expand Down
4 changes: 2 additions & 2 deletions ant-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ zip = "2"
tower-http = { version = "0.6.8", features = ["cors"] }

# Data operations
evmlib = "0.5.0"
evmlib = "0.8"
xor_name = "5"
self_encryption = "0.35.0"
futures = "0.3"
Expand All @@ -38,7 +38,7 @@ tracing = "0.1"
bytes = "1"
lru = "0.16"
rand = "0.8"
ant-node = "0.9.0"
ant-node = { git = "https://github.com/WithAutonomi/ant-node", rev = "44358c1b4e35cc9aff5841a746bb2b1fcb6dcbee" }
saorsa-pqc = "0.5"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

Expand Down
93 changes: 30 additions & 63 deletions ant-core/src/data/client/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ use ant_node::core::{MultiAddr, PeerId};
use ant_node::payment::{serialize_single_node_proof, PaymentProof, SingleNodePayment};
use bytes::Bytes;
use evmlib::common::{Amount, QuoteHash, TxHash};
use evmlib::contract::payment_vault::get_market_price;
use evmlib::wallet::PayForQuotesError;
use evmlib::{EncodedPeerId, PaymentQuote, ProofOfPayment, RewardsAddress};
use futures::stream::{self, StreamExt};
Expand Down Expand Up @@ -144,7 +143,7 @@ pub fn finalize_batch_payment(
impl Client {
/// Prepare a single chunk for batch payment.
///
/// Collects quotes and fetches contract prices without making any
/// Collects quotes and uses node-reported prices without making any
/// on-chain transaction. Returns `Ok(None)` if the chunk is already
/// stored on the network.
///
Expand All @@ -168,44 +167,21 @@ impl Client {
Err(e) => return Err(e),
};

let evm_network = self.require_evm_network()?;

// Capture all quoted peers for close-group replication.
let quoted_peers: Vec<(PeerId, Vec<MultiAddr>)> = quotes_with_peers
.iter()
.map(|(peer_id, addrs, _, _)| (*peer_id, addrs.clone()))
.collect();

// Fetch authoritative prices from the on-chain contract.
let metrics_batch: Vec<_> = quotes_with_peers
.iter()
.map(|(_, _, quote, _)| quote.quoting_metrics.clone())
.collect();

let contract_prices = get_market_price(evm_network, metrics_batch)
.await
.map_err(|e| {
Error::Payment(format!("Failed to get market prices from contract: {e}"))
})?;

if contract_prices.len() != quotes_with_peers.len() {
return Err(Error::Payment(format!(
"Contract returned {} prices for {} quotes",
contract_prices.len(),
quotes_with_peers.len()
)));
}

// Build peer_quotes for ProofOfPayment + quotes for SingleNodePayment.
// Use node-reported prices directly — no contract price fetch needed.
let mut peer_quotes = Vec::with_capacity(quotes_with_peers.len());
let mut quotes_for_payment = Vec::with_capacity(quotes_with_peers.len());

for ((peer_id, _addrs, quote, _local_price), contract_price) in
quotes_with_peers.into_iter().zip(contract_prices)
{
for (peer_id, _addrs, quote, price) in quotes_with_peers {
let encoded = peer_id_to_encoded(&peer_id)?;
peer_quotes.push((encoded, quote.clone()));
quotes_for_payment.push((quote, contract_price));
quotes_for_payment.push((quote, price));
}

let payment = SingleNodePayment::from_quotes(quotes_for_payment)
Expand Down Expand Up @@ -437,31 +413,23 @@ mod tests {
use super::*;
use ant_node::payment::single_node::QuotePaymentInfo;
use ant_node::CLOSE_GROUP_SIZE;
use evmlib::quoting_metrics::QuotingMetrics;

fn test_metrics() -> QuotingMetrics {
QuotingMetrics {
data_size: 0,
data_type: 0,
close_records_stored: 0,
records_per_type: vec![],
max_records: 0,
received_payment_count: 0,
live_time: 0,
network_density: None,
network_size: None,
}
}

/// Helper: build a PreparedChunk with specified payment amounts.
fn make_prepared_chunk(amounts: [u64; CLOSE_GROUP_SIZE]) -> PreparedChunk {
let quotes: [QuotePaymentInfo; CLOSE_GROUP_SIZE] =
std::array::from_fn(|i| QuotePaymentInfo {
/// Median index in the quotes array.
const MEDIAN_INDEX: usize = CLOSE_GROUP_SIZE / 2;

/// Helper: build a `PreparedChunk` with `median_amount` at the median
/// quote index and zero for all other quotes. Adapts automatically to
/// `CLOSE_GROUP_SIZE` changes.
fn make_prepared_chunk(median_amount: u64) -> PreparedChunk {
let quotes: [QuotePaymentInfo; CLOSE_GROUP_SIZE] = std::array::from_fn(|i| {
let amount = if i == MEDIAN_INDEX { median_amount } else { 0 };
QuotePaymentInfo {
quote_hash: QuoteHash::from([i as u8 + 1; 32]),
rewards_address: RewardsAddress::new([i as u8 + 10; 20]),
amount: Amount::from(amounts[i]),
quoting_metrics: test_metrics(),
});
amount: Amount::from(amount),
price: Amount::from(amount),
}
});

PreparedChunk {
content: Bytes::from(vec![0xAA; 32]),
Expand All @@ -474,23 +442,22 @@ mod tests {

#[test]
fn payment_intent_from_single_chunk() {
// Median (index 2) gets 3x price, others get 0 — matches SingleNodePayment layout
let chunk = make_prepared_chunk([0, 0, 300, 0, 0]);
let chunk = make_prepared_chunk(300);
let intent = PaymentIntent::from_prepared_chunks(&[chunk]);

assert_eq!(intent.payments.len(), 1, "only non-zero amounts");
assert_eq!(intent.total_amount, Amount::from(300));

let (hash, addr, amt) = &intent.payments[0];
assert_eq!(*hash, QuoteHash::from([3u8; 32])); // index 2 → byte 3
assert_eq!(*addr, RewardsAddress::new([12u8; 20])); // index 2 → byte 12
assert_eq!(*hash, QuoteHash::from([MEDIAN_INDEX as u8 + 1; 32]));
assert_eq!(*addr, RewardsAddress::new([MEDIAN_INDEX as u8 + 10; 20]));
assert_eq!(*amt, Amount::from(300));
}

#[test]
fn payment_intent_from_multiple_chunks() {
let c1 = make_prepared_chunk([0, 0, 100, 0, 0]);
let c2 = make_prepared_chunk([0, 0, 250, 0, 0]);
let c1 = make_prepared_chunk(100);
let c2 = make_prepared_chunk(250);
let intent = PaymentIntent::from_prepared_chunks(&[c1, c2]);

assert_eq!(intent.payments.len(), 2);
Expand All @@ -499,7 +466,7 @@ mod tests {

#[test]
fn payment_intent_skips_all_zero_chunks() {
let chunk = make_prepared_chunk([0, 0, 0, 0, 0]);
let chunk = make_prepared_chunk(0);
let intent = PaymentIntent::from_prepared_chunks(&[chunk]);

assert!(intent.payments.is_empty());
Expand All @@ -515,8 +482,8 @@ mod tests {

#[test]
fn finalize_batch_payment_builds_proofs() {
let chunk = make_prepared_chunk([0, 0, 500, 0, 0]);
let quote_hash = chunk.payment.quotes[2].quote_hash;
let chunk = make_prepared_chunk(500);
let quote_hash = chunk.payment.quotes[MEDIAN_INDEX].quote_hash;

let mut tx_map = HashMap::new();
tx_map.insert(quote_hash, TxHash::from([0xBB; 32]));
Expand All @@ -538,7 +505,7 @@ mod tests {
fn finalize_batch_payment_missing_tx_hash_errors() {
// Missing tx hash for a non-zero-amount quote should error,
// since the chunk would be rejected by the network without a valid proof.
let chunk = make_prepared_chunk([0, 0, 500, 0, 0]);
let chunk = make_prepared_chunk(500);

let result = finalize_batch_payment(vec![chunk], &HashMap::new());
assert!(result.is_err());
Expand All @@ -548,9 +515,9 @@ mod tests {

#[test]
fn finalize_batch_payment_multiple_chunks() {
let c1 = make_prepared_chunk([0, 0, 100, 0, 0]);
let c2 = make_prepared_chunk([0, 0, 200, 0, 0]);
let q1 = c1.payment.quotes[2].quote_hash;
let c1 = make_prepared_chunk(100);
let c2 = make_prepared_chunk(200);
let q1 = c1.payment.quotes[MEDIAN_INDEX].quote_hash;
let mut tx_map = HashMap::new();
// Both chunks have the same quote_hash (same index/byte pattern)
// so one tx_hash covers both
Expand Down
67 changes: 5 additions & 62 deletions ant-core/src/data/client/merkle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,8 @@ impl Client {
candidate_futures.push(fut);
}

self.collect_validated_candidates(
&mut candidate_futures,
merkle_payment_timestamp,
data_type,
)
.await
self.collect_validated_candidates(&mut candidate_futures, merkle_payment_timestamp)
.await
}

/// Collect and validate merkle candidate responses until we have enough.
Expand All @@ -396,7 +392,6 @@ impl Client {
>,
>,
merkle_payment_timestamp: u64,
expected_data_type: u32,
) -> Result<[MerklePaymentCandidateNode; CANDIDATES_PER_POOL]> {
let mut candidates = Vec::with_capacity(CANDIDATES_PER_POOL);
let mut failures: Vec<String> = Vec::new();
Expand All @@ -414,14 +409,6 @@ impl Client {
failures.push(format!("{peer_id}: timestamp mismatch"));
continue;
}
if candidate.quoting_metrics.data_type != expected_data_type {
warn!(
"Data type mismatch from {peer_id}: expected {expected_data_type}, got {}",
candidate.quoting_metrics.data_type
);
failures.push(format!("{peer_id}: wrong data_type"));
continue;
}
candidates.push(candidate);
if candidates.len() >= CANDIDATES_PER_POOL {
break;
Expand Down Expand Up @@ -633,8 +620,8 @@ mod tests {
#[test]
fn test_merkle_proof_serialize_deserialize_roundtrip() {
use ant_node::payment::{deserialize_merkle_proof, serialize_merkle_proof};
use evmlib::common::Amount;
use evmlib::merkle_payments::MerklePaymentCandidateNode;
use evmlib::quoting_metrics::QuotingMetrics;
use evmlib::RewardsAddress;

let addrs = make_test_addresses(4);
Expand All @@ -654,17 +641,7 @@ mod tests {
let candidate_nodes: [MerklePaymentCandidateNode; CANDIDATES_PER_POOL] =
std::array::from_fn(|i| MerklePaymentCandidateNode {
pub_key: vec![i as u8; 32],
quoting_metrics: QuotingMetrics {
data_size: 1024,
data_type: 0,
close_records_stored: 0,
records_per_type: vec![],
max_records: 100,
received_payment_count: 0,
live_time: 0,
network_density: None,
network_size: None,
},
price: Amount::from(1024u64),
reward_address: RewardsAddress::new([i as u8; 20]),
merkle_payment_timestamp: timestamp,
signature: vec![i as u8; 64],
Expand Down Expand Up @@ -702,17 +679,7 @@ mod tests {
// Simulates what collect_validated_candidates checks
let candidate = MerklePaymentCandidateNode {
pub_key: vec![0u8; 32],
quoting_metrics: evmlib::quoting_metrics::QuotingMetrics {
data_size: 0,
data_type: 0,
close_records_stored: 0,
records_per_type: vec![],
max_records: 0,
received_payment_count: 0,
live_time: 0,
network_density: None,
network_size: None,
},
price: evmlib::common::Amount::ZERO,
reward_address: evmlib::RewardsAddress::new([0u8; 20]),
merkle_payment_timestamp: 1000,
signature: vec![0u8; 64],
Expand All @@ -722,30 +689,6 @@ mod tests {
assert_ne!(candidate.merkle_payment_timestamp, 2000);
}

#[test]
fn test_candidate_wrong_data_type_rejected() {
let candidate = MerklePaymentCandidateNode {
pub_key: vec![0u8; 32],
quoting_metrics: evmlib::quoting_metrics::QuotingMetrics {
data_size: 0,
data_type: 1, // scratchpad
close_records_stored: 0,
records_per_type: vec![],
max_records: 0,
received_payment_count: 0,
live_time: 0,
network_density: None,
network_size: None,
},
reward_address: evmlib::RewardsAddress::new([0u8; 20]),
merkle_payment_timestamp: 1000,
signature: vec![0u8; 64],
};

// data_type check: 1 (scratchpad) != 0 (chunk)
assert_ne!(candidate.quoting_metrics.data_type, 0);
}

// =========================================================================
// Batch splitting edge cases
// =========================================================================
Expand Down
6 changes: 3 additions & 3 deletions ant-core/src/data/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,17 +118,17 @@ impl Client {
/// Set the wallet for payment operations.
///
/// Also populates the EVM network from the wallet so that
/// price queries work without a separate `with_evm_network` call.
/// token approvals work without a separate `with_evm_network` call.
#[must_use]
pub fn with_wallet(mut self, wallet: Wallet) -> Self {
self.evm_network = Some(wallet.network().clone());
self.wallet = Some(Arc::new(wallet));
self
}

/// Set the EVM network for price queries without requiring a wallet.
/// Set the EVM network without requiring a wallet.
///
/// This enables operations like quote collection and cost estimation
/// This enables token approval and contract interactions
/// for external-signer flows where the private key lives outside Rust.
#[must_use]
pub fn with_evm_network(mut self, network: evmlib::Network) -> Self {
Expand Down
Loading
Loading