Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b36a77b
feat(replication): commitment foundation for storage-bound audit (pha…
grumbach May 26, 2026
0496c63
feat(replication): plumb commitment fields through existing wire types
grumbach May 26, 2026
5799104
feat(replication): commitment builder + auditor verifier (phases 2b+2c)
grumbach May 26, 2026
8ce607c
feat(replication): recent_provers cache for holder eligibility (phase…
grumbach May 26, 2026
6cbc323
feat(replication): responder commitment-bound challenge handler + e2e…
grumbach May 26, 2026
89738e6
test(replication): backward-compat wire tests + tighten e2e claims
grumbach May 26, 2026
32b6c4b
revert(replication): un-extend wire types; defer to phase 3
grumbach May 26, 2026
0a01465
test(replication): threat-model PoC tests for v12 storage-bound audit
grumbach May 26, 2026
f321835
fix(replication): add cross-peer binding + cover real Path A + close …
grumbach May 26, 2026
18acbd5
test(replication): make Path A test structurally distinct from happy …
grumbach May 26, 2026
ea4cce4
docs: testnet plan + security notes for v12 storage-bound audit
grumbach May 26, 2026
093d36b
revert: un-revert wire-type extension; old peers are allowed to break
grumbach May 26, 2026
8d4be62
feat(replication): phase-3 wiring — responder rotation tick + gossip …
grumbach May 26, 2026
4052613
feat(replication): responder dispatches commitment-bound audits
grumbach May 26, 2026
108d16b
feat(replication): wire auditor side of v12 commitment-bound audit
grumbach May 26, 2026
2b2e612
fix(replication): address codex round-5 findings on auditor side
grumbach May 26, 2026
7d76cbf
fix(replication): codex round-6 — strict gating + cache cap + churn c…
grumbach May 26, 2026
7bae4df
fix(replication): codex round-7 — RT gate at commitment ingest
grumbach May 26, 2026
0e15c55
fix(replication): codex round-8 — keep pin on unknown commitment
grumbach May 26, 2026
3f599d1
fix(replication): codex round-9 — pin-contract enforcement + streamin…
grumbach May 26, 2026
b8e636f
fix(replication): codex round-10 — align rotation cadence + downgrade…
grumbach May 26, 2026
005a020
fix(replication): codex round-11 — retention window + startup + benig…
grumbach May 26, 2026
edf975f
fix(replication): codex round-12 + David's PR review — TTL eviction +…
grumbach May 26, 2026
41951da
feat(replication): complete v12 design — sticky capable flag, holder …
grumbach May 26, 2026
4d19a24
fix(replication): codex round-13 — rate limit on every attempt + corr…
grumbach May 26, 2026
10d47b8
fix(replication): codex round-14 — close sig-verify rate-limit race
grumbach May 26, 2026
02335ae
chore: cleanup notes
grumbach May 27, 2026
e19f45f
fix(replication): tighten audit_response_timeout to catch relay attac…
grumbach May 28, 2026
24ed852
chore(replication): clear pre-existing clippy + rustdoc errors
grumbach May 28, 2026
62090e8
fix(replication): use saturating_add for audit_response_timeout
grumbach May 28, 2026
aa594ef
fix(replication): reviewer findings on v12 audit + holder-credit paths
grumbach May 28, 2026
a886611
test(replication): cover clear_all on empty-storage rotation path
grumbach May 28, 2026
15cf4e2
fix(replication): round-2 reviewer findings on v12 holder-credit + au…
grumbach May 28, 2026
9d0a0ad
fix(replication): wire ever_capable_peers into the audit shield + cap…
grumbach May 28, 2026
65d9d3c
fix(replication): round-3 codex findings — no-op rotation + per-key p…
grumbach May 28, 2026
c49eeb2
fix(replication): keep commitment pinned on None-downgrade gossip
grumbach May 29, 2026
38a62b6
refactor(replication): trim dead surface + gate test-only helpers + d…
grumbach May 29, 2026
3bec825
test(replication): live responder-handler audit tests + run PoCs in CI
grumbach May 29, 2026
143b260
fix(replication): revoke holder credit on confirmed audit failure + c…
grumbach May 29, 2026
fdab5d3
test(replication): regression-guard the audit-failure credit revocation
grumbach May 29, 2026
7fc5a39
feat(replication): audit-timeout strike grace + deletion-aware quoting
grumbach Jun 3, 2026
3d39822
fix(replication): disable timeout-driven eviction during the breaking…
grumbach Jun 3, 2026
2afb93f
fix(replication): address multi-agent review — rollout safety + hygiene
grumbach Jun 3, 2026
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ 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

doc:
name: Documentation
Expand Down
16 changes: 16 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,22 @@ 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"]

[features]
default = ["logging"]
# Enable tracing/logging infrastructure.
Expand Down
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 @@ -180,6 +180,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
Loading
Loading