Skip to content

Commit acae990

Browse files
committed
feat: Escrow based signer validation
1 parent fed7619 commit acae990

File tree

6 files changed

+216
-56
lines changed

6 files changed

+216
-56
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/dips/Cargo.toml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,28 @@ thiserror.workspace = true
1010
anyhow.workspace = true
1111
alloy-rlp = "0.3.10"
1212
thegraph-core.workspace = true
13-
tonic.workspace = true
14-
async-trait.workspace = true
15-
prost.workspace = true
16-
prost-types.workspace = true
13+
tonic.workspace = true
14+
async-trait.workspace = true
15+
prost.workspace = true
16+
prost-types.workspace = true
1717
uuid.workspace = true
1818
base64.workspace = true
1919
tokio.workspace = true
2020
sqlx.workspace = true
2121
futures = "0.3"
22+
indexer-monitor = { path = "../monitor" }
2223

2324
http = "0.2"
2425
derivative = "2.2.0"
25-
ipfs-api-backend-hyper = {version = "0.6.0", features = ["with-send-sync"] }
26-
ipfs-api-prelude = {version = "0.6.0", features = ["with-send-sync"] }
26+
ipfs-api-backend-hyper = { version = "0.6.0", features = ["with-send-sync"] }
27+
ipfs-api-prelude = { version = "0.6.0", features = ["with-send-sync"] }
2728
bytes = "1.10.0"
2829
serde_yaml.workspace = true
2930
serde.workspace = true
3031

3132
[dev-dependencies]
3233
rand = "0.9.0"
34+
indexer-watcher = { path = "../watcher" }
3335

3436
[build-dependencies]
3537
tonic-build = { workspace = true }

crates/dips/src/lib.rs

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44
use std::{str::FromStr, sync::Arc};
55

6-
use ipfs::IpfsFetcher;
7-
use price::PriceCalculator;
6+
use server::DipsServerContext;
87
use thegraph_core::alloy::{
98
core::primitives::Address,
109
primitives::{b256, ChainId, PrimitiveSignature as Signature, Uint, B256},
@@ -18,6 +17,7 @@ pub mod ipfs;
1817
pub mod price;
1918
pub mod proto;
2019
pub mod server;
20+
pub mod signers;
2121
pub mod store;
2222

2323
use store::AgreementStore;
@@ -190,14 +190,16 @@ impl SignedIndexingAgreementVoucher {
190190
// TODO: Validate all values
191191
pub fn validate(
192192
&self,
193+
signer_validator: &Arc<dyn signers::SignerValidator>,
193194
domain: &Eip712Domain,
194195
expected_payee: &Address,
195196
allowed_payers: impl AsRef<[Address]>,
196197
) -> Result<(), DipsError> {
197198
let sig = Signature::from_str(&self.signature.to_string())
198199
.map_err(|err| DipsError::InvalidSignature(err.to_string()))?;
199200

200-
let payer = sig
201+
let payer = self.voucher.payer;
202+
let signer = sig
201203
.recover_address_from_prehash(&self.voucher.eip712_signing_hash(domain))
202204
.map_err(|err| DipsError::InvalidSignature(err.to_string()))?;
203205

@@ -207,6 +209,10 @@ impl SignedIndexingAgreementVoucher {
207209
return Err(DipsError::PayerNotAuthorised(payer));
208210
}
209211

212+
signer_validator
213+
.validate(&payer, &signer)
214+
.map_err(|_| DipsError::SignerNotAuthorised(signer))?;
215+
210216
if !self.voucher.recipient.eq(expected_payee) {
211217
return Err(DipsError::UnexpectedPayee {
212218
expected: *expected_payee,
@@ -284,14 +290,18 @@ impl CollectionRequest {
284290
}
285291

286292
pub async fn validate_and_create_agreement(
287-
store: Arc<dyn AgreementStore>,
293+
ctx: Arc<DipsServerContext>,
288294
domain: &Eip712Domain,
289295
expected_payee: &Address,
290296
allowed_payers: impl AsRef<[Address]>,
291297
voucher: Vec<u8>,
292-
price_calculator: &PriceCalculator,
293-
ipfs_client: Arc<dyn IpfsFetcher>,
294298
) -> Result<Uuid, DipsError> {
299+
let DipsServerContext {
300+
store,
301+
ipfs_fetcher,
302+
price_calculator,
303+
signer_validator,
304+
} = ctx.as_ref();
295305
let decoded_voucher = SignedIndexingAgreementVoucher::abi_decode(voucher.as_ref(), true)
296306
.map_err(|e| DipsError::AbiDecoding(e.to_string()))?;
297307
let metadata = SubgraphIndexingVoucherMetadata::abi_decode(
@@ -300,9 +310,9 @@ pub async fn validate_and_create_agreement(
300310
)
301311
.map_err(|e| DipsError::AbiDecoding(e.to_string()))?;
302312

303-
decoded_voucher.validate(domain, expected_payee, allowed_payers)?;
313+
decoded_voucher.validate(signer_validator, domain, expected_payee, allowed_payers)?;
304314

305-
let manifest = ipfs_client.fetch(&metadata.subgraphDeploymentId).await?;
315+
let manifest = ipfs_fetcher.fetch(&metadata.subgraphDeploymentId).await?;
306316
match manifest.network() {
307317
Some(chain_id) if chain_id == metadata.chainId => {}
308318
Some(chain_id) => {
@@ -370,10 +380,12 @@ pub async fn validate_and_cancel_agreement(
370380
#[cfg(test)]
371381
mod test {
372382
use std::{
383+
collections::HashMap,
373384
sync::Arc,
374385
time::{Duration, SystemTime, UNIX_EPOCH},
375386
};
376387

388+
use indexer_monitor::EscrowAccounts;
377389
use rand::{distr::Alphanumeric, Rng};
378390
use thegraph_core::alloy::{
379391
primitives::{Address, FixedBytes, U256},
@@ -384,9 +396,9 @@ mod test {
384396

385397
pub use crate::store::{AgreementStore, InMemoryAgreementStore};
386398
use crate::{
387-
dips_agreement_eip712_domain, dips_cancellation_eip712_domain, ipfs::TestIpfsClient,
388-
price::PriceCalculator, CancellationRequest, DipsError, IndexingAgreementVoucher,
389-
SignedIndexingAgreementVoucher, SubgraphIndexingVoucherMetadata,
399+
dips_agreement_eip712_domain, dips_cancellation_eip712_domain, server::DipsServerContext,
400+
CancellationRequest, DipsError, IndexingAgreementVoucher, SignedIndexingAgreementVoucher,
401+
SubgraphIndexingVoucherMetadata,
390402
};
391403

392404
#[tokio::test]
@@ -424,22 +436,19 @@ mod test {
424436
let abi_voucher = voucher.abi_encode();
425437
let id = Uuid::from_bytes(voucher.voucher.agreement_id.into());
426438

427-
let store = Arc::new(InMemoryAgreementStore::default());
428-
439+
let ctx = DipsServerContext::for_testing();
429440
let actual_id = super::validate_and_create_agreement(
430-
store.clone(),
441+
ctx.clone(),
431442
&domain,
432443
&payee_addr,
433444
vec![payer_addr],
434445
abi_voucher,
435-
&PriceCalculator::for_testing(),
436-
Arc::new(TestIpfsClient::mainnet()),
437446
)
438447
.await
439448
.unwrap();
440449
assert_eq!(actual_id, id);
441450

442-
let stored_agreement = store.get_by_id(actual_id).await.unwrap().unwrap();
451+
let stored_agreement = ctx.store.get_by_id(actual_id).await.unwrap().unwrap();
443452

444453
assert_eq!(voucher, stored_agreement.voucher);
445454
assert!(!stored_agreement.cancelled);
@@ -448,6 +457,7 @@ mod test {
448457

449458
#[test]
450459
fn voucher_signature_verification() {
460+
let ctx = DipsServerContext::for_testing();
451461
let deployment_id = "Qmbg1qF4YgHjiVfsVt6a13ddrVcRtWyJQfD4LA3CwHM29f".to_string();
452462
let payee = PrivateKeySigner::random();
453463
let payee_addr = payee.address();
@@ -480,23 +490,34 @@ mod test {
480490
let signed = voucher.sign(&domain, payer).unwrap();
481491
assert_eq!(
482492
signed
483-
.validate(&domain, &payee_addr, vec![])
493+
.validate(&ctx.signer_validator, &domain, &payee_addr, vec![])
484494
.unwrap_err()
485495
.to_string(),
486496
DipsError::PayerNotAuthorised(voucher.payer).to_string()
487497
);
488498
assert!(signed
489-
.validate(&domain, &payee_addr, vec![payer_addr])
499+
.validate(
500+
&ctx.signer_validator,
501+
&domain,
502+
&payee_addr,
503+
vec![payer_addr]
504+
)
490505
.is_ok());
491506
}
492507

493-
#[test]
494-
fn check_voucher_modified() {
495-
let deployment_id = "Qmbg1qF4YgHjiVfsVt6a13ddrVcRtWyJQfD4LA3CwHM29f".to_string();
508+
#[tokio::test]
509+
async fn check_voucher_modified() {
496510
let payee = PrivateKeySigner::random();
497511
let payee_addr = payee.address();
498512
let payer = PrivateKeySigner::random();
499513
let payer_addr = payer.address();
514+
let ctx = DipsServerContext::for_testing_mocked_accounts(EscrowAccounts::new(
515+
HashMap::default(),
516+
HashMap::from_iter(vec![(payer_addr, vec![payer_addr])]),
517+
))
518+
.await;
519+
520+
let deployment_id = "Qmbg1qF4YgHjiVfsVt6a13ddrVcRtWyJQfD4LA3CwHM29f".to_string();
500521

501522
let metadata = SubgraphIndexingVoucherMetadata {
502523
basePricePerEpoch: U256::from(10000_u64),
@@ -526,9 +547,14 @@ mod test {
526547

527548
assert!(matches!(
528549
signed
529-
.validate(&domain, &payee_addr, vec![payer_addr])
550+
.validate(
551+
&ctx.signer_validator,
552+
&domain,
553+
&payee_addr,
554+
vec![payer_addr]
555+
)
530556
.unwrap_err(),
531-
DipsError::PayerNotAuthorised(_)
557+
DipsError::SignerNotAuthorised(_)
532558
));
533559
}
534560

@@ -630,6 +656,7 @@ mod test {
630656

631657
#[tokio::test]
632658
async fn test_create_and_cancel_agreement() -> anyhow::Result<()> {
659+
let ctx = DipsServerContext::for_testing();
633660
let voucher_ctx = VoucherContext::random();
634661
let store = Arc::new(InMemoryAgreementStore::default());
635662

@@ -645,13 +672,11 @@ mod test {
645672

646673
// Create agreement
647674
let agreement_id = super::validate_and_create_agreement(
648-
store.clone(),
675+
ctx.clone(),
649676
&voucher_ctx.domain(),
650677
&voucher_ctx.payee.address(),
651678
vec![voucher_ctx.payer.address()],
652679
signed_voucher.encode_vec(),
653-
&PriceCalculator::for_testing(),
654-
Arc::new(TestIpfsClient::mainnet()),
655680
)
656681
.await?;
657682

@@ -672,7 +697,7 @@ mod test {
672697

673698
assert_eq!(agreement_id, cancelled_id);
674699

675-
// Verify agreement is cancelled
700+
// Verify agreement is cancel
676701
let stored_agreement = store.get_by_id(agreement_id).await?.unwrap();
677702
assert!(stored_agreement.cancelled);
678703

@@ -681,8 +706,8 @@ mod test {
681706

682707
#[tokio::test]
683708
async fn test_create_validations_errors() -> anyhow::Result<()> {
709+
let ctx = DipsServerContext::for_testing();
684710
let voucher_ctx = VoucherContext::random();
685-
let store = Arc::new(InMemoryAgreementStore::default());
686711

687712
let metadata = SubgraphIndexingVoucherMetadata {
688713
basePricePerEpoch: U256::from(10000_u64),
@@ -734,13 +759,11 @@ mod test {
734759
let cases = vec![wrong_network_voucher, low_price_voucher, valid_voucher];
735760
for (voucher, result) in cases.into_iter().zip(expected_result.into_iter()) {
736761
let out = super::validate_and_create_agreement(
737-
store.clone(),
762+
ctx.clone(),
738763
&voucher_ctx.domain(),
739764
&voucher_ctx.payee.address(),
740765
vec![voucher_ctx.payer.address()],
741766
voucher.encode_vec(),
742-
&PriceCalculator::for_testing(),
743-
Arc::new(TestIpfsClient::mainnet()),
744767
)
745768
.await;
746769

crates/dips/src/server.rs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use std::sync::Arc;
55

66
use async_trait::async_trait;
7+
use indexer_monitor::EscrowAccounts;
78
use thegraph_core::alloy::{primitives::Address, sol_types::Eip712Domain};
89
use tonic::{Request, Response, Status};
910

@@ -15,18 +16,53 @@ use crate::{
1516
CancelAgreementResponse, ProposalResponse, SubmitAgreementProposalRequest,
1617
SubmitAgreementProposalResponse,
1718
},
19+
signers::SignerValidator,
1820
store::AgreementStore,
1921
validate_and_cancel_agreement, validate_and_create_agreement,
2022
};
2123

24+
#[derive(Debug)]
25+
pub struct DipsServerContext {
26+
pub store: Arc<dyn AgreementStore>,
27+
pub ipfs_fetcher: Arc<dyn IpfsFetcher>,
28+
pub price_calculator: PriceCalculator,
29+
pub signer_validator: Arc<dyn SignerValidator>,
30+
}
31+
32+
impl DipsServerContext {
33+
#[cfg(test)]
34+
pub fn for_testing() -> Arc<Self> {
35+
use std::sync::Arc;
36+
37+
use crate::{ipfs::TestIpfsClient, signers, test::InMemoryAgreementStore};
38+
39+
Arc::new(DipsServerContext {
40+
store: Arc::new(InMemoryAgreementStore::default()),
41+
ipfs_fetcher: Arc::new(TestIpfsClient::mainnet()),
42+
price_calculator: PriceCalculator::for_testing(),
43+
signer_validator: Arc::new(signers::NoopSignerValidator),
44+
})
45+
}
46+
47+
#[cfg(test)]
48+
pub async fn for_testing_mocked_accounts(accounts: EscrowAccounts) -> Arc<Self> {
49+
use crate::{ipfs::TestIpfsClient, signers, test::InMemoryAgreementStore};
50+
51+
Arc::new(DipsServerContext {
52+
store: Arc::new(InMemoryAgreementStore::default()),
53+
ipfs_fetcher: Arc::new(TestIpfsClient::mainnet()),
54+
price_calculator: PriceCalculator::for_testing(),
55+
signer_validator: Arc::new(signers::EscrowSignerValidator::mock(accounts).await),
56+
})
57+
}
58+
}
59+
2260
#[derive(Debug)]
2361
pub struct DipsServer {
24-
pub agreement_store: Arc<dyn AgreementStore>,
62+
pub ctx: Arc<DipsServerContext>,
2563
pub expected_payee: Address,
2664
pub allowed_payers: Vec<Address>,
2765
pub domain: Eip712Domain,
28-
pub ipfs_fetcher: Arc<dyn IpfsFetcher>,
29-
pub price_calculator: PriceCalculator,
3066
}
3167

3268
#[async_trait]
@@ -50,13 +86,11 @@ impl IndexerDipsService for DipsServer {
5086
// - The subgraph deployment is for a chain we support
5187
// - The subgraph deployment is available on IPFS
5288
validate_and_create_agreement(
53-
self.agreement_store.clone(),
89+
self.ctx.clone(),
5490
&self.domain,
5591
&self.expected_payee,
5692
&self.allowed_payers,
5793
signed_voucher,
58-
&self.price_calculator,
59-
self.ipfs_fetcher.clone(),
6094
)
6195
.await
6296
.map_err(Into::<tonic::Status>::into)?;
@@ -80,13 +114,9 @@ impl IndexerDipsService for DipsServer {
80114
return Err(Status::invalid_argument("invalid version"));
81115
}
82116

83-
validate_and_cancel_agreement(
84-
self.agreement_store.clone(),
85-
&self.domain,
86-
signed_cancellation,
87-
)
88-
.await
89-
.map_err(Into::<tonic::Status>::into)?;
117+
validate_and_cancel_agreement(self.ctx.store.clone(), &self.domain, signed_cancellation)
118+
.await
119+
.map_err(Into::<tonic::Status>::into)?;
90120

91121
Ok(tonic::Response::new(CancelAgreementResponse {}))
92122
}

0 commit comments

Comments
 (0)