use alloy::{
primitives::{Address, B256, ChainId, Signature, b256, keccak256, normalize_v},
signers::SignerSync,
sol_types::{Eip712Domain, SolStruct, eip712_domain},
};
use crate::{allocation_id::AllocationId, deployment_id::DeploymentId};
const ATTESTATION_EIP712_DOMAIN_SALT: B256 =
b256!("a070ffb1cd7409649bf77822cce74495468e06dbfaef09556838bf188679b9c2");
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Attestation {
#[cfg_attr(feature = "serde", serde(rename = "requestCID"))]
pub request_cid: B256,
#[cfg_attr(feature = "serde", serde(rename = "responseCID"))]
pub response_cid: B256,
#[cfg_attr(feature = "serde", serde(rename = "subgraphDeploymentID"))]
pub deployment: B256,
pub r: B256,
pub s: B256,
pub v: u8,
}
alloy::sol! {
struct Receipt {
bytes32 requestCID;
bytes32 responseCID;
bytes32 subgraphDeploymentID;
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, thiserror::Error)]
pub enum VerificationError {
#[error("invalid request hash")]
InvalidRequestHash,
#[error("invalid response hash")]
InvalidResponseHash,
#[error("failed to recover signer")]
FailedSignerRecovery,
#[error("recovered signer is not expected")]
RecoveredSignerNotExpected,
}
pub fn eip712_domain(chain_id: ChainId, dispute_manager: Address) -> Eip712Domain {
eip712_domain! {
name: "Graph Protocol",
version: "0",
chain_id: chain_id,
verifying_contract: dispute_manager,
salt: ATTESTATION_EIP712_DOMAIN_SALT,
}
}
pub fn verify(
domain: &Eip712Domain,
attestation: &Attestation,
expected_signer: &Address,
request: &str,
response: &str,
) -> Result<(), VerificationError> {
if attestation.request_cid != keccak256(request) {
return Err(VerificationError::InvalidRequestHash);
}
if attestation.response_cid != keccak256(response) {
return Err(VerificationError::InvalidResponseHash);
}
let signer = recover_allocation(domain, attestation)?;
if &signer != expected_signer {
return Err(VerificationError::RecoveredSignerNotExpected);
}
Ok(())
}
pub fn create<S: SignerSync>(
domain: &Eip712Domain,
signer: &S,
deployment: &DeploymentId,
request: &str,
response: &str,
) -> Attestation {
let msg = Receipt {
requestCID: keccak256(request),
responseCID: keccak256(response),
subgraphDeploymentID: deployment.into(),
};
let signature = signer
.sign_typed_data_sync(&msg, domain)
.expect("failed to sign attestation");
Attestation {
request_cid: msg.requestCID,
response_cid: msg.responseCID,
deployment: deployment.into(),
r: signature.r().into(),
s: signature.s().into(),
v: signature.recid().into(),
}
}
pub fn recover_allocation(
domain: &Eip712Domain,
attestation: &Attestation,
) -> Result<AllocationId, VerificationError> {
let signature_parity =
normalize_v(attestation.v as u64).ok_or(VerificationError::FailedSignerRecovery)?;
let signature_r = attestation.r.into();
let signature_s = attestation.s.into();
let msg = Receipt {
requestCID: attestation.request_cid,
responseCID: attestation.response_cid,
subgraphDeploymentID: attestation.deployment,
};
let signing_hash = msg.eip712_signing_hash(domain);
Signature::new(signature_r, signature_s, signature_parity)
.recover_address_from_prehash(&signing_hash)
.map(Into::into)
.map_err(|_| VerificationError::FailedSignerRecovery)
}
#[cfg(feature = "fake")]
impl fake::Dummy<fake::Faker> for Attestation {
fn dummy_with_rng<R: fake::Rng + ?Sized>(config: &fake::Faker, rng: &mut R) -> Self {
Self {
request_cid: B256::from(<[u8; 32]>::dummy_with_rng(config, rng)),
response_cid: B256::from(<[u8; 32]>::dummy_with_rng(config, rng)),
deployment: DeploymentId::dummy_with_rng(config, rng).into(),
r: B256::from(<[u8; 32]>::dummy_with_rng(config, rng)),
s: B256::from(<[u8; 32]>::dummy_with_rng(config, rng)),
v: u8::dummy_with_rng(config, rng),
}
}
}
#[cfg(test)]
mod tests {
use alloy::{
primitives::{Address, B256, ChainId, address, b256},
signers::{SignerSync, local::PrivateKeySigner},
sol_types::Eip712Domain,
};
use super::{Attestation, create, eip712_domain, verify};
use crate::{DeploymentId, deployment_id};
const CHAIN_ID: ChainId = 1337;
const DISPUTE_MANAGER_ADDRESS: Address = address!("16def7e0108a5467a106DBd7537F8591F470342e");
const ALLOCATION_ADDRESS: Address = address!("90f8bf6a479f320ead074411a4b0e7944ea8c9c1");
const ALLOCATION_PRIVATE_KEY: B256 =
b256!("4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d");
const DEPLOYMENT: DeploymentId =
deployment_id!("QmeVg9Da6uyBvjUEy5JqCgw2VKdkTxjPvcYuE5riGpkqw1");
fn domain() -> Eip712Domain {
eip712_domain(CHAIN_ID, DISPUTE_MANAGER_ADDRESS)
}
fn signer() -> (Address, impl SignerSync) {
(
ALLOCATION_ADDRESS,
PrivateKeySigner::from_bytes(&ALLOCATION_PRIVATE_KEY).expect("failed to create signer"),
)
}
#[test]
fn verify_attestation() {
let domain = domain();
let (address, _signer) = signer();
let deployment = DEPLOYMENT;
let request = "foo";
let response = "bar";
let attestation = Attestation {
request_cid: b256!("41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d"),
response_cid: b256!("435cd288e3694b535549c3af56ad805c149f92961bf84a1c647f7d86fc2431b4"),
deployment: deployment.into(),
r: b256!("e1fb47e7f0b278d4c88564c3a3b46180e476edcb2b783f253f3eec3b36f8fd4f"),
s: b256!("467a881937edf2faf76e2e497085caf370c9689a1d83b245030757f70a1f64de"),
v: 28,
};
let result = verify(&domain, &attestation, &address, request, response);
assert_eq!(result, Ok(()));
}
#[test]
fn create_and_sign_an_attestation() {
let domain = domain();
let (address, signer) = signer();
let deployment = DEPLOYMENT;
let request = "foo";
let response = "bar";
let attestation = create(&domain, &signer, &deployment, request, response);
let result = verify(&domain, &attestation, &address, request, response);
assert_eq!(result, Ok(()));
}
}