Skip to content

Commit 8f9e6ae

Browse files
committed
feat(service): enforce V2 data_service via DataServiceCheck
- Add tap/checks/data_service_check.rs to validate V2 receipt.message.data_service against an allowlist(from configuration) - Return CheckError::Failed with clear message; maps to HTTP 400 - Extend IndexerTapContext::get_checks(..) to accept allowed_data_services and append the check when provided - Wire ServiceRouter to pass blockchain.subgraph_service_address (Horizon mode), activating the check only when configured - V1 receipts unaffected; persistence logic unchanged
1 parent accf10b commit 8f9e6ae

File tree

4 files changed

+69
-5
lines changed

4 files changed

+69
-5
lines changed

crates/service/src/service/router.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,13 +278,19 @@ impl ServiceRouter {
278278
let receipt_max_value = max_receipt_value_grt.get_value();
279279

280280
// Create checks
281+
let allowed_data_services = self
282+
.blockchain
283+
.subgraph_service_address
284+
.map(|addr| vec![addr]);
285+
281286
let checks = IndexerTapContext::get_checks(
282287
self.database,
283288
allocations.clone(),
284289
escrow_accounts_v1.clone(),
285290
escrow_accounts_v2.clone(),
286291
timestamp_error_tolerance,
287292
receipt_max_value,
293+
allowed_data_services,
288294
)
289295
.await;
290296

crates/service/src/tap.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ use tokio::sync::{
1616
use tokio_util::sync::CancellationToken;
1717

1818
use crate::tap::checks::{
19-
allocation_eligible::AllocationEligible, deny_list_check::DenyListCheck,
20-
receipt_max_val_check::ReceiptMaxValueCheck, sender_balance_check::SenderBalanceCheck,
21-
timestamp_check::TimestampCheck, value_check::MinimumValue,
19+
allocation_eligible::AllocationEligible, data_service_check::DataServiceCheck,
20+
deny_list_check::DenyListCheck, receipt_max_val_check::ReceiptMaxValueCheck,
21+
sender_balance_check::SenderBalanceCheck, timestamp_check::TimestampCheck,
22+
value_check::MinimumValue,
2223
};
2324

2425
mod checks;
@@ -56,8 +57,9 @@ impl IndexerTapContext {
5657
escrow_accounts_v2: Option<Receiver<EscrowAccounts>>,
5758
timestamp_error_tolerance: Duration,
5859
receipt_max_value: u128,
60+
allowed_data_services: Option<Vec<Address>>,
5961
) -> Vec<ReceiptCheck<TapReceipt>> {
60-
vec![
62+
let mut checks: Vec<ReceiptCheck<TapReceipt>> = vec![
6163
Arc::new(AllocationEligible::new(indexer_allocations)),
6264
Arc::new(SenderBalanceCheck::new(
6365
escrow_accounts_v1,
@@ -67,7 +69,13 @@ impl IndexerTapContext {
6769
Arc::new(DenyListCheck::new(pgpool.clone()).await),
6870
Arc::new(ReceiptMaxValueCheck::new(receipt_max_value)),
6971
Arc::new(MinimumValue::new(pgpool, Duration::from_secs(GRACE_PERIOD)).await),
70-
]
72+
];
73+
74+
if let Some(addrs) = allowed_data_services {
75+
checks.push(Arc::new(DataServiceCheck::new(addrs)));
76+
}
77+
78+
checks
7179
}
7280

7381
pub async fn new(

crates/service/src/tap/checks.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
pub mod allocation_eligible;
55
pub mod deny_list_check;
6+
pub mod data_service_check;
67
pub mod receipt_max_val_check;
78
pub mod sender_balance_check;
89
pub mod timestamp_check;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2023-, Edge & Node, GraphOps, and Semiotic Labs.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use tap_core::receipt::checks::{Check, CheckError, CheckResult};
5+
use thegraph_core::alloy::{hex::ToHexExt, primitives::Address};
6+
7+
use crate::tap::{CheckingReceipt, TapReceipt};
8+
9+
/// Validates that the V2 receipt's `data_service` field matches an
10+
/// allowed SubgraphService address (or one of them).
11+
///
12+
/// - V1 receipts are ignored by this check (always Ok).
13+
/// - On mismatch, returns a CheckFailure with a descriptive message.
14+
pub struct DataServiceCheck {
15+
allowed: Vec<Address>,
16+
}
17+
18+
impl DataServiceCheck {
19+
pub fn new(allowed: Vec<Address>) -> Self {
20+
Self { allowed }
21+
}
22+
}
23+
24+
#[async_trait::async_trait]
25+
impl Check<TapReceipt> for DataServiceCheck {
26+
async fn check(
27+
&self,
28+
_: &tap_core::receipt::Context,
29+
receipt: &CheckingReceipt,
30+
) -> CheckResult {
31+
match receipt.signed_receipt() {
32+
// Not applicable for V1
33+
TapReceipt::V1(_) => Ok(()),
34+
35+
// Validate data_service for V2
36+
TapReceipt::V2(r) => {
37+
let got = r.message.data_service;
38+
if self.allowed.contains(&got) {
39+
Ok(())
40+
} else {
41+
Err(CheckError::Failed(anyhow::anyhow!(
42+
"Invalid data_service: {} is not allowed for this indexer",
43+
got.encode_hex()
44+
)))
45+
}
46+
}
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)