Skip to content

Commit

Permalink
refactor: Move common utilities into indexer-common crate
Browse files Browse the repository at this point in the history
  • Loading branch information
Jannis committed Sep 18, 2023
1 parent db65564 commit 029a8b6
Show file tree
Hide file tree
Showing 25 changed files with 742 additions and 546 deletions.
672 changes: 368 additions & 304 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
[workspace]
members = [
"native",
"common",
"service",
# "common",
]
resolver = "2"

Expand Down
25 changes: 25 additions & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "indexer-common"
version = "0.1.0"
edition = "2021"

[dependencies]
alloy-primitives = { version = "0.3.3", features = ["serde"] }
anyhow = "1.0.75"
arc-swap = "1.6.0"
bs58 = "0.5.0"
eip-712-derive = { git = "https://github.com/graphprotocol/eip-712-derive" }
ethereum-types = "0.14.1"
ethers = "2.0.10"
ethers-core = "2.0.10"
keccak-hash = "0.10.0"
lazy_static = "1.4.0"
log = "0.4.20"
reqwest = "0.11.20"
secp256k1 = { version = "0.27.0", features = ["recovery"] }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
tokio = { version = "1.32.0", features = ["full", "macros"] }

[dev-dependencies]
wiremock = "0.5.19"
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
use alloy_primitives::Address;
use anyhow::Result;
use ethers::signers::coins_bip39::English;
use ethers::signers::MnemonicBuilder;
use ethers::signers::Signer;
use ethers::signers::Wallet;
use ethers::signers::{MnemonicBuilder, Signer, Wallet};
use ethers_core::k256::ecdsa::SigningKey;
use ethers_core::types::U256;
use serde::Deserialize;
use serde::Deserializer;

use crate::common::types::SubgraphDeploymentID;
use crate::types::SubgraphDeploymentID;

pub mod monitor;

#[derive(Debug, Eq, PartialEq)]
pub struct Allocation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use log::{info, warn};
use tokio::sync::watch::{Receiver, Sender};
use tokio::sync::RwLock;

use crate::{common::allocation::Allocation, common::network_subgraph::NetworkSubgraph};
use crate::prelude::{Allocation, NetworkSubgraph};

#[derive(Debug)]
struct AllocationMonitorInner {
Expand Down Expand Up @@ -79,18 +79,15 @@ impl AllocationMonitor {
.to_string(),
Some(serde_json::json!({ "id": graph_network_id })),
)
.await?;

let res_json: serde_json::Value = serde_json::from_str(res.graphql_response.as_str())
.await
.map_err(|e| {
anyhow::anyhow!(
"Failed to parse current epoch response from network subgraph: {}",
e
)
})?;

res_json
.get("data")
res.get("data")
.and_then(|d| d.get("graphNetwork"))
.and_then(|d| d.get("currentEpoch"))
.and_then(|d| d.as_u64())
Expand All @@ -104,7 +101,7 @@ impl AllocationMonitor {
indexer_address: &Address,
closed_at_epoch_threshold: u64,
) -> Result<HashMap<Address, Allocation>> {
let res = network_subgraph
let mut res = network_subgraph
.network_query(
r#"
query allocations($indexer: ID!, $closedAtEpochThreshold: Int!) {
Expand Down Expand Up @@ -157,17 +154,15 @@ impl AllocationMonitor {
.to_string(),
Some(serde_json::json!({ "indexer": indexer_address, "closedAtEpochThreshold": closed_at_epoch_threshold })),
)
.await;

let mut res_json: serde_json::Value = serde_json::from_str(res?.graphql_response.as_str())
.await
.map_err(|e| {
anyhow::anyhow!(
"Failed to fetch current allocations from network subgraph: {}",
e
)
})?;

let indexer_json = res_json
let indexer_json = res
.get_mut("data")
.and_then(|d| d.get_mut("indexer"))
.ok_or_else(|| anyhow::anyhow!("No data / indexer not found on chain",))?;
Expand Down
220 changes: 220 additions & 0 deletions common/src/attestations/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright 2023-, GraphOps and Semiotic Labs.
// SPDX-License-Identifier: Apache-2.0

use anyhow::Result;
use ethers::signers::coins_bip39::English;
use ethers::signers::MnemonicBuilder;
use ethers::signers::Signer;
use ethers::signers::Wallet;
use ethers_core::k256::ecdsa::SigningKey;

use crate::prelude::{Allocation, SubgraphDeploymentID};

pub mod signer;
pub mod signers;

pub fn derive_key_pair(
indexer_mnemonic: &str,
epoch: u64,
deployment: &SubgraphDeploymentID,
index: u64,
) -> Result<Wallet<SigningKey>> {
let mut derivation_path = format!("m/{}/", epoch);
derivation_path.push_str(
&deployment
.ipfs_hash()
.as_bytes()
.iter()
.map(|char| char.to_string())
.collect::<Vec<String>>()
.join("/"),
);
derivation_path.push_str(format!("/{}", index).as_str());

Ok(MnemonicBuilder::<English>::default()
.derivation_path(&derivation_path)
.expect("Valid derivation path")
.phrase(indexer_mnemonic)
.build()?)
}

pub fn attestation_signer_for_allocation(
indexer_mnemonic: &str,
allocation: &Allocation,
) -> Result<SigningKey> {
// Guess the allocation index by enumerating all indexes in the
// range [0, 100] and checking for a match
for i in 0..100 {
// The allocation was either created at the epoch it intended to or one
// epoch later. So try both both.
for created_at_epoch in [allocation.created_at_epoch, allocation.created_at_epoch - 1] {
let allocation_wallet = derive_key_pair(
indexer_mnemonic,
created_at_epoch,
&allocation.subgraph_deployment.id,
i,
)?;
if allocation_wallet.address().as_fixed_bytes() == allocation.id {
return Ok(allocation_wallet.signer().clone());
}
}
}
Err(anyhow::anyhow!(
"Could not find allocation signer for allocation {}",
allocation.id
))
}

#[cfg(test)]
mod tests {
use std::str::FromStr;
use test_log::test;

use crate::test_vectors;
use crate::types::SubgraphDeploymentID;

use super::*;

const INDEXER_OPERATOR_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";

#[test]
fn test_derive_key_pair() {
assert_eq!(
derive_key_pair(
INDEXER_OPERATOR_MNEMONIC,
953,
&SubgraphDeploymentID::new(
"0xbbde25a2c85f55b53b7698b9476610c3d1202d88870e66502ab0076b7218f98a"
)
.unwrap(),
0
)
.unwrap()
.address()
.as_fixed_bytes(),
Address::from_str("0xfa44c72b753a66591f241c7dc04e8178c30e13af").unwrap()
);

assert_eq!(
derive_key_pair(
INDEXER_OPERATOR_MNEMONIC,
940,
&SubgraphDeploymentID::new(
"0xbbde25a2c85f55b53b7698b9476610c3d1202d88870e66502ab0076b7218f98a"
)
.unwrap(),
2
)
.unwrap()
.address()
.as_fixed_bytes(),
Address::from_str("0xa171cd12c3dde7eb8fe7717a0bcd06f3ffa65658").unwrap()
);
}

#[test]
fn test_allocation_signer() {
// Note that we use `derive_key_pair` to derive the private key

let allocation = Allocation {
id: Address::from_str("0xa171cd12c3dde7eb8fe7717a0bcd06f3ffa65658").unwrap(),
status: AllocationStatus::Null,
subgraph_deployment: SubgraphDeployment {
id: SubgraphDeploymentID::new(
"0xbbde25a2c85f55b53b7698b9476610c3d1202d88870e66502ab0076b7218f98a",
)
.unwrap(),
denied_at: None,
staked_tokens: U256::zero(),
signalled_tokens: U256::zero(),
query_fees_amount: U256::zero(),
},
indexer: Address::ZERO,
allocated_tokens: U256::zero(),
created_at_epoch: 940,
created_at_block_hash: "".to_string(),
closed_at_epoch: None,
closed_at_epoch_start_block_hash: None,
previous_epoch_start_block_hash: None,
poi: None,
query_fee_rebates: None,
query_fees_collected: None,
};
assert_eq!(
attestation_signer_for_allocation(INDEXER_OPERATOR_MNEMONIC, &allocation).unwrap(),
*derive_key_pair(
INDEXER_OPERATOR_MNEMONIC,
940,
&allocation.subgraph_deployment.id,
2
)
.unwrap()
.signer()
);
}

#[test]
fn test_allocation_signer_error() {
// Note that because allocation will try 200 derivations paths, this is a slow test

let allocation = Allocation {
// Purposefully wrong address
id: Address::from_str("0xdeadbeefcafebabedeadbeefcafebabedeadbeef").unwrap(),
status: AllocationStatus::Null,
subgraph_deployment: SubgraphDeployment {
id: SubgraphDeploymentID::new(
"0xbbde25a2c85f55b53b7698b9476610c3d1202d88870e66502ab0076b7218f98a",
)
.unwrap(),
denied_at: None,
staked_tokens: U256::zero(),
signalled_tokens: U256::zero(),
query_fees_amount: U256::zero(),
},
indexer: Address::ZERO,
allocated_tokens: U256::zero(),
created_at_epoch: 940,
created_at_block_hash: "".to_string(),
closed_at_epoch: None,
closed_at_epoch_start_block_hash: None,
previous_epoch_start_block_hash: None,
poi: None,
query_fee_rebates: None,
query_fees_collected: None,
};
assert!(attestation_signer_for_allocation(INDEXER_OPERATOR_MNEMONIC, &allocation).is_err());
}

#[test(tokio::test)]
async fn test_update_attestation_signers() {
unsafe {
let mut mock_allocation_monitor = AllocationMonitor::faux();

faux::when!(mock_allocation_monitor.get_eligible_allocations).then_unchecked(|_| {
// Spawn a thread to be able to call `blocking_read` on the RwLock, which actually spins its own async
// runtime.
// This is needed because `faux` will also use a runtime to mock the async function.
let t = std::thread::spawn(|| {
let eligible_allocations = Box::leak(Box::new(Arc::new(RwLock::new(
test_vectors::expected_eligible_allocations(),
))));
eligible_allocations.blocking_read()
});
t.join().unwrap()
});

let inner = Arc::new(AttestationSignersInner {
attestation_signers: Arc::new(RwLock::new(HashMap::new())),
allocation_monitor: mock_allocation_monitor,
indexer_mnemonic: test_vectors::INDEXER_OPERATOR_MNEMONIC.to_string(),
chain_id: U256::from(1),
dispute_manager: Address::from_str(test_vectors::DISPUTE_MANAGER_ADDRESS).unwrap(),
});

AttestationSigners::update_attestation_signers(inner.clone()).await;

// Check that the attestation signers were found for the allocations
assert_eq!(inner.attestation_signers.read().await.len(), 4);
}
}
}
32 changes: 28 additions & 4 deletions native/src/attestation.rs → common/src/attestations/signer.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
// Copyright 2023-, GraphOps and Semiotic Labs.
// SPDX-License-Identifier: Apache-2.0

use super::*;
use alloy_primitives::Address;
use eip_712_derive::{
sign_typed, Bytes32, DomainSeparator, Eip712Domain, MemberVisitor, StructType, U256,
sign_typed, Bytes32, DomainSeparator, Eip712Domain, MemberVisitor, StructType,
};
use ethers::utils::hex;
use ethers_core::k256::ecdsa::SigningKey;
use ethers_core::types::U256;
use keccak_hash::keccak;
use secp256k1::SecretKey;
use std::convert::TryInto;

/// An attestation signer tied to a specific allocation via its signer key
#[derive(Debug, Clone)]
pub struct AttestationSigner {
subgraph_deployment_id: Bytes32,
Expand All @@ -17,7 +22,7 @@ pub struct AttestationSigner {

impl AttestationSigner {
pub fn new(
chain_id: U256,
chain_id: eip_712_derive::U256,
dispute_manager: Address,
signer: SecretKey,
subgraph_deployment_id: Bytes32,
Expand All @@ -31,7 +36,7 @@ impl AttestationSigner {
name: "Graph Protocol".to_owned(),
version: "0".to_owned(),
chain_id,
verifying_contract: eip_712_derive::Address(dispute_manager),
verifying_contract: eip_712_derive::Address(dispute_manager.into()),
salt,
};
let domain_separator = DomainSeparator::new(&domain);
Expand Down Expand Up @@ -95,3 +100,22 @@ pub struct Attestation {
pub r: Bytes32,
pub s: Bytes32,
}

/// Helper for creating an AttestationSigner
pub fn create_attestation_signer(
chain_id: U256,
dispute_manager_address: Address,
signer: SigningKey,
deployment_id: [u8; 32],
) -> anyhow::Result<AttestationSigner> {
// Tedious conversions to the "indexer_native" types
let mut chain_id_bytes = [0u8; 32];
chain_id.to_big_endian(&mut chain_id_bytes);
let signer = AttestationSigner::new(
eip_712_derive::U256(chain_id_bytes),
dispute_manager_address,
secp256k1::SecretKey::from_slice(&signer.to_bytes())?,
deployment_id,
);
Ok(signer)
}
Loading

0 comments on commit 029a8b6

Please sign in to comment.