diff --git a/Cargo.lock b/Cargo.lock index 42e402119..fa8acf2f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4257,7 +4257,9 @@ dependencies = [ "ipfs-api-backend-hyper", "prost 0.14.1", "rand 0.8.5", + "reqwest 0.12.24", "serde", + "serde_json", "serde_yaml", "sqlx", "test-assets", diff --git a/crates/dips/Cargo.toml b/crates/dips/Cargo.toml index c3d4edaf7..bca182d4f 100644 --- a/crates/dips/Cargo.toml +++ b/crates/dips/Cargo.toml @@ -35,6 +35,8 @@ tracing.workspace = true bs58 = "0.5" build-info = { workspace = true, optional = true } indexer-monitor = { path = "../monitor", optional = true } +reqwest.workspace = true +serde_json.workspace = true thiserror.workspace = true graph-networks-registry = { workspace = true, optional = true } serde = { workspace = true, optional = true } diff --git a/crates/dips/src/lib.rs b/crates/dips/src/lib.rs index bd9085232..1a1a0623c 100644 --- a/crates/dips/src/lib.rs +++ b/crates/dips/src/lib.rs @@ -74,7 +74,6 @@ pub mod proto; mod registry; #[cfg(feature = "rpc")] pub mod server; -pub mod signers; pub mod store; use thiserror::Error; @@ -120,6 +119,7 @@ sol! { /// Matches `IRecurringCollector.RecurringCollectionAgreement` exactly. /// The agreement ID is derived on-chain via /// `bytes16(keccak256(abi.encode(payer, dataService, serviceProvider, deadline, nonce)))`. + /// Note: `conditions` is NOT included in the agreement ID preimage. #[derive(Debug, PartialEq)] struct RecurringCollectionAgreement { uint64 deadline; @@ -131,6 +131,7 @@ sol! { uint256 maxOngoingTokensPerSecond; uint32 minSecondsPerCollection; uint32 maxSecondsPerCollection; + uint16 conditions; uint256 nonce; bytes metadata; } @@ -214,8 +215,6 @@ pub enum DipsError { }, #[error("tokens per entity per second {offered} is below configured minimum {minimum}")] TokensPerEntityPerSecondTooLow { minimum: U256, offered: U256 }, - #[error("signer {0} not authorised")] - SignerNotAuthorised(Address), #[error("cancelled_by is expected to match the signer")] UnexpectedSigner, // misc @@ -298,37 +297,19 @@ impl RecurringCollectionAgreement { } impl SignedRecurringCollectionAgreement { - /// Validate the RCA signature and basic fields. + /// Validate proposal-time fields. /// - /// Checks: - /// - EIP-712 signature is valid and recovers to an authorized signer for the payer - /// - Signer is authorized for the payer (via escrow accounts) - /// - Service provider matches expected indexer address - pub fn validate( - &self, - signer_validator: &Arc, - domain: &Eip712Domain, - expected_service_provider: &Address, - ) -> Result<(), DipsError> { - let sig = Signature::try_from(self.signature.as_ref()) - .map_err(|err| DipsError::InvalidSignature(err.to_string()))?; - - let payer = self.agreement.payer; - let signer = sig - .recover_address_from_prehash(&self.agreement.eip712_signing_hash(domain)) - .map_err(|err| DipsError::InvalidSignature(err.to_string()))?; - - signer_validator - .validate(&payer, &signer) - .map_err(|_| DipsError::SignerNotAuthorised(signer))?; - + /// Checks that the service provider matches the expected indexer + /// address. On-chain offer existence is NOT checked here — the offer + /// does not exist yet at proposal time. The contract enforces offer + /// existence when the indexer-agent calls `acceptIndexingAgreement`. + pub fn validate(&self, expected_service_provider: &Address) -> Result<(), DipsError> { if !self.agreement.serviceProvider.eq(expected_service_provider) { return Err(DipsError::UnexpectedServiceProvider { expected: *expected_service_provider, actual: self.agreement.serviceProvider, }); } - Ok(()) } @@ -363,14 +344,17 @@ pub(crate) fn try_extract_deployment_id(rca_bytes: &[u8]) -> Option { /// Validate and create a RecurringCollectionAgreement. /// /// Performs validation: -/// - EIP-712 signature verification +/// - Service provider match +/// - Deadline and expiry checks /// - IPFS manifest fetching and network validation /// - Price minimum enforcement /// +/// On-chain offer existence is NOT checked here — the offer doesn't exist +/// yet at proposal time. The contract enforces it at `acceptIndexingAgreement`. +/// /// Returns the agreement ID if successful, stores in database. pub async fn validate_and_create_rca( ctx: Arc, - domain: &Eip712Domain, expected_service_provider: &Address, rca_bytes: Vec, ) -> Result { @@ -378,7 +362,6 @@ pub async fn validate_and_create_rca( rca_store, ipfs_fetcher, price_calculator, - signer_validator, registry, additional_networks, .. @@ -388,8 +371,8 @@ pub async fn validate_and_create_rca( let signed_rca = SignedRecurringCollectionAgreement::abi_decode(rca_bytes.as_ref()) .map_err(|e| DipsError::AbiDecoding(e.to_string()))?; - // Validate signature and basic fields - signed_rca.validate(signer_validator, domain, expected_service_provider)?; + // Validate service provider + signed_rca.validate(expected_service_provider)?; // Validate deadline hasn't passed let now = std::time::SystemTime::now() @@ -534,36 +517,40 @@ mod test { derive_agreement_id, ipfs::{FailingIpfsFetcher, MockIpfsFetcher}, price::PriceCalculator, - rca_eip712_domain, server::DipsServerContext, - signers::{NoopSignerValidator, RejectingSignerValidator}, store::{FailingRcaStore, InMemoryRcaStore}, AcceptIndexingAgreementMetadata, DipsError, IndexingAgreementTermsV1, - RecurringCollectionAgreement, + RecurringCollectionAgreement, SignedRecurringCollectionAgreement, }; use thegraph_core::alloy::{ primitives::{keccak256, Address, FixedBytes, U256}, - signers::local::PrivateKeySigner, sol_types::SolValue, }; - const CHAIN_ID: u64 = 42161; // Arbitrum One - fn create_test_context() -> Arc { Arc::new(DipsServerContext { rca_store: Arc::new(InMemoryRcaStore::default()), - ipfs_fetcher: Arc::new(MockIpfsFetcher::default()), // Returns "mainnet" + ipfs_fetcher: Arc::new(MockIpfsFetcher::default()), price_calculator: Arc::new(PriceCalculator::new( HashSet::from(["mainnet".to_string()]), BTreeMap::from([("mainnet".to_string(), U256::from(100))]), U256::from(50), )), - signer_validator: Arc::new(NoopSignerValidator), registry: Arc::new(crate::registry::test_registry()), additional_networks: Arc::new(BTreeMap::new()), }) } + /// Helper: encode an RCA as a `SignedRecurringCollectionAgreement` + /// wire payload with an empty signature (the offer path). + fn encode_empty_sig(rca: RecurringCollectionAgreement) -> Vec { + SignedRecurringCollectionAgreement { + agreement: rca, + signature: Default::default(), + } + .abi_encode() + } + fn create_test_rca( payer: Address, service_provider: Address, @@ -592,6 +579,7 @@ mod test { maxOngoingTokensPerSecond: U256::from(100), minSecondsPerCollection: 60, maxSecondsPerCollection: 3600, + conditions: 0, nonce: U256::from(1), metadata: metadata.abi_encode().into(), } @@ -609,6 +597,7 @@ mod test { maxOngoingTokensPerSecond: U256::from(10), minSecondsPerCollection: 60, maxSecondsPerCollection: 3600, + conditions: 0, nonce: U256::from(42), metadata: Default::default(), }; @@ -653,6 +642,7 @@ mod test { maxOngoingTokensPerSecond: U256::from(1_000_000_000_000_000u64), minSecondsPerCollection: 3600, maxSecondsPerCollection: 86400, + conditions: 0, nonce: U256::from(0x019d44a86ac97e938672e2501fe630f2u128), metadata: Default::default(), }; @@ -681,24 +671,19 @@ mod test { #[tokio::test] async fn test_validate_and_create_rca_success() { - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); + let payer = Address::repeat_byte(0x42); let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); let rca = create_test_rca(payer, service_provider, U256::from(200), U256::from(100)); let agreement_id = derive_agreement_id(&rca); - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); - let ctx = create_test_context(); + let rca_bytes = encode_empty_sig(rca); + let result = - super::validate_and_create_rca(ctx.clone(), &domain, &service_provider, rca_bytes) - .await; + super::validate_and_create_rca(ctx.clone(), &service_provider, rca_bytes).await; - assert!(result.is_ok()); + assert!(result.is_ok(), "got: {:?}", result); assert_eq!(result.unwrap(), agreement_id); // Verify it was stored @@ -711,11 +696,9 @@ mod test { #[tokio::test] async fn test_validate_and_create_rca_wrong_service_provider() { - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); + let payer = Address::repeat_byte(0x42); let service_provider = Address::repeat_byte(0x11); let wrong_service_provider = Address::repeat_byte(0x99); - let recurring_collector = Address::repeat_byte(0x22); let rca = create_test_rca( payer, @@ -724,13 +707,10 @@ mod test { U256::from(100), ); - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); - let ctx = create_test_context(); - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, rca_bytes).await; + let rca_bytes = encode_empty_sig(rca); + + let result = super::validate_and_create_rca(ctx, &service_provider, rca_bytes).await; assert!(matches!( result, @@ -740,21 +720,16 @@ mod test { #[tokio::test] async fn test_validate_and_create_rca_tokens_per_second_too_low() { - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); + let payer = Address::repeat_byte(0x42); let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); // Offer 50, minimum is 100 let rca = create_test_rca(payer, service_provider, U256::from(50), U256::from(100)); - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); - let ctx = create_test_context(); - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, rca_bytes).await; + let rca_bytes = encode_empty_sig(rca); + + let result = super::validate_and_create_rca(ctx, &service_provider, rca_bytes).await; assert!(matches!( result, @@ -764,21 +739,16 @@ mod test { #[tokio::test] async fn test_validate_and_create_rca_entity_price_too_low() { - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); + let payer = Address::repeat_byte(0x42); let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); // Offer 200 tokens/sec (ok), but only 10 entity price (minimum is 50) let rca = create_test_rca(payer, service_provider, U256::from(200), U256::from(10)); - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); - let ctx = create_test_context(); - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, rca_bytes).await; + let rca_bytes = encode_empty_sig(rca); + + let result = super::validate_and_create_rca(ctx, &service_provider, rca_bytes).await; assert!(matches!( result, @@ -788,17 +758,11 @@ mod test { #[tokio::test] async fn test_validate_and_create_rca_unsupported_network() { - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); + let payer = Address::repeat_byte(0x42); let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); let rca = create_test_rca(payer, service_provider, U256::from(200), U256::from(100)); - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); - // Create context with IPFS fetcher returning unsupported network let ctx = Arc::new(DipsServerContext { rca_store: Arc::new(InMemoryRcaStore::default()), @@ -810,23 +774,20 @@ mod test { BTreeMap::from([("mainnet".to_string(), U256::from(100))]), U256::from(50), )), - signer_validator: Arc::new(NoopSignerValidator), registry: Arc::new(crate::registry::test_registry()), additional_networks: Arc::new(BTreeMap::new()), }); - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, rca_bytes).await; + let rca_bytes = encode_empty_sig(rca); + let result = super::validate_and_create_rca(ctx, &service_provider, rca_bytes).await; assert!(matches!(result, Err(DipsError::UnsupportedNetwork(_)))); } #[tokio::test] async fn test_validate_and_create_rca_invalid_metadata_version() { - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); + let payer = Address::repeat_byte(0x42); let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); let terms = IndexingAgreementTermsV1 { tokensPerSecond: U256::from(200), @@ -850,17 +811,15 @@ mod test { maxOngoingTokensPerSecond: U256::from(100), minSecondsPerCollection: 60, maxSecondsPerCollection: 3600, + conditions: 0, nonce: U256::from(1), metadata: metadata.abi_encode().into(), }; - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); - let ctx = create_test_context(); - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, rca_bytes).await; + let rca_bytes = encode_empty_sig(rca); + + let result = super::validate_and_create_rca(ctx, &service_provider, rca_bytes).await; assert!(matches!( result, @@ -870,10 +829,8 @@ mod test { #[tokio::test] async fn test_validate_and_create_rca_deadline_expired() { - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); + let payer = Address::repeat_byte(0x42); let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); let terms = IndexingAgreementTermsV1 { tokensPerSecond: U256::from(200), @@ -897,27 +854,23 @@ mod test { maxOngoingTokensPerSecond: U256::from(100), minSecondsPerCollection: 60, maxSecondsPerCollection: 3600, + conditions: 0, nonce: U256::from(1), metadata: metadata.abi_encode().into(), }; - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); - let ctx = create_test_context(); - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, rca_bytes).await; + let rca_bytes = encode_empty_sig(rca); + + let result = super::validate_and_create_rca(ctx, &service_provider, rca_bytes).await; assert!(matches!(result, Err(DipsError::DeadlineExpired { .. }))); } #[tokio::test] async fn test_validate_and_create_rca_agreement_expired() { - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); + let payer = Address::repeat_byte(0x42); let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); let terms = IndexingAgreementTermsV1 { tokensPerSecond: U256::from(200), @@ -941,17 +894,15 @@ mod test { maxOngoingTokensPerSecond: U256::from(100), minSecondsPerCollection: 60, maxSecondsPerCollection: 3600, + conditions: 0, nonce: U256::from(1), metadata: metadata.abi_encode().into(), }; - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); - let ctx = create_test_context(); - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, rca_bytes).await; + let rca_bytes = encode_empty_sig(rca); + + let result = super::validate_and_create_rca(ctx, &service_provider, rca_bytes).await; assert!(matches!(result, Err(DipsError::AgreementExpired { .. }))); } @@ -964,15 +915,12 @@ mod test { async fn test_validate_and_create_rca_malformed_abi() { // Arrange let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); let ctx = create_test_context(); let malformed_bytes = vec![0xDE, 0xAD, 0xBE, 0xEF]; // Not valid ABI // Act - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, malformed_bytes).await; + let result = super::validate_and_create_rca(ctx, &service_provider, malformed_bytes).await; // Assert assert!( @@ -982,57 +930,13 @@ mod test { ); } - #[tokio::test] - async fn test_validate_and_create_rca_unauthorized_signer() { - // Arrange - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); - let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); - - let rca = create_test_rca(payer, service_provider, U256::from(200), U256::from(100)); - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); - - // Context with rejecting signer validator - let ctx = Arc::new(DipsServerContext { - rca_store: Arc::new(InMemoryRcaStore::default()), - ipfs_fetcher: Arc::new(MockIpfsFetcher::default()), - price_calculator: Arc::new(PriceCalculator::new( - HashSet::from(["mainnet".to_string()]), - BTreeMap::from([("mainnet".to_string(), U256::from(100))]), - U256::from(50), - )), - signer_validator: Arc::new(RejectingSignerValidator), - registry: Arc::new(crate::registry::test_registry()), - additional_networks: Arc::new(BTreeMap::new()), - }); - - // Act - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, rca_bytes).await; - - // Assert - assert!( - matches!(result, Err(DipsError::SignerNotAuthorised(_))), - "Expected SignerNotAuthorised error, got: {:?}", - result - ); - } - #[tokio::test] async fn test_validate_and_create_rca_ipfs_failure() { // Arrange - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); + let payer = Address::repeat_byte(0x42); let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); let rca = create_test_rca(payer, service_provider, U256::from(200), U256::from(100)); - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); // Context with failing IPFS fetcher let ctx = Arc::new(DipsServerContext { @@ -1043,14 +947,14 @@ mod test { BTreeMap::from([("mainnet".to_string(), U256::from(100))]), U256::from(50), )), - signer_validator: Arc::new(NoopSignerValidator), registry: Arc::new(crate::registry::test_registry()), additional_networks: Arc::new(BTreeMap::new()), }); + let rca_bytes = encode_empty_sig(rca); + // Act - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, rca_bytes).await; + let result = super::validate_and_create_rca(ctx, &service_provider, rca_bytes).await; // Assert assert!( @@ -1063,15 +967,10 @@ mod test { #[tokio::test] async fn test_validate_and_create_rca_manifest_no_network() { // Arrange - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); + let payer = Address::repeat_byte(0x42); let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); let rca = create_test_rca(payer, service_provider, U256::from(200), U256::from(100)); - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); // Context with IPFS fetcher returning manifest without network let ctx = Arc::new(DipsServerContext { @@ -1082,14 +981,14 @@ mod test { BTreeMap::from([("mainnet".to_string(), U256::from(100))]), U256::from(50), )), - signer_validator: Arc::new(NoopSignerValidator), registry: Arc::new(crate::registry::test_registry()), additional_networks: Arc::new(BTreeMap::new()), }); + let rca_bytes = encode_empty_sig(rca); + // Act - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, rca_bytes).await; + let result = super::validate_and_create_rca(ctx, &service_provider, rca_bytes).await; // Assert assert!( @@ -1102,15 +1001,10 @@ mod test { #[tokio::test] async fn test_validate_and_create_rca_store_failure() { // Arrange - let payer_signer = PrivateKeySigner::random(); - let payer = payer_signer.address(); + let payer = Address::repeat_byte(0x42); let service_provider = Address::repeat_byte(0x11); - let recurring_collector = Address::repeat_byte(0x22); let rca = create_test_rca(payer, service_provider, U256::from(200), U256::from(100)); - let domain = rca_eip712_domain(CHAIN_ID, recurring_collector); - let signed_rca = rca.sign(&domain, payer_signer).unwrap(); - let rca_bytes = signed_rca.abi_encode(); // Context with failing store let ctx = Arc::new(DipsServerContext { @@ -1121,14 +1015,14 @@ mod test { BTreeMap::from([("mainnet".to_string(), U256::from(100))]), U256::from(50), )), - signer_validator: Arc::new(NoopSignerValidator), registry: Arc::new(crate::registry::test_registry()), additional_networks: Arc::new(BTreeMap::new()), }); + let rca_bytes = encode_empty_sig(rca); + // Act - let result = - super::validate_and_create_rca(ctx, &domain, &service_provider, rca_bytes).await; + let result = super::validate_and_create_rca(ctx, &service_provider, rca_bytes).await; // Assert assert!( diff --git a/crates/dips/src/server.rs b/crates/dips/src/server.rs index 84a10682c..514ee989e 100644 --- a/crates/dips/src/server.rs +++ b/crates/dips/src/server.rs @@ -50,7 +50,6 @@ use crate::{ CancelAgreementResponse, ProposalResponse, RejectReason, SubmitAgreementProposalRequest, SubmitAgreementProposalResponse, }, - signers::SignerValidator, store::RcaStore, DipsError, }; @@ -58,7 +57,6 @@ use crate::{ /// Context for DIPS server with all validation dependencies. /// /// Used for RCA validation: -/// - Signature verification /// - IPFS manifest fetching /// - Price minimum enforcement /// - Network registry lookups @@ -70,8 +68,6 @@ pub struct DipsServerContext { pub ipfs_fetcher: Arc, /// Price calculator for validating minimum prices pub price_calculator: Arc, - /// Signature validator for EIP-712 verification - pub signer_validator: Arc, /// Network registry for supported networks pub registry: Arc, /// Additional networks beyond the registry @@ -100,7 +96,6 @@ fn reject_reason_from_error(err: &DipsError) -> RejectReason { match err { DipsError::TokensPerSecondTooLow { .. } | DipsError::TokensPerEntityPerSecondTooLow { .. } => RejectReason::PriceTooLow, - DipsError::SignerNotAuthorised(_) => RejectReason::SignerNotAuthorised, DipsError::DeadlineExpired { .. } => RejectReason::DeadlineExpired, DipsError::AgreementExpired { .. } => RejectReason::AgreementExpired, DipsError::UnsupportedNetwork(_) => RejectReason::UnsupportedNetwork, @@ -117,10 +112,13 @@ impl IndexerDipsService for DipsServer { /// /// Validates: /// - Version 2 only - /// - EIP-712 signature + /// - Service provider match /// - IPFS manifest and network compatibility /// - Price minimums /// + /// On-chain offer existence is NOT checked — the offer doesn't exist yet + /// at proposal time. The contract enforces it at `acceptIndexingAgreement`. + /// /// Returns Accept/Reject based on validation results. async fn submit_agreement_proposal( &self, @@ -151,15 +149,9 @@ impl IndexerDipsService for DipsServer { } // Validate and store RCA - let domain = crate::rca_eip712_domain(self.chain_id, self.recurring_collector); let deployment_id = crate::try_extract_deployment_id(&signed_voucher); - match crate::validate_and_create_rca( - self.ctx.clone(), - &domain, - &self.expected_payee, - signed_voucher, - ) - .await + match crate::validate_and_create_rca(self.ctx.clone(), &self.expected_payee, signed_voucher) + .await { Ok(agreement_id) => { tracing::info!(%agreement_id, "RCA accepted"); @@ -200,10 +192,7 @@ impl IndexerDipsService for DipsServer { #[cfg(test)] mod tests { use super::*; - use crate::{ - ipfs::MockIpfsFetcher, price::PriceCalculator, signers::NoopSignerValidator, - store::InMemoryRcaStore, - }; + use crate::{ipfs::MockIpfsFetcher, price::PriceCalculator, store::InMemoryRcaStore}; impl DipsServerContext { pub fn for_testing() -> Arc { @@ -218,7 +207,6 @@ mod tests { BTreeMap::from([("mainnet".to_string(), U256::from(200))]), U256::from(100), )), - signer_validator: Arc::new(NoopSignerValidator), registry: Arc::new(crate::registry::test_registry()), additional_networks: Arc::new(BTreeMap::new()), }) @@ -380,18 +368,6 @@ mod tests { assert_eq!(reason, RejectReason::Other); } - #[test] - fn test_reject_reason_signer_not_authorised() { - // Arrange - let err = DipsError::SignerNotAuthorised(Address::ZERO); - - // Act - let reason = super::reject_reason_from_error(&err); - - // Assert - assert_eq!(reason, RejectReason::SignerNotAuthorised); - } - #[test] fn test_reject_reason_deadline_expired() { // Arrange diff --git a/crates/dips/src/signers.rs b/crates/dips/src/signers.rs deleted file mode 100644 index afde36034..000000000 --- a/crates/dips/src/signers.rs +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2023-, Edge & Node, GraphOps, and Semiotic Labs. -// SPDX-License-Identifier: Apache-2.0 - -//! Signer authorization for DIPS agreements. -//! -//! When Dipper sends an RCA proposal, it's signed by a key that may differ from -//! the payer's address. Payers authorize signers via the PaymentsEscrow contract, -//! and this authorization data is indexed by the network subgraph. -//! -//! # How It Works -//! -//! [`EscrowSignerValidator`] wraps an `EscrowAccountsWatcher` that periodically -//! syncs escrow account data from the network subgraph. When validating an RCA: -//! -//! 1. Recover the signer address from the EIP-712 signature -//! 2. Look up authorized signers for the payer address -//! 3. Verify the recovered signer is in the authorized list -//! -//! # Security Considerations -//! -//! The network subgraph may lag behind chain state. This means: -//! - A newly authorized signer might be rejected briefly (UX issue, not security) -//! - A revoked signer might be accepted briefly (security concern) -//! -//! The **thawing period** on escrow withdrawals mitigates the second case. -//! Payers cannot withdraw funds instantly - they must wait through a thawing -//! period that exceeds the maximum expected subgraph lag. This gives indexers -//! time to collect owed fees before funds disappear. - -use anyhow::anyhow; -use thegraph_core::alloy::primitives::Address; - -pub trait SignerValidator: Sync + Send + std::fmt::Debug { - fn validate(&self, payer: &Address, signer: &Address) -> Result<(), anyhow::Error>; -} - -#[cfg(feature = "db")] -mod escrow_validator { - use super::*; - #[cfg(test)] - use indexer_monitor::EscrowAccounts; - use indexer_monitor::EscrowAccountsWatcher; - - #[derive(Debug)] - pub struct EscrowSignerValidator { - watcher: EscrowAccountsWatcher, - } - - impl EscrowSignerValidator { - pub fn new(watcher: EscrowAccountsWatcher) -> Self { - Self { watcher } - } - - #[cfg(test)] - pub fn mock(accounts: EscrowAccounts) -> Self { - let (_tx, rx) = tokio::sync::watch::channel(accounts); - Self::new(rx) - } - } - - impl SignerValidator for EscrowSignerValidator { - fn validate(&self, payer: &Address, signer: &Address) -> Result<(), anyhow::Error> { - let signers = self.watcher.borrow().get_signers_for_sender(payer); - - if signers.is_empty() { - tracing::warn!( - payer = %payer, - signer = %signer, - "no escrow accounts found for payer — signer authorization may not be indexed yet" - ); - return Err(anyhow!("Signer is not a valid signer for the sender")); - } - - if !signers.contains(signer) { - tracing::warn!( - payer = %payer, - signer = %signer, - authorized_count = signers.len(), - "signer not in authorized list for payer" - ); - return Err(anyhow!("Signer is not a valid signer for the sender")); - } - - tracing::debug!( - payer = %payer, - signer = %signer, - "signer authorization validated" - ); - Ok(()) - } - } -} - -#[cfg(feature = "db")] -pub use escrow_validator::EscrowSignerValidator; - -#[derive(Debug)] -pub struct NoopSignerValidator; - -impl SignerValidator for NoopSignerValidator { - fn validate(&self, _payer: &Address, _signer: &Address) -> Result<(), anyhow::Error> { - Ok(()) - } -} - -/// Test validator that always rejects signers. -#[derive(Debug)] -pub struct RejectingSignerValidator; - -impl SignerValidator for RejectingSignerValidator { - fn validate(&self, _payer: &Address, _signer: &Address) -> Result<(), anyhow::Error> { - Err(anyhow!("Signer not authorized (test validator)")) - } -} - -#[cfg(test)] -mod test { - use thegraph_core::alloy::primitives::Address; - - use crate::signers::{NoopSignerValidator, RejectingSignerValidator, SignerValidator}; - - #[test] - fn test_noop_validator_always_accepts() { - // Arrange - let validator = NoopSignerValidator; - let payer = Address::ZERO; - let signer = Address::from_slice(&[0xAB; 20]); - - // Act - let result = validator.validate(&payer, &signer); - - // Assert - assert!(result.is_ok(), "NoopSignerValidator should always accept"); - } - - #[test] - fn test_rejecting_validator_always_rejects() { - // Arrange - let validator = RejectingSignerValidator; - let payer = Address::ZERO; - let signer = Address::from_slice(&[0xAB; 20]); - - // Act - let result = validator.validate(&payer, &signer); - - // Assert - assert!( - result.is_err(), - "RejectingSignerValidator should always reject" - ); - } -} - -#[cfg(all(test, feature = "db"))] -mod escrow_tests { - use std::collections::HashMap; - - use indexer_monitor::EscrowAccounts; - use thegraph_core::alloy::primitives::Address; - - use crate::signers::SignerValidator; - - #[tokio::test] - async fn test_escrow_validator_authorized_signer() { - // Arrange - let payer = Address::ZERO; - let authorized_signer = Address::from_slice(&[1u8; 20]); - let (_tx, watcher) = tokio::sync::watch::channel(EscrowAccounts::new( - HashMap::default(), - HashMap::from_iter(vec![(payer, vec![authorized_signer])]), - )); - let validator = super::EscrowSignerValidator::new(watcher); - - // Act & Assert - assert!( - validator.validate(&payer, &authorized_signer).is_ok(), - "Authorized signer should be accepted" - ); - } - - #[tokio::test] - async fn test_escrow_validator_unauthorized_signer() { - // Arrange - let payer = Address::ZERO; - let authorized_signer = Address::from_slice(&[1u8; 20]); - let unauthorized_signer = Address::from_slice(&[2u8; 20]); - let (_tx, watcher) = tokio::sync::watch::channel(EscrowAccounts::new( - HashMap::default(), - HashMap::from_iter(vec![(payer, vec![authorized_signer])]), - )); - let validator = super::EscrowSignerValidator::new(watcher); - - // Act - let result = validator.validate(&payer, &unauthorized_signer); - - // Assert - assert!(result.is_err(), "Unauthorized signer should be rejected"); - } - - #[tokio::test] - async fn test_escrow_validator_payer_not_signer() { - // Arrange - payer authorizes someone else, not themselves - let payer = Address::ZERO; - let other_signer = Address::from_slice(&[1u8; 20]); - let (_tx, watcher) = tokio::sync::watch::channel(EscrowAccounts::new( - HashMap::default(), - HashMap::from_iter(vec![(payer, vec![other_signer])]), - )); - let validator = super::EscrowSignerValidator::new(watcher); - - // Act - let result = validator.validate(&payer, &payer); - - // Assert - assert!( - result.is_err(), - "Payer signing for themselves without authorization should be rejected" - ); - } -} diff --git a/crates/service/src/service.rs b/crates/service/src/service.rs index cdca73525..10348f16d 100644 --- a/crates/service/src/service.rs +++ b/crates/service/src/service.rs @@ -16,7 +16,6 @@ use indexer_dips::{ IndexerDipsService, IndexerDipsServiceServer, }, server::{DipsServer, DipsServerContext}, - signers::EscrowSignerValidator, }; use indexer_monitor::{DeploymentDetails, SubgraphClient}; use release::IndexerServiceRelease; @@ -226,6 +225,7 @@ pub async fn run() -> anyhow::Result<()> { min_grt_per_30_days, min_grt_per_billion_entities_per_30_days, additional_networks, + .. } = dips; // Validate required configuration @@ -288,7 +288,6 @@ pub async fn run() -> anyhow::Result<()> { tokens_per_second, tokens_per_entity_per_second, )), - signer_validator: Arc::new(EscrowSignerValidator::new(v2_watcher.clone())), registry, additional_networks: Arc::new(additional_networks.clone()), });