Skip to content

Commit a1ca33d

Browse files
committed
feat: Escrow based signer validation
1 parent e9f7d1a commit a1ca33d

File tree

6 files changed

+256
-63
lines changed

6 files changed

+256
-63
lines changed

Cargo.lock

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/dips/Cargo.toml

+14-7
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,34 @@ rpc = ["dep:prost", "dep:tonic", "dep:tonic-build", "dep:bytes"]
99
db = ["dep:sqlx"]
1010

1111
[dependencies]
12+
build-info.workspace = true
13+
thiserror.workspace = true
1214
anyhow.workspace = true
15+
alloy-rlp = "0.3.10"
16+
thegraph-core.workspace = true
1317
async-trait.workspace = true
14-
build-info.workspace = true
18+
prost-types.workspace = true
19+
uuid.workspace = true
20+
base64.workspace = true
21+
tokio.workspace = true
22+
indexer-monitor = { path = "../monitor" }
23+
1524
bytes = { version = "1.10.0", optional = true }
1625
derivative = "2.2.0"
26+
1727
futures = "0.3"
1828
http = "0.2"
29+
prost = { workspace = true, optional = true }
1930
ipfs-api-backend-hyper = { version = "0.6.0", features = ["with-send-sync"] }
2031
ipfs-api-prelude = { version = "0.6.0", features = ["with-send-sync"] }
21-
prost = { workspace = true, optional = true }
22-
serde.workspace = true
2332
serde_yaml.workspace = true
33+
serde.workspace = true
2434
sqlx = { workspace = true, optional = true }
25-
thegraph-core.workspace = true
26-
thiserror.workspace = true
27-
tokio.workspace = true
2835
tonic = { workspace = true, optional = true }
29-
uuid.workspace = true
3036

3137
[dev-dependencies]
3238
rand = "0.9.0"
39+
indexer-watcher = { path = "../watcher" }
3340

3441
[build-dependencies]
3542
tonic-build = { workspace = true, optional = true }

crates/dips/src/lib.rs

+83-38
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},
@@ -21,6 +20,7 @@ pub mod price;
2120
pub mod proto;
2221
#[cfg(feature = "rpc")]
2322
pub mod server;
23+
pub mod signers;
2424
pub mod store;
2525

2626
use store::AgreementStore;
@@ -194,14 +194,16 @@ impl SignedIndexingAgreementVoucher {
194194
// TODO: Validate all values
195195
pub fn validate(
196196
&self,
197+
signer_validator: &Arc<dyn signers::SignerValidator>,
197198
domain: &Eip712Domain,
198199
expected_payee: &Address,
199200
allowed_payers: impl AsRef<[Address]>,
200201
) -> Result<(), DipsError> {
201202
let sig = Signature::from_str(&self.signature.to_string())
202203
.map_err(|err| DipsError::InvalidSignature(err.to_string()))?;
203204

204-
let payer = sig
205+
let payer = self.voucher.payer;
206+
let signer = sig
205207
.recover_address_from_prehash(&self.voucher.eip712_signing_hash(domain))
206208
.map_err(|err| DipsError::InvalidSignature(err.to_string()))?;
207209

@@ -211,6 +213,10 @@ impl SignedIndexingAgreementVoucher {
211213
return Err(DipsError::PayerNotAuthorised(payer));
212214
}
213215

216+
signer_validator
217+
.validate(&payer, &signer)
218+
.map_err(|_| DipsError::SignerNotAuthorised(signer))?;
219+
214220
if !self.voucher.recipient.eq(expected_payee) {
215221
return Err(DipsError::UnexpectedPayee {
216222
expected: *expected_payee,
@@ -288,14 +294,18 @@ impl CollectionRequest {
288294
}
289295

290296
pub async fn validate_and_create_agreement(
291-
store: Arc<dyn AgreementStore>,
297+
ctx: Arc<DipsServerContext>,
292298
domain: &Eip712Domain,
293299
expected_payee: &Address,
294300
allowed_payers: impl AsRef<[Address]>,
295301
voucher: Vec<u8>,
296-
price_calculator: &PriceCalculator,
297-
ipfs_client: Arc<dyn IpfsFetcher>,
298302
) -> Result<Uuid, DipsError> {
303+
let DipsServerContext {
304+
store,
305+
ipfs_fetcher,
306+
price_calculator,
307+
signer_validator,
308+
} = ctx.as_ref();
299309
let decoded_voucher = SignedIndexingAgreementVoucher::abi_decode(voucher.as_ref(), true)
300310
.map_err(|e| DipsError::AbiDecoding(e.to_string()))?;
301311
let metadata = SubgraphIndexingVoucherMetadata::abi_decode(
@@ -304,9 +314,9 @@ pub async fn validate_and_create_agreement(
304314
)
305315
.map_err(|e| DipsError::AbiDecoding(e.to_string()))?;
306316

307-
decoded_voucher.validate(domain, expected_payee, allowed_payers)?;
317+
decoded_voucher.validate(signer_validator, domain, expected_payee, allowed_payers)?;
308318

309-
let manifest = ipfs_client.fetch(&metadata.subgraphDeploymentId).await?;
319+
let manifest = ipfs_fetcher.fetch(&metadata.subgraphDeploymentId).await?;
310320
match manifest.network() {
311321
Some(chain_id) if chain_id == metadata.chainId => {}
312322
Some(chain_id) => {
@@ -374,10 +384,11 @@ pub async fn validate_and_cancel_agreement(
374384
#[cfg(test)]
375385
mod test {
376386
use std::{
377-
sync::Arc,
387+
collections::HashMap,
378388
time::{Duration, SystemTime, UNIX_EPOCH},
379389
};
380390

391+
use indexer_monitor::EscrowAccounts;
381392
use rand::{distr::Alphanumeric, Rng};
382393
use thegraph_core::alloy::{
383394
primitives::{Address, FixedBytes, U256},
@@ -388,9 +399,9 @@ mod test {
388399

389400
pub use crate::store::{AgreementStore, InMemoryAgreementStore};
390401
use crate::{
391-
dips_agreement_eip712_domain, dips_cancellation_eip712_domain, ipfs::TestIpfsClient,
392-
price::PriceCalculator, CancellationRequest, DipsError, IndexingAgreementVoucher,
393-
SignedIndexingAgreementVoucher, SubgraphIndexingVoucherMetadata,
402+
dips_agreement_eip712_domain, dips_cancellation_eip712_domain, server::DipsServerContext,
403+
CancellationRequest, DipsError, IndexingAgreementVoucher, SignedIndexingAgreementVoucher,
404+
SubgraphIndexingVoucherMetadata,
394405
};
395406

396407
#[tokio::test]
@@ -428,22 +439,19 @@ mod test {
428439
let abi_voucher = voucher.abi_encode();
429440
let id = Uuid::from_bytes(voucher.voucher.agreement_id.into());
430441

431-
let store = Arc::new(InMemoryAgreementStore::default());
432-
442+
let ctx = DipsServerContext::for_testing();
433443
let actual_id = super::validate_and_create_agreement(
434-
store.clone(),
444+
ctx.clone(),
435445
&domain,
436446
&payee_addr,
437447
vec![payer_addr],
438448
abi_voucher,
439-
&PriceCalculator::for_testing(),
440-
Arc::new(TestIpfsClient::mainnet()),
441449
)
442450
.await
443451
.unwrap();
444452
assert_eq!(actual_id, id);
445453

446-
let stored_agreement = store.get_by_id(actual_id).await.unwrap().unwrap();
454+
let stored_agreement = ctx.store.get_by_id(actual_id).await.unwrap().unwrap();
447455

448456
assert_eq!(voucher, stored_agreement.voucher);
449457
assert!(!stored_agreement.cancelled);
@@ -452,6 +460,7 @@ mod test {
452460

453461
#[test]
454462
fn voucher_signature_verification() {
463+
let ctx = DipsServerContext::for_testing();
455464
let deployment_id = "Qmbg1qF4YgHjiVfsVt6a13ddrVcRtWyJQfD4LA3CwHM29f".to_string();
456465
let payee = PrivateKeySigner::random();
457466
let payee_addr = payee.address();
@@ -484,23 +493,34 @@ mod test {
484493
let signed = voucher.sign(&domain, payer).unwrap();
485494
assert_eq!(
486495
signed
487-
.validate(&domain, &payee_addr, vec![])
496+
.validate(&ctx.signer_validator, &domain, &payee_addr, vec![])
488497
.unwrap_err()
489498
.to_string(),
490499
DipsError::PayerNotAuthorised(voucher.payer).to_string()
491500
);
492501
assert!(signed
493-
.validate(&domain, &payee_addr, vec![payer_addr])
502+
.validate(
503+
&ctx.signer_validator,
504+
&domain,
505+
&payee_addr,
506+
vec![payer_addr]
507+
)
494508
.is_ok());
495509
}
496510

497-
#[test]
498-
fn check_voucher_modified() {
499-
let deployment_id = "Qmbg1qF4YgHjiVfsVt6a13ddrVcRtWyJQfD4LA3CwHM29f".to_string();
511+
#[tokio::test]
512+
async fn check_voucher_modified() {
500513
let payee = PrivateKeySigner::random();
501514
let payee_addr = payee.address();
502515
let payer = PrivateKeySigner::random();
503516
let payer_addr = payer.address();
517+
let ctx = DipsServerContext::for_testing_mocked_accounts(EscrowAccounts::new(
518+
HashMap::default(),
519+
HashMap::from_iter(vec![(payer_addr, vec![payer_addr])]),
520+
))
521+
.await;
522+
523+
let deployment_id = "Qmbg1qF4YgHjiVfsVt6a13ddrVcRtWyJQfD4LA3CwHM29f".to_string();
504524

505525
let metadata = SubgraphIndexingVoucherMetadata {
506526
basePricePerEpoch: U256::from(10000_u64),
@@ -530,9 +550,14 @@ mod test {
530550

531551
assert!(matches!(
532552
signed
533-
.validate(&domain, &payee_addr, vec![payer_addr])
553+
.validate(
554+
&ctx.signer_validator,
555+
&domain,
556+
&payee_addr,
557+
vec![payer_addr]
558+
)
534559
.unwrap_err(),
535-
DipsError::PayerNotAuthorised(_)
560+
DipsError::SignerNotAuthorised(_)
536561
));
537562
}
538563

@@ -603,9 +628,10 @@ mod test {
603628
dips_agreement_eip712_domain()
604629
}
605630

606-
pub fn test_voucher(
631+
pub fn test_voucher_with_signer(
607632
&self,
608633
metadata: SubgraphIndexingVoucherMetadata,
634+
signer: PrivateKeySigner,
609635
) -> SignedIndexingAgreementVoucher {
610636
let agreement_id = Uuid::now_v7();
611637

@@ -628,14 +654,21 @@ mod test {
628654
metadata: metadata.abi_encode().into(),
629655
};
630656

631-
voucher.sign(&domain, self.payer.clone()).unwrap()
657+
voucher.sign(&domain, signer).unwrap()
658+
}
659+
660+
pub fn test_voucher(
661+
&self,
662+
metadata: SubgraphIndexingVoucherMetadata,
663+
) -> SignedIndexingAgreementVoucher {
664+
self.test_voucher_with_signer(metadata, self.payer.clone())
632665
}
633666
}
634667

635668
#[tokio::test]
636669
async fn test_create_and_cancel_agreement() -> anyhow::Result<()> {
670+
let ctx = DipsServerContext::for_testing();
637671
let voucher_ctx = VoucherContext::random();
638-
let store = Arc::new(InMemoryAgreementStore::default());
639672

640673
// Create metadata and voucher
641674
let metadata = SubgraphIndexingVoucherMetadata {
@@ -649,13 +682,11 @@ mod test {
649682

650683
// Create agreement
651684
let agreement_id = super::validate_and_create_agreement(
652-
store.clone(),
685+
ctx.clone(),
653686
&voucher_ctx.domain(),
654687
&voucher_ctx.payee.address(),
655688
vec![voucher_ctx.payer.address()],
656689
signed_voucher.encode_vec(),
657-
&PriceCalculator::for_testing(),
658-
Arc::new(TestIpfsClient::mainnet()),
659690
)
660691
.await?;
661692

@@ -668,7 +699,7 @@ mod test {
668699

669700
// Cancel agreement
670701
let cancelled_id = super::validate_and_cancel_agreement(
671-
store.clone(),
702+
ctx.store.clone(),
672703
&cancel_domain,
673704
signed_cancel.encode_vec(),
674705
)
@@ -677,7 +708,7 @@ mod test {
677708
assert_eq!(agreement_id, cancelled_id);
678709

679710
// Verify agreement is cancelled
680-
let stored_agreement = store.get_by_id(agreement_id).await?.unwrap();
711+
let stored_agreement = ctx.store.get_by_id(agreement_id).await?.unwrap();
681712
assert!(stored_agreement.cancelled);
682713

683714
Ok(())
@@ -686,7 +717,14 @@ mod test {
686717
#[tokio::test]
687718
async fn test_create_validations_errors() -> anyhow::Result<()> {
688719
let voucher_ctx = VoucherContext::random();
689-
let store = Arc::new(InMemoryAgreementStore::default());
720+
let ctx = DipsServerContext::for_testing_mocked_accounts(EscrowAccounts::new(
721+
HashMap::default(),
722+
HashMap::from_iter(vec![(
723+
voucher_ctx.payer.address(),
724+
vec![voucher_ctx.payer.address()],
725+
)]),
726+
))
727+
.await;
690728

691729
let metadata = SubgraphIndexingVoucherMetadata {
692730
basePricePerEpoch: U256::from(10000_u64),
@@ -716,6 +754,9 @@ mod test {
716754
subgraphDeploymentId: voucher_ctx.deployment_id.clone(),
717755
};
718756

757+
let signer = PrivateKeySigner::random();
758+
let valid_voucher_invalid_signer =
759+
voucher_ctx.test_voucher_with_signer(metadata.clone(), signer.clone());
719760
let valid_voucher = voucher_ctx.test_voucher(metadata);
720761

721762
let expected_result: Vec<Result<[u8; 16], DipsError>> = vec![
@@ -728,23 +769,27 @@ mod test {
728769
100,
729770
"10".to_string(),
730771
)),
772+
Err(DipsError::SignerNotAuthorised(signer.address())),
731773
Ok(valid_voucher
732774
.voucher
733775
.agreement_id
734776
.as_slice()
735777
.try_into()
736778
.unwrap()),
737779
];
738-
let cases = vec![wrong_network_voucher, low_price_voucher, valid_voucher];
780+
let cases = vec![
781+
wrong_network_voucher,
782+
low_price_voucher,
783+
valid_voucher_invalid_signer,
784+
valid_voucher,
785+
];
739786
for (voucher, result) in cases.into_iter().zip(expected_result.into_iter()) {
740787
let out = super::validate_and_create_agreement(
741-
store.clone(),
788+
ctx.clone(),
742789
&voucher_ctx.domain(),
743790
&voucher_ctx.payee.address(),
744791
vec![voucher_ctx.payer.address()],
745792
voucher.encode_vec(),
746-
&PriceCalculator::for_testing(),
747-
Arc::new(TestIpfsClient::mainnet()),
748793
)
749794
.await;
750795

0 commit comments

Comments
 (0)