1use alloy::{
4 primitives::{Address, B256, ChainId, Signature, b256, keccak256, normalize_v},
5 signers::SignerSync,
6 sol_types::{Eip712Domain, SolStruct, eip712_domain},
7};
8
9use crate::{allocation_id::AllocationId, deployment_id::DeploymentId};
10
11const ATTESTATION_EIP712_DOMAIN_SALT: B256 =
13 b256!("a070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2");
14
15#[derive(Clone, Debug, PartialEq, Eq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct Attestation {
26 #[cfg_attr(feature = "serde", serde(rename = "requestCID"))]
28 pub request_cid: B256,
29 #[cfg_attr(feature = "serde", serde(rename = "responseCID"))]
31 pub response_cid: B256,
32 #[cfg_attr(feature = "serde", serde(rename = "subgraphDeploymentID"))]
34 pub deployment: B256,
35 pub r: B256,
37 pub s: B256,
39 pub v: u8,
41}
42
43alloy::sol! {
44 struct Receipt {
46 bytes32 requestCID;
47 bytes32 responseCID;
48 bytes32 subgraphDeploymentID;
49 }
50}
51
52#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, thiserror::Error)]
54pub enum VerificationError {
55 #[error("invalid request hash")]
57 InvalidRequestHash,
58
59 #[error("invalid response hash")]
61 InvalidResponseHash,
62
63 #[error("failed to recover signer")]
65 FailedSignerRecovery,
66
67 #[error("recovered signer is not expected")]
69 RecoveredSignerNotExpected,
70}
71
72pub fn eip712_domain(chain_id: ChainId, dispute_manager: Address) -> Eip712Domain {
74 eip712_domain! {
75 name: "Graph Protocol",
76 version: "0",
77 chain_id: chain_id,
78 verifying_contract: dispute_manager,
79 salt: ATTESTATION_EIP712_DOMAIN_SALT,
80 }
81}
82
83pub fn verify(
88 domain: &Eip712Domain,
89 attestation: &Attestation,
90 expected_signer: &Address,
91 request: &str,
92 response: &str,
93) -> Result<(), VerificationError> {
94 if attestation.request_cid != keccak256(request) {
96 return Err(VerificationError::InvalidRequestHash);
97 }
98
99 if attestation.response_cid != keccak256(response) {
101 return Err(VerificationError::InvalidResponseHash);
102 }
103
104 let signer = recover_allocation(domain, attestation)?;
107 if &signer != expected_signer {
108 return Err(VerificationError::RecoveredSignerNotExpected);
109 }
110
111 Ok(())
112}
113
114pub fn create<S: SignerSync>(
118 domain: &Eip712Domain,
119 signer: &S,
120 deployment: &DeploymentId,
121 request: &str,
122 response: &str,
123) -> Attestation {
124 let msg = Receipt {
125 requestCID: keccak256(request),
126 responseCID: keccak256(response),
127 subgraphDeploymentID: deployment.into(),
128 };
129
130 let signature = signer
131 .sign_typed_data_sync(&msg, domain)
132 .expect("failed to sign attestation");
133
134 Attestation {
135 request_cid: msg.requestCID,
136 response_cid: msg.responseCID,
137 deployment: deployment.into(),
138 r: signature.r().into(),
139 s: signature.s().into(),
140 v: signature.recid().into(),
141 }
142}
143
144pub fn recover_allocation(
146 domain: &Eip712Domain,
147 attestation: &Attestation,
148) -> Result<AllocationId, VerificationError> {
149 let signature_parity =
151 normalize_v(attestation.v as u64).ok_or(VerificationError::FailedSignerRecovery)?;
152 let signature_r = attestation.r.into();
153 let signature_s = attestation.s.into();
154
155 let msg = Receipt {
157 requestCID: attestation.request_cid,
158 responseCID: attestation.response_cid,
159 subgraphDeploymentID: attestation.deployment,
160 };
161 let signing_hash = msg.eip712_signing_hash(domain);
162
163 Signature::new(signature_r, signature_s, signature_parity)
165 .recover_address_from_prehash(&signing_hash)
166 .map(Into::into)
167 .map_err(|_| VerificationError::FailedSignerRecovery)
168}
169
170#[cfg(feature = "fake")]
171impl fake::Dummy<fake::Faker> for Attestation {
172 fn dummy_with_rng<R: fake::Rng + ?Sized>(config: &fake::Faker, rng: &mut R) -> Self {
173 Self {
174 request_cid: B256::from(<[u8; 32]>::dummy_with_rng(config, rng)),
175 response_cid: B256::from(<[u8; 32]>::dummy_with_rng(config, rng)),
176 deployment: DeploymentId::dummy_with_rng(config, rng).into(),
177 r: B256::from(<[u8; 32]>::dummy_with_rng(config, rng)),
178 s: B256::from(<[u8; 32]>::dummy_with_rng(config, rng)),
179 v: u8::dummy_with_rng(config, rng),
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use alloy::{
187 primitives::{Address, B256, ChainId, address, b256},
188 signers::{SignerSync, local::PrivateKeySigner},
189 sol_types::Eip712Domain,
190 };
191
192 use super::{Attestation, create, eip712_domain, verify};
193 use crate::{DeploymentId, deployment_id};
194
195 const CHAIN_ID: ChainId = 1337;
196 const DISPUTE_MANAGER_ADDRESS: Address = address!("16def7e0108a5467a106DBd7537F8591F470342e");
197 const ALLOCATION_ADDRESS: Address = address!("90f8bf6a479f320ead074411a4b0e7944ea8c9c1");
198 const ALLOCATION_PRIVATE_KEY: B256 =
199 b256!("4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d");
200 const DEPLOYMENT: DeploymentId =
201 deployment_id!("QmeVg9Da6uyBvjUEy5JqCgw2VKdkTxjPvcYuE5riGpkqw1");
202
203 fn domain() -> Eip712Domain {
207 eip712_domain(CHAIN_ID, DISPUTE_MANAGER_ADDRESS)
208 }
209
210 fn signer() -> (Address, impl SignerSync) {
217 (
218 ALLOCATION_ADDRESS,
219 PrivateKeySigner::from_bytes(&ALLOCATION_PRIVATE_KEY).expect("failed to create signer"),
220 )
221 }
222
223 #[test]
225 fn verify_attestation() {
226 let domain = domain();
228 let (address, _signer) = signer();
229 let deployment = DEPLOYMENT;
230
231 let request = "foo";
232 let response = "bar";
233
234 let attestation = Attestation {
235 request_cid: b256!("41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d"),
236 response_cid: b256!("435cd288e3694b535549c3af56ad805c149f92961bf84a1c647f7d86fc2431b4"),
237 deployment: deployment.into(),
238 r: b256!("e1fb47e7f0b278d4c88564c3a3b46180e476edcb2b783f253f3eec3b36f8fd4f"),
239 s: b256!("467a881937edf2faf76e2e497085caf370c9689a1d83b245030757f70a1f64de"),
240 v: 28,
241 };
242
243 let result = verify(&domain, &attestation, &address, request, response);
245
246 assert_eq!(result, Ok(()));
248 }
249
250 #[test]
251 fn create_and_sign_an_attestation() {
252 let domain = domain();
254 let (address, signer) = signer();
255 let deployment = DEPLOYMENT;
256
257 let request = "foo";
258 let response = "bar";
259
260 let attestation = create(&domain, &signer, &deployment, request, response);
262
263 let result = verify(&domain, &attestation, &address, request, response);
265 assert_eq!(result, Ok(()));
266 }
267}