Skip to content

Commit d49c40d

Browse files
committed
feat: signed voucher handling
1 parent b5ff77a commit d49c40d

File tree

12 files changed

+833
-129
lines changed

12 files changed

+833
-129
lines changed

Cargo.lock

Lines changed: 484 additions & 108 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
@@ -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",

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
@@ -35,6 +35,7 @@ pub struct Config {
3535
pub blockchain: BlockchainConfig,
3636
pub service: ServiceConfig,
3737
pub tap: TapConfig,
38+
pub dips: DipsConfig,
3839
}
3940

4041
// Newtype wrapping Config to be able use serde_ignored with Figment
@@ -350,6 +351,13 @@ pub struct TapConfig {
350351
pub sender_aggregator_endpoints: HashMap<Address, Url>,
351352
}
352353

354+
#[derive(Debug, Deserialize)]
355+
#[cfg_attr(test, derive(PartialEq))]
356+
pub struct DipsConfig {
357+
pub expected_payee: String,
358+
pub allowed_payers: Vec<String>,
359+
}
360+
353361
impl TapConfig {
354362
pub fn get_trigger_value(&self) -> u128 {
355363
let grt_wei = self.max_amount_willing_to_lose_grt.get_value();

dips/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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-primitives = { version = "0.8.5", default-features = true, features = ["rlp"]}
9+
alloy-sol-types = "0.8.5"
10+
alloy-signer = "0.4.2"
11+
alloy-rlp = "0.3.8"
12+
13+
[dev-dependencies]
14+
alloy-signer-local = "0.4.2"

dips/src/lib.rs

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

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)