diff --git a/src/consensus_client.rs b/src/consensus_client.rs new file mode 100644 index 0000000..ee7f9ba --- /dev/null +++ b/src/consensus_client.rs @@ -0,0 +1,149 @@ +use eyre::Result; +use ssz_rs::prelude::*; +use super::utils::*; +use serde::de::Error; + +pub struct ConsensusClient { + rpc: String, +} + +impl ConsensusClient { + pub fn new(rpc: &str) -> Self { + ConsensusClient { rpc: rpc.to_string() } + } + + pub async fn get_bootstrap(&self, block_root: &str) -> Result { + let req = format!("{}/eth/v0/beacon/light_client/bootstrap/{}", self.rpc, block_root); + let res = reqwest::get(req).await?.json::().await?; + Ok(res.data.v) + } + + pub async fn get_updates(&self, period: u64) -> Result> { + let req = format!("{}/eth/v0/beacon/light_client/updates?start_period={}&count=1000", self.rpc, period); + let res = reqwest::get(req).await?.json::().await?; + Ok(res.data) + } + + pub async fn get_finality_update(&self) -> Result { + let req = format!("{}/eth/v0/beacon/light_client/finality_update", self.rpc); + let res = reqwest::get(req).await?.json::().await?; + Ok(res.data) + } +} + +pub type BLSPubKey = Vector; +pub type Bytes32 = Vector; + +#[derive(serde::Deserialize, Debug)] +pub struct Bootstrap { + pub header: Header, + pub current_sync_committee: SyncCommittee, + #[serde(deserialize_with = "branch_deserialize")] + pub current_sync_committee_branch: Vec, +} + +#[derive(serde::Deserialize, Debug, Clone)] +pub struct Update { + pub attested_header: Header, + pub next_sync_committee: Option, + #[serde(deserialize_with = "branch_deserialize")] + pub next_sync_committee_branch: Vec, + pub finalized_header: Header, + #[serde(deserialize_with = "branch_deserialize")] + pub finality_branch: Vec, + pub sync_aggregate: SyncAggregate, + #[serde(deserialize_with = "u64_deserialize")] + pub signature_slot: u64, +} + +#[derive(serde::Deserialize, Debug)] +pub struct FinalityUpdate { + pub attested_header: Header, + pub finalized_header: Header, + #[serde(deserialize_with = "branch_deserialize")] + pub finality_branch: Vec, + pub sync_aggregate: SyncAggregate, + #[serde(deserialize_with = "u64_deserialize")] + pub signature_slot: u64, +} + +#[derive(serde::Deserialize, Debug, Clone, Default, SimpleSerialize)] +pub struct Header { + #[serde(deserialize_with = "u64_deserialize")] + pub slot: u64, + #[serde(deserialize_with = "u64_deserialize")] + pub proposer_index: u64, + #[serde(deserialize_with = "bytes32_deserialize")] + pub parent_root: Bytes32, + #[serde(deserialize_with = "bytes32_deserialize")] + pub state_root: Bytes32, + #[serde(deserialize_with = "bytes32_deserialize")] + pub body_root: Bytes32, +} + +#[derive(Debug, Clone, Default, SimpleSerialize, serde::Deserialize)] +pub struct SyncCommittee { + #[serde(deserialize_with = "pubkeys_deserialize")] + pub pubkeys: Vector, + #[serde(deserialize_with = "pubkey_deserialize")] + pub aggregate_pubkey: BLSPubKey, +} + +#[derive(serde::Deserialize, Debug, Clone)] +pub struct SyncAggregate { + pub sync_committee_bits: String, + pub sync_committee_signature: String, +} + +#[derive(serde::Deserialize, Debug)] +struct UpdateResponse { + data: Vec, +} + +#[derive(serde::Deserialize, Debug)] +struct FinalityUpdateResponse { + data: FinalityUpdate, +} + +#[derive(serde::Deserialize, Debug)] +struct BootstrapResponse { + data: BootstrapData, +} + +#[derive(serde::Deserialize, Debug)] +struct BootstrapData { + v: Bootstrap, +} + +fn pubkey_deserialize<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de> { + let key: String = serde::Deserialize::deserialize(deserializer)?; + let key_bytes = hex_str_to_bytes(&key).map_err(D::Error::custom)?; + Ok(Vector::from_iter(key_bytes)) +} + +fn pubkeys_deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de> { + let keys: Vec = serde::Deserialize::deserialize(deserializer)?; + Ok(keys.iter().map(|key| { + let key_bytes = hex_str_to_bytes(key)?; + Ok(Vector::from_iter(key_bytes)) + }).collect::>>().map_err(D::Error::custom)?) +} + +fn branch_deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de> { + let branch: Vec = serde::Deserialize::deserialize(deserializer)?; + Ok(branch.iter().map(|elem| { + let elem_bytes = hex_str_to_bytes(elem)?; + Ok(Vector::from_iter(elem_bytes)) + }).collect::>().map_err(D::Error::custom)?) +} + +fn u64_deserialize<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de> { + let val: String = serde::Deserialize::deserialize(deserializer)?; + Ok(val.parse().unwrap()) +} + +fn bytes32_deserialize<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de> { + let bytes: String = serde::Deserialize::deserialize(deserializer)?; + let bytes = hex::decode(bytes.strip_prefix("0x").unwrap()).unwrap(); + Ok(bytes.to_vec().try_into().unwrap()) +} diff --git a/src/main.rs b/src/main.rs index 63d1cfe..4007e38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,11 @@ use eyre::Result; use ssz_rs::prelude::*; use blst::{min_pk::*, BLST_ERROR}; +use consensus_client::*; +use utils::*; + +pub mod consensus_client; +pub mod utils; #[tokio::main] async fn main() -> Result<()> { @@ -16,7 +21,7 @@ async fn main() -> Result<()> { } struct LightClient { - nimbus_rpc: String, + consensus_client: ConsensusClient, store: Store, } @@ -30,18 +35,18 @@ struct Store { impl LightClient { async fn new(nimbus_rpc: &str, checkpoint_block_root: &str) -> Result { - let mut bootstrap = Self::get_bootstrap(nimbus_rpc, checkpoint_block_root).await?; + let consensus_client = ConsensusClient::new(nimbus_rpc); - let committee_hash = bootstrap.current_sync_committee.hash_tree_root()?; - let root = Node::from_bytes(bootstrap.header.state_root); - let committee_branch = branch_to_nodes(bootstrap.current_sync_committee_branch); - - let committee_valid = is_valid_merkle_branch(&committee_hash, committee_branch.iter(), 5, 22, &root); - println!("bootstrap committee valid: {}", committee_valid); + let mut bootstrap = consensus_client.get_bootstrap(checkpoint_block_root).await?; + + let committee_valid = is_current_committee_proof_valid( + &bootstrap.header, + &mut bootstrap.current_sync_committee, + &bootstrap.current_sync_committee_branch + ); let header_hash = bootstrap.header.hash_tree_root()?; let header_valid = header_hash.to_string() == checkpoint_block_root.to_string(); - println!("bootstrap header valid: {}", header_valid); if !(header_valid && committee_valid) { return Err(eyre::eyre!("Invalid Bootstrap")); @@ -53,23 +58,20 @@ impl LightClient { next_sync_committee: None, }; - Ok(LightClient { nimbus_rpc: nimbus_rpc.to_string(), store }) + Ok(LightClient { consensus_client, store }) } async fn sync(&mut self) -> Result<()> { let current_period = calc_sync_period(self.store.header.slot); - let next_period = current_period + 0; - - let updates = self.get_updates(next_period).await?; + let updates = self.consensus_client.get_updates(current_period).await?; for mut update in updates { self.verify_update(&mut update)?; self.apply_update(&update); - println!("================================"); } - let finality_update = self.get_finality_update().await?; + let finality_update = self.consensus_client.get_finality_update().await?; let mut finality_update_generic = Update { attested_header: finality_update.attested_header, next_sync_committee: None, @@ -95,9 +97,6 @@ impl LightClient { let current_period = calc_sync_period(current_slot); let update_period = calc_sync_period(update_slot); - println!("current period: {}", current_period); - println!("update period: {}", update_period); - if !(update_period == current_period + 1 || update_period == current_period) { return Err(eyre::eyre!("Invalid Update")); } @@ -106,67 +105,46 @@ impl LightClient { return Err(eyre::eyre!("Invalid Update")); } - let finality_header_hash = update.finalized_header.hash_tree_root()?; - let update_header_root = Node::from_bytes(update.attested_header.state_root); - let finality_branch = branch_to_nodes(update.finality_branch.clone()); - let finality_branch_valid = is_valid_merkle_branch(&finality_header_hash, finality_branch.iter(), 6, 41, &update_header_root); - println!("finality branch valid: {}", finality_branch_valid); + let finality_branch_valid = is_finality_proof_valid( + &update.attested_header, + &mut update.finalized_header, + &update.finality_branch + ); + + if !(finality_branch_valid) { + return Err(eyre::eyre!("Invalid Update")); + } if update.next_sync_committee.is_some() { - let next_committee_hash = update.next_sync_committee.as_ref().unwrap().clone().hash_tree_root()?; - let next_committee_branch = branch_to_nodes(update.next_sync_committee_branch.clone()); - let next_committee_branch_valid = is_valid_merkle_branch(&next_committee_hash, next_committee_branch.iter(), 5, 23, &update_header_root); - println!("next sync committee branch valid: {}", next_committee_branch_valid); + let next_committee_branch_valid = is_next_committee_proof_valid( + &update.attested_header, + &mut update.next_sync_committee.clone().unwrap(), + &update.next_sync_committee_branch + ); if !next_committee_branch_valid { return Err(eyre::eyre!("Invalid Update")); } } - if !(finality_branch_valid) { - return Err(eyre::eyre!("Invalid Update")); - } - let sync_committee = if current_period == update_period { &self.store.current_sync_committee } else { self.store.next_sync_committee.as_ref().unwrap() }; - let bytes = hex::decode(update.sync_aggregate.sync_committee_bits.strip_prefix("0x").unwrap())?; - let mut bits = String::new(); - let mut count = 0; - for byte in bytes { - let byte_str = format!("{:08b}", byte); - byte_str.chars().for_each(|b| if b == '1' { count += 1 }); - bits.push_str(&byte_str.chars().rev().collect::()); - } - - let mut pks: Vec = Vec::new(); - bits.chars().enumerate().for_each(|(i, bit)| { - if bit == '1' { - let pk = sync_committee.pubkeys[i].clone(); - let pk = PublicKey::from_bytes(&pk).unwrap(); - pks.push(pk) - } - }); + let pks = get_participating_keys(sync_committee, &update.sync_aggregate.sync_committee_bits)?; let pks: Vec<&PublicKey> = pks.iter().map(|pk| pk).collect(); - let committee_quorum = count as f64 > 2.0 / 3.0 * 512.0; - println!("sync committee quorum: {}", committee_quorum); - + let committee_quorum = pks.len() as f64 > 2.0 / 3.0 * 512.0; if !committee_quorum { return Err(eyre::eyre!("Invalid Update")); } let header_root = bytes_to_bytes32(update.attested_header.hash_tree_root()?.as_bytes()); let signing_root = compute_committee_sign_root(header_root)?; - - let sig_bytes = hex::decode(update.sync_aggregate.sync_committee_signature.strip_prefix("0x").unwrap())?; - let sig = Signature::from_bytes(&sig_bytes).unwrap(); - let dst: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; - let is_valid_sig = sig.fast_aggregate_verify(true, signing_root.as_bytes(), dst, &pks) == BLST_ERROR::BLST_SUCCESS; - println!("committee signature valid: {}", is_valid_sig); + let sig_bytes = hex_str_to_bytes(&update.sync_aggregate.sync_committee_signature)?; + let is_valid_sig = is_aggregate_valid(sig_bytes, signing_root.as_bytes(), &pks); if !is_valid_sig { return Err(eyre::eyre!("Invalid Update")); @@ -189,24 +167,115 @@ impl LightClient { self.store.next_sync_committee = Some(update.next_sync_committee.as_ref().unwrap().clone()); } } +} - async fn get_bootstrap(rpc: &str, block_root: &str) -> Result { - let req = format!("{}/eth/v0/beacon/light_client/bootstrap/{}", rpc, block_root); - let res = reqwest::get(req).await?.json::().await?; - Ok(res.data.v) +fn get_participating_keys(committee: &SyncCommittee, bitfield: &str) -> Result> { + let bytes = hex_str_to_bytes(bitfield)?; + let mut pks: Vec = Vec::new(); + bytes.iter().enumerate().for_each(|(i, byte)| { + format!("{:08b}", byte).chars().rev().enumerate().for_each(|(j, bit)| { + if bit == '1' { + let index = 8 * i + j; + let pk = &committee.pubkeys[index]; + let pk = PublicKey::from_bytes(&pk).unwrap(); + pks.push(pk); + } + }); + }); + + Ok(pks) +} + +fn is_aggregate_valid(sig_bytes: Vec, msg: &[u8], pks: &[&PublicKey]) -> bool { + let dst: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; + let sig_res = Signature::from_bytes(&sig_bytes); + match sig_res { + Ok(sig) => sig.fast_aggregate_verify(true, msg, dst, &pks) == BLST_ERROR::BLST_SUCCESS, + Err(_) => false, + } +} + +fn is_finality_proof_valid(attested_header: &Header, finality_header: &mut Header, finality_branch: &Vec) -> bool { + let finality_header_hash_res = finality_header.hash_tree_root(); + if finality_header_hash_res.is_err() { + return false; + } + + let attested_header_state_root_res = bytes32_to_node(&attested_header.state_root); + if attested_header_state_root_res.is_err() { + return false; + } + + let finality_branch_res = branch_to_nodes(finality_branch.clone()); + if finality_branch_res.is_err() { + return false; + } + + is_valid_merkle_branch( + &finality_header_hash_res.unwrap(), + finality_branch_res.unwrap().iter(), + 6, + 41, + &attested_header_state_root_res.unwrap() + ) +} + +fn is_next_committee_proof_valid( + attested_header: &Header, + next_committee: &mut SyncCommittee, + next_committee_branch: &Vec +) -> bool { + let next_committee_hash_res = next_committee.hash_tree_root(); + if next_committee_hash_res.is_err() { + return false; } - async fn get_updates(&self, period: u64) -> Result> { - let req = format!("{}/eth/v0/beacon/light_client/updates?start_period={}&count=1000", self.nimbus_rpc, period); - let res = reqwest::get(req).await?.json::().await?; - Ok(res.data) + let attested_header_state_root_res = bytes32_to_node(&attested_header.state_root); + if attested_header_state_root_res.is_err() { + return false; } - async fn get_finality_update(&self) -> Result { - let req = format!("{}/eth/v0/beacon/light_client/finality_update", self.nimbus_rpc); - let res = reqwest::get(req).await?.json::().await?; - Ok(res.data) + let next_committee_branch_res = branch_to_nodes(next_committee_branch.clone()); + if next_committee_branch_res.is_err() { + return false; } + + is_valid_merkle_branch( + &next_committee_hash_res.unwrap(), + next_committee_branch_res.unwrap().iter(), + 5, + 23, + &attested_header_state_root_res.unwrap() + ) +} + +fn is_current_committee_proof_valid( + attested_header: &Header, + current_committee: &mut SyncCommittee, + current_committee_branch: &Vec +) -> bool { + let next_committee_hash_res = current_committee.hash_tree_root(); + if next_committee_hash_res.is_err() { + return false; + } + + let attested_header_state_root_res = bytes32_to_node(&attested_header.state_root); + if attested_header_state_root_res.is_err() { + return false; + } + + let next_committee_branch_res = branch_to_nodes(current_committee_branch.clone()); + if next_committee_branch_res.is_err() { + return false; + } + + is_valid_merkle_branch( + &next_committee_hash_res.unwrap(), + next_committee_branch_res.unwrap().iter(), + 5, + 22, + &attested_header_state_root_res.unwrap() + ) } fn calc_sync_period(slot: u64) -> u64 { @@ -214,12 +283,8 @@ fn calc_sync_period(slot: u64) -> u64 { epoch / 256 } -fn branch_to_nodes(branch: Vec) -> Vec { - branch.iter().map(|elem| Node::from_bytes(*elem)).collect() -} - -fn bytes_to_bytes32(bytes: &[u8]) -> [u8; 32] { - bytes.to_vec().try_into().unwrap() +fn branch_to_nodes(branch: Vec) -> Result> { + branch.iter().map(|elem| bytes32_to_node(elem)).collect::>>() } fn compute_committee_sign_root(header: Bytes32) -> Result { @@ -230,6 +295,18 @@ fn compute_committee_sign_root(header: Bytes32) -> Result { compute_signing_root(header, domain) } +#[derive(SimpleSerialize, Default, Debug)] +struct SigningData { + object_root: Bytes32, + domain: Bytes32 +} + +#[derive(SimpleSerialize, Default, Debug)] +struct ForkData { + current_version: Vector, + genesis_validator_root: Bytes32, +} + fn compute_signing_root(object_root: Bytes32, domain: Bytes32) -> Result { let mut data = SigningData { object_root, domain }; Ok(data.hash_tree_root()?) @@ -249,132 +326,3 @@ fn compute_fork_data_root(current_version: Vector, genesis_validator_root Ok(fork_data.hash_tree_root()?) } -#[derive(SimpleSerialize, Default, Debug)] -struct ForkData { - current_version: Vector, - genesis_validator_root: Bytes32, -} - -#[derive(SimpleSerialize, Default, Debug)] -struct SigningData { - object_root: Bytes32, - domain: Bytes32 -} - -#[derive(serde::Deserialize, Debug)] -struct BootstrapResponse { - data: BootstrapData, -} - -#[derive(serde::Deserialize, Debug)] -struct BootstrapData { - v: Bootstrap, -} - -#[derive(serde::Deserialize, Debug)] -struct Bootstrap { - header: Header, - current_sync_committee: SyncCommittee, - #[serde(deserialize_with = "branch_deserialize")] - current_sync_committee_branch: Vec, -} - -#[derive(Debug, Clone, Default, SimpleSerialize, serde::Deserialize)] -struct SyncCommittee { - #[serde(deserialize_with = "pubkeys_deserialize")] - pubkeys: Vector, - #[serde(deserialize_with = "pubkey_deserialize")] - aggregate_pubkey: BLSPubKey, -} - -type BLSPubKey = Vector; -type Bytes32 = [u8; 32]; - -fn pubkey_deserialize<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de> { - let key: String = serde::Deserialize::deserialize(deserializer)?; - let key_bytes = hex::decode(key.strip_prefix("0x").unwrap()).unwrap(); - Ok(Vector::from_iter(key_bytes)) -} - -fn pubkeys_deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de> { - let keys: Vec = serde::Deserialize::deserialize(deserializer)?; - Ok(keys.iter().map(|key| { - let key_bytes = hex::decode(key.strip_prefix("0x").unwrap()).unwrap(); - Vector::from_iter(key_bytes) - }).collect::>()) -} - -fn branch_deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de> { - let branch: Vec = serde::Deserialize::deserialize(deserializer)?; - Ok(branch.iter().map(|elem| { - let bytes = hex::decode(elem.strip_prefix("0x").unwrap()).unwrap(); - bytes.to_vec().try_into().unwrap() - }).collect()) -} - -#[derive(serde::Deserialize, Debug, Clone, Default, SimpleSerialize)] -struct Header { - #[serde(deserialize_with = "u64_deserialize")] - slot: u64, - #[serde(deserialize_with = "u64_deserialize")] - proposer_index: u64, - #[serde(deserialize_with = "bytes32_deserialize")] - parent_root: Bytes32, - #[serde(deserialize_with = "bytes32_deserialize")] - state_root: Bytes32, - #[serde(deserialize_with = "bytes32_deserialize")] - body_root: Bytes32, -} - -fn u64_deserialize<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de> { - let val: String = serde::Deserialize::deserialize(deserializer)?; - Ok(val.parse().unwrap()) -} - -fn bytes32_deserialize<'de, D>(deserializer: D) -> Result where D: serde::Deserializer<'de> { - let bytes: String = serde::Deserialize::deserialize(deserializer)?; - let bytes = hex::decode(bytes.strip_prefix("0x").unwrap()).unwrap(); - Ok(bytes.to_vec().try_into().unwrap()) -} - -#[derive(serde::Deserialize, Debug)] -struct UpdateResponse { - data: Vec, -} - -#[derive(serde::Deserialize, Debug, Clone)] -struct Update { - attested_header: Header, - next_sync_committee: Option, - #[serde(deserialize_with = "branch_deserialize")] - next_sync_committee_branch: Vec, - finalized_header: Header, - #[serde(deserialize_with = "branch_deserialize")] - finality_branch: Vec, - sync_aggregate: SyncAggregate, - #[serde(deserialize_with = "u64_deserialize")] - signature_slot: u64, -} - -#[derive(serde::Deserialize, Debug, Clone)] -struct SyncAggregate { - sync_committee_bits: String, - sync_committee_signature: String, -} - -#[derive(serde::Deserialize, Debug)] -struct FinalityUpdateResponse { - data: FinalityUpdate, -} - -#[derive(serde::Deserialize, Debug)] -struct FinalityUpdate { - attested_header: Header, - finalized_header: Header, - #[serde(deserialize_with = "branch_deserialize")] - finality_branch: Vec, - sync_aggregate: SyncAggregate, - #[serde(deserialize_with = "u64_deserialize")] - signature_slot: u64, -} - diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..f518147 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,16 @@ +use eyre::Result; +use ssz_rs::{Node, Vector}; +use super::consensus_client::Bytes32; + +pub fn hex_str_to_bytes(s: &str) -> Result> { + let stripped = s.strip_prefix("0x").unwrap_or(s); + Ok(hex::decode(stripped)?) +} + +pub fn bytes32_to_node(bytes: &Bytes32) -> Result { + Ok(Node::from_bytes(bytes.as_slice().try_into()?)) +} + +pub fn bytes_to_bytes32(bytes: &[u8]) -> Bytes32 { + Vector::from_iter(bytes.to_vec()) +}