chore: companion DIPs PR (do not merge yet)#1038
Draft
MoonBoi9001 wants to merge 33 commits into
Draft
Conversation
…RCA (#942) * feat(dips): implement SignedRCA validation and storage Implement RecurringCollectionAgreement (RCA) protocol for DIPS, aligned with the on-chain IndexingAgreement contract. Changes: - RcaStore trait and PostgreSQL implementation for RCA storage - EIP-712 signature verification via escrow-based authorization - validate_and_create_rca() with full validation pipeline: signature, IPFS manifest, network, pricing, deadline/expiry - Database migration for pending_rca_proposals table The indexer agent queries pending_rca_proposals directly and decides acceptance on-chain via RecurringCollector contract. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(dips): improve configuration ergonomics and validation - Add #[serde(default)] to DipsConfig for minimal config files - Validate recurring_collector != Address::ZERO at startup - Warn when tokens_per_second is empty (all proposals rejected) - Bump pricing rejection logs to info level for visibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(dips): add module-level documentation Add comprehensive documentation explaining architecture, validation flow, trust model, and component responsibilities. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(dips): expand unit test coverage to 43 tests Add comprehensive test suite with AAA pattern: - validate_and_create_rca: 11 tests covering all validation paths - PriceCalculator: 7 tests (previously 0) - SignerValidator implementations: 5 tests - Test doubles: FailingIpfsFetcher, FailingRcaStore, RejectingSignerValidator Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(dips): add IPFS fetch timeout and retry with backoff Add resilience to IPFS manifest fetching: - 30 second timeout per attempt - Up to 4 attempts with exponential backoff (10s, 20s, 40s) - Worst case: ~190 seconds before rejection Dipper gRPC timeout should be >= 220s. See edgeandnode/dipper#557. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…etworks (#947) * feat(dips): improve config ergonomics with GRT pricing and explicit networks Changes human-readable GRT per 30 days pricing config and adds explicit network support list. Addresses #943 and #944. Config changes: - Add supported_networks list (proposals for unlisted networks rejected) - Add min_grt_per_30_days per-network base pricing (GRT/30 days) - Add min_grt_per_million_entities_per_30_days global entity pricing - Add 90+ networks with calculated pricing examples from IISA model Pricing derived from archive node costs (storage $25/TB, memory $1.50/GB, CPU $10/vCPU) divided by expected subgraph count per indexer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(dips): add startup validation tests and fix backoff comment - Fix IPFS retry comment: actual delays are 10s, 20s, 40s (not 1s, 2s, 4s) - Add 5 tests for DIPS startup validation: - test_dips_absent_in_minimal_config - test_dips_config_defaults_recurring_collector_zero - test_dips_config_defaults_empty_supported_networks - test_dips_partial_config_uses_defaults - test_dips_maximal_config_parses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(dips): use ceiling division to protect indexer minimums When converting GRT/30days to wei/second, truncating division caused indexers to accept slightly less than their configured minimum (up to 0.2% loss). Changed to ceiling division so minimums round UP, ensuring indexers never accept offers below their stated price floor. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
When Dipper sends an RCA and times out (network partition, crash after INSERT but before response), it retries. Previously, the retry failed with a duplicate key error, causing Dipper to mark the agreement as failed even though it was stored successfully. Now uses ON CONFLICT DO NOTHING so retries succeed. Both first attempt and retry return success, enabling Dipper to safely retry without creating inconsistent state. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…n reasons (#954) * feat(dips): add /dips/info endpoint and rejection reasons to gRPC Add a public /dips/info HTTP endpoint on port 7600 that advertises the indexer's DIPS pricing configuration (min GRT per 30 days per network, min GRT per million entities, supported networks, and protocol version). This allows the Dipper to discover indexer pricing before sending RCA proposals. Update the gRPC protobuf to include a RejectReason enum on SubmitAgreementProposalResponse, distinguishing PRICE_TOO_LOW from OTHER rejection reasons. The server maps DipsError::TokensPerSecondTooLow and TokensPerEntityPerSecondTooLow to PRICE_TOO_LOW, with all other errors mapped to OTHER. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(dips): gate EscrowSignerValidator behind db feature The signers module was importing indexer_monitor unconditionally, but that crate is only available with the db feature. This caused compilation failures when using only the rpc feature (as dipper does). Changes: - Move EscrowSignerValidator and its imports into a conditionally compiled module (#[cfg(feature = "db")]) - Keep SignerValidator trait, NoopSignerValidator, and RejectingSignerValidator always available since they have no external dependencies - Gate escrow validator tests with #[cfg(all(test, feature = "db"))] - Restore dips_cancellation_eip712_domain function that was accidentally removed during the V2 migration (needed for backwards compatibility) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(dips): rename proto enum values to follow naming conventions The protobuf convention is to prefix enum values with the enum name. Changed PRICE_TOO_LOW -> REJECT_REASON_PRICE_TOO_LOW and OTHER -> REJECT_REASON_OTHER to match REJECT_REASON_UNSPECIFIED. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(dips): add unit tests for reject_reason_from_error Tests verify the mapping from DipsError variants to RejectReason: - TokensPerSecondTooLow -> PriceTooLow - TokensPerEntityPerSecondTooLow -> PriceTooLow - All other errors (UnsupportedNetwork, InvalidSignature, etc.) -> Other Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(dips): extract GRT formatting to helper function The format_grt() function converts wei (10^-18 GRT) to a human-readable GRT string with up to 18 decimal places, trimming trailing zeros. This removes duplicated formatting logic in the dips_info_state setup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(dips): add tests for GRT formatting edge cases Tests cover: - Zero value - Whole numbers (1, 1000 GRT) - Small values less than 1 GRT (0.5 GRT) - Very small values (1 wei = 0.000000000000000001 GRT) - Mixed values with decimals - Trailing zeros are trimmed - Values with many decimal places - Large values with decimals Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: apply nightly rustfmt * fix: use maybe_dips_info for optional builder field * fix: make DipsInfoResponse and DipsInfoPricing public * chore: remove dips_version field from /dips/info response V1 never existed in production, so versioning is unnecessary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: use GRT per billion entities instead of per million The entity pricing unit has been changed from "GRT per million entities" to "GRT per billion entities" for better human readability. At scale, "0.2 GRT per million entities" sounds negligible but actually translates to ~$4.50/TB/month - a meaningful cost that indexers might overlook. Using "200 GRT per billion entities" makes the cost more apparent. Changes: - Config: min_grt_per_million_entities_per_30_days -> min_grt_per_billion_entities_per_30_days - Default value: 0.2 -> 200 (same economics, just different unit) - /dips/info endpoint: field renamed in response - Internal conversion divisor: 1_000_000 -> 1_000_000_000 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(dips): add SIGNER_NOT_AUTHORISED rejection reason (#961) SignerNotAuthorised errors were mapped to RejectReason::Other, which causes dipper to block the indexer for 30 days. Signer authorization is a transient config issue that resolves once the operator registers the signer on the escrow contract, so a dedicated rejection reason allows dipper to apply a much shorter lookback window. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * feat(dips): align RCA struct with indexing-payments-management-audit contracts (#964) The RecurringCollector contract on the `indexing-payments-management-audit` branch removed `bytes16 agreementId` from the RCA struct and replaced it with `uint256 nonce`. Agreement IDs are now derived on-chain via `bytes16(keccak256(abi.encode(payer, dataService, serviceProvider, deadline, nonce)))`. The `deadline` and `endsAt` fields also changed from `uint256` to `uint64`. Updates the sol! struct definition, adds `derive_agreement_id`, simplifies `validate_and_create_rca` by removing fallible U256-to-u64 conversion, and updates all test RCA constructions. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove misleading "v2" from DIPs migration and docs (#965) There is no DIPs v1 -- the off-chain voucher system was abandoned before deployment. DIPs refers exclusively to the on-chain RCA system. Rename the migration from dips_v2 to dips_pending_proposals and clean up doc comments that referenced "V2". Also clarifies the migration ownership comment in service.rs: the indexer-service does not run migrations by convention, the agent owns DDL, and the SQL files here are for local dev and tests only. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * feat(dips): add specific rejection reasons to gRPC proto (#966) The RejectReason proto enum only had 4 values (Unspecified, PriceTooLow, Other, SignerNotAuthorised), so 6 of the 8 validation failures in indexer-service mapped to the generic Other. Dipper uses the reason to set exclusion periods and Other gets 30 days, meaning transient issues like DeadlineExpired would incorrectly exclude an indexer for a month. Added DeadlineExpired, UnsupportedNetwork, SubgraphManifestUnavailable, UnexpectedServiceProvider, AgreementExpired, and UnsupportedMetadataVersion to the proto and updated reject_reason_from_error to map each DipsError variant to its specific reason. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Enables local image builds via `just build-image`, producing ghcr.io/graphprotocol/indexer-service-rs:local and ghcr.io/graphprotocol/indexer-tap-agent:local from the existing per-crate Dockerfiles. Lets downstream consumers (local-network) consume this repo as image tags instead of source clones.
…1029) * ci: add workflow_dispatch trigger to containers.yml Allows on-demand image builds from any branch via gh workflow run, producing :sha-<short> tags for downstream integration testing without widening the push-trigger branches list. * ci(containers): build multi-arch images (linux/amd64,linux/arm64) - Native runner per platform (ubuntu-24.04, ubuntu-24.04-arm), push-by-digest, manifest fused in a follow-up merge job. - Per-platform, per-target buildcache scopes to avoid collisions. - SHA-pin third-party actions with version comments. - Merge gate: !cancelled() && needs.build.result == 'success' + fork-PR check, so workflow_dispatch from a non-default branch doesn't leave orphan per-platform digest blobs in GHCR. - Target list owned by a small prepare job and consumed via fromJSON in build and merge. - Force type=sha,enable=true so meta.outputs.version is populated for the Inspect step on workflow_dispatch.
…tion (#983) The Solidity enum IndexingAgreementVersion has V1 as its first variant, which encodes as 0 in the ABI. The validation check was comparing against 1, causing all valid V1 proposals to be rejected with UnsupportedMetadataVersion. Test data also updated to use version 0. Companion to edgeandnode/dipper#583. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use version 0 for IndexingAgreementVersion.V1 in metadata validation The Solidity enum IndexingAgreementVersion has V1 as its first variant, which encodes as 0 in the ABI. The validation check was comparing against 1, causing all valid V1 proposals to be rejected with UnsupportedMetadataVersion. Test data also updated to use version 0. Companion to edgeandnode/dipper#583. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(dips): log proposal rejections at INFO level When an RCA proposal is rejected, log the rejection reason, error, and deployment ID (when decodable) at INFO level. Previously the rejection was returned via the gRPC response with no server-side log at INFO, making debugging difficult without access to the client. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: fix nightly fmt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(dips): log startup configuration at INFO level Log supported networks, recurring collector address, IPFS URL, and per-network minimum pricing when DIPs is enabled. Previously only a warning was emitted when supported_networks was empty. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: correct iterator and type errors in DIPs startup logging min_grt_per_30_days is destructured from a reference, so use .iter() instead of &ref to avoid &&BTreeMap. min_grt_per_billion_entities is GRT not Option<GRT>, so remove the if-let-Some wrapper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(dips): add chain_id and structured fields to price rejection logs Price rejection logs now include chain_id (CAIP-2 identifier) and use structured tracing fields (offered, minimum) instead of format string interpolation. Makes it easier to filter and query rejection events in production log aggregation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add shared test vector for derive_agreement_id Pins the expected bytes16 output for a fixed set of RCA inputs. The same test vector exists in dipper (dipper-rpc/src/indexer.rs). If either repo's derivation drifts, the test fails with a message pointing to the counterpart. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closed
The merge brought in main's lockfile pins for indexer-dips (rand 0.9.4, plus http, indexer-watcher, serde_json), but the merged Cargo.toml asks for rand 0.8 and no longer references those crates. Running cargo against the merged tree updated the lockfile to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The service tried at startup to check whether Horizon was active, and to talk to a separate escrow data source. Both are no longer needed — Horizon is always on, and escrow moved into the main data feed. Dropping the dead code fixes the build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The escrow-accounts watcher is used in two places: once to wire into the router and once to drive signer validation later on. The first call consumed the value, so the second use no longer had anything to clone from. Clone at the first site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dips): switch to on-chain offer-based authorization The smart contract now verifies authorisation via on-chain offers at agreement acceptance, so the indexer-side signature check has no job left. The RCA struct gains a `conditions` field to match the contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dips): cap IPFS manifest size at 5MB Real subgraph manifests are tens of kilobytes, but the IPFS hash in a proposal is chosen by the caller. Without a cap, a hostile caller can point at very large content and force the indexer to download it all. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(service): apply rate-limit and timeout to DIPs gRPC Adds two tower layers on the DIPs gRPC server: a 220-second timeout covering the IPFS retry budget plus headroom, and a 50-token-per- second global rate limit shared across callers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dips): cut IPFS retries when many proposals are in flight When too many proposals are in flight, the IPFS retry budget lets a hostile caller hold handler slots for up to 190s. The new counter tracks in-flight requests; above 200 the IPFS client tries once. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(service): rate-limit DIPs proposals per source IP A new layer rate-limits per source IP: burst of 10, then 5 tokens per second sustained. Keyed on the peer IP via tonic's TcpConnectInfo extension. Single-source spam is cut off without touching other IPs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dips): allowlist accepted payers via DipsConfig A new `allowed_payers` config field gates which payer addresses the indexer-service will even consider. Omit the field for legacy permissive behaviour; set an empty array to deny everyone; list addresses to allow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(service): wrap DIPs gRPC middleware to make it cloneable The gRPC layer requires the wrapping around it to be cloneable, but the rate-limit and per-IP pieces aren't on their own. Putting a buffer around the whole chain makes it cloneable. The buffer's channel is sized so a healthy burst at the global rate never bumps it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(service): convert response body so DIPs middleware compiles Tower_governor wants axum's body type, while tonic's routing layer hands back its own; the rate-limit stack would not build. Convert in between, and turn any inner stack error into a gRPC status response rather than a dropped connection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(config): drop deprecated [horizon] block from maximal example The [horizon] section sets a field the code now ignores; main already dropped the section from its copy of the example. Merging main back in here kept the block, so the test comparing maximal against the defaults-filled minimal failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(dips): switch to on-chain offer-based authorization The smart contract now verifies authorisation via on-chain offers at agreement acceptance, so the indexer-side signature check has no job left. The RCA struct gains a `conditions` field to match the contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dips): cap IPFS manifest size at 5MB Real subgraph manifests are tens of kilobytes, but the IPFS hash in a proposal is chosen by the caller. Without a cap, a hostile caller can point at very large content and force the indexer to download it all. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(service): apply rate-limit and timeout to DIPs gRPC Adds two tower layers on the DIPs gRPC server: a 220-second timeout covering the IPFS retry budget plus headroom, and a 50-token-per- second global rate limit shared across callers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dips): cut IPFS retries when many proposals are in flight When too many proposals are in flight, the IPFS retry budget lets a hostile caller hold handler slots for up to 190s. The new counter tracks in-flight requests; above 200 the IPFS client tries once. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(service): rate-limit DIPs proposals per source IP A new layer rate-limits per source IP: burst of 10, then 5 tokens per second sustained. Keyed on the peer IP via tonic's TcpConnectInfo extension. Single-source spam is cut off without touching other IPs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dips): allowlist accepted payers via DipsConfig A new `allowed_payers` config field gates which payer addresses the indexer-service will even consider. Omit the field for legacy permissive behaviour; set an empty array to deny everyone; list addresses to allow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(service): wrap DIPs gRPC middleware to make it cloneable The gRPC layer requires the wrapping around it to be cloneable, but the rate-limit and per-IP pieces aren't on their own. Putting a buffer around the whole chain makes it cloneable. The buffer's channel is sized so a healthy burst at the global rate never bumps it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(service): convert response body so DIPs middleware compiles Tower_governor wants axum's body type, while tonic's routing layer hands back its own; the rate-limit stack would not build. Convert in between, and turn any inner stack error into a gRPC status response rather than a dropped connection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(config): drop deprecated [horizon] block from maximal example The [horizon] section sets a field the code now ignores; main already dropped the section from its copy of the example. Merging main back in here kept the block, so the test comparing maximal against the defaults-filled minimal failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(dips): drop CancelAgreement RPC and rename signed_voucher CancelAgreement was a no-op on the indexer side: the handler returned Unimplemented because cancellation now lives on-chain in the RecurringCollector contract. Dipper retried the dead-letter every 20s forever. Also rename signed_voucher to signed_rca to match the bytes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(dips): drop unused CollectPayment RPC and gateway proto CollectPayment was the off-chain voucher-era collection RPC: indexers submitted a signed work claim and dipper returned a TAP micropayment receipt. On-chain RCA collects directly via SubgraphService.collect(), so this RPC is dead. gateway.proto and its bindings can also go. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(dips): drop misleading payer allowlist The allowed_payers config gave operators false access control: the check ran on the payer field inside the proposal, which the caller writes, so anyone could spoof the address and walk past. The on-chain offer at acceptance is the real trust gate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(config): correct DIPs expansion and drop stale Horizon mode line The example listed "Decentralized Indexing Payment System" — the correct expansion of DIPs is "Direct Indexer Payments". The "Requires Horizon mode" line was stale too, since the runtime Horizon check has been dropped and the mode is now always on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The proposal response used one shape for accept and reject, so an accept had to carry a dormant reject reason. Split it into accept and reject variants; a reject carries a reason and optional detail, store failures map to a retryable reason, and malformed ones to a catch-all. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dips): separate accept and reject in the proposal response The proposal response used one shape for accept and reject, so an accept had to carry a dormant reject reason. Split it into accept and reject variants; a reject carries a reason and optional detail, store failures map to a retryable reason, and malformed ones to a catch-all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dips): cap manifest download at Graph Node's default size The indexer caps the IPFS manifest download to bound the cost of a hostile CID, but the 5 MiB cap was below Graph Node's own ~25 MiB limit, so large-but-legitimate manifests were rejected. Raise the cap to 25 MiB and report an oversize manifest with its own area and nature, not the generic one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dips): separate accept and reject in the proposal response The proposal response used one shape for accept and reject, so an accept had to carry a dormant reject reason. Split it into accept and reject variants; a reject carries a reason and optional detail, store failures map to a retryable reason, and malformed ones to a catch-all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dips): verify proposal manifest network is configured for DIPs The indexer reads a proposal's network from the subgraph manifest but only rejected an unconfigured network indirectly, when the price lookup later missed it, and an empty network field slipped through. Check the network against the indexer's configured DIPs networks up front, and reject an empty one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dips): verify the EIP-712 signature on agreement proposals Incoming agreement proposals were no longer signature-checked, so a tampered or unsigned proposal was acted on. Recover the EIP-712 signer over the RecurringCollector domain and reject missing or malformed signatures; the recovered signer feeds a later trust check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dips): fetch the EIP-712 domain from the deployed contract When dips.rpc_url is set, the service reads the EIP-712 domain from the RecurringCollector contract (EIP-5267) at startup, so signer recovery tracks the deployed contract. DIPs startup failures now disable DIPs with an error log instead of taking down the whole service. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(service): report DIPs status on the info endpoint and metrics When DIPs initialization fails the service keeps serving queries, but /dips/info kept advertising prices, so selection systems would still pick the indexer. It now returns a generic unavailable status and an indexer_dips_enabled gauge exposes the degraded state to monitoring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(config): comment out the DIPs RPC URL in the example config The example set rpc_url to a placeholder that answers with garbage, so anyone copying the file verbatim would start their indexer with DIPs disabled. The line is now commented out with a real public Arbitrum endpoint, and copied configs use the built-in domain constants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#1050) Indexers can now set a limit on how many DIPs agreements they take on in a day. The limit is off by default, so nothing changes unless an operator sets it.
* feat(dips): verify the EIP-712 signature on agreement proposals Incoming agreement proposals were no longer signature-checked, so a tampered or unsigned proposal was acted on. Recover the EIP-712 signer over the RecurringCollector domain and reject missing or malformed signatures; the recovered signer feeds a later trust check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dips): reject agreement proposals from untrusted senders The payments service recovers each proposal's signer but accepts any valid signature, with no check on who sent it. Require the signer to hold the on-chain agreement-manager role, read from the indexing-payments subgraph, and reject senders that are not in that set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(dips): fetch agreement-manager roles via typed paginated query The role-holder read hand-built a raw GraphQL string, parsed five bespoke structs, and capped silently at 1000 holders. Switch to a typed, compile-checked query paginated by id cursor so a larger set can't be truncated, and derive the role hash to prevent drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dips): cache rejected signers so repeats skip the subgraph query A proposal from a signer not in the cached role set always triggered a subgraph fetch, so a peer retrying the same unauthorised proposal re-queried every time. Remember a definitive "not a holder" for an hour; a genuinely new signer still fetches at once. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dips): retry a failed role refresh on a short backoff The background refresh runs once a day, so one failed run left the cached role set to go stale and the gate rejecting every proposal as transient until the next day. Now retry on a rising backoff until it succeeds, so a brief outage heals in seconds, not a day. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(config): reject a zero DIPs role-refresh interval A zero role_refresh_interval_secs reached the refresh timer and silently killed the background refresh task on startup, leaving the role gate dead with no error. Reject it in config validation, matching the zero-checks already applied to every other interval. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(dips): cover the fail-open admit and chain-lag success paths The decision a proposal gets after a refresh -- trust, reject, fail-open admit, or transient -- lived inline, so the fail-open admit (a known holder kept during an outage) only ran on a rare race and went untested. Lift it into a pure function and check it all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dips): count proposal outcomes and role-set health as metrics Whether a proposal was accepted, rejected as untrusted, bounced as transient, or refused on other grounds was only in the logs, which don't roll up into rates an operator can alert on. Add counters and gauges for the outcomes, the role-set size and its refresh age. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(config): document the indexing-payments subgraph and role tunables The example enabled DIPs but left out the indexing-payments subgraph it now needs and the three role-refresh knobs, so an operator copying it would hit a startup error. Add the subgraph block and the knobs with their defaults, and update the parse test to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dips): cap on-demand role refreshes to one per 5s An unknown signer triggered a full subgraph refetch on every request, so a spray of distinct signers mapped one-to-one onto refetches. Reuse a fetch from the last 5 seconds instead; a newly-approved manager is still recognised within seconds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(config): validate DIPs collector and subgraph at load time The recurring-collector address and indexing-payments subgraph were only checked partway through service boot. Reject a DIPs-enabled config that omits either at load time, so startup fails fast with a clear message instead of a late error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(config): fix DIPs config contradictions and document tunables The [dips] header described the removed payer-allowlist model (payer, on-chain, at acceptance) and the entity-price example posed as a default it is not. Correct both, rewrite the stale DipsConfig doc, and document every tunable's unit, default, required-ness, and failure mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(config): clarify DIPs and subgraph config comments for indexers Several options in the example config were documented tersely or with contradictory notes -- the role-refresh and rpc_url tunables, the subgraph auth token, and a claim that query_url and deployment_id are mutually exclusive (the code requires query_url). Reword each for clarity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(config): express unlimited signer cap as an absent value The per-payer signer cap used 0 to mean "no limit", overloading a value that reads as "fetch zero signers". It is now an optional positive number: unset means no cap, and 0 is rejected at load. The default (no cap) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(monitor): pass None for the no-limit cap in the v2 escrow test The signer-per-payer cap became an optional value; this test still passed 0, which no longer type-checks. Pass None (no cap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(config): express unlimited signer cap as an absent value The per-payer signer cap used 0 to mean "no limit", overloading a value that reads as "fetch zero signers". It is now an optional positive number: unset means no cap, and 0 is rejected at load. The default (no cap) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(monitor): pass None for the no-limit cap in the v2 escrow test The signer-per-payer cap became an optional value; this test still passed 0, which no longer type-checks. Pass None (no cap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(config): reject zero for subgraph freshness guards The network-subgraph staleness window and the DIPs role-subgraph lag window used 0 to mean "disable the check". They are now non-zero values: 0 is rejected at config load, and disabling is no longer offered. Defaults (30 min and 1800 s) are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When an indexing-payment proposal reaches the indexer a second time, it used to rerun every check — including re-downloading the subgraph manifest — before quietly dropping the duplicate at the end. This change recognises the repeat as soon as the proposal's stable identifier is known and answers from the record it already holds, so the manifest download never runs twice.
The RCA acceptance log recorded only the agreement id, so filtering indexer logs by a subgraph deployment hash never matched an acceptance; the deployment id appeared only on rejection logs. Add it to the acceptance log as well, mirroring the rejection log.
The escrow dust floor is an unvalidated string passed straight to the subgraph, so a typo only fails later as a broken query. Parse it at config load and reject non-numeric input. 0 still means no floor and the default is unchanged. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(config): validate the escrow dust floor at load time The escrow dust floor is an unvalidated string passed straight to the subgraph, so a typo only fails later as a broken query. Parse it at config load and reject non-numeric input. 0 still means no floor and the default is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(config): reject zero for the metrics port and size limits A metrics port of 0 makes the OS bind a random port no scraper can find, and a zero batch or request-body size rejects all work. Reject these at config load instead of failing in confusing ways later. Defaults are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # Cargo.lock
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
Companion to #967 on a new branch. It exists so the team can bring the DIPs work up to date with
mainwithout rewriting the history ofmain-dips, which is what #967 has been reviewed against.Motivation
The DIPs branch
main-dipshas had #967 open againstmainfor a while, and the commits on it have been through review.mainhas moved forward by roughly 50 commits since the last sync, and other PRs that build onmain-dips(currently #1009 and #1037) now have conflicts that need to be worked through. Rather than rewrite the history ofmain-dipsand lose the review record, we are using a new branchmain-dips-rebasedto do that work. Once the dependent PRs are brought up to date and a merge frommainis taken in here, this PR will replace #967.Summary
main-dips-rebasedforked frommain-dipsat its current head.mainhappens later, after feat: switch to on-chain offer-based authorization #1009 and feat: advertise DIPs prices via /dips/info endpoint #1037 land here.