diff --git a/src/consensus.rs b/src/consensus.rs new file mode 100644 index 0000000..37220e9 --- /dev/null +++ b/src/consensus.rs @@ -0,0 +1,325 @@ +use eyre::Result; +use ssz_rs::prelude::*; +use blst::min_pk::{PublicKey, Signature}; +use blst::BLST_ERROR; + +use super::consensus_rpc::*; +use super::utils::*; + +pub struct ConsensusClient { + consensus_rpc: ConsensusRpc, + store: Store, +} + +#[derive(Debug)] +struct Store { + header: Header, + current_sync_committee: SyncCommittee, + next_sync_committee: Option, +} + +impl ConsensusClient { + pub async fn new(nimbus_rpc: &str, checkpoint_block_root: &str) -> Result { + + let consensus_rpc = ConsensusRpc::new(nimbus_rpc); + + let mut bootstrap = consensus_rpc.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(); + + if !(header_valid && committee_valid) { + return Err(eyre::eyre!("Invalid Bootstrap")); + } + + let store = Store { + header: bootstrap.header, + current_sync_committee: bootstrap.current_sync_committee, + next_sync_committee: None, + }; + + Ok(ConsensusClient { consensus_rpc, store }) + } + + pub async fn get_execution_payload(&mut self) -> Result { + let slot = self.store.header.slot; + let mut block = self.consensus_rpc.get_block(slot).await?; + let block_hash = block.hash_tree_root()?; + let verified_block_hash = self.store.header.hash_tree_root()?; + + if verified_block_hash != block_hash { + Err(eyre::eyre!("Block Root Mismatch")) + } else { + Ok(block.body.execution_payload) + } + } + + pub async fn sync(&mut self) -> Result<()> { + + let current_period = calc_sync_period(self.store.header.slot); + let updates = self.consensus_rpc.get_updates(current_period).await?; + + for mut update in updates { + self.verify_update(&mut update)?; + self.apply_update(&update); + } + + let finality_update = self.consensus_rpc.get_finality_update().await?; + let mut finality_update_generic = Update { + attested_header: finality_update.attested_header, + next_sync_committee: None, + next_sync_committee_branch: Vec::new(), + finalized_header: finality_update.finalized_header, + finality_branch: finality_update.finality_branch, + sync_aggregate: finality_update.sync_aggregate, + signature_slot: finality_update.signature_slot, + }; + + self.verify_update(&mut finality_update_generic)?; + self.apply_update(&finality_update_generic); + + println!("synced up to slot: {}", self.store.header.slot); + + self.consensus_rpc.get_block(self.store.header.slot).await?; + + Ok(()) + } + + fn verify_update(&mut self, update: &mut Update) -> Result<()> { + let current_slot = self.store.header.slot; + let update_slot = update.finalized_header.slot; + + let current_period = calc_sync_period(current_slot); + let update_period = calc_sync_period(update_slot); + + if !(update_period == current_period + 1 || update_period == current_period) { + return Err(eyre::eyre!("Invalid Update")); + } + + if !(update.signature_slot > update.attested_header.slot && update.attested_header.slot > update.finalized_header.slot) { + return Err(eyre::eyre!("Invalid Update")); + } + + 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_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")); + } + } + + let sync_committee = if current_period == update_period { + &self.store.current_sync_committee + } else { + self.store.next_sync_committee.as_ref().unwrap() + }; + + 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 = pks.len() > 1; + 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 = &update.sync_aggregate.sync_committee_signature; + let is_valid_sig = is_aggregate_valid(sig, signing_root.as_bytes(), &pks); + + if !is_valid_sig { + return Err(eyre::eyre!("Invalid Update")); + } + + Ok(()) + } + + fn apply_update(&mut self, update: &Update) { + + let current_period = calc_sync_period(self.store.header.slot); + let update_period = calc_sync_period(update.finalized_header.slot); + + self.store.header = update.finalized_header.clone(); + + if self.store.next_sync_committee.is_none() { + self.store.next_sync_committee = Some(update.next_sync_committee.as_ref().unwrap().clone()); + } else if update_period == current_period + 1 { + self.store.current_sync_committee = self.store.next_sync_committee.as_ref().unwrap().clone(); + self.store.next_sync_committee = Some(update.next_sync_committee.as_ref().unwrap().clone()); + } + } +} + +fn get_participating_keys(committee: &SyncCommittee, bitfield: &Bitvector<512>) -> Result> { + let mut pks: Vec = Vec::new(); + bitfield.iter().enumerate().for_each(|(i, bit)| { + if bit == true { + let pk = &committee.pubkeys[i]; + let pk = PublicKey::from_bytes(&pk).unwrap(); + pks.push(pk); + } + }); + + Ok(pks) +} + +fn is_aggregate_valid(sig_bytes: &SignatureBytes, 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; + } + + 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(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 { + let epoch = slot / 32; + epoch / 256 +} + +fn branch_to_nodes(branch: Vec) -> Result> { + branch.iter().map(|elem| bytes32_to_node(elem)).collect::>>() +} + +fn compute_committee_sign_root(header: Bytes32) -> Result { + let genesis_root = hex::decode("043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb")?.to_vec().try_into().unwrap(); + let domain_type = &hex::decode("07000000")?[..]; + let fork_version = Vector::from_iter(hex::decode("02001020").unwrap()); + let domain = compute_domain(domain_type, fork_version, genesis_root)?; + 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()?) +} + +fn compute_domain(domain_type: &[u8], fork_version: Vector, genesis_root: Bytes32) -> Result { + let fork_data_root = compute_fork_data_root(fork_version, genesis_root)?; + let start = domain_type; + let end = &fork_data_root.as_bytes()[..28]; + let d = [start, end].concat(); + Ok(d.to_vec().try_into().unwrap()) +} + +fn compute_fork_data_root(current_version: Vector, genesis_validator_root: Bytes32) -> Result { + let current_version = current_version.try_into()?; + let mut fork_data = ForkData { current_version, genesis_validator_root }; + Ok(fork_data.hash_tree_root()?) +} + diff --git a/src/consensus_client.rs b/src/consensus_rpc.rs similarity index 99% rename from src/consensus_client.rs rename to src/consensus_rpc.rs index 8ab00b7..026635f 100644 --- a/src/consensus_client.rs +++ b/src/consensus_rpc.rs @@ -3,13 +3,13 @@ use ssz_rs::prelude::*; use super::utils::*; use serde::de::Error; -pub struct ConsensusClient { +pub struct ConsensusRpc { rpc: String, } -impl ConsensusClient { +impl ConsensusRpc { pub fn new(rpc: &str) -> Self { - ConsensusClient { rpc: rpc.to_string() } + ConsensusRpc { rpc: rpc.to_string() } } pub async fn get_bootstrap(&self, block_root: &str) -> Result { diff --git a/src/main.rs b/src/main.rs index e48b1ed..33ce23e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,348 +1,24 @@ use eyre::Result; -use ssz_rs::prelude::*; -use blst::{min_pk::*, BLST_ERROR}; -use consensus_client::*; -use utils::*; -use proof::*; -pub mod consensus_client; +use consensus::*; + +pub mod consensus; +pub mod consensus_rpc; pub mod utils; pub mod proof; #[tokio::main] async fn main() -> Result<()> { - // let mut client = LightClient::new( - // "http://testing.prater.beacon-api.nimbus.team", - // "0x172128eadf1da46467f4d6a822206698e2d3f957af117dd650954780d680dc99" - // ).await?; - // - // client.sync().await?; + let rpc = "http://testing.prater.beacon-api.nimbus.team"; + let checkpoint = "0x172128eadf1da46467f4d6a822206698e2d3f957af117dd650954780d680dc99"; + let mut client = ConsensusClient::new(rpc, checkpoint).await?; + + client.sync().await?; - // let payload = client.get_execution_payload().await?; - // println!("verified execution block hash: {}", hex::encode(payload.block_hash)); - - let proof = get_proof("0x88A83e0F661447576fb1f50e5d90d571E379cd72", 15365981).await?; - let is_valid = verify(&proof); - println!("{:?}", is_valid); + let payload = client.get_execution_payload().await?; + println!("verified execution block hash: {}", hex::encode(payload.block_hash)); Ok(()) } -struct LightClient { - consensus_client: ConsensusClient, - store: Store, -} - -#[derive(Debug)] -struct Store { - header: Header, - current_sync_committee: SyncCommittee, - next_sync_committee: Option, -} - -impl LightClient { - async fn new(nimbus_rpc: &str, checkpoint_block_root: &str) -> Result { - - let consensus_client = ConsensusClient::new(nimbus_rpc); - - 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(); - - if !(header_valid && committee_valid) { - return Err(eyre::eyre!("Invalid Bootstrap")); - } - - let store = Store { - header: bootstrap.header, - current_sync_committee: bootstrap.current_sync_committee, - next_sync_committee: None, - }; - - Ok(LightClient { consensus_client, store }) - } - - async fn get_execution_payload(&mut self) -> Result { - let slot = self.store.header.slot; - let mut block = self.consensus_client.get_block(slot).await?; - let block_hash = block.hash_tree_root()?; - let verified_block_hash = self.store.header.hash_tree_root()?; - - if verified_block_hash != block_hash { - Err(eyre::eyre!("Block Root Mismatch")) - } else { - Ok(block.body.execution_payload) - } - } - - async fn sync(&mut self) -> Result<()> { - - let current_period = calc_sync_period(self.store.header.slot); - let updates = self.consensus_client.get_updates(current_period).await?; - - for mut update in updates { - self.verify_update(&mut update)?; - self.apply_update(&update); - } - - 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, - next_sync_committee_branch: Vec::new(), - finalized_header: finality_update.finalized_header, - finality_branch: finality_update.finality_branch, - sync_aggregate: finality_update.sync_aggregate, - signature_slot: finality_update.signature_slot, - }; - - self.verify_update(&mut finality_update_generic)?; - self.apply_update(&finality_update_generic); - - println!("synced up to slot: {}", self.store.header.slot); - - self.consensus_client.get_block(self.store.header.slot).await?; - - Ok(()) - } - - fn verify_update(&mut self, update: &mut Update) -> Result<()> { - let current_slot = self.store.header.slot; - let update_slot = update.finalized_header.slot; - - let current_period = calc_sync_period(current_slot); - let update_period = calc_sync_period(update_slot); - - if !(update_period == current_period + 1 || update_period == current_period) { - return Err(eyre::eyre!("Invalid Update")); - } - - if !(update.signature_slot > update.attested_header.slot && update.attested_header.slot > update.finalized_header.slot) { - return Err(eyre::eyre!("Invalid Update")); - } - - 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_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")); - } - } - - let sync_committee = if current_period == update_period { - &self.store.current_sync_committee - } else { - self.store.next_sync_committee.as_ref().unwrap() - }; - - 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 = pks.len() > 1; - 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 = &update.sync_aggregate.sync_committee_signature; - let is_valid_sig = is_aggregate_valid(sig, signing_root.as_bytes(), &pks); - - if !is_valid_sig { - return Err(eyre::eyre!("Invalid Update")); - } - - Ok(()) - } - - fn apply_update(&mut self, update: &Update) { - - let current_period = calc_sync_period(self.store.header.slot); - let update_period = calc_sync_period(update.finalized_header.slot); - - self.store.header = update.finalized_header.clone(); - - if self.store.next_sync_committee.is_none() { - self.store.next_sync_committee = Some(update.next_sync_committee.as_ref().unwrap().clone()); - } else if update_period == current_period + 1 { - self.store.current_sync_committee = self.store.next_sync_committee.as_ref().unwrap().clone(); - self.store.next_sync_committee = Some(update.next_sync_committee.as_ref().unwrap().clone()); - } - } -} - -fn get_participating_keys(committee: &SyncCommittee, bitfield: &Bitvector<512>) -> Result> { - let mut pks: Vec = Vec::new(); - bitfield.iter().enumerate().for_each(|(i, bit)| { - if bit == true { - let pk = &committee.pubkeys[i]; - let pk = PublicKey::from_bytes(&pk).unwrap(); - pks.push(pk); - } - }); - - Ok(pks) -} - -fn is_aggregate_valid(sig_bytes: &SignatureBytes, 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; - } - - 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(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 { - let epoch = slot / 32; - epoch / 256 -} - -fn branch_to_nodes(branch: Vec) -> Result> { - branch.iter().map(|elem| bytes32_to_node(elem)).collect::>>() -} - -fn compute_committee_sign_root(header: Bytes32) -> Result { - let genesis_root = hex::decode("043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb")?.to_vec().try_into().unwrap(); - let domain_type = &hex::decode("07000000")?[..]; - let fork_version = Vector::from_iter(hex::decode("02001020").unwrap()); - let domain = compute_domain(domain_type, fork_version, genesis_root)?; - 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()?) -} - -fn compute_domain(domain_type: &[u8], fork_version: Vector, genesis_root: Bytes32) -> Result { - let fork_data_root = compute_fork_data_root(fork_version, genesis_root)?; - let start = domain_type; - let end = &fork_data_root.as_bytes()[..28]; - let d = [start, end].concat(); - Ok(d.to_vec().try_into().unwrap()) -} - -fn compute_fork_data_root(current_version: Vector, genesis_validator_root: Bytes32) -> Result { - let current_version = current_version.try_into()?; - let mut fork_data = ForkData { current_version, genesis_validator_root }; - Ok(fork_data.hash_tree_root()?) -} - diff --git a/src/proof.rs b/src/proof.rs index a2eaeb0..7ca20dd 100644 --- a/src/proof.rs +++ b/src/proof.rs @@ -21,11 +21,11 @@ pub fn verify_proof(proof: &Vec>, root: &Vec, path: &Vec, value: // let state_root = hex_str_to_bytes("0x1d006918a3fef7ff7c843f20747c757a38a0a13fe7723f53e349f462c2cfdd71").unwrap(); // let path = keccak256(proof.address).to_vec(); - let mut expected_hash = root; + let mut expected_hash = root.clone(); let mut path_offset = 0; for (i, node) in proof.iter().enumerate() { - if expected_hash != &keccak256(node).to_vec() { + if expected_hash != keccak256(node).to_vec() { return false; } @@ -34,7 +34,7 @@ pub fn verify_proof(proof: &Vec>, root: &Vec, path: &Vec, value: if node_list.len() == 17 { let nibble = get_nibble(&path, path_offset); - expected_hash = &node_list[nibble as usize].clone(); + expected_hash = node_list[nibble as usize].clone(); path_offset += 1; @@ -44,7 +44,7 @@ pub fn verify_proof(proof: &Vec>, root: &Vec, path: &Vec, value: return false; } } else { - expected_hash = &node_list[1].clone(); + expected_hash = node_list[1].clone(); } } else { return false; diff --git a/src/utils.rs b/src/utils.rs index f518147..cbc85db 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,6 @@ use eyre::Result; use ssz_rs::{Node, Vector}; -use super::consensus_client::Bytes32; +use super::consensus_rpc::Bytes32; pub fn hex_str_to_bytes(s: &str) -> Result> { let stripped = s.strip_prefix("0x").unwrap_or(s);