Skip to content

Commit 3d2728d

Browse files
mangaspcarranzav
andauthored
fix: validate dips pricing (#675)
* fix: validate dips pricing * fix: expose dips price on config file * fix(dips): allow specifying additional networks in config * fix(dips): re-add the manifest network check * fix: quotes in config key * fix(dips): reject the agreement on certain error conditions --------- Co-authored-by: Pablo Carranza Velez <[email protected]>
1 parent 0a11da4 commit 3d2728d

File tree

12 files changed

+296
-44
lines changed

12 files changed

+296
-44
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ members = [
44
"crates/attestation",
55
"crates/config",
66
"crates/dips",
7-
"crates/indexer-receipt",
7+
"crates/indexer-receipt",
88
"crates/monitor",
99
"crates/query",
1010
"crates/service",
@@ -82,6 +82,7 @@ tonic-build = "0.12.3"
8282
serde_yaml = "0.9.21"
8383
bon = "3.3"
8484
test-log = { version = "0.2.12", features = ["trace"] }
85+
graph-networks-registry = "0.6.1"
8586

8687
[patch.crates-io.tap_core]
8788
git = "https://github.com/semiotic-ai/timeline-aggregation-protocol"

crates/config/maximal-config-example.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,12 @@ max_receipts_per_request = 10000
159159
host = "0.0.0.0"
160160
port = "7601"
161161
allowed_payers = ["0x3333333333333333333333333333333333333333"]
162+
163+
price_per_entity = "1000"
164+
165+
[dips.price_per_epoch]
166+
mainnet = "100"
167+
hardhat = "100"
168+
169+
[dips.additional_networks]
170+
"eip155:1337" = "hardhat"

crates/config/minimal-config-example.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,3 @@ receipts_verifier_address = "0x2222222222222222222222222222222222222222"
6363
# Key-Value of all senders and their aggregator endpoints
6464
0xdeadbeefcafebabedeadbeefcafebabedeadbeef = "https://example.com/aggregate-receipts"
6565
0x0123456789abcdef0123456789abcdef01234567 = "https://other.example.com/aggregate-receipts"
66-

crates/config/src/config.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
use std::{
5-
collections::{HashMap, HashSet},
5+
collections::{BTreeMap, HashMap, HashSet},
66
env,
77
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
88
path::PathBuf,
@@ -20,7 +20,10 @@ use regex::Regex;
2020
use serde::Deserialize;
2121
use serde_repr::Deserialize_repr;
2222
use serde_with::{serde_as, DurationSecondsWithFrac};
23-
use thegraph_core::{alloy::primitives::Address, DeploymentId};
23+
use thegraph_core::{
24+
alloy::primitives::{Address, U256},
25+
DeploymentId,
26+
};
2427
use url::Url;
2528

2629
use crate::NonZeroGRT;
@@ -396,6 +399,10 @@ pub struct DipsConfig {
396399
pub host: String,
397400
pub port: String,
398401
pub allowed_payers: Vec<Address>,
402+
403+
pub price_per_entity: U256,
404+
pub price_per_epoch: BTreeMap<String, U256>,
405+
pub additional_networks: HashMap<String, String>,
399406
}
400407

401408
impl Default for DipsConfig {
@@ -404,6 +411,9 @@ impl Default for DipsConfig {
404411
host: "0.0.0.0".to_string(),
405412
port: "7601".to_string(),
406413
allowed_payers: vec![],
414+
price_per_entity: U256::from(100),
415+
price_per_epoch: BTreeMap::new(),
416+
additional_networks: HashMap::new(),
407417
}
408418
}
409419
}
@@ -437,11 +447,16 @@ pub struct RavRequestConfig {
437447

438448
#[cfg(test)]
439449
mod tests {
440-
use std::{collections::HashSet, env, fs, path::PathBuf, str::FromStr};
450+
use std::{
451+
collections::{BTreeMap, HashMap, HashSet},
452+
env, fs,
453+
path::PathBuf,
454+
str::FromStr,
455+
};
441456

442457
use figment::value::Uncased;
443458
use sealed_test::prelude::*;
444-
use thegraph_core::alloy::primitives::{address, Address, FixedBytes};
459+
use thegraph_core::alloy::primitives::{address, Address, FixedBytes, U256};
445460
use tracing_test::traced_test;
446461

447462
use super::{DatabaseConfig, SHARED_PREFIX};
@@ -470,6 +485,15 @@ mod tests {
470485
allowed_payers: vec![Address(
471486
FixedBytes::<20>::from_str("0x3333333333333333333333333333333333333333").unwrap(),
472487
)],
488+
price_per_entity: U256::from(1000),
489+
price_per_epoch: BTreeMap::from_iter(vec![
490+
("mainnet".to_string(), U256::from(100)),
491+
("hardhat".to_string(), U256::from(100)),
492+
]),
493+
additional_networks: HashMap::from([(
494+
"eip155:1337".to_string(),
495+
"hardhat".to_string(),
496+
)]),
473497
..Default::default()
474498
});
475499

crates/dips/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,23 @@ uuid.workspace = true
1818
tokio.workspace = true
1919
indexer-monitor = { path = "../monitor" }
2020
tracing.workspace = true
21+
graph-networks-registry.workspace = true
2122

2223
bytes = { version = "1.10.0", optional = true }
2324
derivative = "2.2.0"
2425

2526
futures = "0.3"
2627
http = "0.2"
2728
prost = { workspace = true, optional = true }
28-
ipfs-api-backend-hyper = { version = "0.6.0", features = ["with-send-sync", "with-hyper-tls"] }
29+
ipfs-api-backend-hyper = { version = "0.6.0", features = [
30+
"with-send-sync",
31+
"with-hyper-tls",
32+
] }
2933
serde_yaml.workspace = true
3034
serde.workspace = true
3135
sqlx = { workspace = true, optional = true }
3236
tonic = { workspace = true, optional = true }
37+
serde_json.workspace = true
3338

3439
[dev-dependencies]
3540
rand = "0.9.0"

crates/dips/src/lib.rs

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use std::{str::FromStr, sync::Arc};
66
use server::DipsServerContext;
77
use thegraph_core::alloy::{
88
core::primitives::Address,
9-
primitives::{b256, ChainId, PrimitiveSignature as Signature, Uint, B256},
9+
primitives::{
10+
b256, ruint::aliases::U256, ChainId, PrimitiveSignature as Signature, Uint, B256,
11+
},
1012
signers::SignerSync,
1113
sol,
1214
sol_types::{eip712_domain, Eip712Domain, SolStruct, SolValue},
@@ -18,6 +20,8 @@ pub mod ipfs;
1820
pub mod price;
1921
#[cfg(feature = "rpc")]
2022
pub mod proto;
23+
#[cfg(test)]
24+
mod registry;
2125
#[cfg(feature = "rpc")]
2226
pub mod server;
2327
pub mod signers;
@@ -145,8 +149,12 @@ pub enum DipsError {
145149
InvalidSubgraphManifest(String),
146150
#[error("chainId {0} is not supported")]
147151
UnsupportedChainId(String),
148-
#[error("price per block is below configured price for chain {0}, minimum: {1}, offered: {2}")]
149-
PricePerBlockTooLow(String, u64, String),
152+
#[error("price per epoch is below configured price for chain {0}, minimum: {1}, offered: {2}")]
153+
PricePerEpochTooLow(String, U256, String),
154+
#[error(
155+
"price per entity is below configured price for chain {0}, minimum: {1}, offered: {2}"
156+
)]
157+
PricePerEntityTooLow(String, U256, String),
150158
// cancellation
151159
#[error("cancelled_by is expected to match the signer")]
152160
UnexpectedSigner,
@@ -304,6 +312,8 @@ pub async fn validate_and_create_agreement(
304312
ipfs_fetcher,
305313
price_calculator,
306314
signer_validator,
315+
registry,
316+
additional_networks,
307317
} = ctx.as_ref();
308318
let decoded_voucher = SignedIndexingAgreementVoucher::abi_decode(voucher.as_ref(), true)
309319
.map_err(|e| DipsError::AbiDecoding(e.to_string()))?;
@@ -316,12 +326,23 @@ pub async fn validate_and_create_agreement(
316326
decoded_voucher.validate(signer_validator, domain, expected_payee, allowed_payers)?;
317327

318328
let manifest = ipfs_fetcher.fetch(&metadata.subgraphDeploymentId).await?;
329+
330+
let network = match registry.get_network_by_id(&metadata.chainId) {
331+
Some(network) => network.id.clone(),
332+
None => match additional_networks.get(&metadata.chainId) {
333+
Some(network) => network.clone(),
334+
None => return Err(DipsError::UnsupportedChainId(metadata.chainId)),
335+
},
336+
};
337+
319338
match manifest.network() {
320-
Some(network_name) => {
321-
tracing::debug!("Subgraph manifest network: {}", network_name);
322-
// TODO: Check if the network is supported
323-
// This will require a mapping of network names to chain IDs
324-
// by querying the supported networks from the EBO subgraph
339+
Some(manifest_network_name) => {
340+
tracing::debug!("Subgraph manifest network: {}", manifest_network_name);
341+
if manifest_network_name != network {
342+
return Err(DipsError::InvalidSubgraphManifest(
343+
metadata.subgraphDeploymentId,
344+
));
345+
}
325346
}
326347
None => {
327348
return Err(DipsError::InvalidSubgraphManifest(
@@ -330,19 +351,28 @@ pub async fn validate_and_create_agreement(
330351
}
331352
}
332353

333-
let offered_price = metadata.pricePerEntity;
354+
let offered_epoch_price = metadata.basePricePerEpoch;
334355
match price_calculator.get_minimum_price(&metadata.chainId) {
335-
Some(price) if offered_price.lt(&Uint::from(price)) => {
336-
return Err(DipsError::PricePerBlockTooLow(
337-
metadata.chainId,
356+
Some(price) if offered_epoch_price.lt(&Uint::from(price)) => {
357+
return Err(DipsError::PricePerEpochTooLow(
358+
network,
338359
price,
339-
offered_price.to_string(),
360+
offered_epoch_price.to_string(),
340361
))
341362
}
342363
Some(_) => {}
343364
None => return Err(DipsError::UnsupportedChainId(metadata.chainId)),
344365
}
345366

367+
let offered_entity_price = metadata.pricePerEntity;
368+
if offered_entity_price < price_calculator.entity_price() {
369+
return Err(DipsError::PricePerEntityTooLow(
370+
network,
371+
price_calculator.entity_price(),
372+
offered_entity_price.to_string(),
373+
));
374+
}
375+
346376
store
347377
.create_agreement(decoded_voucher.clone(), metadata)
348378
.await?;
@@ -754,7 +784,17 @@ mod test {
754784
subgraphDeploymentId: voucher_ctx.deployment_id.clone(),
755785
};
756786

757-
let low_price_voucher = voucher_ctx.test_voucher(metadata);
787+
let low_entity_price_voucher = voucher_ctx.test_voucher(metadata);
788+
789+
let metadata = SubgraphIndexingVoucherMetadata {
790+
basePricePerEpoch: U256::from(10_u64),
791+
pricePerEntity: U256::from(10000_u64),
792+
protocolNetwork: "eip155:42161".to_string(),
793+
chainId: "mainnet".to_string(),
794+
subgraphDeploymentId: voucher_ctx.deployment_id.clone(),
795+
};
796+
797+
let low_epoch_price_voucher = voucher_ctx.test_voucher(metadata);
758798

759799
let metadata = SubgraphIndexingVoucherMetadata {
760800
basePricePerEpoch: U256::from(10000_u64),
@@ -775,9 +815,14 @@ mod test {
775815
Err(DipsError::InvalidSubgraphManifest(
776816
voucher_ctx.deployment_id.clone(),
777817
)),
778-
Err(DipsError::PricePerBlockTooLow(
818+
Err(DipsError::PricePerEntityTooLow(
819+
"mainnet".to_string(),
820+
U256::from(100),
821+
"10".to_string(),
822+
)),
823+
Err(DipsError::PricePerEpochTooLow(
779824
"mainnet".to_string(),
780-
100,
825+
U256::from(200),
781826
"10".to_string(),
782827
)),
783828
Err(DipsError::SignerNotAuthorised(signer.address())),
@@ -790,7 +835,8 @@ mod test {
790835
];
791836
let cases = vec![
792837
no_network_voucher,
793-
low_price_voucher,
838+
low_entity_price_voucher,
839+
low_epoch_price_voucher,
794840
valid_voucher_invalid_signer,
795841
valid_voucher,
796842
];

0 commit comments

Comments
 (0)