Skip to content

Commit 359f110

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

File tree

6 files changed

+216
-59
lines changed

6 files changed

+216
-59
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: 56 additions & 35 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,11 @@ pub async fn validate_and_cancel_agreement(
370380
#[cfg(test)]
371381
mod test {
372382
use std::{
373-
sync::Arc,
383+
collections::HashMap,
374384
time::{Duration, SystemTime, UNIX_EPOCH},
375385
};
376386

387+
use indexer_monitor::EscrowAccounts;
377388
use rand::{distr::Alphanumeric, Rng};
378389
use thegraph_core::alloy::{
379390
primitives::{Address, FixedBytes, U256},
@@ -384,9 +395,9 @@ mod test {
384395

385396
pub use crate::store::{AgreementStore, InMemoryAgreementStore};
386397
use crate::{
387-
dips_agreement_eip712_domain, dips_cancellation_eip712_domain, ipfs::TestIpfsClient,
388-
price::PriceCalculator, CancellationRequest, DipsError, IndexingAgreementVoucher,
389-
SignedIndexingAgreementVoucher, SubgraphIndexingVoucherMetadata,
398+
dips_agreement_eip712_domain, dips_cancellation_eip712_domain, server::DipsServerContext,
399+
CancellationRequest, DipsError, IndexingAgreementVoucher, SignedIndexingAgreementVoucher,
400+
SubgraphIndexingVoucherMetadata,
390401
};
391402

392403
#[tokio::test]
@@ -424,22 +435,19 @@ mod test {
424435
let abi_voucher = voucher.abi_encode();
425436
let id = Uuid::from_bytes(voucher.voucher.agreement_id.into());
426437

427-
let store = Arc::new(InMemoryAgreementStore::default());
428-
438+
let ctx = DipsServerContext::for_testing();
429439
let actual_id = super::validate_and_create_agreement(
430-
store.clone(),
440+
ctx.clone(),
431441
&domain,
432442
&payee_addr,
433443
vec![payer_addr],
434444
abi_voucher,
435-
&PriceCalculator::for_testing(),
436-
Arc::new(TestIpfsClient::mainnet()),
437445
)
438446
.await
439447
.unwrap();
440448
assert_eq!(actual_id, id);
441449

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

444452
assert_eq!(voucher, stored_agreement.voucher);
445453
assert!(!stored_agreement.cancelled);
@@ -448,6 +456,7 @@ mod test {
448456

449457
#[test]
450458
fn voucher_signature_verification() {
459+
let ctx = DipsServerContext::for_testing();
451460
let deployment_id = "Qmbg1qF4YgHjiVfsVt6a13ddrVcRtWyJQfD4LA3CwHM29f".to_string();
452461
let payee = PrivateKeySigner::random();
453462
let payee_addr = payee.address();
@@ -480,23 +489,34 @@ mod test {
480489
let signed = voucher.sign(&domain, payer).unwrap();
481490
assert_eq!(
482491
signed
483-
.validate(&domain, &payee_addr, vec![])
492+
.validate(&ctx.signer_validator, &domain, &payee_addr, vec![])
484493
.unwrap_err()
485494
.to_string(),
486495
DipsError::PayerNotAuthorised(voucher.payer).to_string()
487496
);
488497
assert!(signed
489-
.validate(&domain, &payee_addr, vec![payer_addr])
498+
.validate(
499+
&ctx.signer_validator,
500+
&domain,
501+
&payee_addr,
502+
vec![payer_addr]
503+
)
490504
.is_ok());
491505
}
492506

493-
#[test]
494-
fn check_voucher_modified() {
495-
let deployment_id = "Qmbg1qF4YgHjiVfsVt6a13ddrVcRtWyJQfD4LA3CwHM29f".to_string();
507+
#[tokio::test]
508+
async fn check_voucher_modified() {
496509
let payee = PrivateKeySigner::random();
497510
let payee_addr = payee.address();
498511
let payer = PrivateKeySigner::random();
499512
let payer_addr = payer.address();
513+
let ctx = DipsServerContext::for_testing_mocked_accounts(EscrowAccounts::new(
514+
HashMap::default(),
515+
HashMap::from_iter(vec![(payer_addr, vec![payer_addr])]),
516+
))
517+
.await;
518+
519+
let deployment_id = "Qmbg1qF4YgHjiVfsVt6a13ddrVcRtWyJQfD4LA3CwHM29f".to_string();
500520

501521
let metadata = SubgraphIndexingVoucherMetadata {
502522
basePricePerEpoch: U256::from(10000_u64),
@@ -526,9 +546,14 @@ mod test {
526546

527547
assert!(matches!(
528548
signed
529-
.validate(&domain, &payee_addr, vec![payer_addr])
549+
.validate(
550+
&ctx.signer_validator,
551+
&domain,
552+
&payee_addr,
553+
vec![payer_addr]
554+
)
530555
.unwrap_err(),
531-
DipsError::PayerNotAuthorised(_)
556+
DipsError::SignerNotAuthorised(_)
532557
));
533558
}
534559

@@ -630,8 +655,8 @@ mod test {
630655

631656
#[tokio::test]
632657
async fn test_create_and_cancel_agreement() -> anyhow::Result<()> {
658+
let ctx = DipsServerContext::for_testing();
633659
let voucher_ctx = VoucherContext::random();
634-
let store = Arc::new(InMemoryAgreementStore::default());
635660

636661
// Create metadata and voucher
637662
let metadata = SubgraphIndexingVoucherMetadata {
@@ -645,13 +670,11 @@ mod test {
645670

646671
// Create agreement
647672
let agreement_id = super::validate_and_create_agreement(
648-
store.clone(),
673+
ctx.clone(),
649674
&voucher_ctx.domain(),
650675
&voucher_ctx.payee.address(),
651676
vec![voucher_ctx.payer.address()],
652677
signed_voucher.encode_vec(),
653-
&PriceCalculator::for_testing(),
654-
Arc::new(TestIpfsClient::mainnet()),
655678
)
656679
.await?;
657680

@@ -664,7 +687,7 @@ mod test {
664687

665688
// Cancel agreement
666689
let cancelled_id = super::validate_and_cancel_agreement(
667-
store.clone(),
690+
ctx.store.clone(),
668691
&cancel_domain,
669692
signed_cancel.encode_vec(),
670693
)
@@ -673,16 +696,16 @@ mod test {
673696
assert_eq!(agreement_id, cancelled_id);
674697

675698
// Verify agreement is cancelled
676-
let stored_agreement = store.get_by_id(agreement_id).await?.unwrap();
699+
let stored_agreement = ctx.store.get_by_id(agreement_id).await?.unwrap();
677700
assert!(stored_agreement.cancelled);
678701

679702
Ok(())
680703
}
681704

682705
#[tokio::test]
683706
async fn test_create_validations_errors() -> anyhow::Result<()> {
707+
let ctx = DipsServerContext::for_testing();
684708
let voucher_ctx = VoucherContext::random();
685-
let store = Arc::new(InMemoryAgreementStore::default());
686709

687710
let metadata = SubgraphIndexingVoucherMetadata {
688711
basePricePerEpoch: U256::from(10000_u64),
@@ -734,13 +757,11 @@ mod test {
734757
let cases = vec![wrong_network_voucher, low_price_voucher, valid_voucher];
735758
for (voucher, result) in cases.into_iter().zip(expected_result.into_iter()) {
736759
let out = super::validate_and_create_agreement(
737-
store.clone(),
760+
ctx.clone(),
738761
&voucher_ctx.domain(),
739762
&voucher_ctx.payee.address(),
740763
vec![voucher_ctx.payer.address()],
741764
voucher.encode_vec(),
742-
&PriceCalculator::for_testing(),
743-
Arc::new(TestIpfsClient::mainnet()),
744765
)
745766
.await;
746767

crates/dips/src/server.rs

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,53 @@ use crate::{
1515
CancelAgreementResponse, ProposalResponse, SubmitAgreementProposalRequest,
1616
SubmitAgreementProposalResponse,
1717
},
18+
signers::SignerValidator,
1819
store::AgreementStore,
1920
validate_and_cancel_agreement, validate_and_create_agreement,
2021
};
2122

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

3267
#[async_trait]
@@ -50,13 +85,11 @@ impl IndexerDipsService for DipsServer {
5085
// - The subgraph deployment is for a chain we support
5186
// - The subgraph deployment is available on IPFS
5287
validate_and_create_agreement(
53-
self.agreement_store.clone(),
88+
self.ctx.clone(),
5489
&self.domain,
5590
&self.expected_payee,
5691
&self.allowed_payers,
5792
signed_voucher,
58-
&self.price_calculator,
59-
self.ipfs_fetcher.clone(),
6093
)
6194
.await
6295
.map_err(Into::<tonic::Status>::into)?;
@@ -80,13 +113,9 @@ impl IndexerDipsService for DipsServer {
80113
return Err(Status::invalid_argument("invalid version"));
81114
}
82115

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)?;
116+
validate_and_cancel_agreement(self.ctx.store.clone(), &self.domain, signed_cancellation)
117+
.await
118+
.map_err(Into::<tonic::Status>::into)?;
90119

91120
Ok(tonic::Response::new(CancelAgreementResponse {}))
92121
}

0 commit comments

Comments
 (0)