Skip to content
Open
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ jobs:
run: cargo test --lib --features test-utils
- name: Run e2e tests
run: cargo test --test e2e --features test-utils -- --test-threads=1
- name: Run v12 storage-bound audit attack PoCs
run: cargo test --test poc_commitment_audit_attacks --features test-utils
- name: Run v12 live audit-handler tests
run: cargo test --test poc_audit_handler_live --features test-utils
- name: Run bootstrap-stall PoC regression marker
run: cargo test --test poc_bootstrap_stall --features test-utils

doc:
name: Documentation
Expand Down
24 changes: 24 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,30 @@ name = "e2e"
path = "tests/e2e/mod.rs"
required-features = ["test-utils"]

# v12 storage-bound audit attack PoCs. Uses the test-only one-shot
# commitment builder/verifier helpers, so it requires the test-utils
# feature. CI runs it via `cargo test --test poc_commitment_audit_attacks
# --features test-utils`.
[[test]]
name = "poc_commitment_audit_attacks"
path = "tests/poc_commitment_audit_attacks.rs"
required-features = ["test-utils"]

# Live responder-handler tests for the v12 audit. Use
# LmdbStorageConfig::test_default(), gated on test-utils.
[[test]]
name = "poc_audit_handler_live"
path = "tests/poc_audit_handler_live.rs"
required-features = ["test-utils"]

Comment on lines +137 to +152
# Bootstrap-stall DoS regression marker (documents the unfixed attack; the
# eventual fix must land with a follow-up test asserting bounded drain).
# Declared like the other PoC suites so CI invokes it explicitly.
[[test]]
name = "poc_bootstrap_stall"
path = "tests/poc_bootstrap_stall.rs"
required-features = ["test-utils"]

[features]
default = ["logging"]
# Enable tracing/logging infrastructure.
Expand Down
251 changes: 251 additions & 0 deletions docs/adr/ADR-0002-gossip-triggered-contiguous-subtree-audit.md

Large diffs are not rendered by default.

52 changes: 32 additions & 20 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,36 @@ impl NodeBuilder {
Self { config }
}

/// Reject startup in production mode without a usable rewards address.
///
/// A node that cannot receive payment must not silently run on the
/// production network. The placeholder address shipped in the example
/// config and an empty string both count as "unconfigured".
///
/// # Errors
///
/// Returns [`Error::Config`] if `network_mode` is `Production` and
/// `payment.rewards_address` is unset, empty, or the example placeholder.
fn validate_production_rewards_address(config: &NodeConfig) -> Result<()> {
if config.network_mode != NetworkMode::Production {
return Ok(());
}
let configured = config
.payment
.rewards_address
.as_deref()
.is_some_and(|addr| !addr.is_empty() && addr != "0xYOUR_ARBITRUM_ADDRESS_HERE");
if configured {
Ok(())
} else {
Err(Error::Config(
"CRITICAL: Rewards address is not configured. \
Set payment.rewards_address in config to your Arbitrum wallet address."
.to_string(),
))
}
}

/// Build and start the node.
///
/// # Errors
Expand All @@ -54,26 +84,7 @@ impl NodeBuilder {
pub async fn build(mut self) -> Result<RunningNode> {
info!("Building ant-node with config: {:?}", self.config);

// Validate rewards address in production
if self.config.network_mode == NetworkMode::Production {
match self.config.payment.rewards_address {
None => {
return Err(Error::Config(
"CRITICAL: Rewards address is not configured. \
Set payment.rewards_address in config to your Arbitrum wallet address."
.to_string(),
));
}
Some(ref addr) if addr == "0xYOUR_ARBITRUM_ADDRESS_HERE" || addr.is_empty() => {
return Err(Error::Config(
"CRITICAL: Rewards address is not configured. \
Set payment.rewards_address in config to your Arbitrum wallet address."
.to_string(),
));
}
Some(_) => {}
}
}
Self::validate_production_rewards_address(&self.config)?;

// Resolve identity and root_dir (may update self.config.root_dir)
let identity = Arc::new(Self::resolve_identity(&mut self.config).await?);
Expand Down Expand Up @@ -150,6 +161,7 @@ impl NodeBuilder {
Arc::clone(&p2p_arc),
storage_arc,
payment_verifier_arc,
Arc::clone(&identity),
&self.config.root_dir,
fresh_rx,
shutdown.clone(),
Expand Down
30 changes: 30 additions & 0 deletions src/payment/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ impl QuotingMetricsTracker {
self.close_records_stored.fetch_add(1, Ordering::SeqCst);
}

/// Overwrite the counter with an authoritative count of held records.
///
/// This is the deletion-aware path and the SINGLE source of truth for the
/// priced record count: the handler calls it at quote time with the live
/// LMDB entry count (`current_chunks()`), so any record removed from
/// storage — by delete, prune, or otherwise — is reflected on the next
/// quote with no per-delete bookkeeping to keep in sync. `record_store`
/// remains only an optimistic between-quote hint; the resync overwrites it.
pub fn set_records(&self, count: usize) {
self.close_records_stored.store(count, Ordering::SeqCst);
}

/// Get the number of records stored.
#[must_use]
pub fn records_stored(&self) -> usize {
Expand Down Expand Up @@ -62,4 +74,22 @@ mod tests {
tracker.record_store();
assert_eq!(tracker.records_stored(), 3);
}

#[test]
fn test_set_records_resyncs_to_authoritative_count() {
let tracker = QuotingMetricsTracker::new(100);
assert_eq!(tracker.records_stored(), 100);

// Resync down (e.g. after deletions/pruning the store now holds fewer).
tracker.set_records(42);
assert_eq!(tracker.records_stored(), 42);

// Resync up (e.g. after new stores).
tracker.set_records(57);
assert_eq!(tracker.records_stored(), 57);

// Resync to zero (empty store).
tracker.set_records(0);
assert_eq!(tracker.records_stored(), 0);
}
}
11 changes: 11 additions & 0 deletions src/payment/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,17 @@ impl QuoteGenerator {
self.metrics_tracker.record_store();
}

/// Resync the quoting metric to an authoritative count of held records.
///
/// The quote price is driven by `records_stored()`. A monotonic store
/// counter would let a node delete chunks it was paid to hold yet keep
/// quoting as if it still held everything. Callers pass the authoritative
/// count of records the node ACTUALLY HOLDS (from the storage layer) so the
/// price reflects current holdings, including deletions and pruning.
pub fn resync_records(&self, count: usize) {
self.metrics_tracker.set_records(count);
}

/// Create a merkle candidate quote for batch payment using ML-DSA-65.
///
/// Returns a `MerklePaymentCandidateNode` constructed with the node's
Expand Down
2 changes: 1 addition & 1 deletion src/replication/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ pub async fn check_bootstrap_drained(
// Hints capacity-rejected at the pending_verify bounds during bootstrap
// must be re-delivered by the originating source before drain can be
// claimed; otherwise we'd silently mark ourselves complete with
// outstanding work the source still owes us (codex round-2 BLOCKER).
// outstanding work the source still owes us.
// The set retires per-source as each source's next admission cycle
// completes with zero rejections — see `clear_capacity_rejected`.
if !state.capacity_rejected_sources.is_empty() {
Expand Down
Loading
Loading