From 97de372190c55dcb937338f3b2adc6fc5a5c28ae Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Fri, 23 Oct 2026 14:01:54 +0200 Subject: [PATCH 1/3] fix(config): refuse to start with an unusable per-chain price ceiling Dipper's per-chain cap on what it pays each 30 days was never checked when the service started. A cap of 0 shuts out every priced indexer, and IISA refuses a negative one, which dipper retries every 60 seconds forever. Startup now refuses any cap that is not positive, naming its chain. --- bin/dipper-service/src/config.rs | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/bin/dipper-service/src/config.rs b/bin/dipper-service/src/config.rs index 90854a6d..b9feae9d 100644 --- a/bin/dipper-service/src/config.rs +++ b/bin/dipper-service/src/config.rs @@ -998,6 +998,15 @@ impl DipsAgreementConfig { "max_grt_per_billion_entities_per_30_days ({entity_ceiling}) must be a positive number" )); } + // A chain's cap of 0 excludes every priced indexer there, and IISA refuses a negative + // one, which dipper takes for an outage and retries forever; refuse to start instead. + for (chain, cap) in &self.max_grt_per_30_days { + if !(cap.is_finite() && *cap > 0.0) { + return Err(format!( + "max_grt_per_30_days for chain {chain} ({cap}) must be a positive number" + )); + } + } if self.mass_unresponsive_reset_fraction >= self.mass_unresponsive_trip_fraction { return Err(format!( "mass_unresponsive_reset_fraction ({}) must be below mass_unresponsive_trip_fraction ({})", @@ -1699,6 +1708,33 @@ mod tests { } } + #[test] + fn validate_rejects_a_per_chain_ceiling_that_excludes_every_priced_indexer() { + let mut conf: DipsAgreementConfig = serde_json::from_str( + r#"{ + "data_service": "0x1111111111111111111111111111111111111111", + "recurring_collector": "0x2222222222222222222222222222222222222222", + "recurring_agreement_manager": "0x3333333333333333333333333333333333333333", + "min_seconds_per_collection": 60, + "max_seconds_per_collection": 3600, + "pricing_table": {} + }"#, + ) + .expect("deserialization"); + for ceiling in [0.0, -1.0, f64::NAN, f64::INFINITY] { + conf.max_grt_per_30_days + .insert("arbitrum-one".to_string(), ceiling); + let err = conf.validate().expect_err(&format!("{ceiling}")); + assert!(err.contains("arbitrum-one"), "{err}"); + assert!(err.contains(&format!("({ceiling})")), "{err}"); + } + for ceiling in [0.01, 4500.0] { + conf.max_grt_per_30_days + .insert("arbitrum-one".to_string(), ceiling); + assert!(conf.validate().is_ok(), "{ceiling}"); + } + } + #[test] fn validate_rejects_a_collection_window_the_contract_would_refuse() { // The RecurringCollector requires max - min >= 600; an offer built from From 87e805ccdc1a7271e03f1e7daafd88df68c68ef7 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Tue, 27 Oct 2026 10:47:56 +0100 Subject: [PATCH 2/3] fix(config): refuse to start with an unusable agreement price cap Every offer's on-chain payment limit comes from this cap, so 0, a negative or NaN offered indexers nothing and infinity lifted the limit. Test configs now use the default of 20,000. --- bin/dipper-service/src/config.rs | 38 +++++++++++++++++-- .../src/network/service/escrow_reconciler.rs | 2 +- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/bin/dipper-service/src/config.rs b/bin/dipper-service/src/config.rs index aa119940..593ac75b 100644 --- a/bin/dipper-service/src/config.rs +++ b/bin/dipper-service/src/config.rs @@ -990,6 +990,14 @@ impl DipsAgreementConfig { "recurring_agreement_manager must be set to a non-zero address".to_string(), ); } + // Every offer's on-chain payment limit comes from this, so 0, a negative or NaN would offer + // indexers nothing and infinity would lift the limit; refuse to start instead. + let agreement_cap = self.max_agreement_grt_per_30_days; + if !(agreement_cap.is_finite() && agreement_cap > 0.0) { + return Err(format!( + "max_agreement_grt_per_30_days ({agreement_cap}) must be a positive number" + )); + } // IISA drops every indexer priced above this per entity, so a cap of 0 shuts out every // priced indexer, and a negative or non-finite one makes IISA reject the request or // ignore the cap; refuse to start instead. @@ -1316,14 +1324,14 @@ pub struct IndexingAgreementConfig { #[cfg(test)] impl IndexingAgreementConfig { /// Addresses and limits all set to 0, with permissive breaker and cache settings, for tests - /// to adjust the fields they care about. The per-entity price cap keeps its default, since - /// the service refuses to start with 0. + /// to adjust the fields they care about. The agreement and per-entity price caps keep their + /// defaults, since dipper refuses to start with either at 0. pub fn for_tests() -> Self { Self { data_service: Address::ZERO, recurring_collector: Address::ZERO, recurring_agreement_manager: Address::ZERO, - max_agreement_grt_per_30_days: 0.0, + max_agreement_grt_per_30_days: default_max_agreement_grt_per_30_days(), max_seconds_per_collection: 0, min_seconds_per_collection: 0, duration_seconds: 0, @@ -1688,6 +1696,30 @@ mod tests { ); } + #[test] + fn validate_rejects_an_unusable_agreement_cap() { + let mut conf: DipsAgreementConfig = serde_json::from_str( + r#"{ + "data_service": "0x1111111111111111111111111111111111111111", + "recurring_collector": "0x2222222222222222222222222222222222222222", + "recurring_agreement_manager": "0x3333333333333333333333333333333333333333", + "min_seconds_per_collection": 60, + "max_seconds_per_collection": 3600, + "pricing_table": {} + }"#, + ) + .expect("deserialization"); + for cap in [0.0, -1.0, f64::NAN, f64::INFINITY] { + conf.max_agreement_grt_per_30_days = cap; + let err = conf.validate().expect_err(&format!("{cap}")); + assert!(err.contains("max_agreement_grt_per_30_days"), "{err}"); + } + for cap in [0.01, 20_000.0] { + conf.max_agreement_grt_per_30_days = cap; + assert!(conf.validate().is_ok(), "{cap}"); + } + } + #[test] fn validate_rejects_an_unusable_per_entity_ceiling() { let mut conf: DipsAgreementConfig = serde_json::from_str( diff --git a/bin/dipper-service/src/network/service/escrow_reconciler.rs b/bin/dipper-service/src/network/service/escrow_reconciler.rs index 4879f8bd..b4bb7894 100644 --- a/bin/dipper-service/src/network/service/escrow_reconciler.rs +++ b/bin/dipper-service/src/network/service/escrow_reconciler.rs @@ -744,7 +744,7 @@ mod tests { data_service: Address::ZERO, recurring_collector: Address::ZERO, recurring_agreement_manager: Address::ZERO, - max_agreement_grt_per_30_days: 0.0, + max_agreement_grt_per_30_days: 20_000.0, max_seconds_per_collection: 0, min_seconds_per_collection: 0, duration_seconds: 0, From 8ffd8b75b5ae80852f04bc50d99df1c11b6b68c1 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Tue, 27 Oct 2026 10:47:56 +0100 Subject: [PATCH 3/3] test(config): name the per-chain cap test after what it checks It rejects caps of 0, negative, NaN and infinity, and only a cap of 0 excludes every priced indexer, so the old name described 1 case out of 4. --- bin/dipper-service/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/dipper-service/src/config.rs b/bin/dipper-service/src/config.rs index 593ac75b..85be6c95 100644 --- a/bin/dipper-service/src/config.rs +++ b/bin/dipper-service/src/config.rs @@ -1744,7 +1744,7 @@ mod tests { } #[test] - fn validate_rejects_a_per_chain_ceiling_that_excludes_every_priced_indexer() { + fn validate_rejects_an_unusable_per_chain_ceiling() { let mut conf: DipsAgreementConfig = serde_json::from_str( r#"{ "data_service": "0x1111111111111111111111111111111111111111",