From 580ceb64223f551264f924b7c8c8fdea9d6f5762 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Wed, 7 Oct 2026 21:15:49 +0100 Subject: [PATCH 1/3] refactor(listener): drop the date that left old agreements unannounced Dipper skipped announcing the accept and end of agreements created before its first release with lifecycle events, but that only spared consumers late events for old testnet agreements. Every agreement is now announced when the chain shows it accepted or ended. --- .../src/network/service/chain_listener.rs | 64 +------------------ 1 file changed, 1 insertion(+), 63 deletions(-) diff --git a/bin/dipper-service/src/network/service/chain_listener.rs b/bin/dipper-service/src/network/service/chain_listener.rs index eacc5ebb..70287353 100644 --- a/bin/dipper-service/src/network/service/chain_listener.rs +++ b/bin/dipper-service/src/network/service/chain_listener.rs @@ -946,11 +946,6 @@ where } } -/// When v0.1.10, the first release that announces lifecycle events, came out (2026-08-11 -/// UTC). Agreements dipper created before then are never announced, even when a replay of -/// the chain reads them again. -const LIFECYCLE_EVENTS_START: i64 = 1_786_406_400; - /// Record the accept and cancel of an agreement dipper had already marked /// cancelled that went live on-chain first, so its accepted and terminated /// events go out. Cancel first: the terminated sweep waits only for the accept. @@ -960,7 +955,7 @@ async fn record_accept_and_cancel_from_chain( agreement: &IndexingAgreement, registry: &R, ) { - if snapshot.accepted_at == 0 || !created_after_events_started(agreement) { + if snapshot.accepted_at == 0 { return; } let canceled_by = snapshot.canceled_by.to_string(); @@ -990,12 +985,6 @@ async fn record_accept_and_cancel_from_chain( } } -/// Whether dipper created the agreement once it announced lifecycle events. Its own clock -/// at creation, unlike any read of the chain, doesn't depend on how far the listener lags. -fn created_after_events_started(agreement: &IndexingAgreement) -> bool { - agreement.created_at.unix_timestamp() >= LIFECYCLE_EVENTS_START -} - /// Safety net for an agreement dipper cancelled whose offer the indexer accepted /// anyway, such as one that landed after dipper's cancel. Nothing else would end /// it: reconciliation ignores an accept on a cancelled row. It goes back to @@ -1035,7 +1024,6 @@ async fn record_accept_of_cancelling( if (agreement.status != IndexingAgreementStatus::Cancelling && !reopened) || !snapshot.state.reached_accepted() || snapshot.accepted_at == 0 - || !created_after_events_started(agreement) { return; } @@ -1991,12 +1979,6 @@ mod tests { } } - fn set_agreement_created_at(&self, agreement_id: IndexingAgreementId, unix: i64) { - if let Some(a) = self.state.lock().unwrap().agreements.get_mut(&agreement_id) { - a.created_at = OffsetDateTime::from_unix_timestamp(unix).unwrap(); - } - } - fn set_agreement_request_id( &self, agreement_id: IndexingAgreementId, @@ -2705,27 +2687,6 @@ mod tests { assert!(!registry.was_reopened(&agreement_id)); } - #[tokio::test] - async fn reconcile_records_no_accept_of_an_agreement_from_before_events_existed() { - let registry = MockRegistry::new(); - let chain_client = MockChainClient::default(); - let agreement_id = IndexingAgreementId::from_bytes(rand::random()); - registry.add_agreement(agreement_id, IndexingAgreementStatus::Cancelling); - registry.set_agreement_created_at(agreement_id, LIFECYCLE_EVENTS_START - 1); - - let snapshot = make_snapshot(agreement_id, AgreementState::Accepted, Address::ZERO); - reconcile_agreement( - &snapshot, - ®istry, - &chain_client, - test_agreement_conf().as_ref(), - ) - .await - .expect("reconcile ok"); - - assert!(registry.audit_writes().is_empty()); - } - #[tokio::test] async fn reconcile_records_no_accept_for_a_withdrawn_offer() { // The subgraph reports a withdrawn offer as cancelled by the payer with no accept @@ -3218,29 +3179,6 @@ mod tests { assert!(registry.audit_writes().is_empty()); } - #[tokio::test] - async fn test_reconcile_does_not_announce_an_agreement_accepted_before_events_existed() { - // Agreements created before lifecycle events existed have no recorded accept - // and are never announced, even when a replay of the chain reads them again. - let registry = MockRegistry::new(); - let chain_client = MockChainClient::default(); - let agreement_id = IndexingAgreementId::from_bytes(rand::random()); - registry.add_agreement(agreement_id, IndexingAgreementStatus::CanceledByRequester); - registry.set_agreement_created_at(agreement_id, LIFECYCLE_EVENTS_START - 1); - - let snapshot = make_snapshot(agreement_id, AgreementState::CanceledByPayer, Address::ZERO); - let result = reconcile_agreement( - &snapshot, - ®istry, - &chain_client, - test_agreement_conf().as_ref(), - ) - .await; - - assert!(result.is_ok()); - assert!(registry.audit_writes().is_empty()); - } - #[tokio::test] async fn test_reconcile_records_nothing_for_cancelled_agreement_never_accepted() { // A withdrawn offer was never accepted, so there is nothing to announce. From 1d6635d213ec8277fd502f2844c108482371b1f9 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Wed, 7 Oct 2026 21:19:13 +0100 Subject: [PATCH 2/3] fix(listener): let an end recorded before the accept give way When the listener recorded an agreement's accept and end from the chain, it recorded the end before the accept, so an older end from before the accept, such as its offer's withdrawal, was kept. The end is now recorded again after the accept, so the real one replaces it. --- .../src/network/service/chain_listener.rs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/bin/dipper-service/src/network/service/chain_listener.rs b/bin/dipper-service/src/network/service/chain_listener.rs index 70287353..deb3fc87 100644 --- a/bin/dipper-service/src/network/service/chain_listener.rs +++ b/bin/dipper-service/src/network/service/chain_listener.rs @@ -949,7 +949,9 @@ where /// Record the accept and cancel of an agreement dipper had already marked /// cancelled that went live on-chain first, so its accepted and terminated /// events go out. Cancel first: the terminated sweep waits only for the accept. -/// Existing values win, so an agreement dipper already recorded is unchanged. +/// Existing values win, so an agreement dipper already recorded is unchanged, +/// except an end recorded before the accept, such as its offer's withdrawal: the +/// cancel is recorded again once the accept is, so that end gives way to this one. async fn record_accept_and_cancel_from_chain( snapshot: &AgreementStateSnapshot, agreement: &IndexingAgreement, @@ -975,6 +977,19 @@ async fn record_accept_and_cancel_from_chain( } Err(err) => Err(err), }; + let recorded = match recorded { + Ok(()) => { + registry + .record_cancel_audit( + &agreement.id, + snapshot.canceled_at, + &canceled_by, + Some(&snapshot.canceled_tx), + ) + .await + } + Err(err) => Err(err), + }; if let Err(err) = recorded { tracing::warn!( agreement_id = %agreement.id, @@ -3131,7 +3146,7 @@ mod tests { // Dipper had marked the agreement cancelled, but it was accepted on-chain // before being ended there. Recording both lets the accepted and terminated // events go out; the cancel goes first because the terminated sweep only - // waits for the accept. + // waits for the accept, and again after it, to replace an end from before it. let registry = MockRegistry::new(); let chain_client = MockChainClient::default(); let agreement_id = IndexingAgreementId::from_bytes(rand::random()); @@ -3152,7 +3167,11 @@ mod tests { assert!(result.is_ok()); assert_eq!( registry.audit_writes(), - vec![("cancel", agreement_id), ("accept", agreement_id)] + vec![ + ("cancel", agreement_id), + ("accept", agreement_id), + ("cancel", agreement_id) + ] ); } From 2e28c2635ac753b61e90fde0548ecbd363192614 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Wed, 7 Oct 2026 21:19:20 +0100 Subject: [PATCH 3/3] docs(listener): stop saying old accepted agreements are never announced A comment on the accepted-event sweep said agreements from before events existed are never announced, which stopped being true once the date that skipped them was removed. --- bin/dipper-service/src/network/service/chain_listener.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/dipper-service/src/network/service/chain_listener.rs b/bin/dipper-service/src/network/service/chain_listener.rs index deb3fc87..15c00d57 100644 --- a/bin/dipper-service/src/network/service/chain_listener.rs +++ b/bin/dipper-service/src/network/service/chain_listener.rs @@ -1595,7 +1595,7 @@ where /// confirmed send. /// /// Eligibility (`get_agreements_pending_accepted_emission`) requires -/// `accepted_at IS NOT NULL` (so pre-feature rows are never backfilled) but is +/// `accepted_at IS NOT NULL` (only rows whose accept was recorded) but is /// NOT gated on current status: an agreement accepted and then cancelled in a /// single snapshot is already terminal yet must still emit its `accepted` (which /// is why this sweep runs before the terminated sweep).