Skip to content

fix: payment verification hardening -- cost units, close group, DoS prevention#58

Open
grumbach wants to merge 3 commits intomainfrom
fix/payment-upload-hardening
Open

fix: payment verification hardening -- cost units, close group, DoS prevention#58
grumbach wants to merge 3 commits intomainfrom
fix/payment-upload-hardening

Conversation

@grumbach
Copy link
Copy Markdown
Collaborator

@grumbach grumbach commented Apr 2, 2026

Summary

  • Wire verify_cost_units() into merkle payment verification: nodes now check that on-chain packed commitments match what signed metrics produce, preventing clients from submitting inflated/deflated cost units to underpay
  • Add CloseGroupChecker callback to PaymentVerifierConfig: when wired up, nodes verify they are in the close group before accepting merkle-paid data (currently None, ready for routing table integration)
  • Move ML-DSA-65 signature validation BEFORE on-chain RPC calls to prevent DoS via garbage proofs triggering expensive chain queries

Depends on

Related PRs

Test plan

  • cfd passes
  • 3-agent adversarial review -- all issues fixed

grumbach added 3 commits April 1, 2026 17:32
Fetch on-chain packed commitments and verify they match what the
signed node metrics produce. This prevents clients from submitting
inflated/deflated cost units to manipulate pricing. The verification
runs only on cache miss (first time per pool hash) to avoid
redundant RPC calls.
Add CloseGroupChecker callback to PaymentVerifierConfig. When provided,
the verifier checks that the local node is in the close group for the
data address before accepting merkle-paid chunks. This prevents nodes
from storing data they're not responsible for.

The checker is currently set to None everywhere (no behavior change).
The node runtime can wire it up to query its routing table.
- Move ML-DSA-65 signature validation BEFORE on-chain queries to
  prevent DoS via garbage proofs triggering expensive RPC lookups
- Move verify_merkle_candidate_signature import to top-level
- Import Arc at top of file instead of inline std::sync::Arc
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens merkle payment verification by validating candidate signatures earlier, adding optional close-group gating, and enforcing cost-unit integrity against on-chain commitments to prevent under/over-payment manipulation.

Changes:

  • Added close_group_checker callback option to PaymentVerifierConfig and threaded it through node/devnet/test configurations.
  • Moved ML-DSA-65 candidate signature validation before on-chain RPC calls to reduce DoS potential.
  • Added cost-unit verification by comparing signed metrics-derived commitments against on-chain packed commitments.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/payment/verifier.rs Implements close-group callback, earlier signature validation, and cost-unit verification against on-chain commitments.
src/node.rs Supplies close_group_checker: None when constructing PaymentVerifierConfig.
src/devnet.rs Supplies close_group_checker: None for devnet nodes’ payment verifier config.
src/storage/handler.rs Updates storage handler tests to include the new config field.
tests/e2e/testnet.rs Updates e2e testnet node protocol construction to include the new config field.
tests/e2e/data_types/chunk.rs Updates e2e chunk tests’ protocol construction to include the new config field.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 73 to +89
@@ -76,6 +80,26 @@ pub struct PaymentVerifierConfig {
/// Local node's rewards address.
/// The verifier rejects payments that don't include this node as a recipient.
pub local_rewards_address: RewardsAddress,
/// Optional close group checker for merkle payments.
///
/// Given a data address, returns the rewards addresses of nodes in the local
/// close group view for that address. Used to verify the storing node is
/// actually responsible for the data. If `None`, the check is skipped.
pub close_group_checker: Option<CloseGroupChecker>,
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PaymentVerifierConfig used to derive Clone (and is re-exported as part of the public API). The new manual Debug impl removed the Clone derive, which is a breaking change for downstream crates and makes it harder to reuse configs. Consider re-adding #[derive(Clone)] (custom Debug can stay) or implementing Clone manually so existing callers continue to compile.

Copilot uses AI. Check for mistakes.
Comment on lines +593 to +616
// Verify cost units: ensure on-chain packed commitments match what
// the signed node metrics produce. This prevents clients from submitting
// inflated/deflated cost units to manipulate pricing.
let packed_commitments = merkle_payment_vault::get_merkle_payment_packed_commitments(
&self.config.evm.network,
pool_hash,
on_chain_info.merkle_payment_timestamp,
)
.await
.map_err(|e| {
Error::Payment(format!(
"Failed to get packed commitments for cost unit verification: {e}"
))
})?;

merkle_proof
.winner_pool
.verify_cost_units(&packed_commitments, &pool_hash)
.map_err(|e| {
Error::Payment(format!(
"Cost unit verification failed for pool {}: {e}",
hex::encode(pool_hash)
))
})?;
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_merkle_payment_packed_commitments() is an additional on-chain RPC call that currently happens immediately after fetching get_merkle_payment_info(), before running other local validations that might fail (e.g., verify_merkle_proof, paid-node index checks). To minimize RPC amplification from invalid proofs, consider deferring the packed-commitments RPC until after the local proof validations succeed, and only then run verify_cost_units() + cache the pool.

Copilot uses AI. Check for mistakes.
Comment on lines +704 to +714
// Verify this node is in the close group for the data address.
// This prevents nodes from accepting data they're not responsible for.
if let Some(ref checker) = self.config.close_group_checker {
let close_group_addrs = checker(xorname);
if !close_group_addrs.contains(&self.config.local_rewards_address) {
return Err(Error::Payment(format!(
"This node is not in the close group for address {}",
hex::encode(xorname)
)));
}
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close-group membership check is performed after the on-chain queries and other verification steps. Since it only depends on xorname and local state, consider moving it earlier (right after the address match, or at least before any RPC calls) so nodes can reject out-of-responsibility PUTs without spending resources on chain lookups.

Copilot uses AI. Check for mistakes.
Comment on lines +704 to +714
// Verify this node is in the close group for the data address.
// This prevents nodes from accepting data they're not responsible for.
if let Some(ref checker) = self.config.close_group_checker {
let close_group_addrs = checker(xorname);
if !close_group_addrs.contains(&self.config.local_rewards_address) {
return Err(Error::Payment(format!(
"This node is not in the close group for address {}",
hex::encode(xorname)
)));
}
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New behavior when close_group_checker is Some(_) is currently untested. Consider adding unit tests covering both acceptance (local rewards address is in returned close group) and rejection (not in close group), using the existing merkle-proof helpers + pre-populated pool_cache to avoid RPC calls.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants