Skip to content

feat(dips): switch to offer-based RCA authorization#1009

Draft
MoonBoi9001 wants to merge 3 commits intomb9/dips-price-rejection-loggingfrom
mb9/dips-switch-to-offer-authorization
Draft

feat(dips): switch to offer-based RCA authorization#1009
MoonBoi9001 wants to merge 3 commits intomb9/dips-price-rejection-loggingfrom
mb9/dips-switch-to-offer-authorization

Conversation

@MoonBoi9001
Copy link
Copy Markdown
Member

@MoonBoi9001 MoonBoi9001 commented Apr 15, 2026

Motivation

Today indexer-service's DIPs validator takes the signature from an incoming SignedRecurringCollectionAgreement, recovers the signer, and checks it against the authorized signer set by querying the network subgraph for escrow accounts. This mirrors the TAP path and reuses EscrowSignerValidator. If the signer is not in the authorized set, indexer-service rejects the proposal as SIGNER_NOT_AUTHORISED.

The audit-bound contracts branch (graphprotocol/contracts#1312) introduces a second authorization path. Under this path the payer submits an offer() transaction to RecurringCollector, and acceptance compares the stored hash against the locally computed one instead of recovering a signature. The same branch adds a uint16 conditions field to RecurringCollectionAgreement, which must also be threaded through the mirroring sol! struct here or the struct hash will not match what the contract expects.

This PR replaces indexer-service's signature path with subgraph-based offer lookup. The companion indexing-payments-subgraph change exposes an Offer entity keyed by agreementId, populated from RecurringCollector.OfferStored events. indexer-service computes the agreement hash locally, derives the agreementId, queries the indexing-payments-subgraph for the stored hash, and compares. Validation stays in the subgraph layer rather than requiring a new JSON-RPC dependency, which keeps it consistent with how indexer-service already looks up agreement lifecycle state.

A side effect is that idle indexers without escrow accounts are no longer blanket-rejected as SIGNER_NOT_AUTHORISED — the escrow set is no longer consulted on the DIPs path at all.

Summary

  • Add uint16 conditions to the sol! RecurringCollectionAgreement struct.
  • New OfferLookup async trait in crates/dips/src/offers.rs with a production HttpOfferLookup (GraphQL query against indexing-payments-subgraph) and a MockOfferLookup for tests.
  • Rewrite SignedRecurringCollectionAgreement::validate (now async) to compute the hash locally, look up the offer hash by derived agreementId, and compare.
  • Delete SignerValidator, EscrowSignerValidator, and the internal SignerNotAuthorised error variant. New variants: OfferNotFound, OfferMismatch, OfferLookupFailed.
  • The gRPC RejectReason::SignerNotAuthorised enum value is retained for wire compatibility — internally it now represents any offer-based validation failure.
  • DipsConfig gains indexing_payments_subgraph_url: Url.
  • All validate-path unit tests rewritten to use MockOfferLookup with pre-loaded entries.

Stack

Stacked on #990 (mb9/dips-price-rejection-logging). Paired PRs: edgeandnode/dipper#607 and graphprotocol/indexing-payments-subgraph#4. Paired contracts branch: indexing-payments-management-audit-fix-reduced (graphprotocol/contracts#1312).

Draft until end-to-end verification against local-network is complete.

MoonBoi9001 and others added 2 commits April 13, 2026 15:33
The indexing-payments-management-audit-fix-reduced contract branch adds
a uint16 conditions bitmask to the RCA struct between
maxSecondsPerCollection and nonce. The bitmask enables payer-declared
conditions like eligibility checks.

Threaded through the sol! type in crates/dips/src/lib.rs and all six
test fixtures. The EIP-712 typehash is derived automatically by the
sol! macro once the struct shape matches, so no pinned typehash string
to update here (unlike dipper).

Defaults to 0 (no conditions). Setting CONDITION_ELIGIBILITY_CHECK = 1
requires a contract payer that implements the eligibility callback
interface -- against an EOA payer the on-chain offer() and accept()
calls revert.

The derive_agreement_id preimage is unchanged; the contract's
_generateAgreementId does not hash conditions. Confirmed via the
pinned shared test vector which matches byte-for-byte with dipper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Indexer-service no longer verifies RCA proposals via EIP-712 signature
recovery and escrow-authorized signer lookup. Instead, it validates
that an on-chain RCA offer exists by querying the
graphprotocol/indexing-payments-subgraph for the corresponding Offer
entity and comparing the stored offerHash to the locally-computed
hashRCA(rca).

Changes:
- New crates/dips/src/offers.rs module with OfferLookup trait,
  HttpOfferLookup production impl (queries subgraph via raw GraphQL
  POST), and MockOfferLookup test impl with a preload-friendly API.
- Deleted crates/dips/src/signers.rs (SignerValidator trait,
  EscrowSignerValidator wrapper, Noop/RejectingSignerValidator stubs).
  The underlying EscrowAccountsWatcher in crates/monitor stays in
  place because TAP still uses it for payer-signer authorization.
- SignedRecurringCollectionAgreement::validate is now async and takes
  &Arc<dyn OfferLookup> instead of &Arc<dyn SignerValidator>. On an
  empty signature wire payload, it skips signature recovery entirely
  and branches on offer_lookup.get_offer_hash(agreement_id) results.
- DipsError drops SignerNotAuthorised and adds OfferNotFound,
  OfferMismatch, and OfferLookupFailed. reject_reason_from_error
  maps all three to the existing gRPC RejectReason::SignerNotAuthorised
  variant for wire-protocol compatibility -- dipper's response handler
  only distinguishes Accept vs Reject, not between individual Reject
  reasons, so no proto change is needed in this release.
- DipsServerContext swaps the signer_validator field for offer_lookup.
- DipsConfig gains indexing_payments_subgraph_url (required, Url type)
  and service.rs plumbs it into HttpOfferLookup on startup.
- crates/config/maximal-config-example.toml gains the new field and
  the test_maximal_config fixture is updated to match.
- All 57 indexer-dips unit tests updated to use MockOfferLookup with
  pre-seeded (agreement_id -> hash) pairs. Added two new tests:
  test_validate_and_create_rca_offer_not_found (empty lookup) and
  test_validate_and_create_rca_offer_mismatch (seeded with wrong hash).
- The old test_validate_and_create_rca_unauthorized_signer test is
  removed (replaced by the two offer-path tests above).

The wire-level ABI encoding is unchanged: dipper sends a
SignedRecurringCollectionAgreement wrapper with the bytes signature
field present but empty. The contract takes the offer-path branch in
_requireAuthorization when the indexer-agent forwards this payload to
SubgraphService.acceptIndexingAgreement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The gRPC handler checked the indexing-payments-subgraph for an on-chain
offer before accepting a proposal. This is wrong: the proposal is a
negotiation step where the indexer validates pricing and metadata. The
on-chain offer doesn't exist yet — dipper submits it only after getting
Accept. The contract enforces offer existence at acceptIndexingAgreement
time, not at proposal time.

Delete offers.rs (OfferLookup trait, HttpOfferLookup, MockOfferLookup),
remove the offer_lookup field from DipsServerContext, simplify validate()
to only check serviceProvider, remove OfferNotFound/OfferMismatch/
OfferLookupFailed error variants, and drop indexing_payments_subgraph_url
from DipsConfig.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DIPs Decentralized Indexing Payments

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant