Skip to content

feat!: adapt to evmlib PaymentVault API and simplify pricing#56

Open
mickvandijke wants to merge 6 commits intomainfrom
feat/adapt-evmlib-payment-vault
Open

feat!: adapt to evmlib PaymentVault API and simplify pricing#56
mickvandijke wants to merge 6 commits intomainfrom
feat/adapt-evmlib-payment-vault

Conversation

@mickvandijke
Copy link
Copy Markdown
Collaborator

Summary

  • Breaking: Replace QuotingMetrics with a single price: Amount field on PaymentQuote and MerklePaymentCandidateNode, matching the updated evmlib API
  • Breaking: Unify data_payments_address + merkle_payments_address into a single payment_vault_address (DevnetEvmInfo, CLI args)
  • Replace logarithmic capacity-based pricing with simple quadratic formula (n / 6000)² in wei — removes f64 arithmetic, uses U256 throughout
  • Verify single-node payments via completedPayments mapping directly instead of the removed verify_data_payment batch call
  • Remove zero_quoting_metrics() helper and all QuotingMetrics construction in tests

Note: Cargo.toml currently points evmlib at a local path (../evmlib) for development. This must be updated to the published crate version before merge.

Test plan

  • cargo test passes locally against local evmlib
  • Verify pricing formula produces expected values at key thresholds (0, 6000, 12000 records)
  • E2E merkle payment tests pass with the new candidate node structure
  • Single-node payment verification works against Anvil testnet
  • Update evmlib dependency to published crate before merge

🤖 Generated with Claude Code

mickvandijke and others added 3 commits April 1, 2026 19:36
The merkle payment verifier only checked that paid amounts were non-zero,
not that they met the candidate's quoted price. A malicious client could
submit fake low prices in PoolCommitment candidates while keeping the real
poolHash, causing the contract to charge almost nothing while nodes still
accepted the proof.

Replace `paid_amount.is_zero()` with `paid_amount < node.price` so each
paid candidate must receive at least their ML-DSA-65 signed quoted price.
Also fix existing unit tests that were missing the Amount field in
paid_node_addresses tuples, and add test_merkle_underpayment_rejected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrate from the old QuotingMetrics-based pricing and split
DataPayments/MerklePayments contracts to the unified PaymentVault API
in evmlib. Key changes:

- Replace QuotingMetrics with a single `price: Amount` field on quotes
- Replace logarithmic pricing with simple quadratic formula (n/6000)²
- Unify data_payments_address + merkle_payments_address into
  payment_vault_address
- Verify payments via completedPayments mapping instead of
  verify_data_payment batch call

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mickvandijke added a commit that referenced this pull request Apr 1, 2026
Add unit and e2e tests covering the remaining Section 18 scenarios:

Unit tests (32 new):
- Quorum: #4 fail→abandoned, #16 timeout→inconclusive, #27 single-round
  dual-evidence, #28 dynamic threshold undersized, #33 batched per-key,
  #34 partial response unresolved, #42 quorum-derived paid-list auth
- Admission: #5 unauthorized peer, #7 out-of-range rejected
- Config: #18 invalid config rejected, #26 dynamic paid threshold
- Scheduling: #8 dedup safety, #8 replica/paid collapse
- Neighbor sync: #35 round-robin cooldown skip, #36 cycle completion,
  #38 snapshot stability mid-join, #39 unreachable removal + slot fill,
  #40 cooldown peer removed, #41 cycle termination guarantee,
  consecutive rounds, cycle preserves sync times
- Pruning: #50 hysteresis prevents premature delete, #51 timestamp reset
  on heal, #52 paid/record timestamps independent, #23 entry removal
- Audit: #19/#53 partial failure mixed responsibility, #54 all pass,
  #55 empty failure discard, #56 repair opportunity filter,
  response count validation, digest uses full record bytes
- Types: #13 bootstrap drain, repair opportunity edge cases,
  terminal state variants
- Bootstrap claims: #46 first-seen recorded, #49 cleared on normal

E2e tests (4 new):
- #2 fresh offer with empty PoP rejected
- #5/#37 neighbor sync request returns response
- #11 audit challenge multi-key (present + absent)
- Fetch not-found for non-existent key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mickvandijke and others added 3 commits April 1, 2026 23:49
The verifier checked `paid_amount >= node.price` (individual quote) but
the contract pays each winner `median16(quotes) * 2^depth / depth`. A
winner quoting above the median could be paid less than their quote,
causing the node to incorrectly reject a valid payment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integrate `SingleNodePayment::from_quotes` to derive correct on-chain payment amounts. This ensures exact-match checks in the contract's `verifyPayment` function pass by reconstructing amounts as used by the client.
@mickvandijke mickvandijke marked this pull request as ready for review April 1, 2026 22:56
Copilot AI review requested due to automatic review settings April 1, 2026 22:56
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

Updates ant-node to match the refactored evmlib PaymentVault API by removing QuotingMetrics from quotes/candidate nodes, simplifying pricing to a quadratic function, and adjusting on-chain verification to the new contract surface.

Changes:

  • Replace QuotingMetrics with a single price: Amount across quoting + merkle candidate flows, updating signing/verification and tests accordingly.
  • Switch payment verification to PaymentVault APIs (verifyPayment, completedPayments) and update merkle verification to read completed merkle payment info + validate paid amounts.
  • Replace the previous pricing logic with a U256-based quadratic pricing formula and update devnet manifest wiring to a unified payment_vault_address.

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/payment/verifier.rs Adapts EVM + merkle payment verification to the unified PaymentVault API and new on-chain data structures.
src/payment/single_node.rs Reworks SingleNode verification to use completedPayments and removes QuotingMetrics plumbing.
src/payment/quote.rs Generates quotes/candidate nodes using price (derived from record count) and updates signature bytes accordingly.
src/payment/pricing.rs Implements the new quadratic pricing formula in wei using Amount arithmetic and updates tests.
src/payment/proof.rs Updates proof tests to construct quotes/candidates with price instead of QuotingMetrics.
src/storage/handler.rs Adjusts merkle candidate quote request test expectations for the new candidate node structure.
tests/e2e/merkle_payment.rs Updates E2E merkle payment test helpers to build/sign candidate nodes with price.
src/devnet.rs Unifies devnet manifest EVM contract address fields into payment_vault_address.
src/bin/ant-devnet/main.rs Emits unified vault address into the devnet manifest from the Anvil network config.
Cargo.toml Points evmlib dependency to the refactor branch to pick up the new PaymentVault API.
Cargo.lock Locks the new git-sourced evmlib resolution.

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

Comment on lines +3 to +5
//! Uses the formula `(close_records_stored / 6000)^2` to calculate storage price.
//! Integer division means nodes with fewer than 6000 records get a ratio of 0,
//! but a minimum floor of 1 prevents free storage.
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The module docs claim that “integer division means nodes with fewer than 6000 records get a ratio of 0”, but calculate_price() scales before dividing (n² * 1e18 / 6000²), so prices for 1..5999 records are non-zero (and only n=0 hits the 1 wei floor). Please update the documentation to match the implemented formula/behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +383 to 398
let total_quotes = single_payment.quotes.len();
let mut valid_paid_count: usize = 0;

for result in &results {
if result.isValid && result.amountPaid > Amount::ZERO {
valid_paid_count += 1;
}
}

if paid_results.is_empty() {
if valid_paid_count == 0 {
let xorname_hex = hex::encode(xorname);
return Err(Error::Payment(format!(
"Payment verification failed on-chain for {xorname_hex} (no paid quotes found)"
"Payment verification failed on-chain for {xorname_hex}: \
no valid paid quotes found ({total_quotes} checked)"
)));
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

verify_evm_payment() no longer rejects the case where a quote is paid on-chain (amountPaid > 0) but marked invalid (isValid == false). Currently, the code only checks that at least one result is both paid and valid, which would incorrectly accept a payment set where one paid quote is invalid as long as another is valid. Please restore strictness by failing if any paid result is invalid (while still allowing unpaid quotes to be invalid).

Copilot uses AI. Check for mistakes.
candidate_prices.sort_unstable(); // ascending
// Upper median (index 8 of 16) — matches Solidity's median16 (k = 8)
let median_price = candidate_prices[candidate_prices.len() / 2];
let total_amount = median_price * Amount::from(1u64 << payment_info.depth);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

1u64 << payment_info.depth can overflow/panic (or invoke undefined behavior in release builds) if depth >= 64. Even if current contract depths are small, it’s safer to compute 2^depth using checked shifting (e.g., checked_shl) and return a verification error on overflow, or use an Amount/U256-based shift that cannot overflow for supported depths.

Suggested change
let total_amount = median_price * Amount::from(1u64 << payment_info.depth);
let shift = u32::from(payment_info.depth);
let factor = 1u64
.checked_shl(shift)
.ok_or_else(|| {
Error::Payment(format!(
"Payment depth too large for shift: depth={}",
payment_info.depth
))
})?;
let total_amount = median_price * Amount::from(factor);

Copilot uses AI. Check for mistakes.
Comment on lines +830 to +831
// Node-calculated price based on records stored
assert!(candidate.price >= evmlib::common::Amount::ZERO);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

This assertion is effectively a no-op: candidate.price can never be negative, so >= Amount::ZERO will always pass. Since create_test_protocol() initializes QuotingMetricsTracker::new(1000, 100), you can assert an exact expected price (e.g., candidate.price == calculate_price(100)) to actually validate the new pricing-based candidate construction.

Suggested change
// Node-calculated price based on records stored
assert!(candidate.price >= evmlib::common::Amount::ZERO);
// Node-calculated price based on records stored must be positive
assert!(candidate.price > evmlib::common::Amount::ZERO);

Copilot uses AI. Check for mistakes.

# Payment verification - autonomi network lookup + EVM payment
evmlib = "0.5.0"
evmlib = { git = "https://github.com/WithAutonomi/evmlib/", branch = "refactor/unify-payment-vault-v2" }
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

evmlib is pulled from a moving git branch. For reproducible builds and supply-chain hygiene, prefer a published crate version or pin to an immutable git rev in Cargo.toml (not just in Cargo.lock). The PR description also notes this must be updated before merge.

Suggested change
evmlib = { git = "https://github.com/WithAutonomi/evmlib/", branch = "refactor/unify-payment-vault-v2" }
evmlib = { git = "https://github.com/WithAutonomi/evmlib/", rev = "0123456789abcdef0123456789abcdef01234567" }

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