thegraph_core/
attestation.rs

1//! Attestation types and functions for verifying attestations.
2
3use 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
13/// Attestation EIP-712 domain salt
14const ATTESTATION_EIP712_DOMAIN_SALT: B256 =
15    b256!("a070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2");
16
17/// An attestation of a request-response pair.
18///
19/// An attestation is an EIP-712 signature over a request, `request_cid`, and response,
20/// `response_cid`, keccak-256 hash, and the subgraph deployment ID, `deployment`, being queried.
21///
22/// The attestation is signed by the indexer that processed the request. The indexer signs the
23/// allocation by signing the EIP-712 hash with the private key of the allocation associated with
24/// the deployment being queried.
25#[derive(Clone, Debug, PartialEq, Eq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct Attestation {
28    /// The keccak-256 hash of the request being attested.
29    #[cfg_attr(feature = "serde", serde(rename = "requestCID"))]
30    pub request_cid: B256,
31    /// The keccak-256 hash of the response being attested.
32    #[cfg_attr(feature = "serde", serde(rename = "responseCID"))]
33    pub response_cid: B256,
34    /// The subgraph deployment ID being queried.
35    #[cfg_attr(feature = "serde", serde(rename = "subgraphDeploymentID"))]
36    pub deployment: B256,
37    /// The `r` component of the attestation signature.
38    pub r: B256,
39    /// The `s` component of the attestation signature.
40    pub s: B256,
41    /// The parity indicator of the attestation signature.
42    pub v: u8,
43}
44
45alloy::sol! {
46    /// EIP-712 receipt struct for attestation signing.
47    struct Receipt {
48        bytes32 requestCID;
49        bytes32 responseCID;
50        bytes32 subgraphDeploymentID;
51    }
52}
53
54/// Errors that can occur when verifying an attestation.
55#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, thiserror::Error)]
56pub enum VerificationError {
57    /// The request hash in the attestation does not match the expected request hash
58    #[error("invalid request hash")]
59    InvalidRequestHash,
60
61    /// The response hash in the attestation does not match the expected response hash
62    #[error("invalid response hash")]
63    InvalidResponseHash,
64
65    /// Failed to recover the signer addres (allocation address) from the attestation signature
66    #[error("failed to recover signer")]
67    FailedSignerRecovery,
68
69    /// The recovered signer address does not match the expected signer address
70    #[error("recovered signer is not expected")]
71    RecoveredSignerNotExpected,
72}
73
74/// Create an EIP-712 domain given a chain ID and dispute manager address.
75pub 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
85/// Verify an attestation.
86///
87/// Checks that the request and response hashes match the attestation, and the address recovered
88/// from the signature of the attestation matches the expected signer.
89pub fn verify(
90    domain: &Eip712Domain,
91    attestation: &Attestation,
92    expected_signer: &Address,
93    request: &str,
94    response: &str,
95) -> Result<(), VerificationError> {
96    // Check that the request and response hashes match the attestation
97    if attestation.request_cid != keccak256(request) {
98        return Err(VerificationError::InvalidRequestHash);
99    }
100
101    // Check that the request and response hashes match the attestation
102    if attestation.response_cid != keccak256(response) {
103        return Err(VerificationError::InvalidResponseHash);
104    }
105
106    // Recover the attestation signer public address (the allocation address) from the attestation
107    // and check that it matches the expected signer address
108    let signer = recover_allocation(domain, attestation)?;
109    if &signer != expected_signer {
110        return Err(VerificationError::RecoveredSignerNotExpected);
111    }
112
113    Ok(())
114}
115
116/// Create an attestation.
117///
118/// Signs the attestation with the signer's private key.
119pub 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
146/// Recover the signer's allocation address from the attestation.
147pub fn recover_allocation(
148    domain: &Eip712Domain,
149    attestation: &Attestation,
150) -> Result<AllocationId, VerificationError> {
151    // Recover the signature components
152    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    // Calculate the signing hash
158    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    // Recover the allocation ID from the signature
166    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    /// Create a domain for testing:
206    /// - `chain_id`: `1337`
207    /// - `dispute_manager`: `0x16DEF7E0108A5467A106dbD7537f8591f470342E`
208    fn domain() -> Eip712Domain {
209        eip712_domain(CHAIN_ID, DISPUTE_MANAGER_ADDRESS)
210    }
211
212    /// Create a signer for testing
213    ///
214    /// - `private_key`: `0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d`
215    /// - `address`: `0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1`
216    ///
217    /// Returns the allocation address and signer.
218    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    /// Verify an attestation (created by old indexer-native module from TS indexer implementation)
226    #[test]
227    fn verify_attestation() {
228        //* Given
229        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        //* When
246        let result = verify(&domain, &attestation, &address, request, response);
247
248        //* Then
249        assert_eq!(result, Ok(()));
250    }
251
252    #[test]
253    fn create_and_sign_an_attestation() {
254        //* Given
255        let domain = domain();
256        let (address, signer) = signer();
257        let deployment = DEPLOYMENT;
258
259        let request = "foo";
260        let response = "bar";
261
262        //* When
263        let attestation = create(&domain, &signer, &deployment, request, response);
264
265        //* Then
266        let result = verify(&domain, &attestation, &address, request, response);
267        assert_eq!(result, Ok(()));
268    }
269}