Skip to content

Commit 82ff624

Browse files
committed
signed voucher handling
1 parent f144748 commit 82ff624

File tree

12 files changed

+817
-128
lines changed

12 files changed

+817
-128
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["common", "config", "service", "tap-agent"]
2+
members = ["common", "config", "dips", "service", "tap-agent"]
33
resolver = "2"
44

55
[profile.dev.package."*"]
@@ -19,6 +19,7 @@ anyhow = { version = "1.0.72" }
1919
thiserror = "1.0.49"
2020
async-trait = "0.1.72"
2121
eventuals = "0.6.7"
22+
base64 = "0.22.1"
2223
reqwest = { version = "0.12", features = [
2324
"charset",
2425
"h2",
@@ -47,3 +48,4 @@ thegraph-core = { git = "https://github.com/edgeandnode/toolshed", rev = "85ee00
4748
"subgraph-client",
4849
] }
4950
thegraph-graphql-http = "0.2.0"
51+
alloy-rlp = "0.3.8"

common/src/indexer_service/http/config.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ pub struct IndexerServiceConfig {
4242
pub escrow_subgraph: SubgraphConfig,
4343
pub graph_network: GraphNetworkConfig,
4444
pub tap: TapConfig,
45+
pub dips: IndexerDipsConfig,
46+
}
47+
48+
#[derive(Debug, Deserialize, Serialize, Clone)]
49+
pub struct IndexerDipsConfig {
50+
pub expected_payee: String,
51+
pub allowed_payers: Vec<String>,
4552
}
4653

4754
#[derive(Clone, Debug, Deserialize, Serialize)]

common/src/indexer_service/http/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ mod static_subgraph;
88
mod tap_receipt_header;
99

1010
pub use config::{
11-
DatabaseConfig, GraphNetworkConfig, GraphNodeConfig, IndexerConfig, IndexerServiceConfig,
12-
ServerConfig, SubgraphConfig, TapConfig,
11+
DatabaseConfig, GraphNetworkConfig, GraphNodeConfig, IndexerConfig, IndexerDipsConfig,
12+
IndexerServiceConfig, ServerConfig, SubgraphConfig, TapConfig,
1313
};
1414
pub use indexer_service::{
1515
AttestationOutput, IndexerService, IndexerServiceImpl, IndexerServiceOptions,

config/src/config.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub struct Config {
3131
pub blockchain: BlockchainConfig,
3232
pub service: ServiceConfig,
3333
pub tap: TapConfig,
34+
pub dips: DipsConfig,
3435
}
3536

3637
// Newtype wrapping Config to be able use serde_ignored with Figment
@@ -307,6 +308,13 @@ pub struct TapConfig {
307308
pub sender_aggregator_endpoints: HashMap<Address, Url>,
308309
}
309310

311+
#[derive(Debug, Deserialize)]
312+
#[cfg_attr(test, derive(PartialEq))]
313+
pub struct DipsConfig {
314+
pub expected_payee: String,
315+
pub allowed_payers: Vec<String>,
316+
}
317+
310318
impl TapConfig {
311319
pub fn get_trigger_value(&self) -> u128 {
312320
let grt_wei = self.max_amount_willing_to_lose_grt.get_value();

dips/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "indexer-dips"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
alloy-core = "0.8.5"
8+
alloy-sol-types = "0.8.5"
9+
alloy-signer = "0.4.2"
10+
alloy-rlp.workspace = true
11+
12+
[dev-dependencies]
13+
alloy-signer-local = "0.4.2"

dips/src/lib.rs

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
use std::str::FromStr;
2+
3+
pub use alloy_core;
4+
pub use alloy_rlp;
5+
pub use alloy_signer;
6+
pub use alloy_sol_types;
7+
8+
use alloy_core::primitives::Address;
9+
use alloy_rlp::{RlpDecodable, RlpEncodable};
10+
use alloy_signer::Signature;
11+
use alloy_sol_types::{sol, SolStruct};
12+
13+
sol! {
14+
// EIP712 encoded bytes, ABI - ethers
15+
#[derive(Debug, RlpEncodable, RlpDecodable, PartialEq)]
16+
struct SignedIndexingAgreementVoucher {
17+
IndexingAgreementVoucher voucher;
18+
bytes signature;
19+
}
20+
21+
#[derive(Debug, RlpEncodable, RlpDecodable, PartialEq)]
22+
struct IndexingAgreementVoucher {
23+
// should coincide with signer
24+
address payer;
25+
// should coincide with indexer
26+
address payee;
27+
// data service that will initiate payment collection
28+
address service;
29+
// initial indexing amount max
30+
uint256 maxInitialAmount;
31+
uint256 maxOngoingAmountPerEpoch;
32+
// time to accept the agreement, intended to be on the order
33+
// of hours or mins
34+
uint64 deadline;
35+
uint32 maxEpochsPerCollection;
36+
uint32 minEpochsPerCollection;
37+
// after which the agreement is complete
38+
uint32 durationEpochs;
39+
bytes metadata;
40+
}
41+
42+
// the vouchers are generic to each data service, in the case of subgraphs this is an ABI-encoded SubgraphIndexingVoucherMetadata
43+
#[derive(Debug, RlpEncodable, RlpDecodable, PartialEq)]
44+
struct SubgraphIndexingVoucherMetadata {
45+
uint256 pricePerBlock; // wei GRT
46+
bytes32 protocolNetwork; // eip199:1 format
47+
// differentiate based on indexed chain
48+
bytes32 chainId; // eip199:1 format
49+
}
50+
}
51+
52+
impl SignedIndexingAgreementVoucher {
53+
// TODO: Validate all values, maybe return a useful error on failure.
54+
pub fn is_valid(
55+
&self,
56+
expected_payee: &Address,
57+
allowed_payers: impl AsRef<[Address]>,
58+
) -> bool {
59+
let sig = match Signature::from_str(&self.signature.to_string()) {
60+
Ok(s) => s,
61+
Err(_) => return false,
62+
};
63+
64+
let payer = sig
65+
.recover_address_from_msg(self.voucher.eip712_hash_struct())
66+
.unwrap();
67+
68+
if allowed_payers.as_ref().is_empty()
69+
|| !allowed_payers.as_ref().iter().any(|addr| addr.eq(&payer))
70+
{
71+
return false;
72+
}
73+
74+
if !self.voucher.payee.eq(expected_payee) {
75+
return false;
76+
}
77+
78+
true
79+
}
80+
}
81+
82+
#[cfg(test)]
83+
mod test {
84+
use alloy_core::primitives::{Address, FixedBytes, U256};
85+
use alloy_signer::SignerSync;
86+
use alloy_signer_local::PrivateKeySigner;
87+
use alloy_sol_types::SolStruct;
88+
89+
use crate::{
90+
IndexingAgreementVoucher, SignedIndexingAgreementVoucher, SubgraphIndexingVoucherMetadata,
91+
};
92+
93+
#[test]
94+
fn voucher_signature_verification() {
95+
let payee = PrivateKeySigner::random();
96+
let payee_addr = payee.address();
97+
let signer = PrivateKeySigner::random();
98+
let pubkey = signer.address();
99+
100+
let metadata = SubgraphIndexingVoucherMetadata {
101+
pricePerBlock: U256::from(10000_u64),
102+
protocolNetwork: FixedBytes::left_padding_from("arbitrum-one".as_bytes()),
103+
chainId: FixedBytes::left_padding_from("mainnet".as_bytes()),
104+
};
105+
106+
let voucher = IndexingAgreementVoucher {
107+
payer: pubkey.clone(),
108+
payee: payee.address(),
109+
service: Address(FixedBytes::ZERO),
110+
maxInitialAmount: U256::from(10000_u64),
111+
maxOngoingAmountPerEpoch: U256::from(10000_u64),
112+
deadline: 1000,
113+
maxEpochsPerCollection: 1000,
114+
minEpochsPerCollection: 1000,
115+
durationEpochs: 1000,
116+
metadata: metadata.eip712_hash_struct().to_owned().into(),
117+
};
118+
119+
let signed = SignedIndexingAgreementVoucher {
120+
voucher: voucher.clone(),
121+
signature: signer
122+
.sign_message_sync(voucher.eip712_hash_struct().as_slice())
123+
.unwrap()
124+
.as_bytes()
125+
.into(),
126+
};
127+
128+
assert_eq!(false, signed.is_valid(&payee_addr, vec![]));
129+
assert_eq!(true, signed.is_valid(&payee_addr, vec![pubkey.clone()]));
130+
}
131+
132+
#[test]
133+
fn check_voucher_modified() {
134+
let payee = PrivateKeySigner::random();
135+
let payee_addr = payee.address();
136+
let signer = PrivateKeySigner::random();
137+
let pubkey = signer.address();
138+
139+
let metadata = SubgraphIndexingVoucherMetadata {
140+
pricePerBlock: U256::from(10000_u64),
141+
protocolNetwork: FixedBytes::left_padding_from("arbitrum-one".as_bytes()),
142+
chainId: FixedBytes::left_padding_from("mainnet".as_bytes()),
143+
};
144+
145+
let voucher = IndexingAgreementVoucher {
146+
payer: pubkey.clone(),
147+
payee: payee.address(),
148+
service: Address(FixedBytes::ZERO),
149+
maxInitialAmount: U256::from(10000_u64),
150+
maxOngoingAmountPerEpoch: U256::from(10000_u64),
151+
deadline: 1000,
152+
maxEpochsPerCollection: 1000,
153+
minEpochsPerCollection: 1000,
154+
durationEpochs: 1000,
155+
metadata: metadata.eip712_hash_struct().to_owned().into(),
156+
};
157+
158+
let mut signed = SignedIndexingAgreementVoucher {
159+
voucher: voucher.clone(),
160+
signature: signer
161+
.sign_message_sync(voucher.eip712_hash_struct().as_slice())
162+
.unwrap()
163+
.as_bytes()
164+
.into(),
165+
};
166+
signed.voucher.service = Address::repeat_byte(9);
167+
168+
assert_eq!(false, signed.is_valid(&payee_addr, vec![pubkey.clone()]));
169+
}
170+
}

service/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ license = "Apache-2.0"
99
[dependencies]
1010
indexer-common = { path = "../common" }
1111
indexer-config = { path = "../config" }
12+
indexer-dips = { path = "../dips" }
1213
anyhow = { workspace = true }
1314
prometheus = { workspace = true }
1415
reqwest = { workspace = true }
@@ -27,10 +28,12 @@ build-info.workspace = true
2728
lazy_static.workspace = true
2829
async-graphql = { version = "7.0.11", default-features = false }
2930
async-graphql-axum = "7.0.11"
31+
base64.workspace = true
3032
graphql = { git = "https://github.com/edgeandnode/toolshed", tag = "graphql-v0.3.0" }
3133

3234
[dev-dependencies]
3335
hex-literal = "0.4.1"
36+
alloy-signer-local = "0.4.2"
3437

3538
[build-dependencies]
3639
build-info-build = { version = "0.0.38", default-features = false }

service/src/config.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
55

66
use indexer_common::indexer_service::http::{
7-
DatabaseConfig, GraphNetworkConfig, GraphNodeConfig, IndexerConfig, IndexerServiceConfig,
8-
ServerConfig, SubgraphConfig, TapConfig,
7+
DatabaseConfig, GraphNetworkConfig, GraphNodeConfig, IndexerConfig, IndexerDipsConfig,
8+
IndexerServiceConfig, ServerConfig, SubgraphConfig, TapConfig,
99
};
1010
use indexer_config::Config as MainConfig;
1111
use serde::{Deserialize, Serialize};
@@ -77,6 +77,10 @@ impl From<MainConfig> for Config {
7777
timestamp_error_tolerance: value.tap.rav_request.timestamp_buffer_secs.as_secs(),
7878
receipt_max_value: value.service.tap.max_receipt_value_grt.get_value(),
7979
},
80+
dips: IndexerDipsConfig {
81+
expected_payee: value.dips.expected_payee,
82+
allowed_payers: value.dips.allowed_payers,
83+
},
8084
})
8185
}
8286
}

service/src/database/dips.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub trait AgreementStore: Sync + Send {
1818
async fn cancel_agreement(&self, signature: String) -> anyhow::Result<String>;
1919
}
2020

21+
#[derive(Default)]
2122
pub struct InMemoryAgreementStore {
2223
pub data: tokio::sync::RwLock<HashMap<String, Agreement>>,
2324
}

0 commit comments

Comments
 (0)