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