Skip to content
Draft
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
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ saorsa-core = "0.22.0"
saorsa-pqc = "0.5"

# Payment verification - autonomi network lookup + EVM payment
evmlib = "0.8"
evmlib = { git = "https://github.com/WithAutonomi/evmlib", rev = "a3be57fcb3bd4982bc93ad0b58116255d509db28" }
xor_name = "5"

# Caching - LRU cache for verified XorNames
Expand Down
5 changes: 4 additions & 1 deletion src/ant_protocol/chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,11 @@ pub enum ChunkQuoteResponse {
/// When `already_stored` is `true` the node already holds this chunk and no
/// payment is required — the client should skip the pay-then-PUT cycle for
/// this address. The quote is still included for informational purposes.
///
/// The close group view is embedded inside the serialized `PaymentQuote`
/// and covered by the quote's ML-DSA-65 signature, so it cannot be forged.
Success {
/// Serialized `PaymentQuote`.
/// Serialized `PaymentQuote` (includes the node's close group view).
quote: Vec<u8>,
/// `true` when the chunk already exists on this node (skip payment).
already_stored: bool,
Expand Down
6 changes: 6 additions & 0 deletions src/devnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ impl Devnet {
evm: evm_config,
cache_capacity: DEVNET_PAYMENT_CACHE_CAPACITY,
local_rewards_address: rewards_address,
local_peer_id: *identity.peer_id().as_bytes(),
};
let payment_verifier = PaymentVerifier::new(payment_config);
let metrics_tracker = QuotingMetricsTracker::new(DEVNET_INITIAL_RECORDS);
Expand All @@ -594,6 +595,7 @@ impl Devnet {
Arc::new(storage),
Arc::new(payment_verifier),
Arc::new(quote_generator),
None,
))
}

Expand Down Expand Up @@ -635,6 +637,10 @@ impl Devnet {
*node.state.write().await = NodeState::Running;

if let (Some(ref p2p), Some(ref protocol)) = (&node.p2p_node, &node.ant_protocol) {
// Inject P2P node into protocol handler for close-group lookups.
if protocol.set_p2p_node(Arc::clone(p2p)).is_err() {
warn!("P2P node already set on protocol handler for devnet node {index}");
}
let mut events = p2p.subscribe_events();
let p2p_clone = Arc::clone(p2p);
let protocol_clone = Arc::clone(protocol);
Expand Down
16 changes: 11 additions & 5 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,20 +118,23 @@ impl NodeBuilder {
None
};

// Wrap P2P node in Arc early so it can be shared with the protocol handler.
let p2p_node = Arc::new(p2p_node);

// Initialize ANT protocol handler for chunk storage and
// wire the fresh-write channel so PUTs trigger replication.
let (ant_protocol, fresh_write_rx) = if self.config.storage.enabled {
let (fresh_write_tx, fresh_write_rx) = tokio::sync::mpsc::unbounded_channel();
let mut protocol = Self::build_ant_protocol(&self.config, &identity).await?;
let mut protocol =
Self::build_ant_protocol(&self.config, &identity, Some(Arc::clone(&p2p_node)))
.await?;
protocol.set_fresh_write_sender(fresh_write_tx);
(Some(Arc::new(protocol)), Some(fresh_write_rx))
} else {
info!("Chunk storage disabled");
(None, None)
};

let p2p_arc = Arc::new(p2p_node);

// Initialize replication engine (if storage is enabled)
let replication_engine =
if let (Some(ref protocol), Some(fresh_rx)) = (&ant_protocol, fresh_write_rx) {
Expand All @@ -140,7 +143,7 @@ impl NodeBuilder {
let payment_verifier_arc = protocol.payment_verifier_arc();
match ReplicationEngine::new(
repl_config,
Arc::clone(&p2p_arc),
Arc::clone(&p2p_node),
storage_arc,
payment_verifier_arc,
&self.config.root_dir,
Expand All @@ -161,7 +164,7 @@ impl NodeBuilder {

let node = RunningNode {
config: self.config,
p2p_node: p2p_arc,
p2p_node,
shutdown,
events_tx,
events_rx: Some(events_rx),
Expand Down Expand Up @@ -352,6 +355,7 @@ impl NodeBuilder {
async fn build_ant_protocol(
config: &NodeConfig,
identity: &NodeIdentity,
p2p_node: Option<Arc<P2PNode>>,
) -> Result<AntProtocol> {
// Create LMDB storage
let storage_config = LmdbStorageConfig {
Expand Down Expand Up @@ -385,6 +389,7 @@ impl NodeBuilder {
},
cache_capacity: config.payment.cache_capacity,
local_rewards_address: rewards_address,
local_peer_id: *identity.peer_id().as_bytes(),
};
let payment_verifier = PaymentVerifier::new(payment_config);
let metrics_tracker = QuotingMetricsTracker::new(0);
Expand All @@ -398,6 +403,7 @@ impl NodeBuilder {
Arc::new(storage),
Arc::new(payment_verifier),
Arc::new(quote_generator),
p2p_node,
);

info!(
Expand Down
1 change: 1 addition & 0 deletions src/payment/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ mod tests {
timestamp: SystemTime::now(),
price: Amount::from(1u64),
rewards_address: RewardsAddress::new([1u8; 20]),
close_group: vec![],
pub_key: vec![],
signature: vec![],
}
Expand Down
43 changes: 30 additions & 13 deletions src/payment/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ impl QuoteGenerator {
content: XorName,
data_size: usize,
data_type: u32,
close_group: Vec<[u8; 32]>,
) -> Result<PaymentQuote> {
let sign_fn = self
.sign_fn
Expand All @@ -134,9 +135,15 @@ impl QuoteGenerator {
// Convert XorName to xor_name::XorName
let xor_name = xor_name::XorName(content);

// Create bytes for signing (following autonomi's pattern)
let bytes =
PaymentQuote::bytes_for_signing(xor_name, timestamp, &price, &self.rewards_address);
// Create bytes for signing — includes close_group so it's
// cryptographically bound to this quote.
let bytes = PaymentQuote::bytes_for_signing(
xor_name,
timestamp,
&price,
&self.rewards_address,
&close_group,
);

// Sign the bytes
let signature = sign_fn(&bytes);
Expand All @@ -152,6 +159,7 @@ impl QuoteGenerator {
price,
pub_key: self.pub_key.clone(),
rewards_address: self.rewards_address,
close_group,
signature,
};

Expand Down Expand Up @@ -437,7 +445,7 @@ mod tests {
let generator = create_test_generator();
let content = [42u8; 32];

let quote = generator.create_quote(content, 1024, 0);
let quote = generator.create_quote(content, 1024, 0, vec![]);
assert!(quote.is_ok());

let quote = quote.expect("valid quote");
Expand All @@ -450,7 +458,7 @@ mod tests {
let content = [42u8; 32];

let quote = generator
.create_quote(content, 1024, 0)
.create_quote(content, 1024, 0, vec![])
.expect("valid quote");
assert!(verify_quote_content(&quote, &content));

Expand All @@ -468,7 +476,7 @@ mod tests {
assert!(!generator.can_sign());

let content = [42u8; 32];
let result = generator.create_quote(content, 1024, 0);
let result = generator.create_quote(content, 1024, 0, vec![]);
assert!(result.is_err());
}

Expand All @@ -491,7 +499,7 @@ mod tests {

let content = [7u8; 32];
let quote = generator
.create_quote(content, 2048, 0)
.create_quote(content, 2048, 0, vec![])
.expect("create quote");

// Valid signature should verify
Expand All @@ -511,7 +519,7 @@ mod tests {
let content = [42u8; 32];

let quote = generator
.create_quote(content, 1024, 0)
.create_quote(content, 1024, 0, vec![])
.expect("create quote");

// The dummy signer produces a 64-byte fake signature, not a valid
Expand Down Expand Up @@ -556,9 +564,15 @@ mod tests {
let content = [10u8; 32];

// All data types produce the same price (price depends on records_stored, not data_type)
let q0 = generator.create_quote(content, 1024, 0).expect("type 0");
let q1 = generator.create_quote(content, 512, 1).expect("type 1");
let q2 = generator.create_quote(content, 256, 2).expect("type 2");
let q0 = generator
.create_quote(content, 1024, 0, vec![])
.expect("type 0");
let q1 = generator
.create_quote(content, 512, 1, vec![])
.expect("type 1");
let q2 = generator
.create_quote(content, 256, 2, vec![])
.expect("type 2");

// All quotes should have a valid price (minimum floor of 1)
assert!(q0.price >= Amount::from(1u64));
Expand All @@ -572,7 +586,9 @@ mod tests {
let content = [11u8; 32];

// Price depends on records_stored, not data size
let quote = generator.create_quote(content, 0, 0).expect("zero size");
let quote = generator
.create_quote(content, 0, 0, vec![])
.expect("zero size");
assert!(quote.price >= Amount::from(1u64));
}

Expand All @@ -583,7 +599,7 @@ mod tests {

// Price depends on records_stored, not data size
let quote = generator
.create_quote(content, 10_000_000, 0)
.create_quote(content, 10_000_000, 0, vec![])
.expect("large size");
assert!(quote.price >= Amount::from(1u64));
}
Expand All @@ -595,6 +611,7 @@ mod tests {
timestamp: SystemTime::now(),
price: Amount::from(1u64),
rewards_address: RewardsAddress::new([0u8; 20]),
close_group: vec![],
pub_key: vec![],
signature: vec![],
};
Expand Down
4 changes: 4 additions & 0 deletions src/payment/single_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ mod tests {
timestamp: SystemTime::now(),
price: Amount::from(1u64),
rewards_address: RewardsAddress::new([rewards_addr_seed; 20]),
close_group: vec![],
pub_key: vec![],
signature: vec![],
}
Expand Down Expand Up @@ -456,6 +457,7 @@ mod tests {
timestamp: SystemTime::now(),
price: Amount::from(*price),
rewards_address: RewardsAddress::new([1u8; 20]),
close_group: vec![],
pub_key: vec![],
signature: vec![],
};
Expand Down Expand Up @@ -566,6 +568,7 @@ mod tests {
price: Amount::from(*price),
#[allow(clippy::cast_possible_truncation)] // i is always < 7
rewards_address: RewardsAddress::new([i as u8 + 1; 20]),
close_group: vec![],
pub_key: vec![],
signature: vec![],
};
Expand Down Expand Up @@ -639,6 +642,7 @@ mod tests {
timestamp: SystemTime::now(),
price,
rewards_address: wallet.address(),
close_group: vec![],
pub_key: vec![],
signature: vec![],
};
Expand Down
Loading