From 9b97c59a301dc130149db8436e1a85a220835e50 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 15:01:58 +1000 Subject: [PATCH 01/12] fix(registry): stop benching indexers for offers we never landed An agreement that expires without an offer reaching the chain was treated like a refusal and excluded that indexer from the deployment for 30 days, though it had no offer to accept and had already agreed. Give that case the short window our own faults get, keyed on the missing hash. --- dipper-pgregistry/src/postgres.rs | 7 ++ .../tests/it_registry_postgres.rs | 110 ++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/dipper-pgregistry/src/postgres.rs b/dipper-pgregistry/src/postgres.rs index 1463989e..100aceaa 100644 --- a/dipper-pgregistry/src/postgres.rs +++ b/dipper-pgregistry/src/postgres.rs @@ -726,8 +726,15 @@ impl PgRegistry { (rejection_reason IN ($18, $19) AND updated_at >= timezone('UTC', now()) - make_interval(days => $17)) OR + -- An expiry with no offer transaction means we never landed the offer, so + -- the indexer never had one to accept. Our fault, so it gets the same short + -- lookback as the other dipper-side faults above. + (status = $2 AND offer_tx_hash IS NULL + AND updated_at >= timezone('UTC', now()) - make_interval(mins => $7)) + OR -- All other rejections/expirations/cancellations: standard lookback (COALESCE(rejection_reason, '') NOT IN ($6, $8, $9, $10, $11, $12, $13, $14, $15, $16, $18, $19) + AND NOT (status = $2 AND offer_tx_hash IS NULL) AND updated_at >= timezone('UTC', now()) - make_interval(days => $5)) ) GROUP BY deployment_id diff --git a/dipper-pgregistry/tests/it_registry_postgres.rs b/dipper-pgregistry/tests/it_registry_postgres.rs index 1cd628ed..dabb60a1 100644 --- a/dipper-pgregistry/tests/it_registry_postgres.rs +++ b/dipper-pgregistry/tests/it_registry_postgres.rs @@ -2095,6 +2095,116 @@ async fn get_declined_indexers_other_reason_excluded_after_30_days() { ); } +/// An expiry with no offer transaction means we never landed the offer, so the indexer never +/// had one to accept and must not be benched for a month over it. On 2026-07-29 exactly this +/// benched a willing indexer for 30 days because our RPC provider answered HTTP 500. +#[tokio::test] +async fn get_declined_indexers_expiry_without_an_offer_tx_is_our_fault() { + //* Given + let (db, _temp_db) = temp_registry_db().await; + run_fixture( + &db, + include_str!("fixtures/0003_multi_indexer_agreements.sql"), + ) + .await + .expect("Failed to run fixture"); + + // Age everything the fixture provides out of every window, so only the row under test + // can appear in the result. + sqlx::query( + r#" + UPDATE dipper_reg_indexing_agreements + SET updated_at = timezone('UTC', now()) - interval '31 days' + "#, + ) + .execute(&db) + .await + .expect("Failed to age the fixture agreements"); + + let agreement_id = + IndexingAgreementId::from_bytes([0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + sqlx::query( + r#" + UPDATE dipper_reg_indexing_agreements + SET status = 5, rejection_reason = NULL, offer_tx_hash = NULL, + updated_at = timezone('UTC', now()) - interval '1 hour' + WHERE id = $1 + "#, + ) + .bind(agreement_id) + .execute(&db) + .await + .expect("Failed to update agreement"); + + let registry = PgRegistry::new(db); + + //* When + let result = registry + .get_declined_indexers_by_deployment(30, 1, 5, 1) + .await + .expect("Failed to get declined indexers"); + + //* Then + assert!( + result.is_empty(), + "an expiry we caused must not bench the indexer, got {result:?}" + ); +} + +/// The mirror case: the offer did land, so the indexer had one on chain and let the window +/// close, which is their decision and does earn the standard bench. +#[tokio::test] +async fn get_declined_indexers_expiry_with_an_offer_tx_still_benches() { + //* Given + let (db, _temp_db) = temp_registry_db().await; + run_fixture( + &db, + include_str!("fixtures/0003_multi_indexer_agreements.sql"), + ) + .await + .expect("Failed to run fixture"); + + sqlx::query( + r#" + UPDATE dipper_reg_indexing_agreements + SET updated_at = timezone('UTC', now()) - interval '31 days' + "#, + ) + .execute(&db) + .await + .expect("Failed to age the fixture agreements"); + + let agreement_id = + IndexingAgreementId::from_bytes([0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + sqlx::query( + r#" + UPDATE dipper_reg_indexing_agreements + SET status = 5, rejection_reason = NULL, + offer_tx_hash = decode('11', 'hex'), + updated_at = timezone('UTC', now()) - interval '1 hour' + WHERE id = $1 + "#, + ) + .bind(agreement_id) + .execute(&db) + .await + .expect("Failed to update agreement"); + + let registry = PgRegistry::new(db); + + //* When + let result = registry + .get_declined_indexers_by_deployment(30, 1, 5, 1) + .await + .expect("Failed to get declined indexers"); + + //* Then + assert!( + !result.is_empty(), + "an expiry after the offer landed should still bench the indexer" + ); +} + #[tokio::test] async fn get_declined_indexers_capacity_exceeded_included_within_5_minutes() { //* Given From 078dac7a16c35620838520654c0dceca777b412d Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 19:44:26 +1000 Subject: [PATCH 02/12] fix(registry): excuse an expiry only when it carries no rejection reason An indexing agreement that expires without its offer reaching the chain is dipper's own fault, so it gets a short exclusion window rather than the 30 day default. Restrict that to expiries with no rejection reason, so a reason that later appears on one keeps the window it earns. --- dipper-pgregistry/src/postgres.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dipper-pgregistry/src/postgres.rs b/dipper-pgregistry/src/postgres.rs index 100aceaa..34b15821 100644 --- a/dipper-pgregistry/src/postgres.rs +++ b/dipper-pgregistry/src/postgres.rs @@ -726,15 +726,16 @@ impl PgRegistry { (rejection_reason IN ($18, $19) AND updated_at >= timezone('UTC', now()) - make_interval(days => $17)) OR - -- An expiry with no offer transaction means we never landed the offer, so - -- the indexer never had one to accept. Our fault, so it gets the same short - -- lookback as the other dipper-side faults above. - (status = $2 AND offer_tx_hash IS NULL + -- An expiry with no offer transaction and no rejection reason means we never + -- landed the offer, so the indexer never had one to accept: our fault, so it + -- gets the short dipper-side lookback. An expiry that does carry a reason keeps + -- the window that reason earns. The catch-all below excludes exactly this set. + (status = $2 AND offer_tx_hash IS NULL AND rejection_reason IS NULL AND updated_at >= timezone('UTC', now()) - make_interval(mins => $7)) OR -- All other rejections/expirations/cancellations: standard lookback (COALESCE(rejection_reason, '') NOT IN ($6, $8, $9, $10, $11, $12, $13, $14, $15, $16, $18, $19) - AND NOT (status = $2 AND offer_tx_hash IS NULL) + AND NOT (status = $2 AND offer_tx_hash IS NULL AND rejection_reason IS NULL) AND updated_at >= timezone('UTC', now()) - make_interval(days => $5)) ) GROUP BY deployment_id From 56d3c887e029c2872ef80d523d5b7a9ac92ec9b2 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 19:46:48 +1000 Subject: [PATCH 03/12] test(registry): cover a dipper-caused expiry inside the 5 minute window The tests around a dipper-caused agreement expiry only checked that the indexer stops counting as declined once the 5 minute window has passed, which an implementation that dropped the row from the query altogether would also satisfy. Check the indexer still counts inside the window. --- .../tests/it_registry_postgres.rs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/dipper-pgregistry/tests/it_registry_postgres.rs b/dipper-pgregistry/tests/it_registry_postgres.rs index dabb60a1..9f1a848a 100644 --- a/dipper-pgregistry/tests/it_registry_postgres.rs +++ b/dipper-pgregistry/tests/it_registry_postgres.rs @@ -2095,6 +2095,69 @@ async fn get_declined_indexers_other_reason_excluded_after_30_days() { ); } +/// A dipper-caused expiry moves to the 5 minute window rather than out of the query entirely, +/// so inside that window the indexer still counts as declined and a reassessment seconds later +/// does not hand them the same deployment while our submission path is still failing. +#[tokio::test] +async fn get_declined_indexers_expiry_without_offer_tx_included_within_5_minutes() { + //* Given + let (db, _temp_db) = temp_registry_db().await; + run_fixture( + &db, + include_str!("fixtures/0003_multi_indexer_agreements.sql"), + ) + .await + .expect("Failed to run fixture"); + + // Age everything the fixture provides out of every window, so only the row under test + // can appear in the result. + sqlx::query( + r#" + UPDATE dipper_reg_indexing_agreements + SET updated_at = timezone('UTC', now()) - interval '31 days' + "#, + ) + .execute(&db) + .await + .expect("Failed to age the fixture agreements"); + + let agreement_id = + IndexingAgreementId::from_bytes([0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + sqlx::query( + r#" + UPDATE dipper_reg_indexing_agreements + SET status = 5, rejection_reason = NULL, offer_tx_hash = NULL, + updated_at = timezone('UTC', now()) - interval '1 minute' + WHERE id = $1 + "#, + ) + .bind(agreement_id) + .execute(&db) + .await + .expect("Failed to update agreement"); + + let registry = PgRegistry::new(db); + + //* When + let result = registry + .get_declined_indexers_by_deployment(30, 1, 5, 1) + .await + .expect("Failed to get declined indexers"); + + //* Then + let deployment_1a: DeploymentId = "QmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1a" + .parse() + .unwrap(); + let indexer_a = indexer_id!("1111111111111111111111111111111111111111"); + let declined = result + .get(&deployment_1a) + .expect("Deployment 1a should be in the declined list"); + assert!( + declined.contains(&indexer_a), + "an expiry we caused should still count within the 5 minute window, got {declined:?}" + ); +} + /// An expiry with no offer transaction means we never landed the offer, so the indexer never /// had one to accept and must not be benched for a month over it. On 2026-07-29 exactly this /// benched a willing indexer for 30 days because our RPC provider answered HTTP 500. From d9cc0a1cd695d11434698fc337e38912cf367cc3 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 19:47:19 +1000 Subject: [PATCH 04/12] test(registry): assert which indexer a landed offer benches The test for an agreement that expired after its offer reached the chain only checked that some indexer somewhere came back as declined, so a result naming the wrong deployment or the wrong indexer would have passed. Name both, matching the neighbouring tests. --- dipper-pgregistry/tests/it_registry_postgres.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dipper-pgregistry/tests/it_registry_postgres.rs b/dipper-pgregistry/tests/it_registry_postgres.rs index 9f1a848a..88739728 100644 --- a/dipper-pgregistry/tests/it_registry_postgres.rs +++ b/dipper-pgregistry/tests/it_registry_postgres.rs @@ -2262,9 +2262,16 @@ async fn get_declined_indexers_expiry_with_an_offer_tx_still_benches() { .expect("Failed to get declined indexers"); //* Then + let deployment_1a: DeploymentId = "QmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1a" + .parse() + .unwrap(); + let indexer_a = indexer_id!("1111111111111111111111111111111111111111"); + let declined = result + .get(&deployment_1a) + .expect("Deployment 1a should be in the declined list"); assert!( - !result.is_empty(), - "an expiry after the offer landed should still bench the indexer" + declined.contains(&indexer_a), + "an expiry after the offer landed should still bench the indexer, got {declined:?}" ); } From 89e19cf7217c7b30f9d608cc0d253809e107747e Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 19:47:46 +1000 Subject: [PATCH 05/12] test(registry): stamp a full length transaction hash in the test The test that stands in for an agreement whose offer reached the chain wrote a single byte into a column that holds a 32 byte transaction hash in production. Nothing reads the column back yet, so widen it now rather than leave a trap for whoever starts reading it. --- dipper-pgregistry/tests/it_registry_postgres.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dipper-pgregistry/tests/it_registry_postgres.rs b/dipper-pgregistry/tests/it_registry_postgres.rs index 88739728..c1d41897 100644 --- a/dipper-pgregistry/tests/it_registry_postgres.rs +++ b/dipper-pgregistry/tests/it_registry_postgres.rs @@ -2243,7 +2243,7 @@ async fn get_declined_indexers_expiry_with_an_offer_tx_still_benches() { r#" UPDATE dipper_reg_indexing_agreements SET status = 5, rejection_reason = NULL, - offer_tx_hash = decode('11', 'hex'), + offer_tx_hash = decode(repeat('11', 32), 'hex'), updated_at = timezone('UTC', now()) - interval '1 hour' WHERE id = $1 "#, From e9adc41ea107d12fb213c5a042b6ac713992cdbd Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 19:48:07 +1000 Subject: [PATCH 06/12] test(registry): name the expiry tests after the windows they check Every other test covering how long an indexer stays excluded is named for the window it checks, which is what makes the set readable as a grid of reason against window. The 2 newest ones were named for their conclusion instead, so they sat outside that grid. --- dipper-pgregistry/tests/it_registry_postgres.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dipper-pgregistry/tests/it_registry_postgres.rs b/dipper-pgregistry/tests/it_registry_postgres.rs index c1d41897..84a63bcb 100644 --- a/dipper-pgregistry/tests/it_registry_postgres.rs +++ b/dipper-pgregistry/tests/it_registry_postgres.rs @@ -2162,7 +2162,7 @@ async fn get_declined_indexers_expiry_without_offer_tx_included_within_5_minutes /// had one to accept and must not be benched for a month over it. On 2026-07-29 exactly this /// benched a willing indexer for 30 days because our RPC provider answered HTTP 500. #[tokio::test] -async fn get_declined_indexers_expiry_without_an_offer_tx_is_our_fault() { +async fn get_declined_indexers_expiry_without_offer_tx_excluded_after_5_minutes() { //* Given let (db, _temp_db) = temp_registry_db().await; run_fixture( @@ -2217,7 +2217,7 @@ async fn get_declined_indexers_expiry_without_an_offer_tx_is_our_fault() { /// The mirror case: the offer did land, so the indexer had one on chain and let the window /// close, which is their decision and does earn the standard bench. #[tokio::test] -async fn get_declined_indexers_expiry_with_an_offer_tx_still_benches() { +async fn get_declined_indexers_expiry_with_offer_tx_included_within_30_days() { //* Given let (db, _temp_db) = temp_registry_db().await; run_fixture( From 8f8b4816c18d90a72b7d74ee430fbf4dbbcfa854 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 19:48:47 +1000 Subject: [PATCH 07/12] docs(registry): note the window an expiry gets from its missing offer How long an indexer stays excluded from a deployment used to be decided purely by the reason it gave for declining, and the description still said so. An agreement that expired without its offer reaching the chain now gets a window too, from the absence of that transaction. --- dipper-pgregistry/src/postgres.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dipper-pgregistry/src/postgres.rs b/dipper-pgregistry/src/postgres.rs index 34b15821..47e2253c 100644 --- a/dipper-pgregistry/src/postgres.rs +++ b/dipper-pgregistry/src/postgres.rs @@ -689,7 +689,7 @@ impl PgRegistry { /// Get declined `CanceledByIndexer`/`Expired`/`Rejected` indexers grouped by /// deployment (deployment id -> indexer ids). Each rejection reason gets its own - /// exclusion window (price, transient, uncertain, default); see the constants. + /// exclusion window, as does an expiry that never had an offer transaction. pub async fn get_declined_indexers_by_deployment( &self, default_lookback_days: i32, From 766cd419fd01f3a529add66456d447e7c55a079d Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 19:49:15 +1000 Subject: [PATCH 08/12] docs(agreement): note the window an expiry gets from its missing offer The description of how dipper decides how long to keep an indexer out of a deployment said the window comes purely from the reason the indexer gave for declining. An agreement that expired without its offer reaching the chain gets one too, from the absence of that transaction. --- bin/dipper-service/src/registry/agreement.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/dipper-service/src/registry/agreement.rs b/bin/dipper-service/src/registry/agreement.rs index 8248203f..fac5d5fe 100644 --- a/bin/dipper-service/src/registry/agreement.rs +++ b/bin/dipper-service/src/registry/agreement.rs @@ -173,8 +173,8 @@ pub trait AgreementRegistry { ) -> RegistryResult>>; /// Get declined `CanceledByIndexer`/`Expired`/`Rejected` indexers grouped by - /// deployment. Each rejection reason gets its own exclusion window (price, - /// transient, uncertain, or default); see the `rejection_reason` constants. + /// deployment. Each rejection reason gets its own exclusion window, as does an + /// expiry that never had an offer transaction; see the query for the details. async fn get_declined_indexers_by_deployment( &self, default_lookback_days: i32, From df79b9017eb93805663941e0365972c0088f799c Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 19:49:39 +1000 Subject: [PATCH 09/12] docs(config): correct which expiries the 30 day exclusion covers This setting decides how long an indexer stays out of a deployment after declining, and its description claimed every expired agreement falls under it. Only expiries whose offer reached the chain do; the rest are dipper's own failure to submit and clear in minutes. --- bin/dipper-service/src/config.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/dipper-service/src/config.rs b/bin/dipper-service/src/config.rs index 9be975c8..7f4cb42e 100644 --- a/bin/dipper-service/src/config.rs +++ b/bin/dipper-service/src/config.rs @@ -793,9 +793,9 @@ pub struct DipsAgreementConfig { #[serde(default = "default_max_grt_per_billion_entities_per_30_days")] pub max_grt_per_billion_entities_per_30_days: f64, - /// Number of days to look back for declined indexers (standard exclusion). - /// Covers CanceledByIndexer/Expired agreements and structurally persistent - /// rejections (UNSUPPORTED_NETWORK, MANIFEST_TOO_LARGE). Default: 30 days. + /// Number of days to look back for declined indexers (standard exclusion). Covers + /// CanceledByIndexer, expiries whose offer reached the chain, and structurally + /// persistent rejections (UNSUPPORTED_NETWORK, MANIFEST_TOO_LARGE). Default: 30 days. #[serde(default = "default_declined_indexer_lookback_days")] pub declined_indexer_lookback_days: i32, From aea489bc98a84a85ccefcca6e04fcd7f0f8907a1 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 19:50:38 +1000 Subject: [PATCH 10/12] docs(offer): drop the claim that a removed check guards resubmits Dipper used to ask a subgraph whether an on-chain offer had already been recorded before sending another, and that check went away when every agreement became contract funded. The description of this step and one of its log lines still described it, which flatters what the code does. --- .../src/worker/handlers/submit_offer.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/bin/dipper-service/src/worker/handlers/submit_offer.rs b/bin/dipper-service/src/worker/handlers/submit_offer.rs index 022dd07c..f1966174 100644 --- a/bin/dipper-service/src/worker/handlers/submit_offer.rs +++ b/bin/dipper-service/src/worker/handlers/submit_offer.rs @@ -12,14 +12,9 @@ //! via `RecurringCollector.offer()`. The indexer-agent then calls //! `acceptIndexingAgreement` — the contract checks `rcaOffers`. //! -//! Idempotency is gated on the indexing-payments-subgraph's `Offer` entity, -//! not an RPC call. The `rcaOffers` mapping on `RecurringCollector` lives -//! inside an ERC-7201 namespaced storage struct with no public getter, so -//! dipper reuses the same subgraph indexer-rs queries to check whether a -//! prior submission already landed. The subgraph handler is idempotent on -//! duplicate `OfferStored` events, so a crashed restart that races the -//! subgraph's indexing lag and re-submits will end up as a no-op at the -//! entity level even if it costs a second on-chain transaction. +//! Nothing here is idempotent across a crash: a re-run submits the offer again. The +//! `rcaOffers` mapping on `RecurringCollector` sits in an ERC-7201 namespaced storage +//! struct with no public getter, so the chain cannot cheaply be asked what already landed. use std::time::Duration; @@ -118,9 +113,11 @@ where // through it rather than posting directly. match ctx.chain_client.offer_via_manager(&rca).await { Ok(None) => { + // The chain client reports nothing was submitted. The live client always + // submits, so this only fires for a client that skips the offer itself. tracing::info!( agreement_id = %agreement_id, - "Offer already stored on-chain with matching hash, proceeding to dispatch" + "Offer needed no transaction, proceeding to dispatch" ); } Ok(Some(tx_hash)) => { From a853553c92448033127f3294db880650cece12c4 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 20:37:33 +1000 Subject: [PATCH 11/12] docs(registry): say plainly what a missing offer transaction proves An agreement that expired with no record of its offer reaching the chain is treated as dipper's own failure, on short exclusion. That record can also be missing when the offer did land, so say so where the decision is made, along with the fact that this query is what makes it matter. --- dipper-pgregistry/src/postgres.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dipper-pgregistry/src/postgres.rs b/dipper-pgregistry/src/postgres.rs index 47e2253c..8f2254fc 100644 --- a/dipper-pgregistry/src/postgres.rs +++ b/dipper-pgregistry/src/postgres.rs @@ -730,6 +730,11 @@ impl PgRegistry { -- landed the offer, so the indexer never had one to accept: our fault, so it -- gets the short dipper-side lookback. An expiry that does carry a reason keeps -- the window that reason earns. The catch-all below excludes exactly this set. + -- Read the empty hash as strong evidence, not proof. It is also empty when the + -- write recording it failed, and when the transaction confirmed after this row + -- had already expired, which both let off an indexer that could have accepted: + -- the harmless direction. The column's own migration calls it observability + -- only, so note that this query is what makes it load-bearing. (status = $2 AND offer_tx_hash IS NULL AND rejection_reason IS NULL AND updated_at >= timezone('UTC', now()) - make_interval(mins => $7)) OR From 71b2020747732bab9fa9c5cf318461ffd6515696 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Thu, 30 Jul 2026 20:39:10 +1000 Subject: [PATCH 12/12] test(registry): give the expired fixture agreement an offer transaction One test asserts an indexer who let an agreement expire stays out of a deployment for 30 days, but the agreement it uses has no record of its offer reaching the chain, which now reads as dipper's own failure and a wait of minutes. Record the offer so the test covers what it claims. --- .../tests/fixtures/0003_multi_indexer_agreements.sql | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dipper-pgregistry/tests/fixtures/0003_multi_indexer_agreements.sql b/dipper-pgregistry/tests/fixtures/0003_multi_indexer_agreements.sql index 4c68bfcc..f090f8b3 100644 --- a/dipper-pgregistry/tests/fixtures/0003_multi_indexer_agreements.sql +++ b/dipper-pgregistry/tests/fixtures/0003_multi_indexer_agreements.sql @@ -61,12 +61,16 @@ VALUES ('\xbb000000000000000000000000000001'::bytea, '01930100-0002-7000-8000-00 '{"payer": "0x442a24985444cdc6a4db9503d354918d27b5ea97", "service_provider": "0x2222222222222222222222222222222222222222", "data_service": "0x442a24985444cdc6a4db9503d354918d27b5ea97", "deadline": 1700000300, "ends_at": 1700086400, "max_initial_tokens": "1000", "max_ongoing_tokens_per_second": "100", "min_seconds_per_collection": 86400, "max_seconds_per_collection": 864000, "metadata": {"tokens_per_second": "10", "tokens_per_entity_per_second": "1", "subgraph_deployment_id": "QmDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD4d", "protocol_network": 1, "chain_id": 1}}'::json); -- Indexer C: 0x3333333333333333333333333333333333333333 --- Has NO active agreements (only expired) -INSERT INTO dipper_reg_indexing_agreements (id, nonce_uuid, created_at, updated_at, status, indexing_request_id, deployment_id, indexer_id, indexer_url, terms) +-- Has NO active agreements (only expired). The expiry carries an offer transaction, so it is an +-- indexer who had an offer on chain and let the window close, which earns the standard exclusion. +-- Leave the transaction set: without it the row reads as dipper failing to submit, and every test +-- that expects this row on the standard window would silently move to the short one instead. +INSERT INTO dipper_reg_indexing_agreements (id, nonce_uuid, created_at, updated_at, status, indexing_request_id, deployment_id, indexer_id, indexer_url, terms, offer_tx_hash) VALUES ('\xcc000000000000000000000000000001'::bytea, '01930100-0003-7000-8000-000000000001'::uuid, timezone('UTC', now()), timezone('UTC', now()), 5, -- Expired '01930100-0000-7000-8000-000000000001'::uuid, 'QmEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE5e', '\x3333333333333333333333333333333333333333'::bytea, 'https://indexer-c.com', - '{"payer": "0x442a24985444cdc6a4db9503d354918d27b5ea97", "service_provider": "0x3333333333333333333333333333333333333333", "data_service": "0x442a24985444cdc6a4db9503d354918d27b5ea97", "deadline": 1700000300, "ends_at": 1700086400, "max_initial_tokens": "1000", "max_ongoing_tokens_per_second": "100", "min_seconds_per_collection": 86400, "max_seconds_per_collection": 864000, "metadata": {"tokens_per_second": "10", "tokens_per_entity_per_second": "1", "subgraph_deployment_id": "QmEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE5e", "protocol_network": 1, "chain_id": 1}}'::json); + '{"payer": "0x442a24985444cdc6a4db9503d354918d27b5ea97", "service_provider": "0x3333333333333333333333333333333333333333", "data_service": "0x442a24985444cdc6a4db9503d354918d27b5ea97", "deadline": 1700000300, "ends_at": 1700086400, "max_initial_tokens": "1000", "max_ongoing_tokens_per_second": "100", "min_seconds_per_collection": 86400, "max_seconds_per_collection": 864000, "metadata": {"tokens_per_second": "10", "tokens_per_entity_per_second": "1", "subgraph_deployment_id": "QmEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE5e", "protocol_network": 1, "chain_id": 1}}'::json, + decode(repeat('cc', 32), 'hex'));