thegraph_core/
attestation.rs

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