From ca4687a725f39225e8abb5a219aedae1331375a0 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Fri, 9 Aug 2024 12:11:45 +0000 Subject: [PATCH 01/43] support lighthouse 5.3 --- ...ghthouse_5_2.yml => ci_lighthouse_5_3.yml} | 2 +- Cargo.lock | 134 ++-- Cargo.toml | 4 +- lighthouse | 2 +- src/node/node.rs | 2 + .../account_utils/validator_definitions.rs | 67 +- src/validation/attestation_service.rs | 198 ++++-- src/validation/block_service.rs | 444 ++---------- src/validation/config.rs | 35 +- src/validation/http_api/api_secret.rs | 153 +--- src/validation/http_api/create_validator.rs | 6 +- src/validation/http_api/graffiti.rs | 80 +++ src/validation/http_api/keystores.rs | 110 +-- src/validation/http_api/mod.rs | 651 +++++++----------- src/validation/http_api/remotekeys.rs | 32 +- src/validation/initialized_validators.rs | 106 +++ src/validation/mod.rs | 24 +- src/validation/signing_method.rs | 17 +- src/validation/validator_store.rs | 123 +++- 19 files changed, 949 insertions(+), 1241 deletions(-) rename .github/workflows/{ci_lighthouse_5_2.yml => ci_lighthouse_5_3.yml} (98%) create mode 100644 src/validation/http_api/graffiti.rs diff --git a/.github/workflows/ci_lighthouse_5_2.yml b/.github/workflows/ci_lighthouse_5_3.yml similarity index 98% rename from .github/workflows/ci_lighthouse_5_2.yml rename to .github/workflows/ci_lighthouse_5_3.yml index 13a66c5a..df73eb05 100644 --- a/.github/workflows/ci_lighthouse_5_2.yml +++ b/.github/workflows/ci_lighthouse_5_3.yml @@ -12,7 +12,7 @@ name: UnitTest & Publish Image on: push: branches: - - lighthouse-5.2 + - lighthouse-5.3 jobs: push_to_registry: diff --git a/Cargo.lock b/Cargo.lock index d4e9e216..bcd04558 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -167,54 +167,35 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "alloy-consensus" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy.git?rev=974d488bab5e21e9f17452a39a4bfa56677367b2#974d488bab5e21e9f17452a39a4bfa56677367b2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c309895995eaa4bfcc345f5515a39c7df9447798645cc8bf462b6c5bf1dc96" dependencies = [ "alloy-eips", - "alloy-network", "alloy-primitives", "alloy-rlp", + "c-kzg", ] [[package]] name = "alloy-eips" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy.git?rev=974d488bab5e21e9f17452a39a4bfa56677367b2#974d488bab5e21e9f17452a39a4bfa56677367b2" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "serde", - "thiserror", -] - -[[package]] -name = "alloy-json-rpc" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy.git?rev=974d488bab5e21e9f17452a39a4bfa56677367b2#974d488bab5e21e9f17452a39a4bfa56677367b2" -dependencies = [ - "alloy-primitives", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "alloy-network" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy.git?rev=974d488bab5e21e9f17452a39a4bfa56677367b2#974d488bab5e21e9f17452a39a4bfa56677367b2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9431c99a3b3fe606ede4b3d4043bdfbcb780c45b8d8d226c3804e2b75cfbe68" dependencies = [ - "alloy-eips", - "alloy-json-rpc", "alloy-primitives", "alloy-rlp", + "c-kzg", + "once_cell", "serde", + "sha2 0.10.8", ] [[package]] name = "alloy-primitives" -version = "0.6.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600d34d8de81e23b6d909c094e23b3d357e01ca36b78a8c5424c501eedbe86f0" +checksum = "ccb3ead547f4532bc8af961649942f0b9c16ee9226e26caa3f38420651cc0bf4" dependencies = [ "alloy-rlp", "bytes", @@ -803,7 +784,7 @@ dependencies = [ [[package]] name = "beacon_node" -version = "5.2.1" +version = "5.3.0" dependencies = [ "beacon_chain", "clap", @@ -1005,7 +986,7 @@ dependencies = [ [[package]] name = "boot_node" -version = "5.2.1" +version = "5.3.0" dependencies = [ "beacon_node", "clap", @@ -1110,19 +1091,7 @@ dependencies = [ "glob", "hex", "libc", -] - -[[package]] -name = "cached_tree_hash" -version = "0.1.0" -dependencies = [ - "ethereum-types 0.14.1", - "ethereum_hashing", - "ethereum_ssz", - "ethereum_ssz_derive", - "smallvec", - "ssz_types", - "tree_hash", + "serde", ] [[package]] @@ -1233,6 +1202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -1248,6 +1218,18 @@ dependencies = [ "terminal_size", ] +[[package]] +name = "clap_derive" +version = "4.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "clap_lex" version = "0.7.1" @@ -1725,6 +1707,7 @@ dependencies = [ "clap_utils", "environment", "hex", + "serde", "slog", "store 0.2.0", "strum", @@ -1924,7 +1907,7 @@ dependencies = [ "enr", "fnv", "futures", - "hashlink", + "hashlink 0.8.4", "hex", "hkdf", "lazy_static", @@ -2016,6 +1999,7 @@ dependencies = [ "lighthouse_version", "lockfile", "log", + "logging", "malloc_utils", "mempool", "miracl_core", @@ -2049,6 +2033,8 @@ dependencies = [ "store 0.1.0", "strum", "subtle", + "sysinfo", + "system_health", "task_executor", "tempfile", "tokio", @@ -2659,6 +2645,7 @@ name = "execution_layer" version = "0.1.0" dependencies = [ "alloy-consensus", + "alloy-primitives", "alloy-rlp", "arc-swap", "builder_client", @@ -3150,6 +3137,7 @@ dependencies = [ "futures-ticker", "futures-timer", "getrandom 0.2.15", + "hashlink 0.9.1", "hex_fmt", "libp2p", "prometheus-client", @@ -3244,6 +3232,15 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "headers" version = "0.3.9" @@ -3300,9 +3297,6 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] [[package]] name = "hex-literal" @@ -4703,6 +4697,7 @@ dependencies = [ "futures", "gossipsub", "hex", + "itertools", "lazy_static", "libp2p", "libp2p-mplex", @@ -4765,27 +4760,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" -[[package]] -name = "lmdb-rkv" -version = "0.14.0" -source = "git+https://github.com/sigp/lmdb-rs?rev=f33845c6469b94265319aac0ed5085597862c27e#f33845c6469b94265319aac0ed5085597862c27e" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "libc", - "lmdb-rkv-sys", -] - -[[package]] -name = "lmdb-rkv-sys" -version = "0.11.2" -source = "git+https://github.com/sigp/lmdb-rs?rev=f33845c6469b94265319aac0ed5085597862c27e#f33845c6469b94265319aac0ed5085597862c27e" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "lock_api" version = "0.4.12" @@ -6680,7 +6654,7 @@ dependencies = [ "bitflags 1.3.2", "fallible-iterator", "fallible-streaming-iterator", - "hashlink", + "hashlink 0.8.4", "libsqlite3-sys", "smallvec", ] @@ -7342,20 +7316,20 @@ version = "0.1.0" dependencies = [ "bincode", "byteorder", + "derivative", "ethereum_ssz", "ethereum_ssz_derive", "filesystem", "flate2", "lazy_static", "lighthouse_metrics", - "lmdb-rkv", - "lmdb-rkv-sys", "lru", "parking_lot 0.12.3", "rand 0.8.5", "safe_arith", "serde", "slog", + "ssz_types", "strum", "tree_hash", "tree_hash_derive", @@ -7512,6 +7486,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "arbitrary", +] [[package]] name = "snap" @@ -7643,10 +7620,12 @@ dependencies = [ "lazy_static", "lighthouse_metrics", "merkle_proof", + "rand 0.8.5", "rayon", "safe_arith", "smallvec", "ssz_types", + "test_random_derive", "tree_hash", "types", ] @@ -7731,9 +7710,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "superstruct" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f4e1f478a7728f8855d7e620e9a152cf8932c6614f86564c886f9b8141f3201" +checksum = "bf0f31f730ad9e579364950e10d6172b4a9bd04b447edf5988b066a860cc340e" dependencies = [ "darling", "itertools", @@ -8395,9 +8374,10 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" name = "types" version = "0.2.1" dependencies = [ + "alloy-primitives", + "alloy-rlp", "arbitrary", "bls", - "cached_tree_hash", "compare_fields", "compare_fields_derive", "derivative", diff --git a/Cargo.toml b/Cargo.toml index 89f01ecd..5957806d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,7 +102,8 @@ task_executor = { path = "lighthouse/common/task_executor" } types = { path = "lighthouse/consensus/types" } validator_dir = { path = "lighthouse/common/validator_dir", features = ["insecure_keys"] } warp_utils = { path = "lighthouse/common/warp_utils" } - +logging = { path = "lighthouse/common/logging" } +system_health = { path = "lighthouse/common/system_health" } consensus = { path = "hotstuff/consensus" } hsconfig = { path = "hotstuff/config", package = "hotstuff_config" } hscrypto = { path = "hotstuff/crypto", package = "crypto" } @@ -110,6 +111,7 @@ hsutils = { path = "hotstuff/utils", package = "utils" } mempool = { path = "hotstuff/mempool" } network = { path = "hotstuff/network" } store = { path = "hotstuff/store" } +sysinfo = "0.26" [patch] [patch.crates-io] diff --git a/lighthouse b/lighthouse index 9e12c21f..d6ba8c39 160000 --- a/lighthouse +++ b/lighthouse @@ -1 +1 @@ -Subproject commit 9e12c21f268c80a3f002ae0ca27477f9f512eb6f +Subproject commit d6ba8c397557f5c977b70f0d822a9228e98ca214 diff --git a/src/node/node.rs b/src/node/node.rs index b92af2cd..4aa10656 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -644,6 +644,8 @@ pub async fn add_validator( Some(validator.owner_address), None, None, + None, + None, committee_def_path, keystore_share.master_id, keystore_share.share_id, diff --git a/src/validation/account_utils/validator_definitions.rs b/src/validation/account_utils/validator_definitions.rs index d90be4e8..3340a12c 100644 --- a/src/validation/account_utils/validator_definitions.rs +++ b/src/validation/account_utils/validator_definitions.rs @@ -48,6 +48,31 @@ pub enum Error { UnableToOpenKeystore(eth2_keystore::Error), /// The validator directory could not be created. UnableToCreateValidatorDir(PathBuf), + UnableToReadKeystorePassword(String), + KeystoreWithoutPassword, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize, Hash, Eq)] +pub struct Web3SignerDefinition { + pub url: String, + /// Path to a .pem file. + #[serde(skip_serializing_if = "Option::is_none")] + pub root_certificate_path: Option, + /// Specifies a request timeout. + /// + /// The timeout is applied from when the request starts connecting until the response body has finished. + #[serde(skip_serializing_if = "Option::is_none")] + pub request_timeout_ms: Option, + + /// Path to a PKCS12 file. + #[serde(skip_serializing_if = "Option::is_none")] + pub client_identity_path: Option, + + /// Password for the PKCS12 file. + /// + /// An empty password will be used if this is omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub client_identity_password: Option, } /// Defines how the validator client should attempt to sign messages for this validator. @@ -63,31 +88,11 @@ pub enum SigningDefinition { #[serde(skip_serializing_if = "Option::is_none")] voting_keystore_password: Option, }, - /// A validator that defers to a Web3Signer HTTP server for signing. + /// A validator that defers to a Web3Signer HTTP server for signing. /// /// https://github.com/ConsenSys/web3signer #[serde(rename = "web3signer")] - Web3Signer { - url: String, - /// Path to a .pem file. - #[serde(skip_serializing_if = "Option::is_none")] - root_certificate_path: Option, - /// Specifies a request timeout. - /// - /// The timeout is applied from when the request starts connecting until the response body has finished. - #[serde(skip_serializing_if = "Option::is_none")] - request_timeout_ms: Option, - - /// Path to a PKCS12 file. - #[serde(skip_serializing_if = "Option::is_none")] - client_identity_path: Option, - - /// Password for the PKCS12 file. - /// - /// An empty password will be used if this is omitted. - #[serde(skip_serializing_if = "Option::is_none")] - client_identity_password: Option, - }, + Web3Signer(Web3SignerDefinition), /// A validator whose key is distributed among a set of operators. #[serde(rename = "distributed_keystore")] DistributedKeystore { @@ -134,6 +139,12 @@ pub struct ValidatorDefinition { #[serde(skip_serializing_if = "Option::is_none")] pub builder_proposals: Option, #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub builder_boost_factor: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub prefer_builder_proposals: Option, + #[serde(default)] pub description: String, #[serde(flatten)] pub signing_definition: SigningDefinition, @@ -153,6 +164,8 @@ impl ValidatorDefinition { suggested_fee_recipient: Option
, gas_limit: Option, builder_proposals: Option, + builder_boost_factor: Option, + prefer_builder_proposals: Option, ) -> Result { let voting_keystore_path = voting_keystore_path.as_ref().into(); let keystore = @@ -167,6 +180,8 @@ impl ValidatorDefinition { suggested_fee_recipient, gas_limit, builder_proposals, + builder_boost_factor, + prefer_builder_proposals, signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, voting_keystore_password_path: None, @@ -188,6 +203,8 @@ impl ValidatorDefinition { suggested_fee_recipient: Option
, gas_limit: Option, builder_proposals: Option, + builder_boost_factor: Option, + prefer_builder_proposals: Option, operator_committee_definition_path: P, operator_committee_index: u64, operator_id: u64, @@ -209,6 +226,8 @@ impl ValidatorDefinition { suggested_fee_recipient, gas_limit, builder_proposals, + builder_boost_factor, + prefer_builder_proposals, signing_definition: SigningDefinition::DistributedKeystore { voting_keystore_share_path, voting_keystore_share_password_path: Some( @@ -366,6 +385,8 @@ impl ValidatorDefinitions { suggested_fee_recipient: None, gas_limit: None, builder_proposals: None, + builder_boost_factor: None, + prefer_builder_proposals: None, signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, voting_keystore_password_path, @@ -489,6 +510,8 @@ impl ValidatorDefinitions { suggested_fee_recipient: None, gas_limit: None, builder_proposals: None, + builder_boost_factor: None, + prefer_builder_proposals: None, signing_definition: SigningDefinition::DistributedKeystore { voting_keystore_share_path, voting_keystore_share_password_path, diff --git a/src/validation/attestation_service.rs b/src/validation/attestation_service.rs index d4c17155..bf8991a7 100644 --- a/src/validation/attestation_service.rs +++ b/src/validation/attestation_service.rs @@ -1,18 +1,18 @@ //! Reference: lighthouse/validator_client/attestation_service.rs -use crate::validation::beacon_node_fallback::{BeaconNodeFallback, RequireSynced}; +use crate::validation::beacon_node_fallback::{ApiTopic, BeaconNodeFallback, RequireSynced}; use crate::validation::signing_method::Error as SigningError; use crate::validation::{ duties_service::{DutiesService, DutyAndProof}, http_metrics::metrics, - validator_store::Error as VSError, + validator_store::Error as ValidatorStoreError, validator_store::ValidatorStore, OfflineOnFailure, }; use environment::RuntimeContext; use futures::executor::block_on; use futures::future::join_all; -use slog::{crit, error, info, trace}; +use slog::{crit, error, info, trace, warn, debug}; use slot_clock::SlotClock; use std::collections::HashMap; use std::ops::Deref; @@ -20,7 +20,7 @@ use std::sync::Arc; use tokio::time::{sleep, sleep_until, Duration, Instant}; use tree_hash::TreeHash; use types::{ - AggregateSignature, Attestation, AttestationData, BitList, ChainSpec, CommitteeIndex, EthSpec, + Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Slot, }; @@ -294,7 +294,7 @@ impl AttestationService { // Then download, sign and publish a `SignedAggregateAndProof` for each // validator that is elected to aggregate for this `slot` and // `committee_index`. - self.produce_and_publish_aggregates(&attestation_data, &validator_duties) + self.produce_and_publish_aggregates(&attestation_data, committee_index, &validator_duties) .await .map_err(move |e| { crit!( @@ -390,10 +390,26 @@ impl AttestationService { return None; } - let mut attestation = Attestation { - aggregation_bits: BitList::with_capacity(duty.committee_length as usize).unwrap(), - data: attestation_data.clone(), - signature: AggregateSignature::infinity(), + let mut attestation = match Attestation::::empty_for_signing( + duty.committee_index, + duty.committee_length as usize, + attestation_data.slot, + attestation_data.beacon_block_root, + attestation_data.source, + attestation_data.target, + &self.context.eth2_config.spec, + ) { + Ok(attestation) => attestation, + Err(err) => { + crit!( + log, + "Invalid validator duties during signing"; + "validator" => ?duty.pubkey, + "duty" => ?duty, + "err" => ?err, + ); + return None; + } }; match self @@ -406,8 +422,22 @@ impl AttestationService { ) .await { - Ok(()) => Some(attestation), - Err(VSError::UnableToSign(SigningError::NotLeader)) => None, + Ok(()) => Some((attestation, duty.validator_index)), + Err(ValidatorStoreError::UnableToSign(SigningError::NotLeader)) => None, + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + warn!( + log, + "Missing pubkey for attestation"; + "info" => "a validator may have recently been removed from this VC", + "pubkey" => ?pubkey, + "validator" => ?duty.pubkey, + "committee_index" => committee_index, + "slot" => slot.as_u64(), + ); + None + } Err(e) => { crit!( log, @@ -421,41 +451,45 @@ impl AttestationService { } }); - // Execute all the futures in parallel, collecting any successful results. - let attestations = &join_all(signing_futures) + let (ref attestations, ref validator_indices): (Vec<_>, Vec<_>) = join_all(signing_futures) .await .into_iter() .flatten() - .collect::>>(); - - info!( - log, - "Signed attestation duty"; - "slot" => slot.as_u64(), - "slot's epoch" => slot.epoch(E::slots_per_epoch()).as_u64(), - "current epoch" => current_epoch.as_u64(), - "duties" => attestations.len(), - ); + .unzip(); // No need to further process. This can happen quite often for non-leader operators. if attestations.is_empty() { + warn!(log, "No attestations were published"); return Ok(None); } + let fork_name = self + .context + .eth2_config + .spec + .fork_name_at_slot::(attestation_data.slot); + // Post the attestations to the BN. match self .beacon_nodes - .first_success( + .request( RequireSynced::No, OfflineOnFailure::Yes, + ApiTopic::Attestations, |beacon_node| async move { let _timer = metrics::start_timer_vec( &metrics::ATTESTATION_SERVICE_TIMES, &[metrics::ATTESTATIONS_HTTP_POST], ); - beacon_node - .post_beacon_pool_attestations(attestations) - .await + if fork_name.electra_enabled() { + beacon_node + .post_beacon_pool_attestations_v2(attestations, fork_name) + .await + } else { + beacon_node + .post_beacon_pool_attestations_v1(attestations) + .await + } }, ) .await @@ -464,9 +498,12 @@ impl AttestationService { log, "Successfully published attestations"; "count" => attestations.len(), + "validator_indices" => ?validator_indices, "head_block" => ?attestation_data.beacon_block_root, "committee_index" => attestation_data.index, "slot" => attestation_data.slot.as_u64(), + "slot's epoch" => slot.epoch(E::slots_per_epoch()).as_u64(), + "current epoch" => current_epoch.as_u64(), "type" => "unaggregated", ), Err(e) => error!( @@ -498,10 +535,26 @@ impl AttestationService { async fn produce_and_publish_aggregates( &self, attestation_data: &AttestationData, + committee_index: CommitteeIndex, validator_duties: &[DutyAndProof], ) -> Result<(), String> { let log = self.context.log(); + if !validator_duties + .iter() + .any(|duty_and_proof| duty_and_proof.selection_proof.is_some()) + { + // Exit early if no validator is aggregator + return Ok(()); + } + + let fork_name = self + .context + .eth2_config + .spec + .fork_name_at_slot::(attestation_data.slot); + + let aggregated_attestation = &self .beacon_nodes .first_success( @@ -512,17 +565,36 @@ impl AttestationService { &metrics::ATTESTATION_SERVICE_TIMES, &[metrics::AGGREGATES_HTTP_GET], ); - beacon_node - .get_validator_aggregate_attestation( - attestation_data.slot, - attestation_data.tree_hash_root(), - ) - .await - .map_err(|e| { - format!("Failed to produce an aggregate attestation: {:?}", e) - })? - .ok_or_else(|| format!("No aggregate available for {:?}", attestation_data)) - .map(|result| result.data) + if fork_name.electra_enabled() { + beacon_node + .get_validator_aggregate_attestation_v2( + attestation_data.slot, + attestation_data.tree_hash_root(), + committee_index, + ) + .await + .map_err(|e| { + format!("Failed to produce an aggregate attestation: {:?}", e) + })? + .ok_or_else(|| { + format!("No aggregate available for {:?}", attestation_data) + }) + .map(|result| result.data) + } else { + beacon_node + .get_validator_aggregate_attestation_v1( + attestation_data.slot, + attestation_data.tree_hash_root(), + ) + .await + .map_err(|e| { + format!("Failed to produce an aggregate attestation: {:?}", e) + })? + .ok_or_else(|| { + format!("No aggregate available for {:?}", attestation_data) + }) + .map(|result| result.data) + } }, ) .await @@ -552,7 +624,17 @@ impl AttestationService { .await { Ok(aggregate) => Some(aggregate), - Err(VSError::UnableToSign(SigningError::NotLeader)) => None, + Err(ValidatorStoreError::UnableToSign(SigningError::NotLeader)) => None, + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for aggregate"; + "pubkey" => ?pubkey, + ); + None + } Err(e) => { crit!( log, @@ -584,37 +666,49 @@ impl AttestationService { &metrics::ATTESTATION_SERVICE_TIMES, &[metrics::AGGREGATES_HTTP_POST], ); - beacon_node - .post_validator_aggregate_and_proof(signed_aggregate_and_proofs_slice) - .await + if fork_name.electra_enabled() { + beacon_node + .post_validator_aggregate_and_proof_v2( + signed_aggregate_and_proofs_slice, + fork_name, + ) + .await + } else { + beacon_node + .post_validator_aggregate_and_proof_v1( + signed_aggregate_and_proofs_slice, + ) + .await + } }, ) .await { Ok(()) => { for signed_aggregate_and_proof in signed_aggregate_and_proofs { - let attestation = &signed_aggregate_and_proof.message.aggregate; + let attestation = signed_aggregate_and_proof.message().aggregate(); info!( log, - "Successfully published attestations"; - "aggregator" => signed_aggregate_and_proof.message.aggregator_index, - "signatures" => attestation.aggregation_bits.num_set_bits(), - "head_block" => format!("{:?}", attestation.data.beacon_block_root), - "committee_index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), + "Successfully published attestation"; + "aggregator" => signed_aggregate_and_proof.message().aggregator_index(), + "signatures" => attestation.num_set_aggregation_bits(), + "head_block" => format!("{:?}", attestation.data().beacon_block_root), + "committee_index" => attestation.committee_index(), + "slot" => attestation.data().slot.as_u64(), "type" => "aggregated", ); } } Err(e) => { for signed_aggregate_and_proof in signed_aggregate_and_proofs { - let attestation = &signed_aggregate_and_proof.message.aggregate; + let attestation = &signed_aggregate_and_proof.message().aggregate(); crit!( log, "Failed to publish attestation"; "error" => %e, - "committee_index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), + "aggregator" => signed_aggregate_and_proof.message().aggregator_index(), + "committee_index" => attestation.committee_index(), + "slot" => attestation.data().slot.as_u64(), "type" => "aggregated", ); } diff --git a/src/validation/block_service.rs b/src/validation/block_service.rs index e1ff3db5..5b76f9c7 100644 --- a/src/validation/block_service.rs +++ b/src/validation/block_service.rs @@ -323,223 +323,33 @@ impl BlockService { ) } - if self.validator_store.produce_block_v3() { - for validator_pubkey in proposers { - let builder_proposals = self - .validator_store - .get_builder_proposals(&validator_pubkey) - .await; - // Translate `builder_proposals` to a boost factor. Builder proposals set to `true` - // requires no boost factor, it just means "use a builder proposal if the BN returns - // one". On the contrary, `builder_proposals: false` indicates a preference for - // local payloads, so we set the builder boost factor to 0. - let builder_boost_factor = if !builder_proposals { Some(0) } else { None }; - let service = self.clone(); - let log = log.clone(); - self.inner.context.executor.spawn( - async move { - let result = service - .publish_block_v3(slot, validator_pubkey, builder_boost_factor) - .await; - - match result { - Ok(_) => {} - Err(BlockError::Recoverable(e)) | Err(BlockError::Irrecoverable(e)) => { - error!( - log, - "Error whilst producing block"; - "error" => ?e, - "block_slot" => ?slot, - "info" => "block v3 proposal failed, this error may or may not result in a missed block" - ); - } + for validator_pubkey in proposers { + let builder_boost_factor = self.get_builder_boost_factor(&validator_pubkey).await; + let service = self.clone(); + let log = log.clone(); + self.inner.context.executor.spawn( + async move { + let result = service + .publish_block(slot, validator_pubkey, builder_boost_factor) + .await; + + match result { + Ok(_) => {} + Err(BlockError::Recoverable(e)) | Err(BlockError::Irrecoverable(e)) => { + error!( + log, + "Error whilst producing block"; + "error" => ?e, + "block_slot" => ?slot, + "info" => "block v3 proposal failed, this error may or may not result in a missed block" + ); } - }, - "block service", - ) - } - } else { - for validator_pubkey in proposers { - let builder_proposals = self - .validator_store - .get_builder_proposals(&validator_pubkey) - .await; - let service = self.clone(); - let log = log.clone(); - self.inner.context.executor.spawn( - async move { - if builder_proposals { - let result = service - .publish_block(slot, validator_pubkey, true) - .await; - - match result { - Err(BlockError::Recoverable(e)) => { - error!( - log, - "Error whilst producing block"; - "error" => ?e, - "block_slot" => ?slot, - "info" => "blinded proposal failed, attempting full block" - ); - if let Err(e) = service - .publish_block(slot, validator_pubkey, false) - .await - { - // Log a `crit` since a full block - // (non-builder) proposal failed. - crit!( - log, - "Error whilst producing block"; - "error" => ?e, - "block_slot" => ?slot, - "info" => "full block attempted after a blinded failure", - ); - } - } - Err(BlockError::Irrecoverable(e)) => { - // Only log an `error` since it's common for - // builders to timeout on their response, only - // to publish the block successfully themselves. - error!( - log, - "Error whilst producing block"; - "error" => ?e, - "block_slot" => ?slot, - "info" => "this error may or may not result in a missed block", - ) - } - Ok(_) => {} - }; - } else if let Err(e) = service - .publish_block(slot, validator_pubkey, false) - .await - { - // Log a `crit` since a full block (non-builder) - // proposal failed. - crit!( - log, - "Error whilst producing block"; - "message" => ?e, - "block_slot" => ?slot, - "info" => "proposal did not use a builder", - ); - } - }, - "block service", - ) - } - } - - Ok(()) - } - - async fn publish_block_v3( - self, - slot: Slot, - validator_pubkey: PublicKeyBytes, - builder_boost_factor: Option, - ) -> Result<(), BlockError> { - let log = self.context.log(); - let _timer = - metrics::start_timer_vec(&metrics::BLOCK_SERVICE_TIMES, &[metrics::BEACON_BLOCK]); - - let randao_reveal = match self - .validator_store - .randao_reveal(validator_pubkey, slot.epoch(E::slots_per_epoch())) - .await - { - Ok(signature) => signature.into(), - Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { - // A pubkey can be missing when a validator was recently removed - // via the API. - warn!( - log, - "Missing pubkey for block randao"; - "info" => "a validator may have recently been removed from this VC", - "pubkey" => ?pubkey, - "slot" => ?slot - ); - return Ok(()); - } - Err(e) => { - return Err(BlockError::Recoverable(format!( - "Unable to produce randao reveal signature: {:?}", - e - ))) - } - }; - - let graffiti = determine_graffiti( - &validator_pubkey, - log, - self.graffiti_file.clone(), - self.validator_store.graffiti(&validator_pubkey).await, - self.graffiti, - ); - - let randao_reveal_ref = &randao_reveal; - let self_ref = &self; - let proposer_index = self - .validator_store - .validator_index(&validator_pubkey) - .await; - let proposer_fallback = ProposerFallback { - beacon_nodes: self.beacon_nodes.clone(), - proposer_nodes: self.proposer_nodes.clone(), - }; - - info!( - log, - "Requesting unsigned block"; - "slot" => slot.as_u64(), - ); - - // Request block from first responsive beacon node. - // - // Try the proposer nodes last, since it's likely that they don't have a - // great view of attestations on the network. - let unsigned_block = proposer_fallback - .request_proposers_last( - RequireSynced::No, - OfflineOnFailure::Yes, - |beacon_node| async move { - let _get_timer = metrics::start_timer_vec( - &metrics::BLOCK_SERVICE_TIMES, - &[metrics::BEACON_BLOCK_HTTP_GET], - ); - let block_response = Self::get_validator_block_v3( - beacon_node, - slot, - randao_reveal_ref, - graffiti, - proposer_index, - builder_boost_factor, - log, - ) - .await - .map_err(|e| { - BlockError::Recoverable(format!( - "Error from beacon node when producing block: {:?}", - e - )) - }); - - Ok::<_, BlockError>(block_response) + } }, + "block service", ) - .await??; - - self_ref - .sign_and_publish_block( - proposer_fallback, - slot, - graffiti, - &validator_pubkey, - unsigned_block, - ) - .await?; - + } + Ok(()) } @@ -635,7 +445,7 @@ impl BlockService { &self, slot: Slot, validator_pubkey: PublicKeyBytes, - builder_proposal: bool, + builder_boost_factor: Option, ) -> Result<(), BlockError> { let log = self.context.log(); let _timer = @@ -700,19 +510,31 @@ impl BlockService { .request_proposers_last( RequireSynced::No, OfflineOnFailure::Yes, - move |beacon_node| { + |beacon_node| async move { + let _get_timer = metrics::start_timer_vec( + &metrics::BLOCK_SERVICE_TIMES, + &[metrics::BEACON_BLOCK_HTTP_GET], + ); Self::get_validator_block( beacon_node, slot, randao_reveal_ref, graffiti, proposer_index, - builder_proposal, + builder_boost_factor, log, ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + }) }, ) .await?; + self_ref .sign_and_publish_block( proposer_fallback, @@ -723,107 +545,6 @@ impl BlockService { ) .await?; - // let validator_pubkey_ref = &validator_pubkey; - // let signed_block = self - // .beacon_nodes - // .first_success(RequireSynced::No, OfflineOnFailure::Yes, |beacon_node| async move { - // let get_timer = metrics::start_timer_vec( - // &metrics::BLOCK_SERVICE_TIMES, - // &[metrics::BEACON_BLOCK_HTTP_GET], - // ); - // let block = match Payload::block_type() { - // BlockType::Full => { - // beacon_node - // .get_validator_blocks::( - // slot, - // randao_reveal_ref, - // graffiti.as_ref(), - // ) - // .await - // .map_err(|e| { - // BlockError::Recoverable(format!( - // "Error from beacon node when producing block: {:?}", - // e - // )) - // })? - // .data - // } - // BlockType::Blinded => { - // beacon_node - // .get_validator_blinded_blocks::( - // slot, - // randao_reveal_ref, - // graffiti.as_ref(), - // ) - // .await - // .map_err(|e| { - // BlockError::Recoverable(format!( - // "Error from beacon node when producing block: {:?}", - // e - // )) - // })? - // .data - // } - // }; - // drop(get_timer); - - // if proposer_index != Some(block.proposer_index()) { - // return Err(BlockError::Recoverable( - // "Proposer index does not match block proposer. Beacon chain re-orged" - // .to_string(), - // )); - // } - - // let signed_block = self_ref - // .validator_store - // .sign_block::(*validator_pubkey_ref, block, current_slot) - // .await - // .map_err(|e| { - // match e { - // ValidatorStoreError::UnableToSign(SigningError::NotLeader) => BlockError::SignBlockNotLeader, - // _ => BlockError::Recoverable(format!("Unable to sign block: {:?}", e)) - // } - // })?; - - // let _post_timer = metrics::start_timer_vec( - // &metrics::BLOCK_SERVICE_TIMES, - // &[metrics::BEACON_BLOCK_HTTP_POST], - // ); - - // match Payload::block_type() { - // BlockType::Full => beacon_node - // .post_beacon_blocks(&signed_block) - // .await - // .map_err(|e| { - // BlockError::Irrecoverable(format!( - // "Error from beacon node when publishing block: {:?}", - // e - // )) - // })?, - // BlockType::Blinded => beacon_node - // .post_beacon_blinded_blocks(&signed_block) - // .await - // .map_err(|e| { - // BlockError::Irrecoverable(format!( - // "Error from beacon node when publishing block: {:?}", - // e - // )) - // })?, - // } - - // Ok::<_, BlockError>(signed_block) - // }) - // .await?; - - // info!( - // log, - // "Successfully published block"; - // "deposits" => signed_block.message().body().deposits().len(), - // "attestations" => signed_block.message().body().attestations().len(), - // "graffiti" => ?graffiti.map(|g| g.as_utf8_lossy()), - // "slot" => signed_block.slot().as_u64(), - // ); - Ok(()) } @@ -859,7 +580,7 @@ impl BlockService { Ok::<_, BlockError>(()) } - async fn get_validator_block_v3( + async fn get_validator_block( beacon_node: &BeaconNodeHttpClient, slot: Slot, randao_reveal_ref: &SignatureBytes, @@ -902,63 +623,34 @@ impl BlockService { Ok::<_, BlockError>(unsigned_block) } - async fn get_validator_block( - beacon_node: &BeaconNodeHttpClient, - slot: Slot, - randao_reveal_ref: &SignatureBytes, - graffiti: Option, - proposer_index: Option, - builder_proposal: bool, - log: &Logger, - ) -> Result, BlockError> { - let unsigned_block = if !builder_proposal { - let _get_timer = metrics::start_timer_vec( - &metrics::BLOCK_SERVICE_TIMES, - &[metrics::BEACON_BLOCK_HTTP_GET], - ); - UnsignedBlock::Full( - beacon_node - .get_validator_blocks::(slot, randao_reveal_ref, graffiti.as_ref()) - .await - .map_err(|e| { - BlockError::Recoverable(format!( - "Error from beacon node when producing block: {:?}", - e - )) - })? - .data, - ) - } else { - let _get_timer = metrics::start_timer_vec( - &metrics::BLOCK_SERVICE_TIMES, - &[metrics::BLINDED_BEACON_BLOCK_HTTP_GET], - ); - UnsignedBlock::Blinded( - beacon_node - .get_validator_blinded_blocks::(slot, randao_reveal_ref, graffiti.as_ref()) - .await - .map_err(|e| { - BlockError::Recoverable(format!( - "Error from beacon node when producing block: {:?}", - e - )) - })? - .data, - ) - }; - - info!( - log, - "Received unsigned block"; - "slot" => slot.as_u64(), - ); - if proposer_index != Some(unsigned_block.proposer_index()) { - return Err(BlockError::Recoverable( - "Proposer index does not match block proposer. Beacon chain re-orged".to_string(), - )); + /// Returns the builder boost factor of the given public key. + /// The priority order for fetching this value is: + /// + /// 1. validator_definitions.yml + /// 2. process level flag + async fn get_builder_boost_factor(&self, validator_pubkey: &PublicKeyBytes) -> Option { + // Apply per validator configuration first. + let validator_builder_boost_factor = self + .validator_store + .determine_validator_builder_boost_factor(validator_pubkey).await; + + // Fallback to process-wide configuration if needed. + let maybe_builder_boost_factor = validator_builder_boost_factor.or_else(|| { + self.validator_store + .determine_default_builder_boost_factor() + }); + + if let Some(builder_boost_factor) = maybe_builder_boost_factor { + // if builder boost factor is set to 100 it should be treated + // as None to prevent unnecessary calculations that could + // lead to loss of information. + if builder_boost_factor == 100 { + return None; + } + return Some(builder_boost_factor); } - Ok::<_, BlockError>(unsigned_block) + None } } @@ -1002,8 +694,8 @@ impl SignedBlock { } pub fn num_attestations(&self) -> usize { match self { - SignedBlock::Full(block) => block.signed_block().message().body().attestations().len(), - SignedBlock::Blinded(block) => block.message().body().attestations().len(), + SignedBlock::Full(block) => block.signed_block().message().body().attestations_len(), + SignedBlock::Blinded(block) => block.message().body().attestations_len(), } } } diff --git a/src/validation/config.rs b/src/validation/config.rs index 3971baea..e523b6ea 100644 --- a/src/validation/config.rs +++ b/src/validation/config.rs @@ -19,9 +19,11 @@ use slog::{info, Logger}; use std::fs; use std::net::{IpAddr, Ipv4Addr}; use std::path::PathBuf; +use std::time::Duration; use types::{Address, GRAFFITI_BYTES_LEN}; pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; +pub const DEFAULT_WEB3SIGNER_KEEP_ALIVE: Option = Some(Duration::from_secs(20)); /// Stores the core configuration for this validator instance. #[derive(Clone, Serialize, Deserialize)] @@ -80,8 +82,16 @@ pub struct Config { pub enable_latency_measurement_service: bool, /// Defines the number of validators per `validator/register_validator` request sent to the BN. pub validator_registration_batch_size: usize, - /// Enables block production via the block v3 endpoint. This configuration option can be removed post deneb. - pub produce_block_v3: bool, + /// Enable slashing protection even while using web3signer keys. + pub enable_web3signer_slashing_protection: bool, + /// Specifies the boost factor, a percentage multiplier to apply to the builder's payload value. + pub builder_boost_factor: Option, + /// If true, Lighthouse will prefer builder proposals, if available. + pub prefer_builder_proposals: bool, + /// Whether we are running with distributed network support. + pub distributed: bool, + pub web3_signer_keep_alive_timeout: Option, + pub web3_signer_max_idle_connections: Option, /// Used for Dvf pub dvf_node_config: NodeConfig, } @@ -124,7 +134,12 @@ impl Default for Config { broadcast_topics: vec![ApiTopic::Subscriptions], enable_latency_measurement_service: true, validator_registration_batch_size: 500, - produce_block_v3: false, + enable_web3signer_slashing_protection: true, + builder_boost_factor: None, + prefer_builder_proposals: false, + distributed: false, + web3_signer_keep_alive_timeout: DEFAULT_WEB3SIGNER_KEEP_ALIVE, + web3_signer_max_idle_connections: None, dvf_node_config: NodeConfig::default(), } } @@ -360,10 +375,6 @@ impl Config { config.builder_proposals = true; } - if cli_args.get_flag("produce-block-v3") { - config.produce_block_v3 = true; - } - config.gas_limit = cli_args .get_one::("gas-limit") .map(|gas_limit| { @@ -373,16 +384,6 @@ impl Config { }) .transpose()?; - // if let Some(registration_timestamp_override) = - // cli_args.value_of("builder-registration-timestamp-override") - // { - // config.builder_registration_timestamp_override = Some( - // registration_timestamp_override - // .parse::() - // .map_err(|_| "builder-registration-timestamp-override is not a valid u64.")?, - // ); - // } - config.validator_registration_batch_size = parse_required(cli_args, "validator-registration-batch-size")?; if config.validator_registration_batch_size == 0 { diff --git a/src/validation/http_api/api_secret.rs b/src/validation/http_api/api_secret.rs index 372ff884..75bc7b74 100644 --- a/src/validation/http_api/api_secret.rs +++ b/src/validation/http_api/api_secret.rs @@ -1,43 +1,30 @@ -use eth2::lighthouse_vc::{PK_LEN, SECRET_PREFIX as PK_PREFIX}; use filesystem::create_with_600_perms; -use libsecp256k1::{Message, PublicKey, SecretKey}; -use rand::thread_rng; -use ring::digest::{digest, SHA256}; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; use std::fs; use std::path::{Path, PathBuf}; use warp::Filter; -/// The name of the file which stores the secret key. -/// -/// It is purposefully opaque to prevent users confusing it with the "secret" that they need to -/// share with API consumers (which is actually the public key). -pub const SK_FILENAME: &str = ".secp-sk"; - -/// Length of the raw secret key, in bytes. -pub const SK_LEN: usize = 32; - -/// The name of the file which stores the public key. -/// -/// For users, this public key is a "secret" that can be shared with API consumers to provide them -/// access to the API. We avoid calling it a "public" key to users, since they should not post this -/// value in a public forum. +/// The name of the file which stores the API token. pub const PK_FILENAME: &str = "api-token.txt"; -/// Contains a `secp256k1` keypair that is saved-to/loaded-from disk on instantiation. The keypair -/// is used for authorization/authentication for requests/responses on the HTTP API. +pub const PK_LEN: usize = 33; + +/// Contains a randomly generated string which is used for authorization of requests to the HTTP API. /// /// Provides convenience functions to ultimately provide: /// -/// - A signature across outgoing HTTP responses, applied to the `Signature` header. /// - Verification of proof-of-knowledge of the public key in `self` for incoming HTTP requests, -/// via the `Authorization` header. +/// via the `Authorization` header. /// /// The aforementioned scheme was first defined here: /// /// https://github.com/sigp/lighthouse/issues/1269#issuecomment-649879855 +/// +/// This scheme has since been tweaked to remove VC response signing and secp256k1 key generation. +/// https://github.com/sigp/lighthouse/issues/5423 pub struct ApiSecret { - pk: PublicKey, - sk: SecretKey, + pk: String, pk_path: PathBuf, } @@ -50,36 +37,18 @@ impl ApiSecret { /// If either the secret or public key files are missing on disk, create a new keypair and /// write it to disk (over-writing any existing files). pub fn create_or_open>(dir: P) -> Result { - let sk_path = dir.as_ref().join(SK_FILENAME); let pk_path = dir.as_ref().join(PK_FILENAME); - if !(sk_path.exists() && pk_path.exists()) { - let sk = SecretKey::random(&mut thread_rng()); - let pk = PublicKey::from_secret_key(&sk); - - // Create and write the secret key to file with appropriate permissions - create_with_600_perms( - &sk_path, - serde_utils::hex::encode(&sk.serialize()).as_bytes(), - ) - .map_err(|e| { - format!( - "Unable to create file with permissions for {:?}: {:?}", - sk_path, e - ) - })?; + if !pk_path.exists() { + let length = PK_LEN; + let pk: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect(); // Create and write the public key to file with appropriate permissions - create_with_600_perms( - &pk_path, - format!( - "{}{}", - PK_PREFIX, - serde_utils::hex::encode(&pk.serialize_compressed()[..]) - ) - .as_bytes(), - ) - .map_err(|e| { + create_with_600_perms(&pk_path, pk.to_string().as_bytes()).map_err(|e| { format!( "Unable to create file with permissions for {:?}: {:?}", pk_path, e @@ -87,78 +56,18 @@ impl ApiSecret { })?; } - let sk = fs::read(&sk_path) - .map_err(|e| format!("cannot read {}: {}", SK_FILENAME, e)) - .and_then(|bytes| { - serde_utils::hex::decode(&String::from_utf8_lossy(&bytes)) - .map_err(|_| format!("{} should be 0x-prefixed hex", PK_FILENAME)) - }) - .and_then(|bytes| { - if bytes.len() == SK_LEN { - let mut array = [0; SK_LEN]; - array.copy_from_slice(&bytes); - SecretKey::parse(&array).map_err(|e| format!("invalid {}: {}", SK_FILENAME, e)) - } else { - Err(format!( - "{} expected {} bytes not {}", - SK_FILENAME, - SK_LEN, - bytes.len() - )) - } - })?; - let pk = fs::read(&pk_path) - .map_err(|e| format!("cannot read {}: {}", PK_FILENAME, e)) - .and_then(|bytes| { - let hex = - String::from_utf8(bytes).map_err(|_| format!("{} is not utf8", SK_FILENAME))?; - if let Some(stripped) = hex.strip_prefix(PK_PREFIX) { - serde_utils::hex::decode(stripped) - .map_err(|_| format!("{} should be 0x-prefixed hex", SK_FILENAME)) - } else { - Err(format!("unable to parse {}", SK_FILENAME)) - } - }) - .and_then(|bytes| { - if bytes.len() == PK_LEN { - let mut array = [0; PK_LEN]; - array.copy_from_slice(&bytes); - PublicKey::parse_compressed(&array) - .map_err(|e| format!("invalid {}: {}", PK_FILENAME, e)) - } else { - Err(format!( - "{} expected {} bytes not {}", - PK_FILENAME, - PK_LEN, - bytes.len() - )) - } - })?; + .map_err(|e| format!("cannot read {}: {}", PK_FILENAME, e))? + .iter() + .map(|&c| char::from(c)) + .collect(); - // Ensure that the keys loaded from disk are indeed a pair. - if PublicKey::from_secret_key(&sk) != pk { - fs::remove_file(&sk_path) - .map_err(|e| format!("unable to remove {}: {}", SK_FILENAME, e))?; - fs::remove_file(&pk_path) - .map_err(|e| format!("unable to remove {}: {}", PK_FILENAME, e))?; - return Err(format!( - "{:?} does not match {:?} and the files have been deleted. Please try again.", - sk_path, pk_path - )); - } - - Ok(Self { pk, sk, pk_path }) - } - - /// Returns the public key of `self` as a 0x-prefixed hex string. - fn pubkey_string(&self) -> String { - serde_utils::hex::encode(&self.pk.serialize_compressed()[..]) + Ok(Self { pk, pk_path }) } /// Returns the API token. pub fn api_token(&self) -> String { - format!("{}{}", PK_PREFIX, self.pubkey_string()) + self.pk.clone() } /// Returns the path for the API token file @@ -196,16 +105,4 @@ impl ApiSecret { .untuple_one() .boxed() } - - /// Returns a closure which produces a signature over some bytes using the secret key in - /// `self`. The signature is a 32-byte hash formatted as a 0x-prefixed string. - pub fn signer(&self) -> impl Fn(&[u8]) -> String + Clone { - let sk = self.sk; - move |input: &[u8]| -> String { - let message = - Message::parse_slice(digest(&SHA256, input).as_ref()).expect("sha256 is 32 bytes"); - let (signature, _) = libsecp256k1::sign(&message, &sk); - serde_utils::hex::encode(signature.serialize_der().as_ref()) - } - } } diff --git a/src/validation/http_api/create_validator.rs b/src/validation/http_api/create_validator.rs index 5e26b667..612c7184 100644 --- a/src/validation/http_api/create_validator.rs +++ b/src/validation/http_api/create_validator.rs @@ -22,7 +22,7 @@ use validator_dir::Builder as ValidatorDirBuilder; /// /// If `key_derivation_path_offset` is supplied then the EIP-2334 validator index will start at /// this point. -pub async fn create_validators_mnemonic, T: 'static + SlotClock, E: EthSpec>( +pub async fn _create_validators_mnemonic, T: 'static + SlotClock, E: EthSpec>( mnemonic_opt: Option, key_derivation_path_offset: Option, validator_requests: &[api_types::ValidatorRequest], @@ -142,6 +142,8 @@ pub async fn create_validators_mnemonic, T: 'static + SlotClock, request.suggested_fee_recipient, request.gas_limit, request.builder_proposals, + request.builder_boost_factor, + request.prefer_builder_proposals, ) .await .map_err(|e| { @@ -167,7 +169,7 @@ pub async fn create_validators_mnemonic, T: 'static + SlotClock, Ok((validators, mnemonic)) } -pub async fn create_validators_web3signer( +pub async fn _create_validators_web3signer( validators: Vec, validator_store: &ValidatorStore, ) -> Result<(), warp::Rejection> { diff --git a/src/validation/http_api/graffiti.rs b/src/validation/http_api/graffiti.rs new file mode 100644 index 00000000..24f1c98b --- /dev/null +++ b/src/validation/http_api/graffiti.rs @@ -0,0 +1,80 @@ +use crate::validation::ValidatorStore; +use bls::PublicKey; +use slot_clock::SlotClock; +use std::sync::Arc; +use types::{graffiti::GraffitiString, EthSpec, Graffiti}; + +pub async fn _get_graffiti( + validator_pubkey: PublicKey, + validator_store: Arc>, + graffiti_flag: Option, +) -> Result { + let initialized_validators_rw_lock = validator_store.initialized_validators(); + let initialized_validators = initialized_validators_rw_lock.read().await; + match initialized_validators.validator(&validator_pubkey.compress()) { + None => Err(warp_utils::reject::custom_not_found( + "The key was not found on the server".to_string(), + )), + Some(_) => { + let Some(graffiti) = initialized_validators.graffiti(&validator_pubkey.into()) else { + return graffiti_flag.ok_or(warp_utils::reject::custom_server_error( + "No graffiti found, unable to return the process-wide default".to_string(), + )); + }; + Ok(graffiti) + } + } +} + +pub async fn _set_graffiti( + validator_pubkey: PublicKey, + graffiti: GraffitiString, + validator_store: Arc>, +) -> Result<(), warp::Rejection> { + let initialized_validators_rw_lock = validator_store.initialized_validators(); + let mut initialized_validators = initialized_validators_rw_lock.write().await; + match initialized_validators.validator(&validator_pubkey.compress()) { + None => Err(warp_utils::reject::custom_not_found( + "The key was not found on the server, nothing to update".to_string(), + )), + Some(initialized_validator) => { + if initialized_validator.get_graffiti() == Some(graffiti.clone().into()) { + Ok(()) + } else { + initialized_validators + .set_graffiti(&validator_pubkey, graffiti) + .map_err(|_| { + warp_utils::reject::custom_server_error( + "A graffiti was found, but failed to be updated.".to_string(), + ) + }) + } + } + } +} + +pub async fn _delete_graffiti( + validator_pubkey: PublicKey, + validator_store: Arc>, +) -> Result<(), warp::Rejection> { + let initialized_validators_rw_lock = validator_store.initialized_validators(); + let mut initialized_validators = initialized_validators_rw_lock.write().await; + match initialized_validators.validator(&validator_pubkey.compress()) { + None => Err(warp_utils::reject::custom_not_found( + "The key was not found on the server, nothing to delete".to_string(), + )), + Some(initialized_validator) => { + if initialized_validator.get_graffiti().is_none() { + Ok(()) + } else { + initialized_validators + .delete_graffiti(&validator_pubkey) + .map_err(|_| { + warp_utils::reject::custom_server_error( + "A graffiti was found, but failed to be removed.".to_string(), + ) + }) + } + } + } +} diff --git a/src/validation/http_api/keystores.rs b/src/validation/http_api/keystores.rs index 8af3aada..7e3e78a8 100644 --- a/src/validation/http_api/keystores.rs +++ b/src/validation/http_api/keystores.rs @@ -1,14 +1,13 @@ //! Implementation of the standard keystore management API. use crate::validation::account_utils::ZeroizeString; -use crate::validation::{ - initialized_validators::Error, signing_method::SigningMethod, InitializedValidators, +use crate::validation::{signing_method::SigningMethod, ValidatorStore, }; use eth2::lighthouse_vc::std_types::{ - DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, ImportKeystoreStatus, + ImportKeystoreStatus, ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr, KeystoreJsonStr, ListKeystoresResponse, SingleKeystoreResponse, Status, -}; + }; use eth2_keystore::Keystore; use futures::executor::block_on; use slog::{info, warn, Logger}; @@ -17,10 +16,10 @@ use std::path::PathBuf; use std::sync::Arc; use task_executor::TaskExecutor; use tokio::runtime::Handle; -use types::{EthSpec, PublicKeyBytes}; +use types::EthSpec; use validator_dir::Builder as ValidatorDirBuilder; use warp::Rejection; -use warp_utils::reject::{custom_bad_request, custom_server_error}; +use warp_utils::reject::custom_bad_request; pub fn list( validator_store: Arc>, @@ -58,7 +57,7 @@ pub fn list( ListKeystoresResponse { data: keystores } } -pub fn import( +pub fn _import( request: ImportKeystoresRequest, validator_dir: PathBuf, validator_store: Arc>, @@ -127,7 +126,7 @@ pub fn import( ) } else if let Some(handle) = task_executor.handle() { // Import the keystore. - match import_single_keystore( + match _import_single_keystore( keystore, password, validator_dir.clone(), @@ -157,7 +156,7 @@ pub fn import( Ok(ImportKeystoresResponse { data: statuses }) } -fn import_single_keystore( +fn _import_single_keystore( keystore: Keystore, password: ZeroizeString, validator_dir_path: PathBuf, @@ -209,97 +208,10 @@ fn import_single_keystore( None, None, None, + None, + None )) .map_err(|e| format!("failed to initialize validator: {:?}", e))?; Ok(ImportKeystoreStatus::Imported) -} - -pub async fn delete( - request: DeleteKeystoresRequest, - validator_store: Arc>, - task_executor: TaskExecutor, - log: Logger, -) -> Result { - // Remove from initialized validators. - let initialized_validators_rwlock = validator_store.initialized_validators(); - let mut initialized_validators = initialized_validators_rwlock.write().await; - - let mut statuses = request - .pubkeys - .iter() - .map(|pubkey_bytes| { - match delete_single_keystore::( - pubkey_bytes, - &mut initialized_validators, - task_executor.clone(), - ) { - Ok(status) => Status::ok(status), - Err(error) => { - warn!( - log, - "Error deleting keystore"; - "pubkey" => ?pubkey_bytes, - "error" => ?error, - ); - Status::error(DeleteKeystoreStatus::Error, error) - } - } - }) - .collect::>(); - - // Use `update_validators` to update the key cache. It is safe to let the key cache get a bit out - // of date as it resets when it can't be decrypted. We update it just a single time to avoid - // continually resetting it after each key deletion. - if let Some(handle) = task_executor.handle() { - handle - .block_on(initialized_validators.update_validators()) - .map_err(|e| custom_server_error(format!("unable to update key cache: {:?}", e)))?; - } - - // Export the slashing protection data. - let slashing_protection = validator_store - .export_slashing_protection_for_keys(&request.pubkeys) - .map_err(|e| { - custom_server_error(format!("error exporting slashing protection: {:?}", e)) - })?; - - // Update stasuses based on availability of slashing protection data. - for (pubkey, status) in request.pubkeys.iter().zip(statuses.iter_mut()) { - if status.status == DeleteKeystoreStatus::NotFound - && slashing_protection - .data - .iter() - .any(|interchange_data| interchange_data.pubkey == *pubkey) - { - status.status = DeleteKeystoreStatus::NotActive; - } - } - - Ok(DeleteKeystoresResponse { - data: statuses, - slashing_protection, - }) -} - -fn delete_single_keystore( - pubkey_bytes: &PublicKeyBytes, - initialized_validators: &mut InitializedValidators, - task_executor: TaskExecutor, -) -> Result { - if let Some(handle) = task_executor.handle() { - let pubkey = pubkey_bytes - .decompress() - .map_err(|e| format!("invalid pubkey, {:?}: {:?}", pubkey_bytes, e))?; - - match handle.block_on(initialized_validators.delete_definition_and_keystore(&pubkey)) { - Ok(_) => Ok(DeleteKeystoreStatus::Deleted), - Err(e) => match e { - Error::ValidatorNotInitialized(_) => Ok(DeleteKeystoreStatus::NotFound), - _ => Err(format!("unable to disable and delete: {:?}", e)), - }, - } - } else { - Err("validator client shutdown".into()) - } -} +} \ No newline at end of file diff --git a/src/validation/http_api/mod.rs b/src/validation/http_api/mod.rs index ac49c520..d806313f 100644 --- a/src/validation/http_api/mod.rs +++ b/src/validation/http_api/mod.rs @@ -3,19 +3,17 @@ mod create_validator; mod keystores; mod remotekeys; mod tests; +mod graffiti; -use crate::validation::account_utils::mnemonic_from_phrase; -use crate::validation::account_utils::validator_definitions::{ - SigningDefinition, ValidatorDefinition, -}; -use crate::validation::ValidatorStore; -use create_validator::{create_validators_mnemonic, create_validators_web3signer}; +use crate::validation::{GraffitiFile, ValidatorStore}; use eth2::lighthouse_vc::{ std_types::AuthResponse, - types::{self as api_types, PublicKey, PublicKeyBytes}, + types::{self as api_types, GenericResponse, GetFeeRecipientResponse, GetGasLimitResponse, Graffiti, PublicKey, + PublicKeyBytes}, }; -use futures::executor::block_on; +use logging::SSELoggingComponents; use lighthouse_version::version_with_platform; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use slog::{crit, info, warn, Logger}; use slot_clock::SlotClock; @@ -24,9 +22,10 @@ use std::marker::PhantomData; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; +use sysinfo::{System, SystemExt}; +use system_health::observe_system_health_vc; use task_executor::TaskExecutor; use types::{ChainSpec, ConfigAndPreset, EthSpec}; -use validator_dir::Builder as ValidatorDirBuilder; use warp::{ http::{ header::{HeaderValue, CONTENT_TYPE}, @@ -35,6 +34,7 @@ use warp::{ }, Filter, }; +use warp_utils::task::blocking_json_task; pub use api_secret::ApiSecret; @@ -64,9 +64,14 @@ pub struct Context { pub api_secret: ApiSecret, pub validator_store: Option>>, pub validator_dir: Option, + pub secrets_dir: Option, + pub graffiti_file: Option, + pub graffiti_flag: Option, pub spec: ChainSpec, pub config: Config, pub log: Logger, + pub sse_logging_components: Option, + pub slot_clock: T, pub _phantom: PhantomData, } @@ -77,6 +82,8 @@ pub struct Config { pub listen_addr: IpAddr, pub listen_port: u16, pub allow_origin: Option, + pub allow_keystore_export: bool, + pub store_passwords_in_secrets_dir: bool, } impl Default for Config { @@ -86,6 +93,8 @@ impl Default for Config { listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 5062, allow_origin: None, + allow_keystore_export: false, + store_passwords_in_secrets_dir: false, } } } @@ -115,7 +124,7 @@ pub fn serve( // Configure CORS. let cors_builder = { let builder = warp::cors() - .allow_methods(vec!["GET", "POST", "PATCH", "DELETE"]) + .allow_methods(vec!["GET"]) .allow_headers(vec!["Content-Type", "Authorization"]); warp_utils::cors::set_builder_origins( @@ -148,9 +157,6 @@ pub fn serve( } }; - let signer = ctx.api_secret.signer(); - let signer = warp::any().map(move || signer.clone()); - let inner_validator_store = ctx.validator_store.clone(); let validator_store_filter = warp::any() .map(move || inner_validator_store.clone()) @@ -175,23 +181,48 @@ pub fn serve( ) }) }); - - let inner_ctx = ctx.clone(); - let log_filter = warp::any().map(move || inner_ctx.log.clone()); - - let inner_spec = Arc::new(ctx.spec.clone()); - let spec_filter = warp::any().map(move || inner_spec.clone()); - - let api_token_path_inner = api_token_path.clone(); - let api_token_path_filter = warp::any().map(move || api_token_path_inner.clone()); + + let inner_spec = Arc::new(ctx.spec.clone()); + let spec_filter = warp::any().map(move || inner_spec.clone()); + + let api_token_path_inner = api_token_path.clone(); + let api_token_path_filter = warp::any().map(move || api_token_path_inner.clone()); + + // Create a `warp` filter that provides access to local system information. + let system_info = Arc::new(RwLock::new(sysinfo::System::new())); + { + // grab write access for initialisation + let mut system_info = system_info.write(); + system_info.refresh_disks_list(); + system_info.refresh_networks_list(); + } // end lock + + let system_info_filter = + warp::any() + .map(move || system_info.clone()) + .map(|sysinfo: Arc>| { + { + // refresh stats + let mut sysinfo_lock = sysinfo.write(); + sysinfo_lock.refresh_memory(); + sysinfo_lock.refresh_cpu_specifics(sysinfo::CpuRefreshKind::everything()); + sysinfo_lock.refresh_cpu(); + sysinfo_lock.refresh_system(); + sysinfo_lock.refresh_networks(); + sysinfo_lock.refresh_disks(); + } // end lock + sysinfo + }); + + let app_start = std::time::Instant::now(); + let app_start_filter = warp::any().map(move || app_start); // GET lighthouse/version let get_node_version = warp::path("lighthouse") .and(warp::path("version")) .and(warp::path::end()) - .and(signer.clone()) - .and_then(|signer| { - blocking_signed_json_task(signer, move || { + .then(|| { + blocking_json_task(move || { Ok(api_types::GenericResponse::from(api_types::VersionData { version: version_with_platform(), })) @@ -202,9 +233,8 @@ pub fn serve( let get_lighthouse_health = warp::path("lighthouse") .and(warp::path("health")) .and(warp::path::end()) - .and(signer.clone()) - .and_then(|signer| { - blocking_signed_json_task(signer, move || { + .then(|| { + blocking_json_task(move || { eth2::lighthouse::Health::observe() .map(api_types::GenericResponse::from) .map_err(warp_utils::reject::custom_bad_request) @@ -216,11 +246,9 @@ pub fn serve( .and(warp::path("spec")) .and(warp::path::end()) .and(spec_filter.clone()) - .and(signer.clone()) - .and_then(|spec: Arc<_>, signer| { - blocking_signed_json_task(signer, move || { + .then(|spec: Arc<_>| { + blocking_json_task(move || { let config = ConfigAndPreset::from_chain_spec::(&spec, None); - // config.make_backwards_compat(&spec); Ok(api_types::GenericResponse::from(config)) }) }); @@ -230,12 +258,13 @@ pub fn serve( .and(warp::path("validators")) .and(warp::path::end()) .and(validator_store_filter.clone()) - .and(signer.clone()) - .and_then(|validator_store: Arc>, signer| { - blocking_signed_json_task(signer, move || { - // Zico: It is OK to use 'block_on' here because we know this function will be spawned in OS's blocking threads, - // hence will not block tokio's core thread (for executing async tasks). - let validators = block_on(validator_store.initialized_validators().read()) + .and(task_executor_filter.clone()) + .then(|validator_store: Arc>, task_executor: TaskExecutor| { + blocking_json_task(move || { + if let Some(handle) = task_executor.handle() { + let validators = handle.block_on(validator_store + .initialized_validators() + .read()) .validator_definitions() .iter() .map(|def| api_types::ValidatorData { @@ -244,8 +273,12 @@ pub fn serve( voting_pubkey: PublicKeyBytes::from(&def.voting_public_key), }) .collect::>(); - Ok(api_types::GenericResponse::from(validators)) + } else { + Err(warp_utils::reject::custom_server_error( + "Lighthouse shutting down".into(), + )) + } }) }); @@ -255,244 +288,106 @@ pub fn serve( .and(warp::path::param::()) .and(warp::path::end()) .and(validator_store_filter.clone()) - .and(signer.clone()) - .and_then( - |validator_pubkey: PublicKey, validator_store: Arc>, signer| { - blocking_signed_json_task(signer, move || { - // Zico: It is OK to use 'block_on' here because we know this function will be spawned in OS's blocking threads, - // hence will not block tokio's core thread (for executing async tasks). - let validator = block_on(validator_store.initialized_validators().read()) - .validator_definitions() - .iter() - .find(|def| def.voting_public_key == validator_pubkey) - .map(|def| api_types::ValidatorData { - enabled: def.enabled, - description: def.description.clone(), - voting_pubkey: PublicKeyBytes::from(&def.voting_public_key), - }) - .ok_or_else(|| { - warp_utils::reject::custom_not_found(format!( - "no validator for {:?}", - validator_pubkey - )) - })?; - - Ok(api_types::GenericResponse::from(validator)) - }) - }, - ); - - // POST lighthouse/validators/ - let post_validators = warp::path("lighthouse") - .and(warp::path("validators")) - .and(warp::path::end()) - .and(warp::body::json()) - .and(validator_dir_filter.clone()) - .and(validator_store_filter.clone()) - .and(spec_filter.clone()) - .and(signer.clone()) .and(task_executor_filter.clone()) - .and_then( - |body: Vec, - validator_dir: PathBuf, - validator_store: Arc>, - spec: Arc, - signer, - task_executor: TaskExecutor| { - blocking_signed_json_task(signer, move || { + .then( + |validator_pubkey: PublicKey, validator_store: Arc>, task_executor: TaskExecutor| { + blocking_json_task(move || { if let Some(handle) = task_executor.handle() { - let (validators, mnemonic) = - handle.block_on(create_validators_mnemonic( - None, - None, - &body, - &validator_dir, - &validator_store, - &spec, - ))?; - let response = api_types::PostValidatorsResponseData { - mnemonic: mnemonic.into_phrase().into(), - validators, - }; - Ok(api_types::GenericResponse::from(response)) - } else { - Err(warp_utils::reject::custom_server_error( - "Lighthouse shutting down".into(), - )) - } - }) - }, - ); - - // POST lighthouse/validators/mnemonic - let post_validators_mnemonic = warp::path("lighthouse") - .and(warp::path("validators")) - .and(warp::path("mnemonic")) - .and(warp::path::end()) - .and(warp::body::json()) - .and(validator_dir_filter.clone()) - .and(validator_store_filter.clone()) - .and(spec_filter) - .and(signer.clone()) - .and(task_executor_filter.clone()) - .and_then( - |body: api_types::CreateValidatorsMnemonicRequest, - validator_dir: PathBuf, - validator_store: Arc>, - spec: Arc, - signer, - task_executor: TaskExecutor| { - blocking_signed_json_task(signer, move || { - if let Some(handle) = task_executor.handle() { - let mnemonic = - mnemonic_from_phrase(body.mnemonic.as_str()).map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "invalid mnemonic: {:?}", - e + let validator = handle.block_on(validator_store + .initialized_validators() + .read()) + .validator_definitions() + .iter() + .find(|def| def.voting_public_key == validator_pubkey) + .map(|def| api_types::ValidatorData { + enabled: def.enabled, + description: def.description.clone(), + voting_pubkey: PublicKeyBytes::from(&def.voting_public_key), + }) + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "no validator for {:?}", + validator_pubkey )) })?; - let (validators, _mnemonic) = - handle.block_on(create_validators_mnemonic( - Some(mnemonic), - Some(body.key_derivation_path_offset), - &body.validators, - &validator_dir, - &validator_store, - &spec, - ))?; - Ok(api_types::GenericResponse::from(validators)) + Ok(api_types::GenericResponse::from(validator)) } else { Err(warp_utils::reject::custom_server_error( "Lighthouse shutting down".into(), )) - } + } }) }, ); - // POST lighthouse/validators/keystore - let post_validators_keystore = warp::path("lighthouse") - .and(warp::path("validators")) - .and(warp::path("keystore")) + // GET lighthouse/ui/health + let get_lighthouse_ui_health = warp::path("lighthouse") + .and(warp::path("ui")) + .and(warp::path("health")) .and(warp::path::end()) - .and(warp::body::json()) + .and(system_info_filter) + .and(app_start_filter) .and(validator_dir_filter.clone()) - .and(validator_store_filter.clone()) - .and(signer.clone()) - .and(task_executor_filter.clone()) - .and_then( - |body: api_types::KeystoreValidatorsPostRequest, - validator_dir: PathBuf, - validator_store: Arc>, - signer, - task_executor: TaskExecutor| { - blocking_signed_json_task(signer, move || { - // Check to ensure the password is correct. - let keypair = body - .keystore - .decrypt_keypair(body.password.as_ref()) - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "invalid keystore: {:?}", - e - )) - })?; - - let validator_dir = ValidatorDirBuilder::new(validator_dir.clone()) - .voting_keystore(body.keystore.clone(), body.password.as_ref()) - .store_withdrawal_keystore(false) - .build() - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "failed to build validator directory: {:?}", - e - )) - })?; - - // Drop validator dir so that `add_validator_keystore` can re-lock the keystore. - let voting_keystore_path = validator_dir.voting_keystore_path(); - drop(validator_dir); - let voting_password = body.password.clone(); - let graffiti = body.graffiti.clone(); - let suggested_fee_recipient = body.suggested_fee_recipient; - let gas_limit = body.gas_limit; - let builder_proposals = body.builder_proposals; - - let validator_def = { - if let Some(handle) = task_executor.handle() { - handle - .block_on(validator_store.add_validator_keystore( - voting_keystore_path, - voting_password, - body.enable, - graffiti, - suggested_fee_recipient, - gas_limit, - builder_proposals, - )) - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "failed to initialize validator: {:?}", - e - )) - })? - } else { - return Err(warp_utils::reject::custom_server_error( - "Lighthouse shutting down".into(), - )); - } - }; + .then(|sysinfo, app_start: std::time::Instant, val_dir| { + blocking_json_task(move || { + let app_uptime = app_start.elapsed().as_secs(); + Ok(api_types::GenericResponse::from(observe_system_health_vc( + sysinfo, val_dir, app_uptime, + ))) + }) + }); - Ok(api_types::GenericResponse::from(api_types::ValidatorData { - enabled: body.enable, - description: validator_def.description, - voting_pubkey: keypair.pk.into(), - })) + // GET /lighthouse/auth + let get_auth = warp::path("lighthouse").and(warp::path("auth").and(warp::path::end())); + let get_auth = get_auth + .and(api_token_path_filter) + .then(move |token_path: PathBuf| { + blocking_json_task(move || { + Ok(AuthResponse { + token_path: token_path.display().to_string(), }) - }, - ); + }) + }); - // POST lighthouse/validators/web3signer - let post_validators_web3signer = warp::path("lighthouse") - .and(warp::path("validators")) - .and(warp::path("web3signer")) + // Standard key-manager endpoints. + let eth_v1 = warp::path("eth").and(warp::path("v1")); + let std_keystores = eth_v1.and(warp::path("keystores")).and(warp::path::end()); + let std_remotekeys = eth_v1.and(warp::path("remotekeys")).and(warp::path::end()); + + // GET /eth/v1/validator/{pubkey}/feerecipient + let get_fee_recipient = eth_v1 + .and(warp::path("validator")) + .and(warp::path::param::()) + .and(warp::path("feerecipient")) .and(warp::path::end()) - .and(warp::body::json()) .and(validator_store_filter.clone()) - .and(signer.clone()) .and(task_executor_filter.clone()) - .and_then( - |body: Vec, - validator_store: Arc>, - signer, - task_executor: TaskExecutor| { - blocking_signed_json_task(signer, move || { + .then( + |validator_pubkey: PublicKey, validator_store: Arc>, task_executor: TaskExecutor| { + blocking_json_task(move || { if let Some(handle) = task_executor.handle() { - let web3signers: Vec = body - .into_iter() - .map(|web3signer| ValidatorDefinition { - enabled: web3signer.enable, - voting_public_key: web3signer.voting_public_key, - graffiti: web3signer.graffiti, - suggested_fee_recipient: web3signer.suggested_fee_recipient, - gas_limit: web3signer.gas_limit, - builder_proposals: web3signer.builder_proposals, - description: web3signer.description, - signing_definition: SigningDefinition::Web3Signer { - url: web3signer.url, - root_certificate_path: web3signer.root_certificate_path, - request_timeout_ms: web3signer.request_timeout_ms, - client_identity_path: web3signer.client_identity_path, - client_identity_password: web3signer.client_identity_password, - }, + if handle.block_on(validator_store + .initialized_validators() + .read()).is_enabled(&validator_pubkey) + .is_none() { + return Err(warp_utils::reject::custom_not_found(format!( + "no validator found with pubkey {:?}", + validator_pubkey + ))); + } + handle.block_on(validator_store + .get_fee_recipient(&PublicKeyBytes::from(&validator_pubkey))).map(|fee_recipient| { + GenericResponse::from(GetFeeRecipientResponse { + pubkey: PublicKeyBytes::from(validator_pubkey.clone()), + ethaddress: fee_recipient, + }) }) - .collect(); - handle.block_on(create_validators_web3signer( - web3signers, - &validator_store, - ))?; - Ok(()) - } else { + .ok_or_else(|| { + warp_utils::reject::custom_server_error( + "no fee recipient set".to_string(), + ) + }) + } else { Err(warp_utils::reject::custom_server_error( "Lighthouse shutting down".into(), )) @@ -501,166 +396,99 @@ pub fn serve( }, ); - // PATCH lighthouse/validators/{validator_pubkey} - let patch_validators = warp::path("lighthouse") - .and(warp::path("validators")) + // POST /eth/v1/validator/{pubkey}/feerecipient + // let post_fee_recipient = eth_v1 + // .and(warp::path("validator")) + // .and(warp::path::param::()) + // .and(warp::path("feerecipient")) + // .and(warp::body::json()) + // .and(warp::path::end()) + // .and(validator_store_filter.clone()) + // .then( + // |validator_pubkey: PublicKey, + // request: api_types::UpdateFeeRecipientRequest, + // validator_store: Arc>| { + // blocking_json_task(move || { + // if validator_store + // .initialized_validators() + // .read() + // .is_enabled(&validator_pubkey) + // .is_none() + // { + // return Err(warp_utils::reject::custom_not_found(format!( + // "no validator found with pubkey {:?}", + // validator_pubkey + // ))); + // } + // validator_store + // .initialized_validators() + // .write() + // .set_validator_fee_recipient(&validator_pubkey, request.ethaddress) + // .map_err(|e| { + // warp_utils::reject::custom_server_error(format!( + // "Error persisting fee recipient: {:?}", + // e + // )) + // }) + // }) + // }, + // ) + // .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::ACCEPTED)); + + // GET /eth/v1/validator/{pubkey}/gas_limit + let get_gas_limit = eth_v1 + .and(warp::path("validator")) .and(warp::path::param::()) + .and(warp::path("gas_limit")) .and(warp::path::end()) - .and(warp::body::json()) .and(validator_store_filter.clone()) - .and(signer.clone()) .and(task_executor_filter.clone()) - .and_then( - |validator_pubkey: PublicKey, - body: api_types::ValidatorPatchRequest, - validator_store: Arc>, - signer, - task_executor: TaskExecutor| { - blocking_signed_json_task(signer, move || { - let initialized_validators_rw_lock = validator_store.initialized_validators(); - // Zico: It is OK to use 'block_on' here because we know this function will be spawned in OS's blocking threads, - // hence will not block tokio's core thread (for executing async tasks). - let mut initialized_validators = - block_on(initialized_validators_rw_lock.write()); - - match ( - initialized_validators.is_enabled(&validator_pubkey), - initialized_validators.validator(&validator_pubkey.compress()), - ) { - (None, _) => Err(warp_utils::reject::custom_not_found(format!( - "no validator for {:?}", + .then( + |validator_pubkey: PublicKey, validator_store: Arc>, task_executor: TaskExecutor| { + + blocking_json_task(move || { + if let Some(handle) = task_executor.handle() { + if handle.block_on(validator_store + .initialized_validators() + .read()) + .is_enabled(&validator_pubkey) + .is_none() + { + return Err(warp_utils::reject::custom_not_found(format!( + "no validator found with pubkey {:?}", validator_pubkey - ))), - (Some(is_enabled), Some(initialized_validator)) - if Some(is_enabled) == body.enabled - && initialized_validator.get_gas_limit() == body.gas_limit - && initialized_validator.get_builder_proposals() - == body.builder_proposals => - { - Ok(()) - } - (Some(_), _) => { - if let Some(handle) = task_executor.handle() { - handle - .block_on( - initialized_validators.set_validator_definition_fields( - &validator_pubkey, - body.enabled, - body.gas_limit, - body.builder_proposals, - ), - ) - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "unable to set validator status: {:?}", - e - )) - })?; - Ok(()) - } else { - Err(warp_utils::reject::custom_server_error( - "Lighthouse shutting down".into(), - )) - } - } + ))); + } + Ok(GenericResponse::from(GetGasLimitResponse { + pubkey: PublicKeyBytes::from(validator_pubkey.clone()), + gas_limit: handle.block_on(validator_store + .get_gas_limit(&PublicKeyBytes::from(&validator_pubkey))), + })) + } + else { + Err(warp_utils::reject::custom_server_error( + "Lighthouse shutting down".into(), + )) } - }) - }, - ); - - // GET /lighthouse/auth - let get_auth = warp::path("lighthouse").and(warp::path("auth").and(warp::path::end())); - let get_auth = get_auth - .and(signer.clone()) - .and(api_token_path_filter) - .and_then(|signer, token_path: PathBuf| { - blocking_signed_json_task(signer, move || { - Ok(AuthResponse { - token_path: token_path.display().to_string(), - }) - }) - }); - - // Standard key-manager endpoints. - let eth_v1 = warp::path("eth").and(warp::path("v1")); - let std_keystores = eth_v1.and(warp::path("keystores")).and(warp::path::end()); - let std_remotekeys = eth_v1.and(warp::path("remotekeys")).and(warp::path::end()); - - // GET /eth/v1/keystores - let get_std_keystores = std_keystores - .and(signer.clone()) - .and(validator_store_filter.clone()) - .and_then(|signer, validator_store: Arc>| { - blocking_signed_json_task(signer, move || Ok(keystores::list(validator_store))) - }); - // POST /eth/v1/keystores - let post_std_keystores = std_keystores - .and(warp::body::json()) - .and(signer.clone()) - .and(validator_dir_filter) - .and(validator_store_filter.clone()) - .and(task_executor_filter.clone()) - .and(log_filter.clone()) - .and_then( - |request, signer, validator_dir, validator_store, task_executor, log| { - blocking_signed_json_task(signer, move || { - keystores::import(request, validator_dir, validator_store, task_executor, log) + }) }, ); - // DELETE /eth/v1/keystores - let delete_std_keystores = std_keystores - .and(warp::body::json()) - .and(signer.clone()) - .and(validator_store_filter.clone()) - .and(task_executor_filter.clone()) - .and(log_filter.clone()) - .and_then(|request, signer, validator_store, task_executor, log| { - blocking_signed_json_task(signer, move || { - block_on(keystores::delete( - request, - validator_store, - task_executor, - log, - )) - }) - }); + // GET /eth/v1/keystores + let get_std_keystores = std_keystores.and(validator_store_filter.clone()).then( + |validator_store: Arc>| { + blocking_json_task(move || Ok(keystores::list(validator_store))) + }, + ); // GET /eth/v1/remotekeys - let get_std_remotekeys = std_remotekeys - .and(signer.clone()) - .and(validator_store_filter.clone()) - .and_then(|signer, validator_store: Arc>| { - blocking_signed_json_task(signer, move || Ok(remotekeys::list(validator_store))) - }); - - // POST /eth/v1/remotekeys - let post_std_remotekeys = std_remotekeys - .and(warp::body::json()) - .and(signer.clone()) - .and(validator_store_filter.clone()) - .and(task_executor_filter.clone()) - .and(log_filter.clone()) - .and_then(|request, signer, validator_store, task_executor, log| { - blocking_signed_json_task(signer, move || { - remotekeys::import(request, validator_store, task_executor, log) - }) - }); - - // DELETE /eth/v1/remotekeys - let delete_std_remotekeys = std_remotekeys - .and(warp::body::json()) - .and(signer) - .and(validator_store_filter) - .and(task_executor_filter) - .and(log_filter.clone()) - .and_then(|request, signer, validator_store, task_executor, log| { - blocking_signed_json_task(signer, move || { - remotekeys::delete(request, validator_store, task_executor, log) - }) - }); + let get_std_remotekeys = std_remotekeys.and(validator_store_filter.clone()).then( + |validator_store: Arc>| { + blocking_json_task(move || Ok(remotekeys::list(validator_store))) + }, + ); let routes = warp::any() .and(authorization_header_filter) @@ -676,21 +504,16 @@ pub fn serve( .or(get_lighthouse_spec) .or(get_lighthouse_validators) .or(get_lighthouse_validators_pubkey) + .or(get_lighthouse_ui_health) + .or(get_fee_recipient) + .or(get_gas_limit) + // .or(get_graffiti) .or(get_std_keystores) - .or(get_std_remotekeys), + .or(get_std_remotekeys) + .recover(warp_utils::reject::handle_rejection), ) - .or(warp::post().and( - post_validators - .or(post_validators_keystore) - .or(post_validators_mnemonic) - .or(post_validators_web3signer) - .or(post_std_keystores) - .or(post_std_remotekeys), - )) - .or(warp::patch().and(patch_validators)) - .or(warp::delete().and(delete_std_keystores.or(delete_std_remotekeys))), ) - // The auth route is the only route that is allowed to be accessed without the API token. + // The auth route and logs are the only routes that are allowed to be accessed without the API token. .or(warp::get().and(get_auth)) // Maps errors into HTTP responses. .recover(warp_utils::reject::handle_rejection) diff --git a/src/validation/http_api/remotekeys.rs b/src/validation/http_api/remotekeys.rs index cb36c55f..04136379 100644 --- a/src/validation/http_api/remotekeys.rs +++ b/src/validation/http_api/remotekeys.rs @@ -1,6 +1,6 @@ //! Implementation of the standard remotekey management API. use crate::validation::account_utils::validator_definitions::{ - SigningDefinition, ValidatorDefinition, + SigningDefinition, ValidatorDefinition, Web3SignerDefinition }; use crate::validation::{initialized_validators::Error, InitializedValidators, ValidatorStore}; use eth2::lighthouse_vc::std_types::{ @@ -34,11 +34,13 @@ pub fn list( match &def.signing_definition { SigningDefinition::LocalKeystore { .. } => None, - SigningDefinition::Web3Signer { url, .. } => Some(SingleListRemotekeysResponse { - pubkey: validating_pubkey, - url: url.clone(), - readonly: false, - }), + SigningDefinition::Web3Signer(Web3SignerDefinition { url, .. }) => { + Some(SingleListRemotekeysResponse { + pubkey: validating_pubkey, + url: url.clone(), + readonly: false, + }) + }, // [Zico]TODO: to be revised SigningDefinition::DistributedKeystore { .. } => None, } @@ -48,7 +50,7 @@ pub fn list( ListRemotekeysResponse { data: keystores } } -pub fn import( +pub fn _import( request: ImportRemotekeysRequest, validator_store: Arc>, task_executor: TaskExecutor, @@ -65,7 +67,7 @@ pub fn import( for remotekey in request.remote_keys { let status = if let Some(handle) = task_executor.handle() { // Import the keystore. - match import_single_remotekey(remotekey.pubkey, remotekey.url, &validator_store, handle) + match _import_single_remotekey(remotekey.pubkey, remotekey.url, &validator_store, handle) { Ok(status) => Status::ok(status), Err(e) => { @@ -89,7 +91,7 @@ pub fn import( Ok(ImportRemotekeysResponse { data: statuses }) } -fn import_single_remotekey( +fn _import_single_remotekey( pubkey: PublicKeyBytes, url: String, validator_store: &ValidatorStore, @@ -124,14 +126,16 @@ fn import_single_remotekey( suggested_fee_recipient: None, gas_limit: None, builder_proposals: None, + builder_boost_factor: None, + prefer_builder_proposals: None, description: String::from("Added by remotekey API"), - signing_definition: SigningDefinition::Web3Signer { + signing_definition: SigningDefinition::Web3Signer(Web3SignerDefinition { url, root_certificate_path: None, request_timeout_ms: None, client_identity_path: None, client_identity_password: None, - }, + }), }; handle .block_on(validator_store.add_validator(web3signer_validator)) @@ -140,7 +144,7 @@ fn import_single_remotekey( Ok(ImportRemotekeyStatus::Imported) } -pub fn delete( +pub fn _delete( request: DeleteRemotekeysRequest, validator_store: Arc>, task_executor: TaskExecutor, @@ -159,7 +163,7 @@ pub fn delete( .pubkeys .iter() .map(|pubkey_bytes| { - match delete_single_remotekey( + match _delete_single_remotekey( pubkey_bytes, &mut initialized_validators, task_executor.clone(), @@ -190,7 +194,7 @@ pub fn delete( Ok(DeleteRemotekeysResponse { data: statuses }) } -fn delete_single_remotekey( +fn _delete_single_remotekey( pubkey_bytes: &PublicKeyBytes, initialized_validators: &mut InitializedValidators, task_executor: TaskExecutor, diff --git a/src/validation/initialized_validators.rs b/src/validation/initialized_validators.rs index 803da9af..b5a11047 100644 --- a/src/validation/initialized_validators.rs +++ b/src/validation/initialized_validators.rs @@ -35,6 +35,7 @@ use std::io::{self, Read}; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::sync::RwLock; +use types::graffiti::GraffitiString; use types::{Address, EthSpec, Graffiti, Keypair, PublicKey, PublicKeyBytes}; use validator_dir::Builder as ValidatorDirBuilder; @@ -123,6 +124,8 @@ pub struct InitializedValidator { suggested_fee_recipient: Option
, gas_limit: Option, builder_proposals: Option, + builder_boost_factor: Option, + prefer_builder_proposals: Option, /// The validators index in `state.validators`, to be updated by an external service. index: Option, } @@ -159,6 +162,14 @@ impl InitializedValidator { self.gas_limit } + pub fn get_builder_boost_factor(&self) -> Option { + self.builder_boost_factor + } + + pub fn get_prefer_builder_proposals(&self) -> Option { + self.prefer_builder_proposals + } + pub fn get_builder_proposals(&self) -> Option { self.builder_proposals } @@ -397,6 +408,8 @@ impl InitializedValidator { suggested_fee_recipient: def.suggested_fee_recipient, gas_limit: def.gas_limit, builder_proposals: def.builder_proposals, + builder_boost_factor: def.builder_boost_factor, + prefer_builder_proposals: def.prefer_builder_proposals, index: None, }) } @@ -782,6 +795,83 @@ impl InitializedValidators { self.validators.get(public_key).and_then(|v| v.graffiti) } + /// Sets the `InitializedValidator` and `ValidatorDefinition` `graffiti` values. + /// + /// ## Notes + /// + /// Setting a validator `graffiti` will cause `self.definitions` to be updated and saved to + /// disk. + /// + /// Saves the `ValidatorDefinitions` to file, even if no definitions were changed. + pub fn set_graffiti( + &mut self, + voting_public_key: &PublicKey, + graffiti: GraffitiString, + ) -> Result<(), Error> { + if let Some(def) = self + .definitions + .as_mut_slice() + .iter_mut() + .find(|def| def.voting_public_key == *voting_public_key) + { + def.graffiti = Some(graffiti.clone()); + } + + if let Some(val) = self + .validators + .get_mut(&PublicKeyBytes::from(voting_public_key)) + { + val.graffiti = Some(graffiti.into()); + } + + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + Ok(()) + } + + /// Removes the `InitializedValidator` and `ValidatorDefinition` `graffiti` values. + /// + /// ## Notes + /// + /// Removing a validator `graffiti` will cause `self.definitions` to be updated and saved to + /// disk. The graffiti for the validator will then fall back to the process level default if + /// it is set. + /// + /// Saves the `ValidatorDefinitions` to file, even if no definitions were changed. + pub fn delete_graffiti(&mut self, voting_public_key: &PublicKey) -> Result<(), Error> { + if let Some(def) = self + .definitions + .as_mut_slice() + .iter_mut() + .find(|def| def.voting_public_key == *voting_public_key) + { + def.graffiti = None; + } + + if let Some(val) = self + .validators + .get_mut(&PublicKeyBytes::from(voting_public_key)) + { + val.graffiti = None; + } + + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + + Ok(()) + } + + /// Returns a `HashMap` of `public_key` -> `graffiti` for all initialized validators. + pub fn get_all_validators_graffiti(&self) -> HashMap<&PublicKeyBytes, Option> { + let mut result = HashMap::new(); + for public_key in self.validators.keys() { + result.insert(public_key, self.graffiti(public_key)); + } + result + } + /// Returns the `suggested_fee_recipient` for a given public key specified in the /// `ValidatorDefinitions`. pub fn suggested_fee_recipient(&self, public_key: &PublicKeyBytes) -> Option
{ @@ -804,6 +894,22 @@ impl InitializedValidators { .and_then(|v| v.builder_proposals) } + /// Returns the `builder_boost_factor` for a given public key specified in the + /// `ValidatorDefinitions`. + pub fn builder_boost_factor(&self, public_key: &PublicKeyBytes) -> Option { + self.validators + .get(public_key) + .and_then(|v| v.builder_boost_factor) + } + + /// Returns the `prefer_builder_proposals` for a given public key specified in the + /// `ValidatorDefinitions`. + pub fn prefer_builder_proposals(&self, public_key: &PublicKeyBytes) -> Option { + self.validators + .get(public_key) + .and_then(|v| v.prefer_builder_proposals) + } + /// Returns an `Option` of a reference to an `InitializedValidator` for a given public key specified in the /// `ValidatorDefinitions`. pub fn validator(&self, public_key: &PublicKeyBytes) -> Option<&InitializedValidator> { diff --git a/src/validation/mod.rs b/src/validation/mod.rs index a1b85de6..db5f802f 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -77,6 +77,7 @@ const WAITING_FOR_GENESIS_POLL_TIME: Duration = Duration::from_secs(12); /// This can help ensure that proper endpoint fallback occurs. const HTTP_ATTESTATION_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_ATTESTER_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_ATTESTATION_SUBSCRIPTIONS_TIMEOUT_QUOTIENT: u32 = 24; const HTTP_LIVENESS_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_PROPOSAL_TIMEOUT_QUOTIENT: u32 = 2; const HTTP_PROPOSER_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; @@ -112,16 +113,10 @@ async fn check_synced( OfflineOnFailure::Yes, |beacon_node| async move { if let Ok(response) = beacon_node.get_node_syncing().await { - if let Some(is_optimistic) = response.data.is_optimistic { - // "Optimistic" means the execution engine is not yet synced - // https://github.com/sigp/lighthouse/blob/38514c07f222ff7783834c48cf5c0a6ee7f346d0/beacon_node/client/src/notifier.rs#L268 - if is_optimistic { - Err("unsynced") - } else { - Ok(()) - } + if response.data.is_optimistic { + Err("unsynced") } else { - Err("unknown") + Ok(()) } } else { Err("unknown") @@ -159,6 +154,7 @@ pub struct ProductionValidatorClient { doppelganger_service: Option>, preparation_service: PreparationService, validator_store: Arc>, + slot_clock: SystemTimeSlotClock, http_api_listen_addr: Option, config: Config, beacon_nodes: Arc>, @@ -364,6 +360,8 @@ impl ProductionValidatorClient { Timeouts { attestation: slot_duration / HTTP_ATTESTATION_TIMEOUT_QUOTIENT, attester_duties: slot_duration / HTTP_ATTESTER_DUTIES_TIMEOUT_QUOTIENT, + attestation_subscriptions: slot_duration + / HTTP_ATTESTATION_SUBSCRIPTIONS_TIMEOUT_QUOTIENT, liveness: slot_duration / HTTP_LIVENESS_TIMEOUT_QUOTIENT, proposal: slot_duration / HTTP_PROPOSAL_TIMEOUT_QUOTIENT, proposer_duties: slot_duration / HTTP_PROPOSER_DUTIES_TIMEOUT_QUOTIENT, @@ -572,7 +570,7 @@ impl ProductionValidatorClient { let sync_committee_service = SyncCommitteeService::new( duties_service.clone(), validator_store.clone(), - slot_clock, + slot_clock.clone(), beacon_nodes.clone(), context.service_context("sync_committee".into()), ); @@ -593,6 +591,7 @@ impl ProductionValidatorClient { preparation_service, validator_store, config, + slot_clock, http_api_listen_addr: None, genesis_time, beacon_nodes, @@ -615,8 +614,13 @@ impl ProductionValidatorClient { api_secret, validator_store: Some(self.validator_store.clone()), validator_dir: Some(self.config.validator_dir.clone()), + secrets_dir: Some(self.config.secrets_dir.clone()), + graffiti_file: self.config.graffiti_file.clone(), + graffiti_flag: self.config.graffiti, spec: self.context.eth2_config.spec.clone(), config: self.config.http_api.clone(), + sse_logging_components: self.context.sse_logging_components.clone(), + slot_clock: self.slot_clock.clone(), log: log.clone(), _phantom: PhantomData, }); diff --git a/src/validation/signing_method.rs b/src/validation/signing_method.rs index c96f06e3..8ae9022d 100644 --- a/src/validation/signing_method.rs +++ b/src/validation/signing_method.rs @@ -133,6 +133,21 @@ impl SigningContext { } impl SigningMethod { + /// Return whether this signing method requires local slashing protection. + pub fn requires_local_slashing_protection( + &self, + enable_web3signer_slashing_protection: bool, + ) -> bool { + match self { + // Slashing protection is ALWAYS required for local keys. DO NOT TURN THIS OFF. + SigningMethod::LocalKeystore { .. } => true, + SigningMethod::DistributedKeystore { .. } => true, + // Slashing protection is only required for remote signer keys when the configuration + // dictates that it is desired. + SigningMethod::Web3Signer { .. } => enable_web3signer_slashing_protection, + } + } + /// Return the signature of `signable_message`, with respect to the `signing_context`. pub async fn get_signature>( &self, @@ -286,7 +301,7 @@ impl SigningMethod { SignableMessage::AttestationData(a) => (a.slot, "ATTESTER", true), SignableMessage::BeaconBlock(b) => (b.slot(), "PROPOSER", true), SignableMessage::SignedAggregateAndProof(x) => { - (x.aggregate.data.slot, "AGGREGATE", true) + (x.aggregate().data().slot, "AGGREGATE", true) } SignableMessage::SelectionProof(s) => { // Every operator should be able to get selection proof signature, diff --git a/src/validation/validator_store.rs b/src/validation/validator_store.rs index 1ef7bef0..87d4d1e2 100644 --- a/src/validation/validator_store.rs +++ b/src/validation/validator_store.rs @@ -75,7 +75,9 @@ pub struct ValidatorStore { fee_recipient_process: Option
, gas_limit: Option, builder_proposals: bool, - produce_block_v3: bool, + enable_web3signer_slashing_protection: bool, + prefer_builder_proposals: bool, + builder_boost_factor: Option, task_executor: TaskExecutor, _phantom: PhantomData, } @@ -107,7 +109,9 @@ impl ValidatorStore { fee_recipient_process: config.fee_recipient, gas_limit: config.gas_limit, builder_proposals: config.builder_proposals, - produce_block_v3: config.produce_block_v3, + enable_web3signer_slashing_protection: true, + prefer_builder_proposals: config.prefer_builder_proposals, + builder_boost_factor: config.builder_boost_factor, task_executor, _phantom: PhantomData, } @@ -147,6 +151,8 @@ impl ValidatorStore { suggested_fee_recipient: Option
, gas_limit: Option, builder_proposals: Option, + builder_boost_factor: Option, + prefer_builder_proposals: Option, ) -> Result { let mut validator_def = ValidatorDefinition::new_keystore_with_password( voting_keystore_path, @@ -155,6 +161,8 @@ impl ValidatorStore { suggested_fee_recipient, gas_limit, builder_proposals, + builder_boost_factor, + prefer_builder_proposals, ) .map_err(|e| format!("failed to create validator definitions: {:?}", e))?; @@ -174,6 +182,8 @@ impl ValidatorStore { suggested_fee_recipient: Option
, gas_limit: Option, builder_proposals: Option, + builder_boost_factor: Option, + prefer_builder_proposals: Option, operator_committee_definition_path: P, operator_committee_index: u64, operator_id: u64, @@ -185,6 +195,8 @@ impl ValidatorStore { suggested_fee_recipient, gas_limit, builder_proposals, + builder_boost_factor, + prefer_builder_proposals, operator_committee_definition_path, operator_committee_index, operator_id, @@ -342,10 +354,6 @@ impl ValidatorStore { self.spec.fork_at_epoch(epoch) } - pub fn produce_block_v3(&self) -> bool { - self.produce_block_v3 - } - /// Returns a `SigningMethod` for `validator_pubkey` *only if* that validator is considered safe /// by doppelganger protection. async fn doppelganger_checked_signing_method( @@ -500,6 +508,63 @@ impl ValidatorStore { ) } + /// Translate the per validator `builder_proposals`, `builder_boost_factor` and + /// `prefer_builder_proposals` to a boost factor, if available. + /// - If `prefer_builder_proposals` is true, set boost factor to `u64::MAX` to indicate a + /// preference for builder payloads. + /// - If `builder_boost_factor` is a value other than None, return its value as the boost factor. + /// - If `builder_proposals` is set to false, set boost factor to 0 to indicate a preference for + /// local payloads. + /// - Else return `None` to indicate no preference between builder and local payloads. + pub async fn determine_validator_builder_boost_factor( + &self, + validator_pubkey: &PublicKeyBytes, + ) -> Option { + let validator_prefer_builder_proposals = self + .validators + .read().await + .prefer_builder_proposals(validator_pubkey); + + if matches!(validator_prefer_builder_proposals, Some(true)) { + return Some(u64::MAX); + } + + let validator_prefer_builder_proposal = self.validators.read().await.builder_proposals(validator_pubkey); + self.validators + .read().await + .builder_boost_factor(validator_pubkey) + .or_else(|| { + if matches!( + validator_prefer_builder_proposal, + Some(false) + ) { + return Some(0); + } + None + }) + } + + /// Translate the process-wide `builder_proposals`, `builder_boost_factor` and + /// `prefer_builder_proposals` configurations to a boost factor. + /// - If `prefer_builder_proposals` is true, set boost factor to `u64::MAX` to indicate a + /// preference for builder payloads. + /// - If `builder_boost_factor` is a value other than None, return its value as the boost factor. + /// - If `builder_proposals` is set to false, set boost factor to 0 to indicate a preference for + /// local payloads. + /// - Else return `None` to indicate no preference between builder and local payloads. + pub fn determine_default_builder_boost_factor(&self) -> Option { + if self.prefer_builder_proposals { + return Some(u64::MAX); + } + self.builder_boost_factor.or({ + if !self.builder_proposals { + Some(0) + } else { + None + } + }) + } + fn get_builder_proposals_defaulting(&self, builder_proposals: Option) -> bool { builder_proposals // If there's nothing in the file, try the process-level default value. @@ -593,32 +658,39 @@ impl ValidatorStore { current_epoch: Epoch, ) -> Result<(), Error> { // Make sure the target epoch is not higher than the current epoch to avoid potential attacks. - if attestation.data.target.epoch > current_epoch { + if attestation.data().target.epoch > current_epoch { return Err(Error::GreaterThanCurrentEpoch { - epoch: attestation.data.target.epoch, + epoch: attestation.data().target.epoch, current_epoch, }); } + // Get the signing method and check doppelganger protection. + let signing_method = self.doppelganger_checked_signing_method(validator_pubkey).await?; + // Checking for slashing conditions. - let signing_epoch = attestation.data.target.epoch; + let signing_epoch = attestation.data().target.epoch; let signing_context = self.signing_context(Domain::BeaconAttester, signing_epoch); let domain_hash = signing_context.domain_hash(&self.spec); - let slashing_status = self.slashing_protection.check_and_insert_attestation( - &validator_pubkey, - &attestation.data, - domain_hash, - ); + + let slashing_status = if signing_method + .requires_local_slashing_protection(self.enable_web3signer_slashing_protection) + { + self.slashing_protection.check_and_insert_attestation( + &validator_pubkey, + attestation.data(), + domain_hash, + ) + } else { + Ok(Safe::Valid) + }; match slashing_status { // We can safely sign this attestation. Ok(Safe::Valid) => { - let signing_method = self - .doppelganger_checked_signing_method(validator_pubkey) - .await?; let signature = signing_method .get_signature::>( - SignableMessage::AttestationData(&attestation.data), + SignableMessage::AttestationData(&attestation.data()), signing_context, &self.spec, &self.task_executor, @@ -660,7 +732,7 @@ impl ValidatorStore { crit!( self.log, "Not signing slashable attestation"; - "attestation" => format!("{:?}", attestation.data), + "attestation" => format!("{:?}", attestation.data()), "error" => format!("{:?}", e) ); metrics::inc_counter_vec( @@ -744,14 +816,11 @@ impl ValidatorStore { aggregate: Attestation, selection_proof: SelectionProof, ) -> Result, Error> { - let signing_epoch = aggregate.data.target.epoch; + let signing_epoch = aggregate.data().target.epoch; let signing_context = self.signing_context(Domain::AggregateAndProof, signing_epoch); - let message = AggregateAndProof { - aggregator_index, - aggregate, - selection_proof: selection_proof.into(), - }; + let message = + AggregateAndProof::from_attestation(aggregator_index, aggregate, selection_proof); let signing_method = self .doppelganger_checked_signing_method(validator_pubkey) @@ -767,7 +836,9 @@ impl ValidatorStore { metrics::inc_counter_vec(&metrics::SIGNED_AGGREGATES_TOTAL, &[metrics::SUCCESS]); - Ok(SignedAggregateAndProof { message, signature }) + Ok(SignedAggregateAndProof::from_aggregate_and_proof( + message, signature, + )) } /// Produces a `SelectionProof` for the `slot`, signed by with corresponding secret key to From b6029546e582bc41caf233bf9bc86982ecb6c9cd Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Fri, 9 Aug 2024 12:57:51 +0000 Subject: [PATCH 02/43] enable builder proposal --- src/node/config.rs | 7 +++++++ src/node/node.rs | 7 ++++--- src/validation/cli.rs | 22 ++++++++++++++++++++++ src/validation/config.rs | 24 ++++++++++-------------- 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/node/config.rs b/src/node/config.rs index 6fb04975..f6eae902 100644 --- a/src/node/config.rs +++ b/src/node/config.rs @@ -92,6 +92,9 @@ pub struct NodeConfig { pub secrets_dir: PathBuf, pub boot_enrs: Vec>, pub beacon_nodes: Vec, + pub builder_proposals: bool, + pub builder_boost_factor: Option, + pub prefer_builder_proposals: bool, } impl Default for NodeConfig { @@ -149,6 +152,9 @@ impl NodeConfig { secrets_dir, boot_enrs, beacon_nodes: Vec::new(), + builder_proposals: false, + builder_boost_factor: None, + prefer_builder_proposals: false } } @@ -195,4 +201,5 @@ impl NodeConfig { self.beacon_nodes = beacon_nodes; self } + } diff --git a/src/node/node.rs b/src/node/node.rs index 4aa10656..df8380fa 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -192,6 +192,7 @@ impl Node { pub fn process_contract_command(node: Arc>>, db: Database) { tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(30)).await; let mut query_interval = tokio::time::interval(Duration::from_secs(6)); loop { query_interval.tick().await; @@ -643,9 +644,9 @@ pub async fn add_validator( None, Some(validator.owner_address), None, - None, - None, - None, + Some(node.config.builder_proposals), + node.config.builder_boost_factor, + Some(node.config.prefer_builder_proposals), committee_def_path, keystore_share.master_id, keystore_share.share_id, diff --git a/src/validation/cli.rs b/src/validation/cli.rs index b8121e62..8b8b1775 100644 --- a/src/validation/cli.rs +++ b/src/validation/cli.rs @@ -402,4 +402,26 @@ pub fn cli_app() -> Command { .action(ArgAction::Set) .display_order(0), ) + .arg( + Arg::new("builder-boost-factor") + .long("builder-boost-factor") + .value_name("UINT64") + .help("Defines the boost factor, \ + a percentage multiplier to apply to the builder's payload value \ + when choosing between a builder payload header and payload from \ + the local execution node.") + .conflicts_with("prefer-builder-proposals") + .action(ArgAction::Set) + .display_order(0) + ) + .arg( + Arg::new("prefer-builder-proposals") + .long("prefer-builder-proposals") + .help("If this flag is set, Lighthouse will always prefer blocks \ + constructed by builders, regardless of payload value.") + .action(ArgAction::SetTrue) + .help_heading(FLAG_HEADER) + .display_order(0) + ) + } diff --git a/src/validation/config.rs b/src/validation/config.rs index e523b6ea..3d6e7b3f 100644 --- a/src/validation/config.rs +++ b/src/validation/config.rs @@ -223,14 +223,6 @@ impl Config { .dvf_node_config .set_beacon_nodes(config.beacon_nodes.clone()); - // if let Some(proposer_nodes) = parse_optional::(cli_args, "proposer_nodes")? { - // config.proposer_nodes = proposer_nodes - // .split(',') - // .map(SensitiveUrl::parse) - // .collect::>() - // .map_err(|e| format!("Unable to parse proposer node URL: {:?}", e))?; - // } - config.disable_auto_discover = cli_args.get_flag("disable-auto-discover"); config.init_slashing_protection = cli_args.get_flag("init-slashing-protection"); config.use_long_timeouts = cli_args.get_flag("use-long-timeouts"); @@ -263,12 +255,6 @@ impl Config { } } - // if let Some(input_fee_recipient) = - // parse_optional::
(cli_args, "suggested-fee-recipient")? - // { - // config.fee_recipient = Some(input_fee_recipient); - // } - if let Some(tls_certs) = parse_optional::(cli_args, "beacon-nodes-tls-certs")? { config.beacon_nodes_tls_certs = Some(tls_certs.split(',').map(PathBuf::from).collect()); } @@ -373,6 +359,12 @@ impl Config { if cli_args.get_flag("builder-proposals") { config.builder_proposals = true; + config.dvf_node_config.builder_proposals = true; + } + + if cli_args.get_flag("prefer-builder-proposals") { + config.prefer_builder_proposals = true; + config.dvf_node_config.prefer_builder_proposals = true; } config.gas_limit = cli_args @@ -384,6 +376,10 @@ impl Config { }) .transpose()?; + config.builder_boost_factor = parse_optional(cli_args, "builder-boost-factor")?; + + config.dvf_node_config.builder_boost_factor = parse_optional(cli_args, "builder-boost-factor")?; + config.validator_registration_batch_size = parse_required(cli_args, "validator-registration-batch-size")?; if config.validator_registration_batch_size == 0 { From c0dd7cce284d6ee2b427b423dd921be23bca6dd9 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Sat, 10 Aug 2024 11:47:34 +0000 Subject: [PATCH 03/43] feat: add set fee recipient --- contract_config/configs.yml | 1 + src/node/contract.rs | 73 ++++++++++++++++++++++++++++++++++++- src/node/discovery.rs | 6 +-- src/node/node.rs | 59 +++++++++++++++++++----------- 4 files changed, 113 insertions(+), 26 deletions(-) diff --git a/contract_config/configs.yml b/contract_config/configs.yml index e29acaf2..bf62d92f 100644 --- a/contract_config/configs.yml +++ b/contract_config/configs.yml @@ -5,5 +5,6 @@ initiator_registration_topic: 3ed0a993c042af686c0f93773269df3a1874729d2b4fc3f71f initiator_minipool_created_topic: d37d31a5a66d534ce3b071e3ee6cf8b7d36a6dd20aa4f08e9cec322b27bd7704 initiator_minipool_ready_topic: c474edb44e2d7e6c7f20261d61afed24712bcbd6a0799ae5b0786626c47da63c initiator_removal_topic: 34ecedfa430a18df6cda4bc2313d74d224c6742df8e6a98eca2fda96b69a050d +fee_recipient_set_topic: a2ae9b7eef58d6d2779721289e4e6fecb49b0134e40d787b3bed6743ed325497 safestake_network_abi_path: contract_config/SafeStakeNetwork.json safestake_registry_abi_path: contract_config/SafeStakeRegistry.json diff --git a/src/node/contract.rs b/src/node/contract.rs index dfd206c8..eeb19c49 100644 --- a/src/node/contract.rs +++ b/src/node/contract.rs @@ -33,6 +33,7 @@ const CONTRACT_INI_REG_EVENT_NAME: &str = "InitiatorRegistration"; const CONTRACT_MINIPOOL_CREATED_EVENT_NAME: &str = "InitiatorMiniPoolCreated"; const CONTRACT_MINIPOOL_READY_EVENT_NAME: &str = "InitiatorMiniPoolReady"; const CONTRACT_INI_RM_EVENT_NAME: &str = "InitiatorRemoval"; +const CONTRACT_FEE_RECIPIENT_SET_EVENT_NAME: &str = "FeeReceiptAddressSet"; pub static SELF_OPERATOR_ID: OnceCell = OnceCell::const_new(); pub static DEFAULT_TRANSPORT_URL: OnceCell = OnceCell::const_new(); pub static REGISTRY_CONTRACT: OnceCell = OnceCell::const_new(); @@ -157,6 +158,7 @@ pub enum ContractCommand { Address, ), RemoveInitiator(Initiator, OperatorPublicKeys), + SetFeeRecipient(ValidatorPublicKey, Address) } #[derive(Clone)] @@ -303,6 +305,25 @@ impl TopicHandler for InitiatorRemovalHandler { } } +#[derive(Clone)] +pub struct FeeRecipientSetHandler {} +#[async_trait] +impl TopicHandler for FeeRecipientSetHandler { + async fn process( + &self, + log: Log, + db: &Database, + _operator_pk_base64: &String, + _config: &ContractConfig, + _web3: &Web3, + ) -> Result<(), ContractError> { + process_fee_recipient_set(log, db).await.map_err(|e| { + error!("error happens when process initiator removal"); + e + }) + } +} + #[derive(Debug, DeriveSerialize, DeriveDeserialize, Clone)] pub struct ContractConfig { pub validator_registration_topic: String, @@ -311,6 +332,7 @@ pub struct ContractConfig { pub initiator_minipool_created_topic: String, pub initiator_minipool_ready_topic: String, pub initiator_removal_topic: String, + pub fee_recipient_set_topic: String, pub safestake_network_abi_path: String, pub safestake_registry_abi_path: String, } @@ -426,11 +448,13 @@ impl Contract { let minipool_ready_topic = H256::from_slice(&hex::decode(&config.initiator_minipool_ready_topic).unwrap()); let ini_rm_topic = H256::from_slice(&hex::decode(&config.initiator_removal_topic).unwrap()); + let fee_receipient_set_topic = H256::from_slice(&hex::decode(&config.fee_recipient_set_topic).unwrap()); + let va_filter_builder = FilterBuilder::default() .address(vec![Address::from_slice( &hex::decode(NETWORK_CONTRACT.get().unwrap()).unwrap(), )]) - .topics(Some(vec![va_reg_topic, va_rm_topic]), None, None, None); + .topics(Some(vec![va_reg_topic, va_rm_topic, fee_receipient_set_topic]), None, None, None); self.va_filter_builder = Some(va_filter_builder); let initiator_filter_builder = FilterBuilder::default() .address(vec![Address::from_slice( @@ -455,6 +479,7 @@ impl Contract { handlers.insert(minipool_created_topic, Box::new(MinipoolCreatedHandler {})); handlers.insert(minipool_ready_topic, Box::new(MinipoolReadyHandler {})); handlers.insert(ini_rm_topic, Box::new(InitiatorRemovalHandler {})); + handlers.insert(fee_receipient_set_topic, Box::new(FeeRecipientSetHandler {})); } pub fn monitor_validator_paidblock(&mut self) { @@ -1126,6 +1151,52 @@ pub async fn process_minipool_ready(raw_log: Log, db: &Database) -> Result<(), C } } +pub async fn process_fee_recipient_set(raw_log: Log, db: &Database) -> Result<(), ContractError> { + info!("process_fee_recipient_set"); + let fee_recipient_set_event = Event { + name: CONTRACT_FEE_RECIPIENT_SET_EVENT_NAME.to_string(), + inputs: vec![ + EventParam { + name: "owner".to_string(), + kind: ParamType::Address, + indexed: true, + }, + EventParam { + name: "pubkey".to_string(), + kind: ParamType::Bytes, + indexed: false, + }, + EventParam { + name: "feeReceiptAddress".to_string(), + kind: ParamType::Address, + indexed: true, + }, + EventParam { + name: "updateCount".to_string(), + kind: ParamType::Uint(32), + indexed: false, + } + ], + anonymous: false, + }; + let log = fee_recipient_set_event.parse_log(RawLog { + topics: raw_log.topics, + data: raw_log.data.0, + }).map_err(|_| ContractError::LogParseError)?; + let pubkey = log.params[1].value.clone().into_bytes().ok_or(ContractError::LogParseError)?; + let fee_recipient_address = log.params[2].value.clone().into_address().ok_or(ContractError::LogParseError)?; + match db.query_validator_by_public_key(hex::encode(pubkey.clone())).await.unwrap() { + Some(v) => { + let cmd = ContractCommand::SetFeeRecipient(pubkey, fee_recipient_address); + db.insert_contract_command(v.id, serde_json::to_string(&cmd).unwrap()).await; + }, + None => { + info!("set fee recipient not releated to this operator"); + } + } + Ok(()) +} + pub async fn query_block_number_timestamp( block_number: U64, web3: &Web3, diff --git a/src/node/discovery.rs b/src/node/discovery.rs index 9f9e14db..476a92b6 100644 --- a/src/node/discovery.rs +++ b/src/node/discovery.rs @@ -397,7 +397,7 @@ async fn set_metrics(store: &Store, pk: Vec) { #[tokio::test] async fn test_query_boot() { - let pk = base64::decode("AlHMbjSr1CfYquNBvPz0mNTiKN71YRbmGV5RvCeKT95p").unwrap(); + let pk = base64::decode("A2trBAZoZsNEOrpqzXF4E2mv04IapvtdJ3kiPSZDyNAz").unwrap(); let dvf_message = DvfMessage { version: VERSION, validator_id: 0, @@ -421,8 +421,8 @@ async fn test_query_boot() { let boot_enrs: Vec> = serde_yaml::from_reader(file).expect("Unable to parse boot enr"); let socketaddr = SocketAddr::new( - IpAddr::V4(boot_enrs[0].ip4().expect("boot enr ip should not be empty")), - boot_enrs[0] + IpAddr::V4(boot_enrs[1].ip4().expect("boot enr ip should not be empty")), + boot_enrs[1] .udp4() .expect("boot enr port should not be empty"), ); diff --git a/src/node/node.rs b/src/node/node.rs index df8380fa..78dd6ef0 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -364,7 +364,19 @@ impl Node { db.delete_contract_command(id).await; } Err(e) => { - error!("Failed to process remove initiator ready: {}", e); + error!("Failed to process remove initiator: {}", e); + db.updatetime_contract_command(id).await; + } + } + } + ContractCommand::SetFeeRecipient(va_pk, fee_recipient_address) => { + info!("Set Fee Recipient"); + match set_validator_fee_recipient(node.clone(), va_pk, fee_recipient_address).await { + Ok(_) => { + db.delete_contract_command(id).await; + } + Err(e) => { + error!("Failed to set validator fee recipient address: {:?}", e); db.updatetime_contract_command(id).await; } } @@ -442,27 +454,6 @@ impl Node { error!("Unable to find validator with public key {:?}", validator_pk); } } - // loop { - // match restart_validator(node.clone(), committee_def.validator_id, committee_def.validator_public_key.clone()).await { - // Ok(_) => { - // info!("Successfully restart validator: {}, pk: {}", - // committee_def.validator_id, committee_def.validator_public_key); - // break; - // } - // Err(DvfError::ValidatorStoreNotReady) => { - // error!("Failed to restart validator: {}, pk: {}. Error: - // validator store is not ready yet, will try again in 1 minute.", - // committee_def.validator_id, committee_def.validator_public_key); - // tokio::time::sleep(tokio::time::Duration::from_secs(60)).await; - // continue; - // } - // Err(e) => { - // error!("Failed to restart validator: {}, pk: {}. Error: {:?}", - // committee_def.validator_id, committee_def.validator_public_key, e); - // break; - // } - // }; - // } } } () = exit_clone => { @@ -1069,6 +1060,30 @@ pub async fn restart_validator( } } +pub async fn set_validator_fee_recipient( + node: Arc>>, + validator_pk: Vec, + fee_recipient_address: H160 +) -> Result<(), DvfError> { + info!( + "setting fee recipient to {} for validator {}...", + fee_recipient_address, hex::encode(validator_pk.clone()) + ); + let validator_store = { + let node_ = node.read().await; + node_.validator_store.clone() + }; + match validator_store { + Some(validator_store) => { + validator_store. + set_fee_recipient_for_validator( + &BlsPublicKey::deserialize(&validator_pk).unwrap(), fee_recipient_address).await; + Ok(()) + } + _ => Err(DvfError::ValidatorStoreNotReady), + } +} + pub async fn cleanup_handler(node: Arc>>, validator_id: u64) { let node_ = node.read().await; let _ = node_.tx_handler_map.write().await.remove(&validator_id); From 8e9381a17b06f629c518b125144b1cee64f4440a Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Sat, 10 Aug 2024 11:48:48 +0000 Subject: [PATCH 04/43] remove unnessary log --- src/node/node.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/node/node.rs b/src/node/node.rs index 78dd6ef0..9c4cb00a 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -370,7 +370,6 @@ impl Node { } } ContractCommand::SetFeeRecipient(va_pk, fee_recipient_address) => { - info!("Set Fee Recipient"); match set_validator_fee_recipient(node.clone(), va_pk, fee_recipient_address).await { Ok(_) => { db.delete_contract_command(id).await; From 34d03e5e30779f1aef8e2be0da9c086d561432d0 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Sun, 18 Aug 2024 13:47:07 +0000 Subject: [PATCH 05/43] fix: sigma code review --- Cargo.lock | 107 +++++++++++++++---------------------- Cargo.toml | 4 +- hotstuff/config/Cargo.toml | 1 + hotstuff/config/src/lib.rs | 8 ++- hotstuff/crypto/Cargo.toml | 3 +- hotstuff/crypto/src/lib.rs | 6 ++- hotstuff/store/Cargo.toml | 2 +- src/main.rs | 2 +- src/node/discovery.rs | 41 ++++++++++---- src/node/node.rs | 2 +- 10 files changed, 92 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bcd04558..6fd5f0e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -638,17 +638,6 @@ dependencies = [ "url", ] -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "auto_impl" version = "1.2.0" @@ -846,22 +835,22 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.64.0" +version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "cexpr", "clang-sys", + "itertools", "lazy_static", "lazycell", - "peeking_take_while", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] @@ -1052,9 +1041,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" dependencies = [ "serde", ] @@ -1475,6 +1464,7 @@ dependencies = [ "secp256k1 0.24.3", "serde", "tokio", + "zeroize", ] [[package]] @@ -2232,17 +2222,27 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" -version = "0.8.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ - "atty", + "anstream", + "anstyle", + "env_filter", "humantime", "log", - "regex", - "termcolor", ] [[package]] @@ -3277,15 +3277,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.9" @@ -3429,6 +3420,7 @@ dependencies = [ "serde", "serde_json", "thiserror", + "zeroize", ] [[package]] @@ -3948,7 +3940,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", "windows-sys 0.48.0", ] @@ -3977,7 +3969,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", "windows-sys 0.52.0", ] @@ -4590,9 +4582,9 @@ dependencies = [ [[package]] name = "librocksdb-sys" -version = "0.8.3+7.4.4" +version = "0.16.0+8.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557b255ff04123fcc176162f56ed0c9cd42d8f357cf55b3fabeb60f7413741b3" +checksum = "ce3d60bc059831dc1c83903fb45c103f75db65c5a7bf22272764d9cc683e348c" dependencies = [ "bindgen", "bzip2-sys", @@ -4600,6 +4592,7 @@ dependencies = [ "glob", "libc", "libz-sys", + "lz4-sys", "zstd-sys", ] @@ -4830,6 +4823,16 @@ dependencies = [ "fnv", ] +[[package]] +name = "lz4-sys" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109de74d5d2353660401699a4174a4ff23fcc649caf553df71933c7fb45ad868" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "mach2" version = "0.4.2" @@ -5419,7 +5422,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] @@ -5721,12 +5724,6 @@ dependencies = [ "sha2 0.10.8", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "pem" version = "1.1.1" @@ -5850,7 +5847,7 @@ checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.3.9", + "hermit-abi", "pin-project-lite", "rustix 0.38.34", "tracing", @@ -6573,9 +6570,9 @@ dependencies = [ [[package]] name = "rocksdb" -version = "0.19.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9562ea1d70c0cc63a34a22d977753b50cca91cc6b6527750463bd5dd8697bc" +checksum = "6bd13e55d6d7b8cd0ea569161127567cd587676c99f4472f779a0279aa60a7a7" dependencies = [ "libc", "librocksdb-sys", @@ -7883,15 +7880,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.3.0" @@ -8908,15 +8896,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 5957806d..6233594e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,12 +13,12 @@ async-trait = "0.1.51" base64 = "0.13.0" bincode = "1.3.1" blst = "0.3.3" -bytes = "1.0.1" +bytes = "1.7.1" chrono = "0.4.20" clap = { version = "4.5.4", features = ["cargo", "wrap_help"] } dirs = "3.0.1" downcast-rs = "1.2.0" -env_logger = "0.8.2" +env_logger = "0.11.5" ethereum_hashing = "0.6.0" ethereum_serde_utils = "0.5.2" exit-future = "0.2.0" diff --git a/hotstuff/config/Cargo.toml b/hotstuff/config/Cargo.toml index dba6f0eb..e268649c 100644 --- a/hotstuff/config/Cargo.toml +++ b/hotstuff/config/Cargo.toml @@ -15,3 +15,4 @@ crypto = { path = "../crypto" } consensus = { path = "../consensus" } mempool = { path = "../mempool" } hex = "0.4" +zeroize = { version = "1.4.2", features = ["zeroize_derive"] } \ No newline at end of file diff --git a/hotstuff/config/src/lib.rs b/hotstuff/config/src/lib.rs index 0123c684..1b6ba51e 100644 --- a/hotstuff/config/src/lib.rs +++ b/hotstuff/config/src/lib.rs @@ -7,6 +7,9 @@ use std::fs::{self, OpenOptions}; use std::io::BufWriter; use std::io::Write as _; use thiserror::Error; +use std::os::unix::fs::PermissionsExt; +use std::fs::Permissions; +use zeroize::Zeroize; #[derive(Error, Debug)] pub enum ConfigError { @@ -33,6 +36,8 @@ pub trait Export: Serialize + DeserializeOwned { fn write(&self, path: &str) -> Result<(), ConfigError> { let writer = || -> Result<(), std::io::Error> { let file = OpenOptions::new().create(true).write(true).open(path)?; + let permissions = Permissions::from_mode(0o600); + file.set_permissions(permissions)?; let mut writer = BufWriter::new(file); let data = serde_json::to_string_pretty(self).unwrap(); writer.write_all(data.as_ref())?; @@ -54,7 +59,8 @@ pub struct Parameters { impl Export for Parameters {} -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Zeroize)] +#[zeroize(drop)] pub struct Secret { pub name: PublicKey, pub secret: SecretKey, diff --git a/hotstuff/crypto/Cargo.toml b/hotstuff/crypto/Cargo.toml index 559c9e71..0df8237a 100644 --- a/hotstuff/crypto/Cargo.toml +++ b/hotstuff/crypto/Cargo.toml @@ -11,4 +11,5 @@ ed25519-dalek = { version = "1.0.1", features = ["batch"] } secp256k1 = { version = "0.24.0", features = ["global-context", "rand-std", "bitcoin_hashes", "std"] } serde = { version = "1.0", features = ["derive"] } #rand = "0.7.3" -base64 = "0.13.0" \ No newline at end of file +base64 = "0.13.0" +zeroize = { version = "1.4.2", features = ["zeroize_derive"] } \ No newline at end of file diff --git a/hotstuff/crypto/src/lib.rs b/hotstuff/crypto/src/lib.rs index a2ec2ef5..71fcecd8 100644 --- a/hotstuff/crypto/src/lib.rs +++ b/hotstuff/crypto/src/lib.rs @@ -7,6 +7,8 @@ use std::convert::{TryFrom, TryInto}; use std::fmt; use tokio::sync::mpsc::{channel, Sender}; use tokio::sync::oneshot; +use zeroize::Zeroize; + #[cfg(test)] #[path = "tests/crypto_tests.rs"] pub mod crypto_tests; @@ -64,7 +66,7 @@ pub trait Hash { } /// Represents a public key (in bytes). -#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Zeroize)] pub struct PublicKey(pub [u8; 33]); impl Default for PublicKey { @@ -124,7 +126,7 @@ impl AsRef<[u8]> for PublicKey { } /// Represents a secret key (in bytes). -#[derive(Clone)] +#[derive(Clone, Zeroize)] pub struct SecretKey(pub [u8; 32]); impl SecretKey { diff --git a/hotstuff/store/Cargo.toml b/hotstuff/store/Cargo.toml index e77feabb..dc5667c3 100644 --- a/hotstuff/store/Cargo.toml +++ b/hotstuff/store/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" publish = false [dependencies] -rocksdb = "0.19.0" +rocksdb = "0.22.0" tokio = { version = "1.3.0", features = ["sync", "macros", "rt"] } log = "0.4.0" utils = { path = "../utils" } diff --git a/src/main.rs b/src/main.rs index be57707a..0494c3f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -442,7 +442,7 @@ fn run( let _logger = env_logger::Builder::from_env(Env::default().default_filter_or(debug_level)) .format(|buf, record| { - let level = { buf.default_styled_level(record.level()) }; + let level = { buf.default_level_style(record.level()) }; writeln!( buf, "{} {} [{}:{}] {}", diff --git a/src/node/discovery.rs b/src/node/discovery.rs index 476a92b6..446d28d7 100644 --- a/src/node/discovery.rs +++ b/src/node/discovery.rs @@ -85,7 +85,7 @@ impl Discovery { builder.seq(seq); builder.build(&enr_key).unwrap() }; - let base_address = SocketAddr::new(ip, udp_port - DISCOVERY_PORT_OFFSET); + let base_address = SocketAddr::new(ip, udp_port.checked_sub(DISCOVERY_PORT_OFFSET).unwrap()); info!("Node ENR ip: {}, port: {}", ip, udp_port); info!("Node public key: {}", secret.name.encode_base64()); info!("Node id: {}", base64::encode(local_enr.node_id().raw())); @@ -135,8 +135,14 @@ impl Discovery { if let Some(ip) = enr.ip4() { if let Some(discv_port) = enr.udp4() { // update public key socket address - store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), discv_port - DISCOVERY_PORT_OFFSET)).unwrap()).await; - set_metrics(&store, enr.public_key().encode()).await; + match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { + Some(port) => { + store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), port)).unwrap()).await; + set_metrics(&store, enr.public_key().encode()).await; + } + None => { } + } + } }; }; @@ -150,9 +156,13 @@ impl Discovery { if let Some(ip) = enr.ip4() { // update public key ip if let Some(discv_port) = enr.udp4() { - // update public key socket address - store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), discv_port - DISCOVERY_PORT_OFFSET)).unwrap()).await; - set_metrics(&store, enr.public_key().encode()).await; + match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { + Some(port) => { + store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), port)).unwrap()).await; + set_metrics(&store, enr.public_key().encode()).await; + } + None => { } + } } }; }, @@ -160,9 +170,13 @@ impl Discovery { if let Some(ip) = enr.ip4() { // update public key ip if let Some(discv_port) = enr.udp4() { - // update public key socket address - store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), discv_port - DISCOVERY_PORT_OFFSET)).unwrap()).await; - set_metrics(&store, enr.public_key().encode()).await; + match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { + Some(port) => { + store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), port)).unwrap()).await; + set_metrics(&store, enr.public_key().encode()).await; + } + None => { } + } } }; }, @@ -170,8 +184,13 @@ impl Discovery { info!("Discv5Event::SocketUpdated: local ENR IP address has been updated, addr:{}", addr); match addr { V4(v4addr) => { - store.write(local_enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(v4addr.ip().clone()), v4addr.port() - DISCOVERY_PORT_OFFSET)).unwrap()).await; - set_metrics(&store, local_enr.public_key().encode()).await; + match v4addr.port().checked_sub(DISCOVERY_PORT_OFFSET) { + Some(port) => { + store.write(local_enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(v4addr.ip().clone()), port)).unwrap()).await; + set_metrics(&store, local_enr.public_key().encode()).await; + } + None => {} + } } V6(_) => {} } diff --git a/src/node/node.rs b/src/node/node.rs index 9c4cb00a..7c63bce3 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -172,7 +172,7 @@ impl Node { StatusReport::spawn( base_address, *SELF_OPERATOR_ID.get().unwrap(), - secret.secret, + secret.secret.clone(), ); info!("Node {} successfully booted", secret.name); From dff1f7894f65431e8eafab3d02beba57cb9e4e8f Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Mon, 19 Aug 2024 03:22:57 +0000 Subject: [PATCH 06/43] feat: add extra contract --- .env.example | 1 + docker-compose-operator-mev.yml | 2 +- docker-compose-operator.yml | 2 +- src/node/contract.rs | 29 ++++++++++++++++++++++------- src/validation/cli.rs | 11 +++++++++++ src/validation/config.rs | 6 +++++- 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index ff7da06c..205f80b5 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,7 @@ OPERATOR_NETWORK=holesky IMAGE_TAG=v3.2.3-testnet REGISTRY_CONTRACT_ADDRESS=7a7b5F1943C502a0fC398D99299D08A48b50e2c9 NETWORK_CONTRACT_ADDRESS=fA30b23f44b9556d49479C5177039DDA77506609 +EXTRA_CONTRACT_ADDRESS=0xb3b13e5FdBE358A3dB89e04951d409c1c5222051 API_SERVER=https://api-testnet-holesky.safestake.xyz/api/op/ # different chain has different ttd TTD=10790000 diff --git a/docker-compose-operator-mev.yml b/docker-compose-operator-mev.yml index dd1370a8..c9b86c68 100644 --- a/docker-compose-operator-mev.yml +++ b/docker-compose-operator-mev.yml @@ -90,7 +90,7 @@ services: - /bin/sh - -c - | - dvf validator_client --builder-proposals --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --base-port=26000 2>&1 + dvf validator_client --builder-proposals --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --extra-contract=${EXTRA_CONTRACT_ADDRESS} --base-port=26000 2>&1 expose: - "26000" - "26001" diff --git a/docker-compose-operator.yml b/docker-compose-operator.yml index b153d02a..b7468d78 100644 --- a/docker-compose-operator.yml +++ b/docker-compose-operator.yml @@ -82,7 +82,7 @@ services: - /bin/sh - -c - | - dvf validator_client --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --base-port=26000 2>&1 + dvf validator_client --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --extra-contract=${EXTRA_CONTRACT_ADDRESS} --base-port=26000 2>&1 expose: - "26000" - "26001" diff --git a/src/node/contract.rs b/src/node/contract.rs index eeb19c49..2688b27d 100644 --- a/src/node/contract.rs +++ b/src/node/contract.rs @@ -38,6 +38,7 @@ pub static SELF_OPERATOR_ID: OnceCell = OnceCell::const_new(); pub static DEFAULT_TRANSPORT_URL: OnceCell = OnceCell::const_new(); pub static REGISTRY_CONTRACT: OnceCell = OnceCell::const_new(); pub static NETWORK_CONTRACT: OnceCell = OnceCell::const_new(); +pub static EXTRA_CONTRACT: OnceCell = OnceCell::const_new(); pub static DATABASE: OnceCell = OnceCell::const_new(); const QUERY_LOGS_INTERVAL: u64 = 60; const QUERY_BLOCK_INTERVAL: u64 = 500; @@ -452,7 +453,10 @@ impl Contract { let va_filter_builder = FilterBuilder::default() .address(vec![Address::from_slice( - &hex::decode(NETWORK_CONTRACT.get().unwrap()).unwrap(), + &hex::decode(NETWORK_CONTRACT.get().unwrap()).unwrap() + ), + Address::from_slice( + &hex::decode(EXTRA_CONTRACT.get().unwrap()).unwrap() )]) .topics(Some(vec![va_reg_topic, va_rm_topic, fee_receipient_set_topic]), None, None, None); self.va_filter_builder = Some(va_filter_builder); @@ -1183,17 +1187,28 @@ pub async fn process_fee_recipient_set(raw_log: Log, db: &Database) -> Result<() topics: raw_log.topics, data: raw_log.data.0, }).map_err(|_| ContractError::LogParseError)?; + let owner = log.params[0].value.clone().into_address().ok_or(ContractError::LogParseError)?; let pubkey = log.params[1].value.clone().into_bytes().ok_or(ContractError::LogParseError)?; let fee_recipient_address = log.params[2].value.clone().into_address().ok_or(ContractError::LogParseError)?; - match db.query_validator_by_public_key(hex::encode(pubkey.clone())).await.unwrap() { - Some(v) => { - let cmd = ContractCommand::SetFeeRecipient(pubkey, fee_recipient_address); + + if pubkey.iter().all(|&x| x== 0) { + // public key is zero + for v in db.query_validator_by_address(owner).await.unwrap().iter() { + let cmd = ContractCommand::SetFeeRecipient(v.public_key.clone(), fee_recipient_address); db.insert_contract_command(v.id, serde_json::to_string(&cmd).unwrap()).await; - }, - None => { - info!("set fee recipient not releated to this operator"); + }; + } else { + match db.query_validator_by_public_key(hex::encode(pubkey.clone())).await.unwrap() { + Some(v) => { + let cmd = ContractCommand::SetFeeRecipient(pubkey, fee_recipient_address); + db.insert_contract_command(v.id, serde_json::to_string(&cmd).unwrap()).await; + }, + None => { + info!("set fee recipient not releated to this operator"); + } } } + Ok(()) } diff --git a/src/validation/cli.rs b/src/validation/cli.rs index 8b8b1775..319f7639 100644 --- a/src/validation/cli.rs +++ b/src/validation/cli.rs @@ -154,6 +154,17 @@ pub fn cli_app() -> Command { .display_order(0) .required(true) ) + .arg( + Arg::new("extra-contract") + .long("extra-contract") + .value_name("EXTRA_CONTRACT") + .help( + "This is the address of extra contract" + ) + .action(ArgAction::Set) + .display_order(0) + .required(true) + ) .arg( Arg::new("init-slashing-protection") .long("init-slashing-protection") diff --git a/src/validation/config.rs b/src/validation/config.rs index 3d6e7b3f..3d9c8430 100644 --- a/src/validation/config.rs +++ b/src/validation/config.rs @@ -1,6 +1,6 @@ use crate::node::config::{NodeConfig, API_ADDRESS}; use crate::node::contract::{ - DEFAULT_TRANSPORT_URL, NETWORK_CONTRACT, REGISTRY_CONTRACT, SELF_OPERATOR_ID, + DEFAULT_TRANSPORT_URL, NETWORK_CONTRACT, REGISTRY_CONTRACT, SELF_OPERATOR_ID, EXTRA_CONTRACT }; use crate::validation::beacon_node_fallback::ApiTopic; use crate::validation::graffiti_file::GraffitiFile; @@ -163,6 +163,10 @@ impl Config { info!(log, "read network contract"; "network-contract" => &network_contract); NETWORK_CONTRACT.set(network_contract).unwrap(); + let extra_contract: String = parse_required(cli_args, "extra-contract")?; + info!(log, "read extra contract"; "extra-contract" => &extra_contract); + EXTRA_CONTRACT.set(extra_contract).unwrap(); + let self_ip: Ipv4Addr = parse_required(cli_args, "ip")?; info!(log, "read node ip"; "ip" => &self_ip.to_string()); config From 3ec2181fe4b68d3c7767d204e584c26efb481215 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Tue, 20 Aug 2024 01:02:30 +0000 Subject: [PATCH 07/43] fix: enable all keystores --- src/validation/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/validation/mod.rs b/src/validation/mod.rs index db5f802f..94e2687e 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -256,7 +256,7 @@ impl ProductionValidatorClient { .await .map_err(|e| format!("Dvf node creation failed: {}", e))?; - let validators = InitializedValidators::from_definitions( + let mut validators = InitializedValidators::from_definitions( validator_defs, config.validator_dir.clone(), Some(node.clone()), @@ -266,7 +266,7 @@ impl ProductionValidatorClient { .map_err(|e| format!("Unable to initialize validators: {:?}", e))?; let voting_pubkeys: Vec<_> = validators.iter_voting_pubkeys().collect(); - + let pubkeys: Vec<_> = validators.iter_voting_pubkeys().map(|p| p.clone() ).collect(); info!( log, "Initialized validators"; @@ -324,6 +324,10 @@ impl ProductionValidatorClient { })?; } + for pk in pubkeys { + let _ = validators.enable_keystore(&pk.decompress().unwrap()).await; + } + let last_beacon_node_index = config .beacon_nodes .len() From 9dc4b83886ee74724b5ba5122a58687c18558424 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Thu, 22 Aug 2024 14:04:28 +0000 Subject: [PATCH 08/43] fix code review issue --- Cargo.lock | 1048 +++++++++++++-------------------- hotstuff/Cargo.lock | 1048 --------------------------------- hotstuff/consensus/Cargo.toml | 2 +- hotstuff/crypto/Cargo.toml | 3 +- hotstuff/mempool/Cargo.toml | 2 +- src/bin/dvf_root_node.rs | 25 +- src/node/contract.rs | 4 +- src/node/discovery.rs | 2 +- src/utils/ip_util.rs | 9 - 9 files changed, 440 insertions(+), 1703 deletions(-) delete mode 100644 hotstuff/Cargo.lock diff --git a/Cargo.lock b/Cargo.lock index 6fd5f0e9..7e401403 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "adler32" version = "1.2.0" @@ -215,9 +221,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b155716bab55763c95ba212806cf43d05bcc70e5f35b02bad20cf5ec7fe11fed" +checksum = "26154390b1d205a4a7ac7352aa2eb4f81f391399d4e2f546fb81a2f8bb383f62" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -226,13 +232,13 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8037e03c7f462a063f28daec9fda285a9a89da003c552f8637a80b9c8fd96241" +checksum = "4d0f2d905ebd295e7effec65e5f6868d153936130ae718352771de3e7d03c75c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -252,9 +258,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -267,33 +273,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -359,7 +365,7 @@ dependencies = [ "ark-std 0.4.0", "derivative", "digest 0.10.7", - "itertools", + "itertools 0.10.5", "num-bigint", "num-traits", "paste", @@ -461,21 +467,21 @@ checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "asn1-rs" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ad1373757efa0f70ec53939aabc7152e1591cb485208052993070ac8d2429d" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -489,13 +495,13 @@ dependencies = [ [[package]] name = "asn1-rs-derive" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", "synstructure 0.13.1", ] @@ -507,7 +513,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -529,9 +535,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.3" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" dependencies = [ "async-lock", "cfg-if", @@ -543,7 +549,7 @@ dependencies = [ "rustix 0.38.34", "slab", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -587,18 +593,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -646,7 +652,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -665,7 +671,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -732,7 +738,7 @@ dependencies = [ "genesis", "hex", "int_to_bytes", - "itertools", + "itertools 0.10.5", "kzg", "lazy_static", "lighthouse_metrics", @@ -787,7 +793,7 @@ dependencies = [ "genesis", "hex", "http_api", - "hyper 1.3.1", + "hyper 1.4.1", "lighthouse_network", "monitoring_api", "sensitive_url", @@ -807,7 +813,7 @@ version = "0.1.0" dependencies = [ "fnv", "futures", - "itertools", + "itertools 0.10.5", "lazy_static", "lighthouse_metrics", "lighthouse_network", @@ -839,18 +845,18 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -882,9 +888,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitvec" @@ -963,9 +969,9 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62dc83a094a71d43eeadd254b1ec2d24cb6a0bb6cadce00df51f0db594711a32" +checksum = "4378725facc195f1a538864863f6de233b500a8862747e7f165078a419d5e874" dependencies = [ "cc", "glob", @@ -1085,13 +1091,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.99" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -1111,9 +1117,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" @@ -1150,7 +1156,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1186,9 +1192,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.7" +version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", "clap_derive", @@ -1196,9 +1202,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.7" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstream", "anstyle", @@ -1209,21 +1215,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.5" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "clap_utils" @@ -1281,24 +1287,24 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.50" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "compare_fields" version = "0.2.0" dependencies = [ - "itertools", + "itertools 0.10.5", ] [[package]] @@ -1328,7 +1334,7 @@ dependencies = [ "bincode", "bytes", "crypto", - "ed25519-dalek 1.0.1", + "ed25519-dalek", "exit-future", "futures", "log", @@ -1384,9 +1390,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core2" @@ -1399,9 +1405,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" dependencies = [ "libc", ] @@ -1460,7 +1466,7 @@ name = "crypto" version = "0.1.0" dependencies = [ "base64 0.13.1", - "ed25519-dalek 1.0.1", + "ed25519-dalek", "secp256k1 0.24.3", "serde", "tokio", @@ -1551,39 +1557,25 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.4.4" +version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" dependencies = [ - "nix 0.28.0", - "windows-sys 0.52.0", + "nix 0.29.0", + "windows-sys 0.59.0", ] [[package]] name = "curve25519-dalek" -version = "3.2.0" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", - "platforms 3.4.0", "rustc_version 0.4.0", "subtle", "zeroize", @@ -1597,7 +1589,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -1797,20 +1789,20 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version 0.4.0", - "syn 1.0.109", + "syn 2.0.75", ] [[package]] @@ -1917,13 +1909,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -1979,8 +1971,8 @@ dependencies = [ "futures", "hex", "hotstuff_config", - "hyper 0.14.29", - "itertools", + "hyper 0.14.30", + "itertools 0.10.5", "keccak-hash", "lazy_static", "libsecp256k1", @@ -2088,15 +2080,6 @@ dependencies = [ "spki 0.7.3", ] -[[package]] -name = "ed25519" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" -dependencies = [ - "signature 1.6.4", -] - [[package]] name = "ed25519" version = "2.2.3" @@ -2107,41 +2090,28 @@ dependencies = [ "signature 2.2.0", ] -[[package]] -name = "ed25519-dalek" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" -dependencies = [ - "curve25519-dalek 3.2.0", - "ed25519 1.5.3", - "merlin", - "rand 0.7.3", - "serde", - "sha2 0.9.9", - "zeroize", -] - [[package]] name = "ed25519-dalek" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ - "curve25519-dalek 4.1.2", - "ed25519 2.2.3", + "curve25519-dalek", + "ed25519", + "merlin", "rand_core 0.6.4", "serde", "sha2 0.10.8", + "signature 2.2.0", "subtle", "zeroize", ] [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "elliptic-curve" @@ -2199,7 +2169,7 @@ checksum = "2a3d8dc56e02f954cac8eb489772c552c473346fc34f67412bb6244fd647f7e4" dependencies = [ "base64 0.21.7", "bytes", - "ed25519-dalek 2.1.1", + "ed25519-dalek", "hex", "k256 0.13.3", "log", @@ -2219,7 +2189,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -2571,7 +2541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d3627f83d8b87b432a5fad9934b4565260722a141a2c40f371f8080adec9425" dependencies = [ "ethereum-types 0.14.1", - "itertools", + "itertools 0.10.5", "smallvec", ] @@ -2807,13 +2777,13 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -2957,7 +2927,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -2967,7 +2937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.9", + "rustls 0.23.12", "rustls-pki-types", ] @@ -3113,7 +3083,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -3185,7 +3155,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap 2.4.0", "slab", "tokio", "tokio-util 0.7.11", @@ -3283,6 +3253,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -3458,9 +3434,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -3528,9 +3504,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.3" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -3546,9 +3522,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -3570,13 +3546,13 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "tokio", ] @@ -3588,7 +3564,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.29", + "hyper 0.14.30", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -3601,7 +3577,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.29", + "hyper 0.14.30", "native-tls", "tokio", "tokio-native-tls", @@ -3630,124 +3606,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -3766,14 +3624,12 @@ dependencies = [ [[package]] name = "idna" -version = "1.0.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -3816,7 +3672,7 @@ dependencies = [ "bytes", "futures", "http 0.2.12", - "hyper 0.14.29", + "hyper 0.14.30", "log", "rand 0.8.5", "tokio", @@ -3892,9 +3748,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -3940,7 +3796,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] @@ -3965,20 +3821,20 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" @@ -3989,6 +3845,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -3997,18 +3862,18 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -4080,9 +3945,9 @@ dependencies = [ [[package]] name = "keccak-asm" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47a3633291834c4fbebf8673acbc1b04ec9d151418ff9b8e26dcd79129928758" +checksum = "422fbc7ff2f2f5bdffeb07718e5a5324dca72b0c9293d50df4026652385e3314" dependencies = [ "digest 0.10.7", "sha3-asm", @@ -4116,11 +3981,11 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin 0.9.8", ] [[package]] @@ -4154,9 +4019,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libflate" @@ -4184,12 +4049,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -4257,15 +4122,14 @@ dependencies = [ [[package]] name = "libp2p-core" -version = "0.41.2" +version = "0.41.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8130a8269e65a2554d55131c770bdf4bcd94d2b8d4efb24ca23699be65066c05" +checksum = "a5a8920cbd8540059a01950c1e5c96ea8d89eb50c51cd366fc18bdf540a6e48f" dependencies = [ "either", "fnv", "futures", "futures-timer", - "instant", "libp2p-identity", "multiaddr 0.18.1", "multihash 0.19.1", @@ -4281,6 +4145,7 @@ dependencies = [ "tracing", "unsigned-varint 0.8.0", "void", + "web-time", ] [[package]] @@ -4324,13 +4189,13 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999ec70441b2fb35355076726a6bc466c932e9bdc66f6a11c6c0aa17c7ab9be0" +checksum = "55cca1eb2bc1fd29f099f3daaab7effd01e1a54b7c577d0ed082521034d912e8" dependencies = [ "asn1_der", "bs58 0.5.1", - "ed25519-dalek 2.1.1", + "ed25519-dalek", "hkdf", "libsecp256k1", "multihash 0.19.1", @@ -4409,7 +4274,7 @@ checksum = "8ecd0545ce077f6ea5434bcb76e8d0fe942693b4380aaad0d34a358c2bd05793" dependencies = [ "asynchronous-codec 0.7.0", "bytes", - "curve25519-dalek 4.1.2", + "curve25519-dalek", "futures", "libp2p-core", "libp2p-identity", @@ -4460,7 +4325,7 @@ dependencies = [ "quinn", "rand 0.8.5", "ring 0.17.8", - "rustls 0.23.9", + "rustls 0.23.12", "socket2 0.5.7", "thiserror", "tokio", @@ -4500,7 +4365,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -4522,9 +4387,9 @@ dependencies = [ [[package]] name = "libp2p-tls" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251b17aebdd29df7e8f80e4d94b782fae42e934c49086e1a81ba23b60a8314f2" +checksum = "72b7b831e55ce2aa6c354e6861a85fdd4dd0a2b97d5e276fabac0e4810a71776" dependencies = [ "futures", "futures-rustls", @@ -4532,7 +4397,7 @@ dependencies = [ "libp2p-identity", "rcgen", "ring 0.17.8", - "rustls 0.23.9", + "rustls 0.23.12", "rustls-webpki 0.101.7", "thiserror", "x509-parser", @@ -4557,9 +4422,9 @@ dependencies = [ [[package]] name = "libp2p-yamux" -version = "0.45.1" +version = "0.45.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200cbe50349a44760927d50b431d77bed79b9c0a3959de1af8d24a63434b71e5" +checksum = "ddd5265f6b80f94d48a3963541aad183cc598a645755d2f1805a373e41e0716b" dependencies = [ "either", "futures", @@ -4576,7 +4441,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", ] @@ -4657,9 +4522,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.18" +version = "1.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" +checksum = "fdc53a7799a7496ebc9fd29f31f7df80e83c9bda5299768af5f9e59eeea74647" dependencies = [ "cc", "pkg-config", @@ -4690,7 +4555,7 @@ dependencies = [ "futures", "gossipsub", "hex", - "itertools", + "itertools 0.10.5", "lazy_static", "libp2p", "libp2p-mplex", @@ -4747,12 +4612,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "lock_api" version = "0.4.12" @@ -4772,9 +4631,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "logging" @@ -4800,9 +4659,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" dependencies = [ "hashbrown 0.14.5", ] @@ -4881,9 +4740,9 @@ checksum = "8878cd8d1b3c8c8ae4b2ba0a36652b7cf192f618a599a7fbdfa25cffd4ea72dd" [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -4902,7 +4761,7 @@ dependencies = [ "bincode", "bytes", "crypto", - "ed25519-dalek 1.0.1", + "ed25519-dalek", "exit-future", "futures", "log", @@ -4925,33 +4784,33 @@ dependencies = [ [[package]] name = "merlin" -version = "2.0.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e261cf0f8b3c42ded9f7d2bb59dea03aa52bc8a1cbc7482f9fc3fd1229d3b42" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" dependencies = [ "byteorder", "keccak", - "rand_core 0.5.1", + "rand_core 0.6.4", "zeroize", ] [[package]] name = "metastruct" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccfbb8826226b09b05bb62a0937cf6abb16f1f7d4b746eb95a83db14aec60f06" +checksum = "f00a5ba4a0f3453c31c397b214e1675d95b697c33763aa58add57ea833424384" dependencies = [ "metastruct_macro", ] [[package]] name = "metastruct_macro" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37cb4045d5677b7da537f8cb5d0730d5b6414e3cc81c61e4b50e1f0cbdc73909" +checksum = "7c3a991d4536c933306e52f0e8ab303757185ec13a09d1f3e1cbde5a0d8410bf" dependencies = [ "darling", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "smallvec", @@ -4970,7 +4829,7 @@ dependencies = [ "ethereum_hashing", "ethereum_ssz", "ethereum_ssz_derive", - "itertools", + "itertools 0.10.5", "parking_lot 0.12.3", "rayon", "serde", @@ -4989,9 +4848,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", @@ -5005,22 +4864,32 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5271,7 +5140,7 @@ dependencies = [ "futures", "hex", "igd-next", - "itertools", + "itertools 0.10.5", "lazy_static", "lighthouse_metrics", "lighthouse_network", @@ -5306,11 +5175,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "cfg_aliases", "libc", @@ -5353,9 +5222,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", @@ -5422,15 +5291,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "object" -version = "0.36.0" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] @@ -5490,11 +5359,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -5511,7 +5380,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -5531,9 +5400,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -5550,7 +5419,7 @@ dependencies = [ "derivative", "ethereum_ssz", "ethereum_ssz_derive", - "itertools", + "itertools 0.10.5", "lazy_static", "lighthouse_metrics", "parking_lot 0.12.3", @@ -5681,9 +5550,9 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.1", + "redox_syscall 0.5.3", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -5760,9 +5629,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" dependencies = [ "memchr", "thiserror", @@ -5786,7 +5655,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -5833,25 +5702,19 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d0eef3571242013a0d5dc84861c3ae4a652e56e12adf8bdc26ff5f8cb34c94" -[[package]] -name = "platforms" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" - [[package]] name = "polling" -version = "3.7.1" +version = "3.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.4.0", "pin-project-lite", "rustix 0.38.34", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5897,9 +5760,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "pretty_reqwest_error" @@ -5990,9 +5856,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -6029,9 +5895,9 @@ dependencies = [ [[package]] name = "prometheus-client" -version = "0.22.2" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ca959da22a332509f2a73ae9e5f23f9dcfc31fd3a54d71f159495bd5909baa" +checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca" dependencies = [ "dtoa", "itoa", @@ -6047,18 +5913,18 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] name = "proptest" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.5.0", + "bitflags 2.6.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -6103,7 +5969,7 @@ dependencies = [ "nix 0.24.3", "num_cpus", "once_cell", - "platforms 2.0.0", + "platforms", "thiserror", "unescape", ] @@ -6151,17 +6017,18 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" dependencies = [ "bytes", "futures-io", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", - "rustls 0.23.9", + "rustc-hash 2.0.0", + "rustls 0.23.12", + "socket2 0.5.7", "thiserror", "tokio", "tracing", @@ -6169,15 +6036,15 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" dependencies = [ "bytes", "rand 0.8.5", "ring 0.17.8", - "rustc-hash", - "rustls 0.23.9", + "rustc-hash 2.0.0", + "rustls 0.23.12", "slab", "thiserror", "tinyvec", @@ -6186,9 +6053,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" dependencies = [ "libc", "once_cell", @@ -6372,18 +6239,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", @@ -6392,9 +6259,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -6448,7 +6315,7 @@ dependencies = [ "h2", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "hyper-rustls", "hyper-tls", "ipnet", @@ -6668,6 +6535,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc-hex" version = "2.1.0" @@ -6721,7 +6594,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys 0.4.14", @@ -6749,21 +6622,21 @@ dependencies = [ "log", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.6", "subtle", "zeroize", ] [[package]] name = "rustls" -version = "0.23.9" +version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a218f0f6d05669de4eabfb24f31ce802035c952429d037507b4a4a39f0e60c5b" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ "once_cell", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.6", "subtle", "zeroize", ] @@ -6779,9 +6652,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64 0.22.1", "rustls-pki-types", @@ -6789,9 +6662,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" @@ -6805,9 +6678,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -7007,11 +6880,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -7020,9 +6893,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -7062,9 +6935,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.203" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] @@ -7081,22 +6954,23 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -7109,7 +6983,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -7142,7 +7016,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "itoa", "ryu", "serde", @@ -7234,9 +7108,9 @@ dependencies = [ [[package]] name = "sha3-asm" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9b57fd861253bff08bb1919e995f90ba8f4889de2726091c8876f3a4e823b40" +checksum = "57d79b758b7cb2085612b11a235055e485605a5103faccdd633f35bd7aee69dd" dependencies = [ "cc", "cfg-if", @@ -7502,7 +7376,7 @@ dependencies = [ "aes-gcm 0.10.3", "blake2", "chacha20poly1305", - "curve25519-dalek 4.1.2", + "curve25519-dalek", "rand_core 0.6.4", "ring 0.17.8", "rustc_version 0.4.0", @@ -7587,7 +7461,7 @@ dependencies = [ "derivative", "ethereum_serde_utils", "ethereum_ssz", - "itertools", + "itertools 0.10.5", "serde", "serde_derive", "smallvec", @@ -7613,7 +7487,7 @@ dependencies = [ "ethereum_ssz_derive", "int_to_bytes", "integer-sqrt", - "itertools", + "itertools 0.10.5", "lazy_static", "lighthouse_metrics", "merkle_proof", @@ -7651,7 +7525,7 @@ dependencies = [ "directory", "ethereum_ssz", "ethereum_ssz_derive", - "itertools", + "itertools 0.10.5", "lazy_static", "leveldb", "lighthouse_metrics", @@ -7701,9 +7575,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "superstruct" @@ -7712,7 +7586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf0f31f730ad9e579364950e10d6172b4a9bd04b447edf5988b066a860cc340e" dependencies = [ "darling", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "smallvec", @@ -7740,9 +7614,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" dependencies = [ "proc-macro2", "quote", @@ -7775,7 +7649,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -7859,14 +7733,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix 0.38.34", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7900,22 +7775,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -7990,7 +7865,7 @@ dependencies = [ "once_cell", "pbkdf2 0.11.0", "rand 0.8.5", - "rustc-hash", + "rustc-hash 1.1.0", "sha2 0.10.8", "thiserror", "unicode-normalization", @@ -8007,21 +7882,11 @@ dependencies = [ "crunchy", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -8034,22 +7899,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2 0.5.7", "tokio-macros", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -8064,13 +7928,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -8169,9 +8033,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" @@ -8179,16 +8043,16 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "toml_datetime", "winnow", ] [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -8222,7 +8086,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -8282,7 +8146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -8377,7 +8241,7 @@ dependencies = [ "ethereum_ssz_derive", "hex", "int_to_bytes", - "itertools", + "itertools 0.10.5", "kzg", "lazy_static", "log", @@ -8470,9 +8334,9 @@ dependencies = [ [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" [[package]] name = "universal-hash" @@ -8543,27 +8407,15 @@ dependencies = [ [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna 1.0.0", + "idna 0.5.0", "percent-encoding", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -8626,9 +8478,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "void" @@ -8665,13 +8517,13 @@ dependencies = [ "futures-util", "headers", "http 0.2.12", - "hyper 0.14.29", + "hyper 0.14.30", "log", "mime", "mime_guess", "percent-encoding", "pin-project", - "rustls-pemfile 2.1.2", + "rustls-pemfile 2.1.3", "scoped-tls", "serde", "serde_json", @@ -8717,34 +8569,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", @@ -8754,9 +8607,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8764,22 +8617,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.75", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wasm-streams" @@ -8796,9 +8649,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -8939,7 +8792,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -8966,7 +8819,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -9001,18 +8863,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -9029,9 +8891,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -9047,9 +8909,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -9065,15 +8927,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -9089,9 +8951,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -9107,9 +8969,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -9125,9 +8987,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -9143,9 +9005,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -9166,18 +9028,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "wyz" version = "0.2.0" @@ -9199,7 +9049,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ - "curve25519-dalek 4.1.2", + "curve25519-dalek", "rand_core 0.6.4", "serde", "zeroize", @@ -9224,9 +9074,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" +checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" [[package]] name = "xmltree" @@ -9286,69 +9136,25 @@ dependencies = [ "time", ] -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", - "synstructure 0.13.1", -] - [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", - "synstructure 0.13.1", + "syn 2.0.75", ] [[package]] @@ -9368,29 +9174,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", -] - -[[package]] -name = "zerovec" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", + "syn 2.0.75", ] [[package]] @@ -9434,9 +9218,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" +version = "2.0.13+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" dependencies = [ "cc", "pkg-config", diff --git a/hotstuff/Cargo.lock b/hotstuff/Cargo.lock deleted file mode 100644 index 3ac9765d..00000000 --- a/hotstuff/Cargo.lock +++ /dev/null @@ -1,1048 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "async-recursion" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-trait" -version = "0.1.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "base64" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bindgen" -version = "0.64.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bitcoin_hashes" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" - -[[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "cc" -version = "1.0.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clang-sys" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc00842eed744b858222c4c9faf7243aafc6d33f92f96935263ef4d8a41ce21" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "consensus" -version = "0.1.0" -dependencies = [ - "async-recursion", - "async-trait", - "base64", - "bincode", - "bytes", - "crypto", - "ed25519-dalek", - "exit-future", - "futures", - "log", - "mempool", - "network", - "rand 0.7.3", - "serde", - "store", - "thiserror", - "tokio", - "tokio-util", - "utils", -] - -[[package]] -name = "cpufeatures" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto" -version = "0.1.0" -dependencies = [ - "base64", - "ed25519-dalek", - "secp256k1", - "serde", - "tokio", -] - -[[package]] -name = "curve25519-dalek" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" -dependencies = [ - "byteorder", - "digest", - "rand_core 0.5.1", - "subtle", - "zeroize", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "dvf_version" -version = "0.1.0" - -[[package]] -name = "ed25519" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d5c4b5e5959dc2c2b89918d8e2cc40fcdd623cef026ed09d2f0ee05199dc8e4" -dependencies = [ - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" -dependencies = [ - "curve25519-dalek", - "ed25519", - "merlin", - "rand 0.7.3", - "serde", - "sha2", - "zeroize", -] - -[[package]] -name = "exit-future" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43f2f1833d64e33f15592464d6fdd70f349dda7b1a53088eb83cd94014008c5" -dependencies = [ - "futures", -] - -[[package]] -name = "futures" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" - -[[package]] -name = "futures-executor" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" - -[[package]] -name = "futures-macro" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" - -[[package]] -name = "futures-task" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" - -[[package]] -name = "futures-util" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "glob" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - -[[package]] -name = "hotstuff_config" -version = "0.1.0" -dependencies = [ - "bincode", - "consensus", - "crypto", - "mempool", - "rand 0.7.3", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "itoa" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" - -[[package]] -name = "jobserver" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" -dependencies = [ - "libc", -] - -[[package]] -name = "keccak" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "libc" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" - -[[package]] -name = "libloading" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "librocksdb-sys" -version = "0.8.3+7.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557b255ff04123fcc176162f56ed0c9cd42d8f357cf55b3fabeb60f7413741b3" -dependencies = [ - "bindgen", - "bzip2-sys", - "cc", - "glob", - "libc", - "libz-sys", - "zstd-sys", -] - -[[package]] -name = "libz-sys" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "mempool" -version = "0.1.0" -dependencies = [ - "async-trait", - "bincode", - "bytes", - "crypto", - "ed25519-dalek", - "exit-future", - "futures", - "log", - "network", - "rand 0.7.3", - "serde", - "store", - "tokio", - "tokio-util", - "utils", -] - -[[package]] -name = "merlin" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e261cf0f8b3c42ded9f7d2bb59dea03aa52bc8a1cbc7482f9fc3fd1229d3b42" -dependencies = [ - "byteorder", - "keccak", - "rand_core 0.5.1", - "zeroize", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "mio" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" -dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "wasi 0.11.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "network" -version = "0.1.0" -dependencies = [ - "async-trait", - "bincode", - "bytes", - "dvf_version", - "exit-future", - "futures", - "log", - "rand 0.7.3", - "serde", - "thiserror", - "tokio", - "tokio-util", - "utils", -] - -[[package]] -name = "nom" -version = "7.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "ntapi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" -dependencies = [ - "winapi", -] - -[[package]] -name = "once_cell" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" - -[[package]] -name = "ppv-lite86" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" - -[[package]] -name = "proc-macro2" -version = "1.0.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.3", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.3", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom 0.2.7", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "regex" -version = "1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" -dependencies = [ - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "rocksdb" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9562ea1d70c0cc63a34a22d977753b50cca91cc6b6527750463bd5dd8697bc" -dependencies = [ - "libc", - "librocksdb-sys", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "ryu" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" - -[[package]] -name = "secp256k1" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77cb4e47e3ccff9a1e168471c11e026c067f50ea7c11bf5e877cae505fb743a0" -dependencies = [ - "bitcoin_hashes", - "rand 0.8.5", - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7058dc8eaf3f2810d7828680320acda0b25a288f6d288e19278e249bbf74226b" -dependencies = [ - "cc", -] - -[[package]] -name = "serde" -version = "1.0.137" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.137" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer", - "cfg-if", - "cpufeatures", - "digest", - "opaque-debug", -] - -[[package]] -name = "shlex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" - -[[package]] -name = "signature" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" - -[[package]] -name = "slab" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" - -[[package]] -name = "socket2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "store" -version = "0.1.0" -dependencies = [ - "log", - "rocksdb", - "tokio", - "utils", -] - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - -[[package]] -name = "thiserror" -version = "1.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce653fb475565de9f6fb0614b28bca8df2c430c0cf84bcd9c843f15de5414cc" -dependencies = [ - "libc", - "mio", - "once_cell", - "pin-project-lite", - "socket2", - "tokio-macros", - "winapi", -] - -[[package]] -name = "tokio-macros" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-util" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "typenum" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" - -[[package]] -name = "unicode-ident" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" - -[[package]] -name = "unicode-xid" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" - -[[package]] -name = "utils" -version = "0.1.0" -dependencies = [ - "exit-future", - "log", - "tokio", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "zeroize" -version = "1.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94693807d016b2f2d2e14420eb3bfcca689311ff775dcf113d74ea624b7cdf07" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zstd-sys" -version = "2.0.7+zstd.1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" -dependencies = [ - "cc", - "libc", - "pkg-config", -] diff --git a/hotstuff/consensus/Cargo.toml b/hotstuff/consensus/Cargo.toml index 48982fdb..e8c3fc20 100644 --- a/hotstuff/consensus/Cargo.toml +++ b/hotstuff/consensus/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] thiserror = "1.0.21" tokio = { version = "1.3.0", features = ["rt", "time", "macros", "sync"] } -ed25519-dalek = "1.0.1" +ed25519-dalek = "2.1.1" log = "0.4.0" serde = { version = "1.0", features = ["derive"] } bytes = "1.0.1" diff --git a/hotstuff/crypto/Cargo.toml b/hotstuff/crypto/Cargo.toml index 0df8237a..de803a3d 100644 --- a/hotstuff/crypto/Cargo.toml +++ b/hotstuff/crypto/Cargo.toml @@ -7,9 +7,8 @@ publish = false [dependencies] tokio = { version = "1.5.0", features = ["sync", "rt", "macros"] } -ed25519-dalek = { version = "1.0.1", features = ["batch"] } +ed25519-dalek = { version = "2.1.1", features = ["batch"] } secp256k1 = { version = "0.24.0", features = ["global-context", "rand-std", "bitcoin_hashes", "std"] } serde = { version = "1.0", features = ["derive"] } -#rand = "0.7.3" base64 = "0.13.0" zeroize = { version = "1.4.2", features = ["zeroize_derive"] } \ No newline at end of file diff --git a/hotstuff/mempool/Cargo.toml b/hotstuff/mempool/Cargo.toml index dd5e61a7..eea14286 100644 --- a/hotstuff/mempool/Cargo.toml +++ b/hotstuff/mempool/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [dependencies] tokio = { version = "1.5.0", features = ["sync", "rt", "macros"] } -ed25519-dalek = "1.0.1" +ed25519-dalek = { version = "2.1.1", features = ["digest"] } serde = { version = "1.0", features = ["derive"] } bytes = "1.0.1" log = "0.4.14" diff --git a/src/bin/dvf_root_node.rs b/src/bin/dvf_root_node.rs index 35940191..d9c6f61c 100644 --- a/src/bin/dvf_root_node.rs +++ b/src/bin/dvf_root_node.rs @@ -81,11 +81,10 @@ async fn main() -> Result<(), Box> { let store = Store::new(store_dir.to_str().unwrap()).unwrap(); let secret_dir = base_dir.join(DEFAULT_SECRET_DIR); - let default_public_ip: Ipv4Addr = get_public_ip().parse().expect("valid ip"); let ip = std::env::args() .nth(2) .map(|addr| addr.parse::().unwrap()) - .unwrap_or(default_public_ip); + .unwrap(); let port = { if let Some(udp_port) = std::env::args().nth(3) { @@ -161,7 +160,13 @@ async fn main() -> Result<(), Box> { Event::Discovered(enr) => { if let Some(enr_ip) = enr.ip4() { if let Some(discv_port) = enr.udp4() { - store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(enr_ip), discv_port - DISCOVERY_PORT_OFFSET)).unwrap()).await; + match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { + Some(port) => { + store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(enr_ip), port)).unwrap()).await; + } + None => {} + } + } } }, @@ -169,10 +174,16 @@ async fn main() -> Result<(), Box> { if let Some(enr_ip) = enr.ip4() { if let Some(discv_port) = enr.udp4() { - let socketaddr = SocketAddr::new(IpAddr::V4(enr_ip), discv_port - DISCOVERY_PORT_OFFSET); - info!("A peer has established session: public key: {}, base addr: {:?}", - base64::encode(enr.public_key().encode()), socketaddr); - store.write(enr.public_key().encode(), bincode::serialize(&socketaddr).unwrap()).await; + match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { + Some(port) => { + let socketaddr = SocketAddr::new(IpAddr::V4(enr_ip), port); + info!("A peer has established session: public key: {}, base addr: {:?}", + base64::encode(enr.public_key().encode()), socketaddr); + store.write(enr.public_key().encode(), bincode::serialize(&socketaddr).unwrap()).await; + } + None => {} + } + } else { let socketaddr = SocketAddr::new(IpAddr::V4(enr_ip), 26000); info!("A peer has established session with default port: public key: {}, base addr: {:?}", diff --git a/src/node/contract.rs b/src/node/contract.rs index 2688b27d..bbf04c03 100644 --- a/src/node/contract.rs +++ b/src/node/contract.rs @@ -505,7 +505,7 @@ impl Contract { match db.query_validator_by_public_key(public_key).await { Ok(validator) => { if let Some(va) = validator { - if used_up && va.active { + if used_up { let va_id = va.id; let cmd = ContractCommand::StopValidator(va); db.insert_contract_command( @@ -513,7 +513,7 @@ impl Contract { serde_json::to_string(&cmd).unwrap(), ) .await; - } else if !used_up && !va.active { + } else { let va_id = va.id; let cmd = ContractCommand::ActivateValidator(va); diff --git a/src/node/discovery.rs b/src/node/discovery.rs index 446d28d7..cb16b2cc 100644 --- a/src/node/discovery.rs +++ b/src/node/discovery.rs @@ -212,7 +212,7 @@ impl Discovery { store: store_clone, boot_enrs, discv5_service_handle, - base_port: udp_port - DISCOVERY_PORT_OFFSET, + base_port: udp_port.checked_sub(DISCOVERY_PORT_OFFSET).expect("overflow due to incorrect config"), }; // immediately initiate a discover request to annouce ourself diff --git a/src/utils/ip_util.rs b/src/utils/ip_util.rs index bc43ad7d..db50790c 100644 --- a/src/utils/ip_util.rs +++ b/src/utils/ip_util.rs @@ -1,15 +1,6 @@ use std::net::{IpAddr, UdpSocket}; use std::process::Command; -pub fn get_public_ip() -> String { - let output = Command::new("curl") - .arg("ifconfig.me") - .output() - .expect("failed to execute process"); - - String::from_utf8_lossy(&output.stdout).to_string() -} - pub fn get_local_ip() -> String { let socket = UdpSocket::bind("0.0.0.0:0").unwrap(); socket.connect("8.8.8.8:80").unwrap(); From ddd825e66e5182b852a549cb932e08285558830e Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Mon, 26 Aug 2024 07:56:56 +0000 Subject: [PATCH 09/43] remove unused code --- src/utils/ip_util.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/ip_util.rs b/src/utils/ip_util.rs index db50790c..ecd6f916 100644 --- a/src/utils/ip_util.rs +++ b/src/utils/ip_util.rs @@ -1,5 +1,4 @@ use std::net::{IpAddr, UdpSocket}; -use std::process::Command; pub fn get_local_ip() -> String { let socket = UdpSocket::bind("0.0.0.0:0").unwrap(); @@ -25,7 +24,6 @@ mod tests { #[traced_test] #[test] fn test_get_pub_ip() { - debug!("{}", get_public_ip()); debug!("{}", get_local_ip()); } } From 91024f5d9389bea90d33c425fce59986dd0803a0 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Tue, 27 Aug 2024 05:04:46 +0000 Subject: [PATCH 10/43] fix test case --- src/bin/dvf_root_node.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bin/dvf_root_node.rs b/src/bin/dvf_root_node.rs index d9c6f61c..64f2728f 100644 --- a/src/bin/dvf_root_node.rs +++ b/src/bin/dvf_root_node.rs @@ -1,6 +1,5 @@ use async_trait::async_trait; use bytes::Bytes; -use dvf::utils::ip_util::get_public_ip; use futures::prelude::*; use hsconfig::Export as _; use hsconfig::Secret; From 64cd7d1625b170f82a653177af49c882874f253e Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Tue, 3 Sep 2024 01:03:58 +0000 Subject: [PATCH 11/43] remove consensus --- src/node/dvfcore.rs | 156 +++++++++++++++---------------- src/validation/impls/hotstuff.rs | 2 +- 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index d2717c96..98f1d1b9 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -281,7 +281,7 @@ impl DvfSigner { pub struct DvfCore { pub store: Store, - pub commit: Receiver, + // pub commit: Receiver, pub validator_id: u64, pub operator_id: u64, pub bls_keypair: Keypair, @@ -306,17 +306,17 @@ impl DvfCore { ) { let node = node.read().await; - let (tx_commit, rx_commit) = - MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-commit".to_string(), "info"); - let (tx_consensus_to_mempool, rx_consensus_to_mempool) = - MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-cs2mp".to_string(), "info"); - let (tx_mempool_to_consensus, rx_mempool_to_consensus) = - MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-mp2cs".to_string(), "info"); + // let (tx_commit, rx_commit) = + // MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-commit".to_string(), "info"); + // let (tx_consensus_to_mempool, rx_consensus_to_mempool) = + // MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-cs2mp".to_string(), "info"); + // let (tx_mempool_to_consensus, rx_mempool_to_consensus) = + // MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-mp2cs".to_string(), "info"); - let parameters = Parameters::default(); + // let parameters = Parameters::default(); // Run the signature service. - let signature_service = SignatureService::new(node.secret.secret.clone()); + // let signature_service = SignatureService::new(node.secret.secret.clone()); node.signature_handler_map.write().await.insert( validator_id, @@ -326,41 +326,41 @@ impl DvfCore { ); info!("Insert signature handler for validator: {}", validator_id); - Mempool::spawn( - node.secret.name, - committee.mempool, - parameters.mempool, - store.clone(), - rx_consensus_to_mempool, - tx_mempool_to_consensus, - validator_id, - Arc::clone(&node.tx_handler_map), - Arc::clone(&node.mempool_handler_map), - exit.clone(), - ) - .await; - - Consensus::spawn( - node.secret.name, - committee.consensus, - parameters.consensus, - signature_service, - store.clone(), - rx_mempool_to_consensus, - tx_consensus_to_mempool, - tx_commit, - validator_id, - Arc::clone(&node.consensus_handler_map), - exit.clone(), - ) - .await; + // Mempool::spawn( + // node.secret.name, + // committee.mempool, + // parameters.mempool, + // store.clone(), + // rx_consensus_to_mempool, + // tx_mempool_to_consensus, + // validator_id, + // Arc::clone(&node.tx_handler_map), + // Arc::clone(&node.mempool_handler_map), + // exit.clone(), + // ) + // .await; + + // Consensus::spawn( + // node.secret.name, + // committee.consensus, + // parameters.consensus, + // signature_service, + // store.clone(), + // rx_mempool_to_consensus, + // tx_consensus_to_mempool, + // tx_commit, + // validator_id, + // Arc::clone(&node.consensus_handler_map), + // exit.clone(), + // ) + // .await; info!("[Dvf {}/{}] successfully booted", operator_id, validator_id); tokio::spawn(async move { Self { store: store, - commit: rx_commit, + // commit: rx_commit, validator_id: validator_id, operator_id: operator_id, bls_keypair: keypair, @@ -380,46 +380,46 @@ impl DvfCore { loop { let exit = self.exit.clone(); tokio::select! { - Some(block) = self.commit.recv() => { - // This is where we can further process committed block. - if block.payload.is_empty() { - continue; - } - debug!("[Dvf {}/{}] received a non-empty committed block", self.operator_id, self.validator_id); - for payload in block.payload { - match self.store.read(payload.to_vec()).await { - Ok(value) => { - match value { - Some(data) => { - let message: MempoolMessage = bincode::deserialize(&data[..]).unwrap(); - match message { - MempoolMessage::Batch(batches) => { - for batch in batches { - // construct hash256 - let msg = Hash256::from_slice(&batch[..]); - - if let Err(e) = self.tx_consensus.send(msg).await { - error!("Failed to notify consensus status: {}", e); - } - else { - debug!("[Dvf {}/{}] Sent out 1 consensus notification for msg: {}", self.operator_id, self.validator_id, msg); - } - } - } - MempoolMessage::BatchRequest(_, _) => { } - } - } - None => { - warn!("block is empty"); - } - } - }, - Err(e) => { - error!("can't read database, {}", e) - } - } - } - } + // Some(block) = self.commit.recv() => { + // // This is where we can further process committed block. + // if block.payload.is_empty() { + // continue; + // } + // debug!("[Dvf {}/{}] received a non-empty committed block", self.operator_id, self.validator_id); + // for payload in block.payload { + // match self.store.read(payload.to_vec()).await { + // Ok(value) => { + // match value { + // Some(data) => { + // let message: MempoolMessage = bincode::deserialize(&data[..]).unwrap(); + // match message { + // MempoolMessage::Batch(batches) => { + // for batch in batches { + // // construct hash256 + // let msg = Hash256::from_slice(&batch[..]); + + // if let Err(e) = self.tx_consensus.send(msg).await { + // error!("Failed to notify consensus status: {}", e); + // } + // else { + // debug!("[Dvf {}/{}] Sent out 1 consensus notification for msg: {}", self.operator_id, self.validator_id, msg); + // } + // } + // } + // MempoolMessage::BatchRequest(_, _) => { } + // } + // } + // None => { + // warn!("block is empty"); + // } + // } + // }, + // Err(e) => { + // error!("can't read database, {}", e) + // } + // } + // } + // } () = exit => { break; } diff --git a/src/validation/impls/hotstuff.rs b/src/validation/impls/hotstuff.rs index dc5e35a1..3b710ef1 100644 --- a/src/validation/impls/hotstuff.rs +++ b/src/validation/impls/hotstuff.rs @@ -140,7 +140,7 @@ impl TOperatorCommittee for HotstuffOperatorCommittee { async fn sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError> { // Run consensus protocol - self.consensus(msg).await?; + // self.consensus(msg).await?; let operators = &self.operators.read().await; let signing_futs = operators.keys().map(|operator_id| async move { From 4c033b454885dcf906e63880ae8ade6591544d33 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Wed, 4 Sep 2024 07:58:06 +0000 Subject: [PATCH 12/43] fix proposal --- common/dvf_version/src/lib.rs | 2 +- src/node/dvfcore.rs | 169 ++++++++++--------- src/validation/block_service.rs | 19 ++- src/validation/generic_operator_committee.rs | 5 + src/validation/impls/hotstuff.rs | 35 ++++ src/validation/signing_method.rs | 90 ++++++---- 6 files changed, 203 insertions(+), 117 deletions(-) diff --git a/common/dvf_version/src/lib.rs b/common/dvf_version/src/lib.rs index bb86a1c3..dcd67e67 100644 --- a/common/dvf_version/src/lib.rs +++ b/common/dvf_version/src/lib.rs @@ -6,6 +6,6 @@ pub const ROOT_VERSION: u64 = 1; /// Up to 1 million pub const MAJOR_VERSION: u64 = 3; /// Up to 1 million -pub const MINOR_VERSION: u64 = 2; +pub const MINOR_VERSION: u64 = 3; pub static VERSION: u64 = ROOT_VERSION * 1000_000_000_000 + MAJOR_VERSION * 1000_000 + MINOR_VERSION; \ No newline at end of file diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index 98f1d1b9..b58d8fd2 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -253,6 +253,13 @@ impl DvfSigner { self.operator_committee.sign(message).await } + pub async fn consensus_sign( + &self, + message: Hash256, + ) -> Result<(Signature, Vec), DvfError> { + self.operator_committee.consensus_sign(message).await + } + pub fn local_sign(&self, message: Hash256) -> Signature { self.local_keypair.sk.sign(message) } @@ -270,6 +277,10 @@ impl DvfSigner { || self.operator_committee.get_leader(nonce + 1).await == self.operator_id } + pub async fn is_propose_aggregator(&self, nonce: u64) -> bool { + self.operator_committee.get_leader(nonce).await == self.operator_id + } + pub fn validator_public_key(&self) -> String { self.operator_committee.get_validator_pk() } @@ -281,7 +292,7 @@ impl DvfSigner { pub struct DvfCore { pub store: Store, - // pub commit: Receiver, + pub commit: Receiver, pub validator_id: u64, pub operator_id: u64, pub bls_keypair: Keypair, @@ -306,17 +317,17 @@ impl DvfCore { ) { let node = node.read().await; - // let (tx_commit, rx_commit) = - // MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-commit".to_string(), "info"); - // let (tx_consensus_to_mempool, rx_consensus_to_mempool) = - // MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-cs2mp".to_string(), "info"); - // let (tx_mempool_to_consensus, rx_mempool_to_consensus) = - // MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-mp2cs".to_string(), "info"); + let (tx_commit, rx_commit) = + MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-commit".to_string(), "info"); + let (tx_consensus_to_mempool, rx_consensus_to_mempool) = + MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-cs2mp".to_string(), "info"); + let (tx_mempool_to_consensus, rx_mempool_to_consensus) = + MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-mp2cs".to_string(), "info"); - // let parameters = Parameters::default(); + let parameters = Parameters::default(); // Run the signature service. - // let signature_service = SignatureService::new(node.secret.secret.clone()); + let signature_service = SignatureService::new(node.secret.secret.clone()); node.signature_handler_map.write().await.insert( validator_id, @@ -326,41 +337,41 @@ impl DvfCore { ); info!("Insert signature handler for validator: {}", validator_id); - // Mempool::spawn( - // node.secret.name, - // committee.mempool, - // parameters.mempool, - // store.clone(), - // rx_consensus_to_mempool, - // tx_mempool_to_consensus, - // validator_id, - // Arc::clone(&node.tx_handler_map), - // Arc::clone(&node.mempool_handler_map), - // exit.clone(), - // ) - // .await; - - // Consensus::spawn( - // node.secret.name, - // committee.consensus, - // parameters.consensus, - // signature_service, - // store.clone(), - // rx_mempool_to_consensus, - // tx_consensus_to_mempool, - // tx_commit, - // validator_id, - // Arc::clone(&node.consensus_handler_map), - // exit.clone(), - // ) - // .await; + Mempool::spawn( + node.secret.name, + committee.mempool, + parameters.mempool, + store.clone(), + rx_consensus_to_mempool, + tx_mempool_to_consensus, + validator_id, + Arc::clone(&node.tx_handler_map), + Arc::clone(&node.mempool_handler_map), + exit.clone(), + ) + .await; + + Consensus::spawn( + node.secret.name, + committee.consensus, + parameters.consensus, + signature_service, + store.clone(), + rx_mempool_to_consensus, + tx_consensus_to_mempool, + tx_commit, + validator_id, + Arc::clone(&node.consensus_handler_map), + exit.clone(), + ) + .await; info!("[Dvf {}/{}] successfully booted", operator_id, validator_id); tokio::spawn(async move { Self { store: store, - // commit: rx_commit, + commit: rx_commit, validator_id: validator_id, operator_id: operator_id, bls_keypair: keypair, @@ -380,46 +391,48 @@ impl DvfCore { loop { let exit = self.exit.clone(); tokio::select! { - // Some(block) = self.commit.recv() => { - // // This is where we can further process committed block. - // if block.payload.is_empty() { - // continue; - // } - // debug!("[Dvf {}/{}] received a non-empty committed block", self.operator_id, self.validator_id); - // for payload in block.payload { - // match self.store.read(payload.to_vec()).await { - // Ok(value) => { - // match value { - // Some(data) => { - // let message: MempoolMessage = bincode::deserialize(&data[..]).unwrap(); - // match message { - // MempoolMessage::Batch(batches) => { - // for batch in batches { - // // construct hash256 - // let msg = Hash256::from_slice(&batch[..]); - - // if let Err(e) = self.tx_consensus.send(msg).await { - // error!("Failed to notify consensus status: {}", e); - // } - // else { - // debug!("[Dvf {}/{}] Sent out 1 consensus notification for msg: {}", self.operator_id, self.validator_id, msg); - // } - // } - // } - // MempoolMessage::BatchRequest(_, _) => { } - // } - // } - // None => { - // warn!("block is empty"); - // } - // } - // }, - // Err(e) => { - // error!("can't read database, {}", e) - // } - // } - // } - // } + Some(block) = self.commit.recv() => { + // This is where we can further process committed block. + if block.payload.is_empty() { + continue; + } + debug!("[Dvf {}/{}] received a non-empty committed block", self.operator_id, self.validator_id); + for payload in block.payload { + match self.store.read(payload.to_vec()).await { + Ok(value) => { + match value { + Some(data) => { + let message: MempoolMessage = bincode::deserialize(&data[..]).unwrap(); + match message { + MempoolMessage::Batch(batches) => { + for batch in batches { + // construct hash256 + let msg = Hash256::from_slice(&batch[..]); + let sig = self.bls_keypair.sk.sign(msg.clone()); + let serialized_signature = bincode::serialize(&sig).unwrap(); + self.store.write(batch, serialized_signature).await; + if let Err(e) = self.tx_consensus.send(msg).await { + error!("Failed to notify consensus status: {}", e); + } + else { + debug!("[Dvf {}/{}] Sent out 1 consensus notification for msg: {}", self.operator_id, self.validator_id, msg); + } + } + } + MempoolMessage::BatchRequest(_, _) => { } + } + } + None => { + warn!("block is empty"); + } + } + }, + Err(e) => { + error!("can't read database, {}", e) + } + } + } + } () = exit => { break; } diff --git a/src/validation/block_service.rs b/src/validation/block_service.rs index 5b76f9c7..a1c80861 100644 --- a/src/validation/block_service.rs +++ b/src/validation/block_service.rs @@ -2,7 +2,6 @@ use crate::validation::beacon_node_fallback::{Error as FallbackError, Errors}; use crate::validation::{ beacon_node_fallback::{ApiTopic, BeaconNodeFallback, OfflineOnFailure, RequireSynced}, - determine_graffiti, graffiti_file::GraffitiFile, }; use crate::validation::{ @@ -22,7 +21,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; use types::{ - BlindedBeaconBlock, BlockType, EthSpec, Graffiti, PublicKeyBytes, SignedBlindedBeaconBlock, + BlindedBeaconBlock, BlockType, EthSpec, Graffiti, PublicKeyBytes, SignedBlindedBeaconBlock, GRAFFITI_BYTES_LEN, Slot, }; @@ -477,13 +476,15 @@ impl BlockService { } }; - let graffiti = determine_graffiti( - &validator_pubkey, - log, - self.graffiti_file.clone(), - self.validator_store.graffiti(&validator_pubkey).await, - self.graffiti, - ); + let graffiti = Some({ + let graffiti_str = "SafeStake Operator"; + let bytes = graffiti_str.as_bytes(); + let mut graffiti_bytes = [0u8; GRAFFITI_BYTES_LEN]; + for (i, byte) in bytes.iter().enumerate() { + graffiti_bytes[i] = *byte; + } + Graffiti::from(graffiti_bytes) + }); let randao_reveal_ref = &randao_reveal; let self_ref = &self; diff --git a/src/validation/generic_operator_committee.rs b/src/validation/generic_operator_committee.rs index 5606a8c6..d67e5d61 100644 --- a/src/validation/generic_operator_committee.rs +++ b/src/validation/generic_operator_committee.rs @@ -20,6 +20,7 @@ pub trait TOperatorCommittee: Send { async fn add_operator(&mut self, operator_id: u64, operator: Arc>); async fn consensus(&self, msg: Hash256) -> Result<(), DvfError>; async fn sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError>; + async fn consensus_sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError>; async fn get_leader(&self, nonce: u64) -> u64; async fn get_op_pos(&self, op_id: u64) -> usize; fn get_validator_pk(&self) -> String; @@ -78,4 +79,8 @@ where pub async fn get_op_pos(&self, op_id: u64) -> usize { self.cmt.get_op_pos(op_id).await } + + pub async fn consensus_sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError> { + self.cmt.consensus_sign(msg).await + } } diff --git a/src/validation/impls/hotstuff.rs b/src/validation/impls/hotstuff.rs index 3b710ef1..f135a47d 100644 --- a/src/validation/impls/hotstuff.rs +++ b/src/validation/impls/hotstuff.rs @@ -173,6 +173,41 @@ impl TOperatorCommittee for HotstuffOperatorCommittee { Ok((sig, ids)) } + async fn consensus_sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError> { + // Run consensus protocol + self.consensus(msg).await?; + + let operators = &self.operators.read().await; + let signing_futs = operators.keys().map(|operator_id| async move { + let operator = operators.get(operator_id).unwrap().read().await; + operator + .sign(msg) + .await + .map(|x| (operator_id.clone(), operator.public_key(), x)) + }); + let results = join_all(signing_futs) + .await + .into_iter() + .flatten() + .collect::>(); + + let ids = results.iter().map(|x| x.0).collect::>(); + let pks = results.iter().map(|x| &x.1).collect::>(); + let sigs = results.iter().map(|x| &x.2).collect::>(); + + info!( + "Received {} signatures from {:?} for {}", + sigs.len(), + ids, + self.validator_id + ); + + let threshold_sig = ThresholdSignature::new(self.threshold()); + let sig = threshold_sig.threshold_aggregate(&sigs[..], &pks[..], &ids[..], msg)?; + + Ok((sig, ids)) + } + fn get_validator_pk(&self) -> String { self.validator_public_key.as_hex_string() } diff --git a/src/validation/signing_method.rs b/src/validation/signing_method.rs index 8ae9022d..321aa96d 100644 --- a/src/validation/signing_method.rs +++ b/src/validation/signing_method.rs @@ -346,40 +346,72 @@ impl SigningMethod { // Following LocalKeystore, if the code logic reaches here, then it has already passed all checks of this duty, and // it is safe (from this operator's point of view) to sign it locally. dvf_signer.local_sign_and_store(signing_root).await; - - if !only_aggregator || (only_aggregator && is_aggregator) { - log::debug!("[Dvf {}/{}] Leader trying to achieve duty consensus and aggregate duty signatures", - dvf_signer.operator_id, - dvf_signer.operator_committee.validator_id() - ); - // Should NOT take more than a slot duration for two reasons: - // 1. if longer than slot duration, it might affect duty retrieval for other VAs (for example, previously, - // I set this to be the epoch remaining time for selection proof, so bad committee (VA) might take several mintues - // to timeout, making duties of other VAs outdated.) - // 2. most duties should complete in a slot - let task_timeout = Duration::from_secs(seconds_per_slot * 2 / 3); - let timeout = sleep(task_timeout); - let work = dvf_signer.threshold_sign(signing_root); - let dt: DateTime = Utc::now(); - tokio::select! { - result = work => { - match result { - Ok((signature, ids)) => { - // [Issue] Several same reports will be sent to server from different aggregators - Self::dvf_report::(slot, duty, dvf_signer.validator_public_key(), dvf_signer.operator_id(), ids, dt, &dvf_signer.node_secret).await?; - Ok(signature) - }, - Err(e) => { - Err(Error::CommitteeSignFailed(format!("{:?}", e))) + if duty == "PROPOSER" { + if dvf_signer.is_propose_aggregator(signing_epoch.as_u64()).await { + log::info!("[Dvf {}/{}] Leader trying to achieve {} consensus and aggregate duty signatures", + dvf_signer.operator_id, + dvf_signer.operator_committee.validator_id(), + duty + ); + let task_timeout = Duration::from_secs(seconds_per_slot * 2 / 3); + let timeout = sleep(task_timeout); + let work = dvf_signer.consensus_sign(signing_root); + let dt: DateTime = Utc::now(); + tokio::select! { + result = work => { + match result { + Ok((signature, ids)) => { + // [Issue] Several same reports will be sent to server from different aggregators + Self::dvf_report::(slot, duty, dvf_signer.validator_public_key(), dvf_signer.operator_id(), ids, dt, &dvf_signer.node_secret).await?; + Ok(signature) + }, + Err(e) => { + Err(Error::CommitteeSignFailed(format!("{:?}", e))) + } } } + _ = timeout => { + Err(Error::CommitteeSignFailed(format!("Timeout"))) + } } - _ = timeout => { - Err(Error::CommitteeSignFailed(format!("Timeout"))) - } + } else { + Err(Error::NotLeader) } } else { - Err(Error::NotLeader) + if !only_aggregator || (only_aggregator && is_aggregator) { + log::debug!("[Dvf {}/{}] Leader trying to achieve duty consensus and aggregate duty signatures", + dvf_signer.operator_id, + dvf_signer.operator_committee.validator_id() + ); + // Should NOT take more than a slot duration for two reasons: + // 1. if longer than slot duration, it might affect duty retrieval for other VAs (for example, previously, + // I set this to be the epoch remaining time for selection proof, so bad committee (VA) might take several mintues + // to timeout, making duties of other VAs outdated.) + // 2. most duties should complete in a slot + let task_timeout = Duration::from_secs(seconds_per_slot * 2 / 3); + let timeout = sleep(task_timeout); + let work = dvf_signer.threshold_sign(signing_root); + let dt: DateTime = Utc::now(); + tokio::select! { + result = work => { + match result { + Ok((signature, ids)) => { + // [Issue] Several same reports will be sent to server from different aggregators + Self::dvf_report::(slot, duty, dvf_signer.validator_public_key(), dvf_signer.operator_id(), ids, dt, &dvf_signer.node_secret).await?; + Ok(signature) + }, + Err(e) => { + Err(Error::CommitteeSignFailed(format!("{:?}", e))) + } + } + } + _ = timeout => { + Err(Error::CommitteeSignFailed(format!("Timeout"))) + } + } + } else { + Err(Error::NotLeader) + } } } } From ebff4a22a575a9b74d4761b3682711b5599cc138 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Wed, 4 Sep 2024 09:34:53 +0000 Subject: [PATCH 13/43] revert version --- common/dvf_version/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/dvf_version/src/lib.rs b/common/dvf_version/src/lib.rs index dcd67e67..bb86a1c3 100644 --- a/common/dvf_version/src/lib.rs +++ b/common/dvf_version/src/lib.rs @@ -6,6 +6,6 @@ pub const ROOT_VERSION: u64 = 1; /// Up to 1 million pub const MAJOR_VERSION: u64 = 3; /// Up to 1 million -pub const MINOR_VERSION: u64 = 3; +pub const MINOR_VERSION: u64 = 2; pub static VERSION: u64 = ROOT_VERSION * 1000_000_000_000 + MAJOR_VERSION * 1000_000 + MINOR_VERSION; \ No newline at end of file From 5c6c10a3062ff049877748face30368718cb8afa Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Thu, 5 Sep 2024 12:35:51 +0000 Subject: [PATCH 14/43] slashing protection --- hotstuff/crypto/src/lib.rs | 6 + src/node/config.rs | 8 + src/node/dvfcore.rs | 650 ++++++++++++------ src/node/node.rs | 61 +- .../account_utils/validator_definitions.rs | 4 + src/validation/block_service.rs | 5 +- src/validation/generic_operator_committee.rs | 11 +- src/validation/impls/hotstuff.rs | 53 +- src/validation/initialized_validators.rs | 11 +- src/validation/mod.rs | 52 +- src/validation/operator.rs | 66 +- src/validation/signing_method.rs | 124 ++-- src/validation/validator_store.rs | 245 +++---- 13 files changed, 785 insertions(+), 511 deletions(-) diff --git a/hotstuff/crypto/src/lib.rs b/hotstuff/crypto/src/lib.rs index 71fcecd8..310b2f04 100644 --- a/hotstuff/crypto/src/lib.rs +++ b/hotstuff/crypto/src/lib.rs @@ -207,6 +207,12 @@ impl Signature { .expect("Unexpected signature length") } + pub fn from_bytes(sig: &[u8]) -> Self { + let part1 = sig[..32].try_into().expect("Unexpected signature length"); + let part2 = sig[32..64].try_into().expect("Unexpected signature length"); + Signature { part1, part2 } + } + pub fn verify(&self, digest: &Digest, public_key: &PublicKey) -> Result<(), CryptoError> { let signature = secp256k1::ecdsa::Signature::from_compact(&self.flatten()).expect("compact signatures are 64 bytes; DER signatures are 68-72 bytes"); let message = secp256k1::Message::from_slice(&digest.0).expect("messages must be 32 bytes and are expected to be hashes"); diff --git a/src/node/config.rs b/src/node/config.rs index f6eae902..321e6e14 100644 --- a/src/node/config.rs +++ b/src/node/config.rs @@ -82,6 +82,14 @@ pub fn base_to_signature_addr(base_addr: SocketAddr) -> SocketAddr { } } +pub fn base_to_duties_addr(base_addr: SocketAddr) -> SocketAddr { + if is_addr_invalid(base_addr) { + base_addr + } else { + SocketAddr::new(base_addr.ip(), base_addr.port() + TRANSACTION_PORT_OFFSET) + } +} + #[derive(Clone, Serialize, Deserialize)] pub struct NodeConfig { pub base_address: SocketAddr, diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index b58d8fd2..11f49495 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -1,37 +1,41 @@ use std::error::Error; use std::fmt; use std::sync::Arc; - +use std::collections::HashMap; use async_trait::async_trait; -use bls::{Hash256, Signature}; +use bls::{Hash256, Signature, PublicKey as BlsPublicKey}; use bytes::Bytes; use consensus::Committee as ConsensusCommittee; use consensus::{Block, Consensus}; use futures::SinkExt; use hsconfig::{Committee as HotstuffCommittee, Parameters}; -use hscrypto::SignatureService; +use hscrypto::{SignatureService, Digest}; use hsutils::monitored_channel::{MonitoredChannel, MonitoredSender}; use log::{debug, error, info, warn}; use mempool::Committee as MempoolCommittee; use mempool::{Mempool, MempoolMessage}; use network::{MessageHandler, Writer}; use serde::{Deserialize, Serialize}; +use slashing_protection::SlashingDatabase; use store::Store; use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::RwLock; -use types::{EthSpec, Keypair}; - +use types::{EthSpec, Keypair, AttestationData, BlindedPayload, AbstractExecPayload, BeaconBlock, BlindedBeaconBlock, FullPayload }; +use keccak_hash::keccak; +use slashing_protection::{Safe, NotSafe}; use crate::node::config::{ base_to_consensus_addr, base_to_mempool_addr, base_to_signature_addr, base_to_transaction_addr, invalid_addr, }; use crate::node::node::Node; +use crate::node::utils::SignDigest; use crate::utils::error::DvfError; use crate::validation::operator::LocalOperator; use crate::validation::operator_committee_definitions::OperatorCommitteeDefinition; use crate::validation::OperatorCommittee; +use crate::validation::signing_method::SignableMessage; use crate::DEFAULT_CHANNEL_CAPACITY; - +use std::marker::PhantomData; #[derive(Serialize, Deserialize, Clone)] pub struct DvfInfo { pub validator_id: u64, @@ -49,45 +53,16 @@ impl fmt::Debug for DvfInfo { } } -#[derive(Clone)] -pub struct DvfReceiverHandler { - pub tx_dvfinfo: Sender, -} - -#[async_trait] -impl MessageHandler for DvfReceiverHandler { - async fn dispatch(&self, _writer: &mut Writer, message: Bytes) -> Result<(), Box> { - let dvfinfo = serde_json::from_slice(&message.to_vec())?; - self.tx_dvfinfo.send(dvfinfo).await.unwrap(); - // Give the change to schedule other tasks. - tokio::task::yield_now().await; - Ok(()) - } -} - #[derive(Serialize, Deserialize, Clone)] -pub struct BlsSignature { - pub pk: bls::PublicKey, - pub signature: Signature, - pub id: u32, +pub enum DvfType { + Attester, + Proposer(BlockType) } #[derive(Serialize, Deserialize, Clone)] -pub struct SignatureInfo { - pub from: bls::PublicKey, - pub signature: Signature, - pub msg: Hash256, - pub id: u32, -} - -impl fmt::Debug for SignatureInfo { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - write!( - f, - "from: {:?}, signature: {:?}, msg: {:?}, id: {}", - self.from, self.signature, self.msg, self.id - ) - } +pub enum BlockType { + Blinded, + Full } #[derive(Clone)] @@ -123,6 +98,193 @@ impl MessageHandler for DvfSignatureReceiverHandler { } } + +#[derive(Serialize, Deserialize, Clone)] +pub struct DvfDutyCheckMessage { + pub operator_id: u64, + pub domain_hash: Hash256, + pub check_type: DvfType, + pub data: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub sign_hex: Option, +} + +impl DvfDutyCheckMessage { + pub fn get_digest(&self) -> Digest { + let mut msg = self.clone(); + msg.sign_hex = None; + let ser_json = + serde_json::to_string(&msg).unwrap(); + Digest::from(keccak(ser_json.as_bytes()).as_fixed_bytes()) + } +} + +impl SignDigest for DvfDutyCheckMessage {} + +#[derive(Clone)] +pub struct DvfDutyCheckHandler { + pub store: Store, + pub slashing_protection: SlashingDatabase, + pub validator_pk: BlsPublicKey, + pub operator_pks: HashMap, + pub keypair: Keypair, + _phantom: PhantomData +} + +impl DvfDutyCheckHandler { + pub async fn sign_block>(&self,writer: &mut Writer, block: BeaconBlock, domain_hash: Hash256) { + match self.slashing_protection.check_and_insert_block_proposal(&self.validator_pk.compress(), &block.block_header(), domain_hash) { + Ok(Safe::Valid) => { + let signable_msg = SignableMessage::BeaconBlock(&block); + let signing_root = signable_msg.signing_root(domain_hash); + info!("valid block duty, local sign and store {}", &signing_root); + let sig = self.keypair.sk.sign(signing_root); + let serialized_signature = bincode::serialize(&sig).unwrap(); + // save to local db + let key = signing_root.as_bytes().into(); + self.store.write(key, serialized_signature).await; + let _ = writer.send(Bytes::from(format!("successfully consensus proposal on {}", signing_root))).await; + }, + Ok(Safe::SameData) => { + let _ = writer.send(Bytes::from( "Skipping signing of previously signed block")).await; + info!("Skipping signing of previously signed block"); + } + Err(NotSafe::UnregisteredValidator(pk)) => { + let _ = writer.send(Bytes::from( format!("Not signing block for unregistered validator public_key {:?}", pk))).await; + warn!("Not signing block for unregistered validator public_key {}", format!("{:?}", pk)); + } + Err(e) => { + let _ = writer.send(Bytes::from( format!("Not signing slashable block error {:?}", e))).await; + error!("Not signing slashable block error {}", format!("{:?}", e)); + } + } + } +} + +#[async_trait] +impl MessageHandler for DvfDutyCheckHandler { + async fn dispatch(&self, writer: &mut Writer, message: Bytes) -> Result<(), Box> { + let check_msg: DvfDutyCheckMessage = match bincode::deserialize(&message.slice(..)) { + Ok(m) => { m }, + Err(_) => { + let _ = writer.send(Bytes::from("failed to deserialize duty check msg")).await; + error!("failed to deserialize duty check msg"); + return Ok(()) + } + }; + let digest = check_msg.get_digest(); + let op_pk = match self.operator_pks.get(&check_msg.operator_id) { + Some(pk) => pk, + None => return { + let _ = writer.send(Bytes::from("failed to find operator")).await; + error!("failed to find operator"); + Ok(()) + } + }; + match check_msg.sign_hex { + Some(s) => { + match hex::decode(s) { + Ok(s) => { + if s.len() != 64 { + let _ = writer.send(Bytes::from("the length of the signature is not 64, ignore the message")).await; + error!("the length of the signature is not 64, ignore the message"); + return Ok(()) + } + let sig = hscrypto::Signature::from_bytes(&s); + match sig.verify(&digest, &op_pk) { + Ok(_) => { + info!("successfully verified duty message"); + }, + Err(_) => { + let _ = writer.send(Bytes::from("failed to verify the signature of the duty message")).await; + error!("failed to verify the signature of the duty message"); + return Ok(()) + } + } + }, + Err(_) => { + let _ = writer.send(Bytes::from("failed to decode signature, ignore the message")).await; + error!("failed to decode signature, ignore the message"); + return Ok(()) + } + }; + }, + None => { + let _ = writer.send(Bytes::from("empty signature, ignore the message")).await; + error!("empty signature, ignore the message"); + return Ok(()); + } + } + match check_msg.check_type { + DvfType::Attester => { + let attestation_data: AttestationData = match bincode::deserialize(&check_msg.data) { + Ok(a) => a, + Err(_) => { + let _ = writer.send(Bytes::from("failed to deserialize attestation data")).await; + error!("failed to deserialize attestation data"); + return Ok(()) + } + }; + match self.slashing_protection.check_and_insert_attestation(&self.validator_pk.compress(), &attestation_data, check_msg.domain_hash) { + Ok(Safe::Valid) => { + let signable_msg = SignableMessage::>::AttestationData(&attestation_data); + let signing_root = signable_msg.signing_root(check_msg.domain_hash); + info!("valid attestation duty, local sign and store {}", &signing_root); + let sig = self.keypair.sk.sign(signing_root); + let serialized_signature = bincode::serialize(&sig).unwrap(); + // save to local db + let key = signing_root.as_bytes().into(); + self.store.write(key, serialized_signature).await; + let _ = writer.send(Bytes::from(format!("successfully consensus attestation on {}", &signing_root))).await; + }, + Ok(Safe::SameData) => { + info!( + "Skipping signing of previously signed attestation" + ); + let _ = writer.send(Bytes::from( "Skipping signing of previously signed attestation")).await; + } + Err(NotSafe::UnregisteredValidator(pk)) => { + let _ = writer.send(Bytes::from(format!("Not signing attestation for unregistered validator public_key {:?}", pk))).await; + warn!("Not signing attestation for unregistered validator public_key {}", format!("{:?}", pk)); + } + Err(e) => { + let _ = writer.send(Bytes::from(format!("Not signing slashable attestation {:?}", e))).await; + error!("Not signing slashable attestation {}", format!("{:?}", e)); + } + } + }, + DvfType::Proposer(block_type) => { + match block_type { + BlockType::Full => { + let block : BeaconBlock> = match bincode::deserialize(&check_msg.data) { + Ok(b) => b, + Err(_) => { + let _ = writer.send(Bytes::from( "failed to deserialize full proposal block")).await; + error!("failed to deserialize full proposal block"); + return Ok(()) + } + }; + self.sign_block(writer, block, check_msg.domain_hash).await; + }, + BlockType::Blinded => { + let block : BeaconBlock> = match bincode::deserialize(&check_msg.data) { + Ok(b) => b, + Err(_) => { + let _ = writer.send(Bytes::from( "failed to deserialize blinded proposal block")).await; + error!("failed to deserialize blinded proposal block"); + return Ok(()) + } + }; + self.sign_block(writer, block, check_msg.domain_hash).await; + }, + + }; + } + } + Ok(()) + } +} + pub struct DvfSigner { pub signal: Option, pub operator_id: u64, @@ -146,6 +308,7 @@ impl DvfSigner { node_para: Arc>>, keypair: Keypair, committee_def: OperatorCommitteeDefinition, + slashing_protection: SlashingDatabase ) -> Result { let node_tmp = Arc::clone(&node_para); let node = node_tmp.read().await; @@ -162,7 +325,7 @@ impl DvfSigner { assert_eq!(operator_index.len(), 1); let operator_id = committee_def.operator_ids[operator_index[0]]; // Construct the committee for validator signing - let (mut operator_committee, tx_consensus) = + let (mut operator_committee, _) = OperatorCommittee::from_definition(committee_def.clone()).await; let local_operator = Arc::new(RwLock::new(LocalOperator::new( validator_id, @@ -175,42 +338,42 @@ impl DvfSigner { .await; // Construct the committee for hotstuff protocol - let epoch = 1; - let stake = 1; - let mempool_committee = MempoolCommittee::new( - committee_def - .node_public_keys - .iter() - .enumerate() - .map(|(i, pk)| { - let addr = committee_def.base_socket_addresses[i].unwrap_or(invalid_addr()); - ( - pk.clone(), - stake, - base_to_transaction_addr(addr), - base_to_mempool_addr(addr), - base_to_signature_addr(addr), - ) - }) - .collect(), - epoch, - ); - let consensus_committee = ConsensusCommittee::new( - committee_def - .node_public_keys - .iter() - .enumerate() - .map(|(i, pk)| { - let addr = committee_def.base_socket_addresses[i].unwrap_or(invalid_addr()); - (pk.clone(), stake, base_to_consensus_addr(addr)) - }) - .collect(), - epoch, - ); - let hotstuff_committee = HotstuffCommittee { - mempool: mempool_committee, - consensus: consensus_committee, - }; + // let epoch = 1; + // let stake = 1; + // let mempool_committee = MempoolCommittee::new( + // committee_def + // .node_public_keys + // .iter() + // .enumerate() + // .map(|(i, pk)| { + // let addr = committee_def.base_socket_addresses[i].unwrap_or(invalid_addr()); + // ( + // pk.clone(), + // stake, + // base_to_transaction_addr(addr), + // base_to_mempool_addr(addr), + // base_to_signature_addr(addr), + // ) + // }) + // .collect(), + // epoch, + // ); + // let consensus_committee = ConsensusCommittee::new( + // committee_def + // .node_public_keys + // .iter() + // .enumerate() + // .map(|(i, pk)| { + // let addr = committee_def.base_socket_addresses[i].unwrap_or(invalid_addr()); + // (pk.clone(), stake, base_to_consensus_addr(addr)) + // }) + // .collect(), + // epoch, + // ); + // let hotstuff_committee = HotstuffCommittee { + // mempool: mempool_committee, + // consensus: consensus_committee, + // }; let store_path = node .config @@ -220,22 +383,47 @@ impl DvfSigner { let store = Store::new(&store_path.to_str().unwrap()) .map_err(|e| DvfError::StoreError(format!("Failed to create store: {:?}", e)))?; - let (signal, exit) = exit_future::signal(); - - DvfCore::spawn( - operator_id, - node_para.clone(), - committee_def.validator_id, - hotstuff_committee, - keypair.clone(), - tx_consensus, - store.clone(), - exit.clone(), - ) - .await; - Node::spawn_committee_ip_monitor(node_para, committee_def, exit); + let (signal, exit) = exit_future::signal(); + Node::spawn_committee_ip_monitor(node_para, committee_def.clone(), exit); + node.signature_handler_map.write().await.insert( + validator_id, + DvfSignatureReceiverHandler { + store: store.clone(), + }, + ); + info!("Insert signature handler for validator: {}", validator_id); + let operator_pks: HashMap = committee_def.operator_ids.into_iter().zip(committee_def.node_public_keys.into_iter()).collect(); + + node.duties_handler_map.write().await.insert( + validator_id, + DvfDutyCheckHandler { + store: store.clone(), + slashing_protection: slashing_protection, + validator_pk: operator_committee.get_validator_pk(), + operator_pks, + keypair: keypair.clone(), + _phantom: PhantomData + } + ); + info!("Insert duties handler for validator: {}", validator_id); + + + + // DvfCore::spawn( + // operator_id, + // node_para.clone(), + // committee_def.validator_id, + // hotstuff_committee, + // keypair.clone(), + // tx_consensus, + // store.clone(), + // exit.clone(), + // ) + // .await; + + Ok(Self { signal: Some(signal), operator_id, @@ -253,13 +441,6 @@ impl DvfSigner { self.operator_committee.sign(message).await } - pub async fn consensus_sign( - &self, - message: Hash256, - ) -> Result<(Signature, Vec), DvfError> { - self.operator_committee.consensus_sign(message).await - } - pub fn local_sign(&self, message: Hash256) -> Signature { self.local_keypair.sk.sign(message) } @@ -277,11 +458,25 @@ impl DvfSigner { || self.operator_committee.get_leader(nonce + 1).await == self.operator_id } - pub async fn is_propose_aggregator(&self, nonce: u64) -> bool { - self.operator_committee.get_leader(nonce).await == self.operator_id + pub async fn consensus_on_duty(&self, domain_hash: Hash256, check_type: DvfType, data: &[u8]) { + let mut msg = DvfDutyCheckMessage { + operator_id: self.operator_id, + domain_hash, + check_type, + data: data.to_vec(), + sign_hex: None + }; + match msg.sign_digest(&self.node_secret) { + Ok(sign_hex) => msg.sign_hex = Some(sign_hex), + Err(_) => { + error!("failed to sign msg"); + return ; + } + } + self.operator_committee.consensus_on_duty(&bincode::serialize(&msg).unwrap()).await; } - pub fn validator_public_key(&self) -> String { + pub fn validator_public_key(&self) -> BlsPublicKey { self.operator_committee.get_validator_pk() } @@ -292,7 +487,7 @@ impl DvfSigner { pub struct DvfCore { pub store: Store, - pub commit: Receiver, + // pub commit: Receiver, pub validator_id: u64, pub operator_id: u64, pub bls_keypair: Keypair, @@ -307,141 +502,138 @@ unsafe impl Sync for DvfCore {} impl DvfCore { pub async fn spawn( operator_id: u64, - node: Arc>>, + _node: Arc>>, validator_id: u64, - committee: HotstuffCommittee, - keypair: Keypair, - tx_consensus: MonitoredSender, - store: Store, - exit: exit_future::Exit, + _committee: HotstuffCommittee, + _keypair: Keypair, + _tx_consensus: MonitoredSender, + _store: Store, + _exit: exit_future::Exit, ) { - let node = node.read().await; + // let node = node.read().await; - let (tx_commit, rx_commit) = - MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-commit".to_string(), "info"); - let (tx_consensus_to_mempool, rx_consensus_to_mempool) = - MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-cs2mp".to_string(), "info"); - let (tx_mempool_to_consensus, rx_mempool_to_consensus) = - MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-mp2cs".to_string(), "info"); + // let (tx_commit, rx_commit) = + // MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-commit".to_string(), "info"); + // let (tx_consensus_to_mempool, rx_consensus_to_mempool) = + // MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-cs2mp".to_string(), "info"); + // let (tx_mempool_to_consensus, rx_mempool_to_consensus) = + // MonitoredChannel::new(DEFAULT_CHANNEL_CAPACITY, "dvf-mp2cs".to_string(), "info"); - let parameters = Parameters::default(); + // let parameters = Parameters::default(); // Run the signature service. - let signature_service = SignatureService::new(node.secret.secret.clone()); - - node.signature_handler_map.write().await.insert( - validator_id, - DvfSignatureReceiverHandler { - store: store.clone(), - }, - ); - info!("Insert signature handler for validator: {}", validator_id); - - Mempool::spawn( - node.secret.name, - committee.mempool, - parameters.mempool, - store.clone(), - rx_consensus_to_mempool, - tx_mempool_to_consensus, - validator_id, - Arc::clone(&node.tx_handler_map), - Arc::clone(&node.mempool_handler_map), - exit.clone(), - ) - .await; - - Consensus::spawn( - node.secret.name, - committee.consensus, - parameters.consensus, - signature_service, - store.clone(), - rx_mempool_to_consensus, - tx_consensus_to_mempool, - tx_commit, - validator_id, - Arc::clone(&node.consensus_handler_map), - exit.clone(), - ) - .await; + // let signature_service = SignatureService::new(node.secret.secret.clone()); + + // node.signature_handler_map.write().await.insert( + // validator_id, + // DvfSignatureReceiverHandler { + // store: store.clone(), + // }, + // ); + // info!("Insert signature handler for validator: {}", validator_id); + + // Mempool::spawn( + // node.secret.name, + // committee.mempool, + // parameters.mempool, + // store.clone(), + // rx_consensus_to_mempool, + // tx_mempool_to_consensus, + // validator_id, + // Arc::clone(&node.tx_handler_map), + // Arc::clone(&node.mempool_handler_map), + // exit.clone(), + // ) + // .await; + + // Consensus::spawn( + // node.secret.name, + // committee.consensus, + // parameters.consensus, + // signature_service, + // store.clone(), + // rx_mempool_to_consensus, + // tx_consensus_to_mempool, + // tx_commit, + // validator_id, + // Arc::clone(&node.consensus_handler_map), + // exit.clone(), + // ) + // .await; info!("[Dvf {}/{}] successfully booted", operator_id, validator_id); - tokio::spawn(async move { - Self { - store: store, - commit: rx_commit, - validator_id: validator_id, - operator_id: operator_id, - bls_keypair: keypair, - tx_consensus, - exit, - } - .run() - .await - }); + // tokio::spawn(async move { + // Self { + // store: store, + // // commit: rx_commit, + // validator_id: validator_id, + // operator_id: operator_id, + // bls_keypair: keypair, + // tx_consensus, + // exit, + // } + // .run() + // .await + // }); } - pub async fn run(&mut self) { - info!( - "[Dvf {}/{}] start receiving committed consensus blocks", - self.operator_id, self.validator_id - ); - loop { - let exit = self.exit.clone(); - tokio::select! { - Some(block) = self.commit.recv() => { - // This is where we can further process committed block. - if block.payload.is_empty() { - continue; - } - debug!("[Dvf {}/{}] received a non-empty committed block", self.operator_id, self.validator_id); - for payload in block.payload { - match self.store.read(payload.to_vec()).await { - Ok(value) => { - match value { - Some(data) => { - let message: MempoolMessage = bincode::deserialize(&data[..]).unwrap(); - match message { - MempoolMessage::Batch(batches) => { - for batch in batches { - // construct hash256 - let msg = Hash256::from_slice(&batch[..]); - let sig = self.bls_keypair.sk.sign(msg.clone()); - let serialized_signature = bincode::serialize(&sig).unwrap(); - self.store.write(batch, serialized_signature).await; - if let Err(e) = self.tx_consensus.send(msg).await { - error!("Failed to notify consensus status: {}", e); - } - else { - debug!("[Dvf {}/{}] Sent out 1 consensus notification for msg: {}", self.operator_id, self.validator_id, msg); - } - } - } - MempoolMessage::BatchRequest(_, _) => { } - } - } - None => { - warn!("block is empty"); - } - } - }, - Err(e) => { - error!("can't read database, {}", e) - } - } - } - } - () = exit => { - break; - } - } - } - self.store.exit().await; - info!( - "[Dvf {}/{}] exit dvf core", - self.operator_id, self.validator_id - ); - } + // pub async fn run(&mut self) { + // info!( + // "[Dvf {}/{}] start receiving committed consensus blocks", + // self.operator_id, self.validator_id + // ); + // loop { + // let exit = self.exit.clone(); + // tokio::select! { + // Some(block) = self.commit.recv() => { + // // This is where we can further process committed block. + // if block.payload.is_empty() { + // continue; + // } + // debug!("[Dvf {}/{}] received a non-empty committed block", self.operator_id, self.validator_id); + // for payload in block.payload { + // match self.store.read(payload.to_vec()).await { + // Ok(value) => { + // match value { + // Some(data) => { + // let message: MempoolMessage = bincode::deserialize(&data[..]).unwrap(); + // match message { + // MempoolMessage::Batch(batches) => { + // for batch in batches { + // // construct hash256 + // let msg = Hash256::from_slice(&batch[..]); + // if let Err(e) = self.tx_consensus.send(msg).await { + // error!("Failed to notify consensus status: {}", e); + // } + // else { + // debug!("[Dvf {}/{}] Sent out 1 consensus notification for msg: {}", self.operator_id, self.validator_id, msg); + // } + // } + // } + // MempoolMessage::BatchRequest(_, _) => { } + // } + // } + // None => { + // warn!("block is empty"); + // } + // } + // }, + // Err(e) => { + // error!("can't read database, {}", e) + // } + // } + // } + // } + // () = exit => { + // break; + // } + // } + // } + // self.store.exit().await; + // info!( + // "[Dvf {}/{}] exit dvf core", + // self.operator_id, self.validator_id + // ); + // } } diff --git a/src/node/node.rs b/src/node/node.rs index 7c63bce3..45e516d7 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -16,7 +16,7 @@ use crate::node::contract::{ use crate::node::{ db::{self, Database}, discovery::Discovery, - dvfcore::DvfSignatureReceiverHandler, + dvfcore::{DvfSignatureReceiverHandler, DvfDutyCheckHandler}, status_report::StatusReport, utils::{ convert_address_to_withdraw_crendentials, request_to_web_server, DepositRequest, @@ -74,6 +74,7 @@ pub struct Node { pub mempool_handler_map: Arc>>, pub consensus_handler_map: Arc>>, pub signature_handler_map: Arc>>, + pub duties_handler_map: Arc >>>, pub validator_store: Option>>, pub discovery: Arc, pub db: Database, @@ -94,35 +95,48 @@ impl Node { let mempool_handler_map = Arc::new(RwLock::new(HashMap::new())); let consensus_handler_map = Arc::new(RwLock::new(HashMap::new())); let signature_handler_map = Arc::new(RwLock::new(HashMap::new())); + let duties_handler_map = Arc::new(RwLock::new(HashMap::new())); - let transaction_address = with_wildcard_ip(base_to_transaction_addr(config.base_address)); + let duties_address = with_wildcard_ip(base_to_transaction_addr(config.base_address)); NetworkReceiver::spawn( - transaction_address, - Arc::clone(&tx_handler_map), - "transaction", + duties_address, + Arc::clone(&duties_handler_map), + "duties consensus", ); info!( - "Node {} listening to client transactions on {}", - secret.name, transaction_address + "Node {} listening to duties consensus on {}", + secret.name, duties_address ); - let mempool_address = with_wildcard_ip(base_to_mempool_addr(config.base_address)); - NetworkReceiver::spawn(mempool_address, Arc::clone(&mempool_handler_map), "mempool"); - info!( - "Node {} listening to mempool messages on {}", - secret.name, mempool_address - ); - let consensus_address = with_wildcard_ip(base_to_consensus_addr(config.base_address)); - NetworkReceiver::spawn( - consensus_address, - Arc::clone(&consensus_handler_map), - "consensus", - ); - info!( - "Node {} listening to consensus messages on {}", - secret.name, consensus_address - ); + // let transaction_address = with_wildcard_ip(base_to_transaction_addr(config.base_address)); + // NetworkReceiver::spawn( + // transaction_address, + // Arc::clone(&tx_handler_map), + // "transaction", + // ); + // info!( + // "Node {} listening to client transactions on {}", + // secret.name, transaction_address + // ); + + // let mempool_address = with_wildcard_ip(base_to_mempool_addr(config.base_address)); + // NetworkReceiver::spawn(mempool_address, Arc::clone(&mempool_handler_map), "mempool"); + // info!( + // "Node {} listening to mempool messages on {}", + // secret.name, mempool_address + // ); + + // let consensus_address = with_wildcard_ip(base_to_consensus_addr(config.base_address)); + // NetworkReceiver::spawn( + // consensus_address, + // Arc::clone(&consensus_handler_map), + // "consensus", + // ); + // info!( + // "Node {} listening to consensus messages on {}", + // secret.name, consensus_address + // ); let signature_address = with_wildcard_ip(base_to_signature_addr(config.base_address)); NetworkReceiver::spawn( @@ -161,6 +175,7 @@ impl Node { mempool_handler_map: Arc::clone(&mempool_handler_map), consensus_handler_map: Arc::clone(&consensus_handler_map), signature_handler_map: Arc::clone(&signature_handler_map), + duties_handler_map: Arc::clone(&duties_handler_map), validator_store: None, discovery: Arc::new(discovery), db: db.clone(), diff --git a/src/validation/account_utils/validator_definitions.rs b/src/validation/account_utils/validator_definitions.rs index 3340a12c..453c25d0 100644 --- a/src/validation/account_utils/validator_definitions.rs +++ b/src/validation/account_utils/validator_definitions.rs @@ -565,6 +565,10 @@ impl ValidatorDefinitions { pub fn as_mut_slice(&mut self) -> &mut [ValidatorDefinition] { self.0.as_mut_slice() } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } /// Perform an exhaustive tree search of `dir`, adding any discovered voting keystore paths to diff --git a/src/validation/block_service.rs b/src/validation/block_service.rs index a1c80861..ab929f3e 100644 --- a/src/validation/block_service.rs +++ b/src/validation/block_service.rs @@ -24,6 +24,7 @@ use types::{ BlindedBeaconBlock, BlockType, EthSpec, Graffiti, PublicKeyBytes, SignedBlindedBeaconBlock, GRAFFITI_BYTES_LEN, Slot, }; +use crate::node::dvfcore::BlockType as DvfBlockType; #[derive(Debug)] pub enum BlockError { @@ -368,13 +369,13 @@ impl BlockService { UnsignedBlock::Full(block_contents) => { let (block, maybe_blobs) = block_contents.deconstruct(); self.validator_store - .sign_block(*validator_pubkey, block, slot) + .sign_block(*validator_pubkey, block, slot, DvfBlockType::Full) .await .map(|b| SignedBlock::Full(PublishBlockRequest::new(Arc::new(b), maybe_blobs))) } UnsignedBlock::Blinded(block) => self .validator_store - .sign_block(*validator_pubkey, block, slot) + .sign_block(*validator_pubkey, block, slot, DvfBlockType::Blinded) .await .map(SignedBlock::Blinded), }; diff --git a/src/validation/generic_operator_committee.rs b/src/validation/generic_operator_committee.rs index d67e5d61..fa6f86c1 100644 --- a/src/validation/generic_operator_committee.rs +++ b/src/validation/generic_operator_committee.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use tokio::sync::mpsc::Receiver; use tokio::sync::RwLock; use types::{Hash256, PublicKey, Signature}; - /// Operator committee for a validator. /// #[async_trait] @@ -19,11 +18,11 @@ pub trait TOperatorCommittee: Send { fn validator_id(&self) -> u64; async fn add_operator(&mut self, operator_id: u64, operator: Arc>); async fn consensus(&self, msg: Hash256) -> Result<(), DvfError>; + async fn consensus_on_duty(&self, data: &[u8]) -> Result<(), DvfError>; async fn sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError>; - async fn consensus_sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError>; async fn get_leader(&self, nonce: u64) -> u64; async fn get_op_pos(&self, op_id: u64) -> usize; - fn get_validator_pk(&self) -> String; + fn get_validator_pk(&self) -> PublicKey; fn threshold(&self) -> usize; } @@ -72,7 +71,7 @@ where self.cmt.get_leader(nonce).await } - pub fn get_validator_pk(&self) -> String { + pub fn get_validator_pk(&self) -> PublicKey { self.cmt.get_validator_pk() } @@ -80,7 +79,7 @@ where self.cmt.get_op_pos(op_id).await } - pub async fn consensus_sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError> { - self.cmt.consensus_sign(msg).await + pub async fn consensus_on_duty(&self, data: &[u8]) { + let _ = self.cmt.consensus_on_duty(data).await; } } diff --git a/src/validation/impls/hotstuff.rs b/src/validation/impls/hotstuff.rs index f135a47d..05f5cd89 100644 --- a/src/validation/impls/hotstuff.rs +++ b/src/validation/impls/hotstuff.rs @@ -120,63 +120,25 @@ impl TOperatorCommittee for HotstuffOperatorCommittee { for operator in operators.values() { operator.read().await.propose(msg).await; } - - // let notify = { - // let mut notes = self.consensus_notifications.write().await; - // if let Some(notify) = notes.get(&msg) { - // notify.clone() - // } - // else { - // let notify = Arc::new(Notify::new()); - // notes.insert(msg, notify.clone()); - // notify - // } - // }; notify.notified().await; let mut notes = self.consensus_notifications.write().await; notes.remove(&msg); Ok(()) } - async fn sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError> { - // Run consensus protocol - // self.consensus(msg).await?; - + async fn consensus_on_duty(&self, data: &[u8]) -> Result<(), DvfError> { let operators = &self.operators.read().await; let signing_futs = operators.keys().map(|operator_id| async move { let operator = operators.get(operator_id).unwrap().read().await; operator - .sign(msg) + .consensus_on_duty(data) .await - .map(|x| (operator_id.clone(), operator.public_key(), x)) }); - let results = join_all(signing_futs) - .await - .into_iter() - .flatten() - .collect::>(); - - let ids = results.iter().map(|x| x.0).collect::>(); - let pks = results.iter().map(|x| &x.1).collect::>(); - let sigs = results.iter().map(|x| &x.2).collect::>(); - - info!( - "Received {} signatures from {:?} for {}", - sigs.len(), - ids, - self.validator_id - ); - - let threshold_sig = ThresholdSignature::new(self.threshold()); - let sig = threshold_sig.threshold_aggregate(&sigs[..], &pks[..], &ids[..], msg)?; - - Ok((sig, ids)) + let _ = join_all(signing_futs).await; + Ok(()) } - async fn consensus_sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError> { - // Run consensus protocol - self.consensus(msg).await?; - + async fn sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError> { let operators = &self.operators.read().await; let signing_futs = operators.keys().map(|operator_id| async move { let operator = operators.get(operator_id).unwrap().read().await; @@ -208,7 +170,8 @@ impl TOperatorCommittee for HotstuffOperatorCommittee { Ok((sig, ids)) } - fn get_validator_pk(&self) -> String { - self.validator_public_key.as_hex_string() + + fn get_validator_pk(&self) -> PublicKey { + self.validator_public_key.clone() } } diff --git a/src/validation/initialized_validators.rs b/src/validation/initialized_validators.rs index b5a11047..4748b8d9 100644 --- a/src/validation/initialized_validators.rs +++ b/src/validation/initialized_validators.rs @@ -28,6 +28,7 @@ use lighthouse_metrics::set_gauge; use lockfile::{Lockfile, LockfileError}; use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; use reqwest::{Certificate, Error as ReqwestError}; +use slashing_protection::SlashingDatabase; use slog::{debug, error, info, warn, Logger}; use std::collections::{HashMap, HashSet}; use std::fs::{self, File}; @@ -214,6 +215,7 @@ impl InitializedValidator { key_cache: &mut KeyCache, key_stores: &mut HashMap, node: Option>>>, + slashing_protection: SlashingDatabase ) -> Result { if !def.enabled { return Err(Error::UnableToInitializeDisabledValidator); @@ -388,7 +390,7 @@ impl InitializedValidator { let committee_def = OperatorCommitteeDefinition::from_file(committee_def_path) .map_err(Error::UnableToParseCommitteeDefinition)?; let validator_public_key = committee_def.validator_public_key.clone(); - let signer = DvfSigner::spawn(node.unwrap(), voting_keypair, committee_def) + let signer = DvfSigner::spawn(node.unwrap(), voting_keypair, committee_def, slashing_protection) .await .map_err(|e| Error::DvfError(format!("{:?}", e)))?; @@ -493,6 +495,8 @@ pub struct InitializedValidators { node: Option>>>, /// For logging via `slog`. log: Logger, + /// For slashing protection in dvf signer + slashing_protection: SlashingDatabase } impl InitializedValidators { @@ -502,6 +506,7 @@ impl InitializedValidators { validators_dir: PathBuf, node: Option>>>, log: Logger, + slashing_protection: SlashingDatabase ) -> Result { let mut this = Self { validators_dir, @@ -509,6 +514,7 @@ impl InitializedValidators { validators: HashMap::::default(), node, log, + slashing_protection }; this.update_validators().await?; Ok(this) @@ -1265,6 +1271,7 @@ impl InitializedValidators { &mut key_stores, //&mut committee_cache, None, + self.slashing_protection.clone() ) .await { @@ -1316,6 +1323,7 @@ impl InitializedValidators { &mut key_stores, //&mut committee_cache, None, + self.slashing_protection.clone() ) .await { @@ -1368,6 +1376,7 @@ impl InitializedValidators { &mut key_stores, //&mut committee_cache, self.node.clone(), + self.slashing_protection.clone() ) .await { diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 94e2687e..1f618978 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -1,6 +1,6 @@ mod attestation_service; mod beacon_node_fallback; -mod block_service; +pub mod block_service; mod check_synced; mod cli; mod config; @@ -11,7 +11,7 @@ mod key_cache; mod latency; mod notifier; mod preparation_service; -mod signing_method; +pub mod signing_method; mod sync_committee_service; pub mod account_utils; @@ -256,11 +256,36 @@ impl ProductionValidatorClient { .await .map_err(|e| format!("Dvf node creation failed: {}", e))?; + + // Initialize slashing protection before initializing validators + // + // Create the slashing database if there are no validators, even if + // `init_slashing_protection` is not supplied. There is no risk in creating a slashing + // database without any validators in it. + let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); + let slashing_protection = if config.init_slashing_protection || validator_defs.is_empty() { + SlashingDatabase::open_or_create(&slashing_db_path).map_err(|e| { + format!( + "Failed to open or create slashing protection database: {:?}", + e + ) + }) + } else { + SlashingDatabase::open(&slashing_db_path).map_err(|e| { + format!( + "Failed to open slashing protection database: {:?}.\n\ + Ensure that `slashing_protection.sqlite` is in {:?} folder", + e, config.validator_dir + ) + }) + }?; + let mut validators = InitializedValidators::from_definitions( validator_defs, config.validator_dir.clone(), Some(node.clone()), log.clone(), + slashing_protection.clone() ) .await .map_err(|e| format!("Unable to initialize validators: {:?}", e))?; @@ -282,28 +307,7 @@ impl ProductionValidatorClient { ); } - // Initialize slashing protection. - // - // Create the slashing database if there are no validators, even if - // `init_slashing_protection` is not supplied. There is no risk in creating a slashing - // database without any validators in it. - let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); - let slashing_protection = if config.init_slashing_protection || voting_pubkeys.is_empty() { - SlashingDatabase::open_or_create(&slashing_db_path).map_err(|e| { - format!( - "Failed to open or create slashing protection database: {:?}", - e - ) - }) - } else { - SlashingDatabase::open(&slashing_db_path).map_err(|e| { - format!( - "Failed to open slashing protection database: {:?}.\n\ - Ensure that `slashing_protection.sqlite` is in {:?} folder", - e, config.validator_dir - ) - }) - }?; + // Check validator registration with slashing protection, or auto-register all validators. if config.init_slashing_protection { diff --git a/src/validation/operator.rs b/src/validation/operator.rs index c032cdd9..ebd5f3de 100644 --- a/src/validation/operator.rs +++ b/src/validation/operator.rs @@ -4,13 +4,13 @@ use std::time::Duration; use crate::node::config::{ base_to_consensus_addr, base_to_mempool_addr, base_to_signature_addr, base_to_transaction_addr, - is_addr_invalid, + is_addr_invalid, base_to_duties_addr }; use crate::utils::error::DvfError; use async_trait::async_trait; use bytes::Bytes; use downcast_rs::DowncastSync; -use log::{debug, warn}; +use log::{debug, warn, info}; use network::{DvfMessage, ReliableSender, SimpleSender, VERSION}; use tokio::time::{sleep_until, timeout, Instant}; use types::{Hash256, Keypair, PublicKey, Signature}; @@ -35,6 +35,10 @@ pub trait TOperator: DowncastSync + Sync + Send { fn signature_address(&self) -> SocketAddr { base_to_signature_addr(self.base_address()) } + fn duties_address(&self) -> SocketAddr { + base_to_duties_addr(self.base_address()) + } + async fn consensus_on_duty(&self, msg: &[u8]); } impl_downcast!(sync TOperator); @@ -77,6 +81,10 @@ impl TOperator for LocalOperator { fn base_address(&self) -> SocketAddr { self.base_address } + + async fn consensus_on_duty(&self, _msg: &[u8]) { + return ; + } } impl LocalOperator { @@ -113,7 +121,7 @@ impl TOperator for RemoteOperator { return Err(DvfError::SocketAddrUnknown); } - let n_try: u64 = 2; + let n_try: u64 = 1; let timeout_mill: u64 = 600; let sleep_mill: u64 = 300; let dvf_message = DvfMessage { @@ -181,6 +189,58 @@ impl TOperator for RemoteOperator { fn base_address(&self) -> SocketAddr { self.base_address } + + async fn consensus_on_duty(&self, msg: &[u8]) { + // skip this function quickly + if is_addr_invalid(self.base_address()) { + warn!("invalid socket address"); + return ; + } + let n_try: u64 = 1; + let timeout_mill: u64 = 800; + let sleep_mill: u64 = 300; + let dvf_message = DvfMessage { + version: VERSION, + validator_id: self.validator_id, + message: msg.to_vec(), + }; + + let serialize_msg = bincode::serialize(&dvf_message).unwrap(); + for i in 0..n_try { + let receiver = self + .network + .send(self.duties_address(), Bytes::from(serialize_msg.clone())) + .await; + let result = timeout(Duration::from_millis(timeout_mill), receiver).await; + match result { + Ok(output) => match output { + Ok(data) => { + info!("Received consensus response from [{}/{}]: {:?}", self.operator_id, self.validator_id, std::str::from_utf8(&data)); + }, + Err(_) => { + warn!("recv is interrupted."); + } + }, + Err(e) => { + warn!( + "Retry from operator {}/{}, error: {}", + self.operator_id, self.validator_id, e + ); + } + } + if i < n_try - 1 { + let next_try_instant = Instant::now() + Duration::from_millis(sleep_mill); + sleep_until(next_try_instant).await; + } + } + warn!( + "Failed to receive a signature from operator {}/{} ({:?})", + self.operator_id, + self.validator_id, + self.signature_address() + ); + + } } impl RemoteOperator { diff --git a/src/validation/signing_method.rs b/src/validation/signing_method.rs index 321aa96d..f0eb79d1 100644 --- a/src/validation/signing_method.rs +++ b/src/validation/signing_method.rs @@ -7,7 +7,7 @@ //! - Via a distributed operator committee use crate::node::config::{API_ADDRESS, COLLECT_PERFORMANCE_URL}; -use crate::node::dvfcore::DvfSigner; +use crate::node::dvfcore::{DvfSigner, DvfType, BlockType}; use crate::node::utils::{request_to_web_server, DvfPerformanceRequest, SignDigest}; use crate::validation::eth2_keystore_share::keystore_share::KeystoreShare; use crate::validation::http_metrics::metrics; @@ -133,6 +133,37 @@ impl SigningContext { } impl SigningMethod { + pub async fn is_leader(&self, epoch: Epoch) -> bool { + match self { + SigningMethod::DistributedKeystore {dvf_signer, .. } => { + dvf_signer.is_aggregator(epoch.as_u64()).await + }, + _ => { + false + } + } + } + + pub async fn distributed_consensus_attestation(&self, domain_hash: Hash256, attestation_data: &AttestationData, ) { + match self { + SigningMethod::DistributedKeystore {dvf_signer, .. } => { + let data = bincode::serialize(attestation_data).unwrap(); + dvf_signer.consensus_on_duty(domain_hash, DvfType::Attester, &data).await; + }, + _ => {} + } + } + + pub async fn distributed_consensus_block>(&self, domain_hash: Hash256, block: &BeaconBlock, block_type: BlockType) { + match self { + SigningMethod::DistributedKeystore {dvf_signer, .. } => { + let data = bincode::serialize(block).unwrap(); + dvf_signer.consensus_on_duty(domain_hash, DvfType::Proposer(block_type), &data).await + }, + _ => {} + } + } + /// Return whether this signing method requires local slashing protection. pub fn requires_local_slashing_protection( &self, @@ -329,7 +360,7 @@ impl SigningMethod { }; let is_aggregator = dvf_signer .is_aggregator( - signing_epoch.as_u64() + dvf_signer.operator_committee.validator_id(), + signing_epoch.as_u64(), ) .await; log::info!( @@ -346,72 +377,39 @@ impl SigningMethod { // Following LocalKeystore, if the code logic reaches here, then it has already passed all checks of this duty, and // it is safe (from this operator's point of view) to sign it locally. dvf_signer.local_sign_and_store(signing_root).await; - if duty == "PROPOSER" { - if dvf_signer.is_propose_aggregator(signing_epoch.as_u64()).await { - log::info!("[Dvf {}/{}] Leader trying to achieve {} consensus and aggregate duty signatures", - dvf_signer.operator_id, - dvf_signer.operator_committee.validator_id(), - duty - ); - let task_timeout = Duration::from_secs(seconds_per_slot * 2 / 3); - let timeout = sleep(task_timeout); - let work = dvf_signer.consensus_sign(signing_root); - let dt: DateTime = Utc::now(); - tokio::select! { - result = work => { - match result { - Ok((signature, ids)) => { - // [Issue] Several same reports will be sent to server from different aggregators - Self::dvf_report::(slot, duty, dvf_signer.validator_public_key(), dvf_signer.operator_id(), ids, dt, &dvf_signer.node_secret).await?; - Ok(signature) - }, - Err(e) => { - Err(Error::CommitteeSignFailed(format!("{:?}", e))) - } + if !only_aggregator || (only_aggregator && is_aggregator) { + log::debug!("[Dvf {}/{}] Leader trying to achieve duty consensus and aggregate duty signatures", + dvf_signer.operator_id, + dvf_signer.operator_committee.validator_id() + ); + // Should NOT take more than a slot duration for two reasons: + // 1. if longer than slot duration, it might affect duty retrieval for other VAs (for example, previously, + // I set this to be the epoch remaining time for selection proof, so bad committee (VA) might take several mintues + // to timeout, making duties of other VAs outdated.) + // 2. most duties should complete in a slot + let task_timeout = Duration::from_secs(seconds_per_slot * 2 / 3); + let timeout = sleep(task_timeout); + let work = dvf_signer.threshold_sign(signing_root); + let dt: DateTime = Utc::now(); + tokio::select! { + result = work => { + match result { + Ok((signature, ids)) => { + // [Issue] Several same reports will be sent to server from different aggregators + Self::dvf_report::(slot, duty, dvf_signer.validator_public_key().as_hex_string(), dvf_signer.operator_id(), ids, dt, &dvf_signer.node_secret).await?; + Ok(signature) + }, + Err(e) => { + Err(Error::CommitteeSignFailed(format!("{:?}", e))) } } - _ = timeout => { - Err(Error::CommitteeSignFailed(format!("Timeout"))) - } } - } else { - Err(Error::NotLeader) - } - } else { - if !only_aggregator || (only_aggregator && is_aggregator) { - log::debug!("[Dvf {}/{}] Leader trying to achieve duty consensus and aggregate duty signatures", - dvf_signer.operator_id, - dvf_signer.operator_committee.validator_id() - ); - // Should NOT take more than a slot duration for two reasons: - // 1. if longer than slot duration, it might affect duty retrieval for other VAs (for example, previously, - // I set this to be the epoch remaining time for selection proof, so bad committee (VA) might take several mintues - // to timeout, making duties of other VAs outdated.) - // 2. most duties should complete in a slot - let task_timeout = Duration::from_secs(seconds_per_slot * 2 / 3); - let timeout = sleep(task_timeout); - let work = dvf_signer.threshold_sign(signing_root); - let dt: DateTime = Utc::now(); - tokio::select! { - result = work => { - match result { - Ok((signature, ids)) => { - // [Issue] Several same reports will be sent to server from different aggregators - Self::dvf_report::(slot, duty, dvf_signer.validator_public_key(), dvf_signer.operator_id(), ids, dt, &dvf_signer.node_secret).await?; - Ok(signature) - }, - Err(e) => { - Err(Error::CommitteeSignFailed(format!("{:?}", e))) - } - } - } - _ = timeout => { - Err(Error::CommitteeSignFailed(format!("Timeout"))) - } + _ = timeout => { + Err(Error::CommitteeSignFailed(format!("Timeout"))) } - } else { - Err(Error::NotLeader) } + } else { + Err(Error::NotLeader) } } } diff --git a/src/validation/validator_store.rs b/src/validation/validator_store.rs index 87d4d1e2..c92f1618 100644 --- a/src/validation/validator_store.rs +++ b/src/validation/validator_store.rs @@ -1,5 +1,6 @@ //! Reference: lighthouse/validator_client/validator_store.rs +use crate::node::dvfcore::BlockType; use crate::validation::account_utils::{validator_definitions::ValidatorDefinition, ZeroizeString}; use crate::validation::preparation_service::ProposalData; use crate::{ @@ -576,6 +577,7 @@ impl ValidatorStore { validator_pubkey: PublicKeyBytes, block: BeaconBlock, current_slot: Slot, + block_type: BlockType ) -> Result, Error> { // Make sure the block slot is not higher than the current slot to avoid potential attacks. if block.slot() > current_slot { @@ -595,58 +597,63 @@ impl ValidatorStore { let signing_context = self.signing_context(Domain::BeaconProposer, signing_epoch); let domain_hash = signing_context.domain_hash(&self.spec); - // Check for slashing conditions. - let slashing_status = self.slashing_protection.check_and_insert_block_proposal( - &validator_pubkey, - &block.block_header(), - domain_hash, - ); - - match slashing_status { - // We can safely sign this block without slashing. - Ok(Safe::Valid) => { - metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SUCCESS]); - - let signing_method = self + let signing_method = self .doppelganger_checked_signing_method(validator_pubkey) .await?; - let signature = signing_method - .get_signature::( - SignableMessage::BeaconBlock(&block), - signing_context, - &self.spec, - &self.task_executor, - ) - .await?; - Ok(SignedBeaconBlock::from_block(block, signature)) - } - Ok(Safe::SameData) => { - warn!( - self.log, - "Skipping signing of previously signed block"; - ); - metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SAME_DATA]); - Err(Error::SameData) - } - Err(NotSafe::UnregisteredValidator(pk)) => { - warn!( - self.log, - "Not signing block for unregistered validator"; - "msg" => "Carefully consider running with --init-slashing-protection (see --help)", - "public_key" => format!("{:?}", pk) - ); - metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::UNREGISTERED]); - Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) - } - Err(e) => { - crit!( - self.log, - "Not signing slashable block"; - "error" => format!("{:?}", e) - ); - metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SLASHABLE]); - Err(Error::Slashable(e)) + + if signing_method.is_leader(signing_epoch).await { + // Check for slashing conditions. + let slashing_status = self.slashing_protection.check_and_insert_block_proposal( + &validator_pubkey, + &block.block_header(), + domain_hash, + ); + match slashing_status { + // We can safely sign this block without slashing. + Ok(Safe::Valid) => { + metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SUCCESS]); + // distributed consensus + signing_method.distributed_consensus_block(domain_hash, &block, block_type).await; + let signature = signing_method + .get_signature::( + SignableMessage::BeaconBlock(&block), + signing_context, + &self.spec, + &self.task_executor, + ) + .await?; + Ok(SignedBeaconBlock::from_block(block, signature)) + } + Ok(Safe::SameData) => { + warn!( + self.log, + "Skipping signing of previously signed block"; + ); + metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SAME_DATA]); + Err(Error::SameData) + } + Err(NotSafe::UnregisteredValidator(pk)) => { + warn!( + self.log, + "Not signing block for unregistered validator"; + "msg" => "Carefully consider running with --init-slashing-protection (see --help)", + "public_key" => format!("{:?}", pk) + ); + metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::UNREGISTERED]); + Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) + } + Err(e) => { + crit!( + self.log, + "Not signing slashable block"; + "error" => format!("{:?}", e) + ); + metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SLASHABLE]); + Err(Error::Slashable(e)) + } } + } else { + return Err(Error::UnableToSign(SigningError::NotLeader)); } } @@ -673,75 +680,83 @@ impl ValidatorStore { let signing_context = self.signing_context(Domain::BeaconAttester, signing_epoch); let domain_hash = signing_context.domain_hash(&self.spec); - let slashing_status = if signing_method + if signing_method.is_leader(signing_epoch).await { + let slashing_status = if signing_method .requires_local_slashing_protection(self.enable_web3signer_slashing_protection) - { - self.slashing_protection.check_and_insert_attestation( - &validator_pubkey, - attestation.data(), - domain_hash, - ) - } else { - Ok(Safe::Valid) - }; - - match slashing_status { - // We can safely sign this attestation. - Ok(Safe::Valid) => { - let signature = signing_method - .get_signature::>( - SignableMessage::AttestationData(&attestation.data()), - signing_context, - &self.spec, - &self.task_executor, - ) - .await?; - attestation - .add_signature(&signature, validator_committee_position) - .map_err(Error::UnableToSignAttestation)?; - - metrics::inc_counter_vec(&metrics::SIGNED_ATTESTATIONS_TOTAL, &[metrics::SUCCESS]); - - Ok(()) - } - Ok(Safe::SameData) => { - warn!( - self.log, - "Skipping signing of previously signed attestation" - ); - metrics::inc_counter_vec( - &metrics::SIGNED_ATTESTATIONS_TOTAL, - &[metrics::SAME_DATA], - ); - Err(Error::SameData) - } - Err(NotSafe::UnregisteredValidator(pk)) => { - warn!( - self.log, - "Not signing attestation for unregistered validator"; - "msg" => "Carefully consider running with --init-slashing-protection (see --help)", - "public_key" => format!("{:?}", pk) - ); - metrics::inc_counter_vec( - &metrics::SIGNED_ATTESTATIONS_TOTAL, - &[metrics::UNREGISTERED], - ); - Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) - } - Err(e) => { - crit!( - self.log, - "Not signing slashable attestation"; - "attestation" => format!("{:?}", attestation.data()), - "error" => format!("{:?}", e) - ); - metrics::inc_counter_vec( - &metrics::SIGNED_ATTESTATIONS_TOTAL, - &[metrics::SLASHABLE], - ); - Err(Error::Slashable(e)) + { + self.slashing_protection.check_and_insert_attestation( + &validator_pubkey, + attestation.data(), + domain_hash, + ) + } else { + Ok(Safe::Valid) + }; + match slashing_status { + // We can safely sign this attestation. + Ok(Safe::Valid) => { + signing_method.distributed_consensus_attestation(domain_hash, attestation.data()).await; + let signature = signing_method + .get_signature::>( + SignableMessage::AttestationData(&attestation.data()), + signing_context, + &self.spec, + &self.task_executor, + ) + .await?; + attestation + .add_signature(&signature, validator_committee_position) + .map_err(Error::UnableToSignAttestation)?; + + metrics::inc_counter_vec(&metrics::SIGNED_ATTESTATIONS_TOTAL, &[metrics::SUCCESS]); + + Ok(()) + } + Ok(Safe::SameData) => { + warn!( + self.log, + "Skipping signing of previously signed attestation" + ); + metrics::inc_counter_vec( + &metrics::SIGNED_ATTESTATIONS_TOTAL, + &[metrics::SAME_DATA], + ); + Err(Error::SameData) + } + Err(NotSafe::UnregisteredValidator(pk)) => { + warn!( + self.log, + "Not signing attestation for unregistered validator"; + "msg" => "Carefully consider running with --init-slashing-protection (see --help)", + "public_key" => format!("{:?}", pk) + ); + metrics::inc_counter_vec( + &metrics::SIGNED_ATTESTATIONS_TOTAL, + &[metrics::UNREGISTERED], + ); + Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) + } + Err(e) => { + crit!( + self.log, + "Not signing slashable attestation"; + "attestation" => format!("{:?}", attestation.data()), + "error" => format!("{:?}", e) + ); + metrics::inc_counter_vec( + &metrics::SIGNED_ATTESTATIONS_TOTAL, + &[metrics::SLASHABLE], + ); + Err(Error::Slashable(e)) + } } + } + else { + return Err(Error::UnableToSign(SigningError::NotLeader)); } + + + } pub async fn sign_voluntary_exit( From 09c7ba98f97a34a57bf1f5edd6c86c3a2eff0696 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Thu, 5 Sep 2024 16:07:16 +0000 Subject: [PATCH 15/43] update serde method --- src/node/dvfcore.rs | 6 +++--- src/node/node.rs | 4 ++-- src/validation/signing_method.rs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index 11f49495..e2fe0690 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -217,7 +217,7 @@ impl MessageHandler for DvfDutyCheckHandler { } match check_msg.check_type { DvfType::Attester => { - let attestation_data: AttestationData = match bincode::deserialize(&check_msg.data) { + let attestation_data: AttestationData = match serde_json::from_slice(&check_msg.data) { Ok(a) => a, Err(_) => { let _ = writer.send(Bytes::from("failed to deserialize attestation data")).await; @@ -256,7 +256,7 @@ impl MessageHandler for DvfDutyCheckHandler { DvfType::Proposer(block_type) => { match block_type { BlockType::Full => { - let block : BeaconBlock> = match bincode::deserialize(&check_msg.data) { + let block : BeaconBlock> = match serde_json::from_slice(&check_msg.data) { Ok(b) => b, Err(_) => { let _ = writer.send(Bytes::from( "failed to deserialize full proposal block")).await; @@ -267,7 +267,7 @@ impl MessageHandler for DvfDutyCheckHandler { self.sign_block(writer, block, check_msg.domain_hash).await; }, BlockType::Blinded => { - let block : BeaconBlock> = match bincode::deserialize(&check_msg.data) { + let block : BeaconBlock> = match serde_json::from_slice(&check_msg.data) { Ok(b) => b, Err(_) => { let _ = writer.send(Bytes::from( "failed to deserialize blinded proposal block")).await; diff --git a/src/node/node.rs b/src/node/node.rs index 45e516d7..b75b89b8 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -4,7 +4,7 @@ use crate::deposit::get_distributed_deposit; use crate::exit::get_distributed_voluntary_exit; use crate::network::io_committee::{SecureNetIOChannel, SecureNetIOCommittee}; use crate::node::config::{ - base_to_consensus_addr, base_to_mempool_addr, base_to_signature_addr, base_to_transaction_addr, + base_to_consensus_addr, base_to_mempool_addr, base_to_signature_addr, base_to_duties_addr, NodeConfig, API_ADDRESS, DB_FILENAME, DISCOVERY_PORT_OFFSET, DKG_PORT_OFFSET, PRESTAKE_SIGNATURE_URL, STAKE_SIGNATURE_URL, VALIDATOR_PK_URL, }; @@ -97,7 +97,7 @@ impl Node { let signature_handler_map = Arc::new(RwLock::new(HashMap::new())); let duties_handler_map = Arc::new(RwLock::new(HashMap::new())); - let duties_address = with_wildcard_ip(base_to_transaction_addr(config.base_address)); + let duties_address = with_wildcard_ip(base_to_duties_addr(config.base_address)); NetworkReceiver::spawn( duties_address, Arc::clone(&duties_handler_map), diff --git a/src/validation/signing_method.rs b/src/validation/signing_method.rs index f0eb79d1..402d67a1 100644 --- a/src/validation/signing_method.rs +++ b/src/validation/signing_method.rs @@ -147,8 +147,8 @@ impl SigningMethod { pub async fn distributed_consensus_attestation(&self, domain_hash: Hash256, attestation_data: &AttestationData, ) { match self { SigningMethod::DistributedKeystore {dvf_signer, .. } => { - let data = bincode::serialize(attestation_data).unwrap(); - dvf_signer.consensus_on_duty(domain_hash, DvfType::Attester, &data).await; + let data = serde_json::to_string(attestation_data).unwrap(); + dvf_signer.consensus_on_duty(domain_hash, DvfType::Attester, data.as_bytes()).await; }, _ => {} } @@ -157,8 +157,8 @@ impl SigningMethod { pub async fn distributed_consensus_block>(&self, domain_hash: Hash256, block: &BeaconBlock, block_type: BlockType) { match self { SigningMethod::DistributedKeystore {dvf_signer, .. } => { - let data = bincode::serialize(block).unwrap(); - dvf_signer.consensus_on_duty(domain_hash, DvfType::Proposer(block_type), &data).await + let data = serde_json::to_string(block).unwrap(); + dvf_signer.consensus_on_duty(domain_hash, DvfType::Proposer(block_type), data.as_bytes()).await }, _ => {} } From 67df61116d9952272f536976281531e58c3262e8 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Thu, 5 Sep 2024 16:28:53 +0000 Subject: [PATCH 16/43] remove unnecesary log --- src/validation/operator.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/validation/operator.rs b/src/validation/operator.rs index ebd5f3de..475b142f 100644 --- a/src/validation/operator.rs +++ b/src/validation/operator.rs @@ -233,13 +233,6 @@ impl TOperator for RemoteOperator { sleep_until(next_try_instant).await; } } - warn!( - "Failed to receive a signature from operator {}/{} ({:?})", - self.operator_id, - self.validator_id, - self.signature_address() - ); - } } From 748b585155216844cd224c9a361afc3763f58e45 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Thu, 5 Sep 2024 23:47:58 +0000 Subject: [PATCH 17/43] remove unused code --- src/bin/dvf_network_tool.rs | 2 +- src/bin/dvf_root_node.rs | 4 +- src/node/config.rs | 3 +- src/node/contract.rs | 79 +++-- src/node/discovery.rs | 13 +- src/node/dvfcore.rs | 302 +++++++++++------- src/node/node.rs | 38 ++- .../account_utils/validator_definitions.rs | 2 +- src/validation/attestation_service.rs | 34 +- src/validation/block_service.rs | 21 +- src/validation/cli.rs | 1 - src/validation/config.rs | 7 +- src/validation/generic_operator_committee.rs | 10 +- src/validation/http_api/keystores.rs | 15 +- src/validation/http_api/mod.rs | 199 ++++++------ src/validation/http_api/remotekeys.rs | 12 +- src/validation/impls/hotstuff.rs | 115 ++++--- src/validation/initialized_validators.rs | 25 +- src/validation/mod.rs | 10 +- src/validation/operator.rs | 21 +- src/validation/operator_committees.rs | 24 +- src/validation/signing_method.rs | 43 +-- src/validation/validator_store.rs | 55 ++-- 23 files changed, 593 insertions(+), 442 deletions(-) diff --git a/src/bin/dvf_network_tool.rs b/src/bin/dvf_network_tool.rs index 62523f7e..192d2833 100644 --- a/src/bin/dvf_network_tool.rs +++ b/src/bin/dvf_network_tool.rs @@ -1,3 +1,4 @@ +use hsconfig::Export as _; use hsconfig::Secret; use lighthouse_network::discv5::{ enr::{CombinedKey, Enr}, @@ -5,7 +6,6 @@ use lighthouse_network::discv5::{ }; use log::{error, info}; use std::net::IpAddr; -use hsconfig::Export as _; pub const DISCOVERY_PORT_OFFSET: u16 = 4; pub const DEFAULT_DISCOVERY_PORT: u16 = 26004; pub const DEFAULT_SECRET_DIR: &str = "node_key.json"; diff --git a/src/bin/dvf_root_node.rs b/src/bin/dvf_root_node.rs index 64f2728f..26265a0a 100644 --- a/src/bin/dvf_root_node.rs +++ b/src/bin/dvf_root_node.rs @@ -165,7 +165,7 @@ async fn main() -> Result<(), Box> { } None => {} } - + } } }, @@ -182,7 +182,7 @@ async fn main() -> Result<(), Box> { } None => {} } - + } else { let socketaddr = SocketAddr::new(IpAddr::V4(enr_ip), 26000); info!("A peer has established session with default port: public key: {}, base addr: {:?}", diff --git a/src/node/config.rs b/src/node/config.rs index 321e6e14..5c20e20a 100644 --- a/src/node/config.rs +++ b/src/node/config.rs @@ -162,7 +162,7 @@ impl NodeConfig { beacon_nodes: Vec::new(), builder_proposals: false, builder_boost_factor: None, - prefer_builder_proposals: false + prefer_builder_proposals: false, } } @@ -209,5 +209,4 @@ impl NodeConfig { self.beacon_nodes = beacon_nodes; self } - } diff --git a/src/node/contract.rs b/src/node/contract.rs index bbf04c03..c0ec45fb 100644 --- a/src/node/contract.rs +++ b/src/node/contract.rs @@ -159,7 +159,7 @@ pub enum ContractCommand { Address, ), RemoveInitiator(Initiator, OperatorPublicKeys), - SetFeeRecipient(ValidatorPublicKey, Address) + SetFeeRecipient(ValidatorPublicKey, Address), } #[derive(Clone)] @@ -322,7 +322,7 @@ impl TopicHandler for FeeRecipientSetHandler { error!("error happens when process initiator removal"); e }) - } + } } #[derive(Debug, DeriveSerialize, DeriveDeserialize, Clone)] @@ -449,16 +449,20 @@ impl Contract { let minipool_ready_topic = H256::from_slice(&hex::decode(&config.initiator_minipool_ready_topic).unwrap()); let ini_rm_topic = H256::from_slice(&hex::decode(&config.initiator_removal_topic).unwrap()); - let fee_receipient_set_topic = H256::from_slice(&hex::decode(&config.fee_recipient_set_topic).unwrap()); + let fee_receipient_set_topic = + H256::from_slice(&hex::decode(&config.fee_recipient_set_topic).unwrap()); let va_filter_builder = FilterBuilder::default() - .address(vec![Address::from_slice( - &hex::decode(NETWORK_CONTRACT.get().unwrap()).unwrap() - ), - Address::from_slice( - &hex::decode(EXTRA_CONTRACT.get().unwrap()).unwrap() - )]) - .topics(Some(vec![va_reg_topic, va_rm_topic, fee_receipient_set_topic]), None, None, None); + .address(vec![ + Address::from_slice(&hex::decode(NETWORK_CONTRACT.get().unwrap()).unwrap()), + Address::from_slice(&hex::decode(EXTRA_CONTRACT.get().unwrap()).unwrap()), + ]) + .topics( + Some(vec![va_reg_topic, va_rm_topic, fee_receipient_set_topic]), + None, + None, + None, + ); self.va_filter_builder = Some(va_filter_builder); let initiator_filter_builder = FilterBuilder::default() .address(vec![Address::from_slice( @@ -483,7 +487,10 @@ impl Contract { handlers.insert(minipool_created_topic, Box::new(MinipoolCreatedHandler {})); handlers.insert(minipool_ready_topic, Box::new(MinipoolReadyHandler {})); handlers.insert(ini_rm_topic, Box::new(InitiatorRemovalHandler {})); - handlers.insert(fee_receipient_set_topic, Box::new(FeeRecipientSetHandler {})); + handlers.insert( + fee_receipient_set_topic, + Box::new(FeeRecipientSetHandler {}), + ); } pub fn monitor_validator_paidblock(&mut self) { @@ -1179,36 +1186,56 @@ pub async fn process_fee_recipient_set(raw_log: Log, db: &Database) -> Result<() name: "updateCount".to_string(), kind: ParamType::Uint(32), indexed: false, - } + }, ], anonymous: false, }; - let log = fee_recipient_set_event.parse_log(RawLog { - topics: raw_log.topics, - data: raw_log.data.0, - }).map_err(|_| ContractError::LogParseError)?; - let owner = log.params[0].value.clone().into_address().ok_or(ContractError::LogParseError)?; - let pubkey = log.params[1].value.clone().into_bytes().ok_or(ContractError::LogParseError)?; - let fee_recipient_address = log.params[2].value.clone().into_address().ok_or(ContractError::LogParseError)?; + let log = fee_recipient_set_event + .parse_log(RawLog { + topics: raw_log.topics, + data: raw_log.data.0, + }) + .map_err(|_| ContractError::LogParseError)?; + let owner = log.params[0] + .value + .clone() + .into_address() + .ok_or(ContractError::LogParseError)?; + let pubkey = log.params[1] + .value + .clone() + .into_bytes() + .ok_or(ContractError::LogParseError)?; + let fee_recipient_address = log.params[2] + .value + .clone() + .into_address() + .ok_or(ContractError::LogParseError)?; - if pubkey.iter().all(|&x| x== 0) { + if pubkey.iter().all(|&x| x == 0) { // public key is zero for v in db.query_validator_by_address(owner).await.unwrap().iter() { let cmd = ContractCommand::SetFeeRecipient(v.public_key.clone(), fee_recipient_address); - db.insert_contract_command(v.id, serde_json::to_string(&cmd).unwrap()).await; - }; + db.insert_contract_command(v.id, serde_json::to_string(&cmd).unwrap()) + .await; + } } else { - match db.query_validator_by_public_key(hex::encode(pubkey.clone())).await.unwrap() { + match db + .query_validator_by_public_key(hex::encode(pubkey.clone())) + .await + .unwrap() + { Some(v) => { let cmd = ContractCommand::SetFeeRecipient(pubkey, fee_recipient_address); - db.insert_contract_command(v.id, serde_json::to_string(&cmd).unwrap()).await; - }, + db.insert_contract_command(v.id, serde_json::to_string(&cmd).unwrap()) + .await; + } None => { info!("set fee recipient not releated to this operator"); } } } - + Ok(()) } diff --git a/src/node/discovery.rs b/src/node/discovery.rs index cb16b2cc..771283da 100644 --- a/src/node/discovery.rs +++ b/src/node/discovery.rs @@ -85,7 +85,8 @@ impl Discovery { builder.seq(seq); builder.build(&enr_key).unwrap() }; - let base_address = SocketAddr::new(ip, udp_port.checked_sub(DISCOVERY_PORT_OFFSET).unwrap()); + let base_address = + SocketAddr::new(ip, udp_port.checked_sub(DISCOVERY_PORT_OFFSET).unwrap()); info!("Node ENR ip: {}, port: {}", ip, udp_port); info!("Node public key: {}", secret.name.encode_base64()); info!("Node id: {}", base64::encode(local_enr.node_id().raw())); @@ -142,7 +143,7 @@ impl Discovery { } None => { } } - + } }; }; @@ -188,7 +189,7 @@ impl Discovery { Some(port) => { store.write(local_enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(v4addr.ip().clone()), port)).unwrap()).await; set_metrics(&store, local_enr.public_key().encode()).await; - } + } None => {} } } @@ -212,7 +213,9 @@ impl Discovery { store: store_clone, boot_enrs, discv5_service_handle, - base_port: udp_port.checked_sub(DISCOVERY_PORT_OFFSET).expect("overflow due to incorrect config"), + base_port: udp_port + .checked_sub(DISCOVERY_PORT_OFFSET) + .expect("overflow due to incorrect config"), }; // immediately initiate a discover request to annouce ourself @@ -331,7 +334,7 @@ impl Discovery { self.base_port, )); } - + if boot_idx >= self.boot_enrs.len() { error!("Invalid boot index"); return None; diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index e2fe0690..dbfb3a05 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -1,41 +1,34 @@ -use std::error::Error; -use std::fmt; -use std::sync::Arc; -use std::collections::HashMap; +use crate::node::node::Node; +use crate::node::utils::SignDigest; +use crate::utils::error::DvfError; +use crate::validation::operator::LocalOperator; +use crate::validation::operator_committee_definitions::OperatorCommitteeDefinition; +use crate::validation::signing_method::SignableMessage; +use crate::validation::OperatorCommittee; use async_trait::async_trait; -use bls::{Hash256, Signature, PublicKey as BlsPublicKey}; +use bls::{Hash256, PublicKey as BlsPublicKey, Signature}; use bytes::Bytes; -use consensus::Committee as ConsensusCommittee; -use consensus::{Block, Consensus}; use futures::SinkExt; -use hsconfig::{Committee as HotstuffCommittee, Parameters}; -use hscrypto::{SignatureService, Digest}; -use hsutils::monitored_channel::{MonitoredChannel, MonitoredSender}; -use log::{debug, error, info, warn}; -use mempool::Committee as MempoolCommittee; -use mempool::{Mempool, MempoolMessage}; +use hsconfig::Committee as HotstuffCommittee; +use hscrypto::Digest; +use hsutils::monitored_channel::MonitoredSender; +use keccak_hash::keccak; +use log::{error, info, warn}; use network::{MessageHandler, Writer}; use serde::{Deserialize, Serialize}; use slashing_protection::SlashingDatabase; +use slashing_protection::{NotSafe, Safe}; +use std::collections::HashMap; +use std::error::Error; +use std::fmt; +use std::marker::PhantomData; +use std::sync::Arc; use store::Store; -use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::RwLock; -use types::{EthSpec, Keypair, AttestationData, BlindedPayload, AbstractExecPayload, BeaconBlock, BlindedBeaconBlock, FullPayload }; -use keccak_hash::keccak; -use slashing_protection::{Safe, NotSafe}; -use crate::node::config::{ - base_to_consensus_addr, base_to_mempool_addr, base_to_signature_addr, base_to_transaction_addr, - invalid_addr, +use types::{ + AbstractExecPayload, AttestationData, BeaconBlock, BlindedPayload, EthSpec, FullPayload, + Keypair, }; -use crate::node::node::Node; -use crate::node::utils::SignDigest; -use crate::utils::error::DvfError; -use crate::validation::operator::LocalOperator; -use crate::validation::operator_committee_definitions::OperatorCommitteeDefinition; -use crate::validation::OperatorCommittee; -use crate::validation::signing_method::SignableMessage; -use crate::DEFAULT_CHANNEL_CAPACITY; -use std::marker::PhantomData; #[derive(Serialize, Deserialize, Clone)] pub struct DvfInfo { pub validator_id: u64, @@ -56,13 +49,13 @@ impl fmt::Debug for DvfInfo { #[derive(Serialize, Deserialize, Clone)] pub enum DvfType { Attester, - Proposer(BlockType) + Proposer(BlockType), } #[derive(Serialize, Deserialize, Clone)] pub enum BlockType { Blinded, - Full + Full, } #[derive(Clone)] @@ -98,10 +91,9 @@ impl MessageHandler for DvfSignatureReceiverHandler { } } - #[derive(Serialize, Deserialize, Clone)] pub struct DvfDutyCheckMessage { - pub operator_id: u64, + pub operator_id: u64, pub domain_hash: Hash256, pub check_type: DvfType, pub data: Vec, @@ -113,8 +105,7 @@ impl DvfDutyCheckMessage { pub fn get_digest(&self) -> Digest { let mut msg = self.clone(); msg.sign_hex = None; - let ser_json = - serde_json::to_string(&msg).unwrap(); + let ser_json = serde_json::to_string(&msg).unwrap(); Digest::from(keccak(ser_json.as_bytes()).as_fixed_bytes()) } } @@ -128,12 +119,21 @@ pub struct DvfDutyCheckHandler { pub validator_pk: BlsPublicKey, pub operator_pks: HashMap, pub keypair: Keypair, - _phantom: PhantomData + _phantom: PhantomData, } impl DvfDutyCheckHandler { - pub async fn sign_block>(&self,writer: &mut Writer, block: BeaconBlock, domain_hash: Hash256) { - match self.slashing_protection.check_and_insert_block_proposal(&self.validator_pk.compress(), &block.block_header(), domain_hash) { + pub async fn sign_block>( + &self, + writer: &mut Writer, + block: BeaconBlock, + domain_hash: Hash256, + ) { + match self.slashing_protection.check_and_insert_block_proposal( + &self.validator_pk.compress(), + &block.block_header(), + domain_hash, + ) { Ok(Safe::Valid) => { let signable_msg = SignableMessage::BeaconBlock(&block); let signing_root = signable_msg.signing_root(domain_hash); @@ -143,18 +143,38 @@ impl DvfDutyCheckHandler { // save to local db let key = signing_root.as_bytes().into(); self.store.write(key, serialized_signature).await; - let _ = writer.send(Bytes::from(format!("successfully consensus proposal on {}", signing_root))).await; - }, + let _ = writer + .send(Bytes::from(format!( + "successfully consensus proposal on {}", + signing_root + ))) + .await; + } Ok(Safe::SameData) => { - let _ = writer.send(Bytes::from( "Skipping signing of previously signed block")).await; + let _ = writer + .send(Bytes::from("Skipping signing of previously signed block")) + .await; info!("Skipping signing of previously signed block"); } Err(NotSafe::UnregisteredValidator(pk)) => { - let _ = writer.send(Bytes::from( format!("Not signing block for unregistered validator public_key {:?}", pk))).await; - warn!("Not signing block for unregistered validator public_key {}", format!("{:?}", pk)); + let _ = writer + .send(Bytes::from(format!( + "Not signing block for unregistered validator public_key {:?}", + pk + ))) + .await; + warn!( + "Not signing block for unregistered validator public_key {}", + format!("{:?}", pk) + ); } Err(e) => { - let _ = writer.send(Bytes::from( format!("Not signing slashable block error {:?}", e))).await; + let _ = writer + .send(Bytes::from(format!( + "Not signing slashable block error {:?}", + e + ))) + .await; error!("Not signing slashable block error {}", format!("{:?}", e)); } } @@ -165,20 +185,24 @@ impl DvfDutyCheckHandler { impl MessageHandler for DvfDutyCheckHandler { async fn dispatch(&self, writer: &mut Writer, message: Bytes) -> Result<(), Box> { let check_msg: DvfDutyCheckMessage = match bincode::deserialize(&message.slice(..)) { - Ok(m) => { m }, + Ok(m) => m, Err(_) => { - let _ = writer.send(Bytes::from("failed to deserialize duty check msg")).await; + let _ = writer + .send(Bytes::from("failed to deserialize duty check msg")) + .await; error!("failed to deserialize duty check msg"); - return Ok(()) + return Ok(()); } }; let digest = check_msg.get_digest(); let op_pk = match self.operator_pks.get(&check_msg.operator_id) { Some(pk) => pk, - None => return { - let _ = writer.send(Bytes::from("failed to find operator")).await; - error!("failed to find operator"); - Ok(()) + None => { + return { + let _ = writer.send(Bytes::from("failed to find operator")).await; + error!("failed to find operator"); + Ok(()) + } } }; match check_msg.sign_hex { @@ -186,98 +210,148 @@ impl MessageHandler for DvfDutyCheckHandler { match hex::decode(s) { Ok(s) => { if s.len() != 64 { - let _ = writer.send(Bytes::from("the length of the signature is not 64, ignore the message")).await; + let _ = writer + .send(Bytes::from( + "the length of the signature is not 64, ignore the message", + )) + .await; error!("the length of the signature is not 64, ignore the message"); - return Ok(()) + return Ok(()); } let sig = hscrypto::Signature::from_bytes(&s); match sig.verify(&digest, &op_pk) { Ok(_) => { info!("successfully verified duty message"); - }, + } Err(_) => { - let _ = writer.send(Bytes::from("failed to verify the signature of the duty message")).await; + let _ = writer + .send(Bytes::from( + "failed to verify the signature of the duty message", + )) + .await; error!("failed to verify the signature of the duty message"); - return Ok(()) + return Ok(()); } } - }, + } Err(_) => { - let _ = writer.send(Bytes::from("failed to decode signature, ignore the message")).await; + let _ = writer + .send(Bytes::from( + "failed to decode signature, ignore the message", + )) + .await; error!("failed to decode signature, ignore the message"); - return Ok(()) + return Ok(()); } }; - }, + } None => { - let _ = writer.send(Bytes::from("empty signature, ignore the message")).await; + let _ = writer + .send(Bytes::from("empty signature, ignore the message")) + .await; error!("empty signature, ignore the message"); return Ok(()); } } match check_msg.check_type { DvfType::Attester => { - let attestation_data: AttestationData = match serde_json::from_slice(&check_msg.data) { - Ok(a) => a, - Err(_) => { - let _ = writer.send(Bytes::from("failed to deserialize attestation data")).await; - error!("failed to deserialize attestation data"); - return Ok(()) - } - }; - match self.slashing_protection.check_and_insert_attestation(&self.validator_pk.compress(), &attestation_data, check_msg.domain_hash) { + let attestation_data: AttestationData = + match serde_json::from_slice(&check_msg.data) { + Ok(a) => a, + Err(_) => { + let _ = writer + .send(Bytes::from("failed to deserialize attestation data")) + .await; + error!("failed to deserialize attestation data"); + return Ok(()); + } + }; + match self.slashing_protection.check_and_insert_attestation( + &self.validator_pk.compress(), + &attestation_data, + check_msg.domain_hash, + ) { Ok(Safe::Valid) => { - let signable_msg = SignableMessage::>::AttestationData(&attestation_data); + let signable_msg = SignableMessage::>::AttestationData( + &attestation_data, + ); let signing_root = signable_msg.signing_root(check_msg.domain_hash); - info!("valid attestation duty, local sign and store {}", &signing_root); + info!( + "valid attestation duty, local sign and store {}", + &signing_root + ); let sig = self.keypair.sk.sign(signing_root); let serialized_signature = bincode::serialize(&sig).unwrap(); // save to local db let key = signing_root.as_bytes().into(); self.store.write(key, serialized_signature).await; - let _ = writer.send(Bytes::from(format!("successfully consensus attestation on {}", &signing_root))).await; - }, + let _ = writer + .send(Bytes::from(format!( + "successfully consensus attestation on {}", + &signing_root + ))) + .await; + } Ok(Safe::SameData) => { - info!( - "Skipping signing of previously signed attestation" - ); - let _ = writer.send(Bytes::from( "Skipping signing of previously signed attestation")).await; + info!("Skipping signing of previously signed attestation"); + let _ = writer + .send(Bytes::from( + "Skipping signing of previously signed attestation", + )) + .await; } Err(NotSafe::UnregisteredValidator(pk)) => { let _ = writer.send(Bytes::from(format!("Not signing attestation for unregistered validator public_key {:?}", pk))).await; - warn!("Not signing attestation for unregistered validator public_key {}", format!("{:?}", pk)); + warn!( + "Not signing attestation for unregistered validator public_key {}", + format!("{:?}", pk) + ); } Err(e) => { - let _ = writer.send(Bytes::from(format!("Not signing slashable attestation {:?}", e))).await; + let _ = writer + .send(Bytes::from(format!( + "Not signing slashable attestation {:?}", + e + ))) + .await; error!("Not signing slashable attestation {}", format!("{:?}", e)); } } - }, + } DvfType::Proposer(block_type) => { match block_type { BlockType::Full => { - let block : BeaconBlock> = match serde_json::from_slice(&check_msg.data) { - Ok(b) => b, - Err(_) => { - let _ = writer.send(Bytes::from( "failed to deserialize full proposal block")).await; - error!("failed to deserialize full proposal block"); - return Ok(()) - } - }; + let block: BeaconBlock> = + match serde_json::from_slice(&check_msg.data) { + Ok(b) => b, + Err(_) => { + let _ = writer + .send(Bytes::from( + "failed to deserialize full proposal block", + )) + .await; + error!("failed to deserialize full proposal block"); + return Ok(()); + } + }; self.sign_block(writer, block, check_msg.domain_hash).await; - }, + } BlockType::Blinded => { - let block : BeaconBlock> = match serde_json::from_slice(&check_msg.data) { - Ok(b) => b, - Err(_) => { - let _ = writer.send(Bytes::from( "failed to deserialize blinded proposal block")).await; - error!("failed to deserialize blinded proposal block"); - return Ok(()) - } - }; + let block: BeaconBlock> = + match serde_json::from_slice(&check_msg.data) { + Ok(b) => b, + Err(_) => { + let _ = writer + .send(Bytes::from( + "failed to deserialize blinded proposal block", + )) + .await; + error!("failed to deserialize blinded proposal block"); + return Ok(()); + } + }; self.sign_block(writer, block, check_msg.domain_hash).await; - }, - + } }; } } @@ -308,7 +382,7 @@ impl DvfSigner { node_para: Arc>>, keypair: Keypair, committee_def: OperatorCommitteeDefinition, - slashing_protection: SlashingDatabase + slashing_protection: SlashingDatabase, ) -> Result { let node_tmp = Arc::clone(&node_para); let node = node_tmp.read().await; @@ -325,7 +399,7 @@ impl DvfSigner { assert_eq!(operator_index.len(), 1); let operator_id = committee_def.operator_ids[operator_index[0]]; // Construct the committee for validator signing - let (mut operator_committee, _) = + let mut operator_committee = OperatorCommittee::from_definition(committee_def.clone()).await; let local_operator = Arc::new(RwLock::new(LocalOperator::new( validator_id, @@ -383,7 +457,6 @@ impl DvfSigner { let store = Store::new(&store_path.to_str().unwrap()) .map_err(|e| DvfError::StoreError(format!("Failed to create store: {:?}", e)))?; - let (signal, exit) = exit_future::signal(); Node::spawn_committee_ip_monitor(node_para, committee_def.clone(), exit); @@ -394,8 +467,12 @@ impl DvfSigner { }, ); info!("Insert signature handler for validator: {}", validator_id); - let operator_pks: HashMap = committee_def.operator_ids.into_iter().zip(committee_def.node_public_keys.into_iter()).collect(); - + let operator_pks: HashMap = committee_def + .operator_ids + .into_iter() + .zip(committee_def.node_public_keys.into_iter()) + .collect(); + node.duties_handler_map.write().await.insert( validator_id, DvfDutyCheckHandler { @@ -404,13 +481,11 @@ impl DvfSigner { validator_pk: operator_committee.get_validator_pk(), operator_pks, keypair: keypair.clone(), - _phantom: PhantomData - } + _phantom: PhantomData, + }, ); info!("Insert duties handler for validator: {}", validator_id); - - // DvfCore::spawn( // operator_id, // node_para.clone(), @@ -423,7 +498,6 @@ impl DvfSigner { // ) // .await; - Ok(Self { signal: Some(signal), operator_id, @@ -464,16 +538,18 @@ impl DvfSigner { domain_hash, check_type, data: data.to_vec(), - sign_hex: None + sign_hex: None, }; match msg.sign_digest(&self.node_secret) { Ok(sign_hex) => msg.sign_hex = Some(sign_hex), Err(_) => { error!("failed to sign msg"); - return ; + return; } } - self.operator_committee.consensus_on_duty(&bincode::serialize(&msg).unwrap()).await; + self.operator_committee + .consensus_on_duty(&bincode::serialize(&msg).unwrap()) + .await; } pub fn validator_public_key(&self) -> BlsPublicKey { diff --git a/src/node/node.rs b/src/node/node.rs index b75b89b8..4cc8431e 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -4,9 +4,9 @@ use crate::deposit::get_distributed_deposit; use crate::exit::get_distributed_voluntary_exit; use crate::network::io_committee::{SecureNetIOChannel, SecureNetIOCommittee}; use crate::node::config::{ - base_to_consensus_addr, base_to_mempool_addr, base_to_signature_addr, base_to_duties_addr, - NodeConfig, API_ADDRESS, DB_FILENAME, DISCOVERY_PORT_OFFSET, DKG_PORT_OFFSET, - PRESTAKE_SIGNATURE_URL, STAKE_SIGNATURE_URL, VALIDATOR_PK_URL, + base_to_duties_addr, base_to_signature_addr, NodeConfig, API_ADDRESS, DB_FILENAME, + DISCOVERY_PORT_OFFSET, DKG_PORT_OFFSET, PRESTAKE_SIGNATURE_URL, STAKE_SIGNATURE_URL, + VALIDATOR_PK_URL, }; use crate::node::contract::{ Contract, ContractCommand, EncryptedSecretKeys, Initiator, InitiatorStoreRecord, OperatorIds, @@ -16,7 +16,7 @@ use crate::node::contract::{ use crate::node::{ db::{self, Database}, discovery::Discovery, - dvfcore::{DvfSignatureReceiverHandler, DvfDutyCheckHandler}, + dvfcore::{DvfDutyCheckHandler, DvfSignatureReceiverHandler}, status_report::StatusReport, utils::{ convert_address_to_withdraw_crendentials, request_to_web_server, DepositRequest, @@ -74,7 +74,7 @@ pub struct Node { pub mempool_handler_map: Arc>>, pub consensus_handler_map: Arc>>, pub signature_handler_map: Arc>>, - pub duties_handler_map: Arc >>>, + pub duties_handler_map: Arc>>>, pub validator_store: Option>>, pub discovery: Arc, pub db: Database, @@ -108,7 +108,6 @@ impl Node { secret.name, duties_address ); - // let transaction_address = with_wildcard_ip(base_to_transaction_addr(config.base_address)); // NetworkReceiver::spawn( // transaction_address, @@ -385,12 +384,21 @@ impl Node { } } ContractCommand::SetFeeRecipient(va_pk, fee_recipient_address) => { - match set_validator_fee_recipient(node.clone(), va_pk, fee_recipient_address).await { + match set_validator_fee_recipient( + node.clone(), + va_pk, + fee_recipient_address, + ) + .await + { Ok(_) => { db.delete_contract_command(id).await; } Err(e) => { - error!("Failed to set validator fee recipient address: {:?}", e); + error!( + "Failed to set validator fee recipient address: {:?}", + e + ); db.updatetime_contract_command(id).await; } } @@ -1077,11 +1085,12 @@ pub async fn restart_validator( pub async fn set_validator_fee_recipient( node: Arc>>, validator_pk: Vec, - fee_recipient_address: H160 + fee_recipient_address: H160, ) -> Result<(), DvfError> { info!( "setting fee recipient to {} for validator {}...", - fee_recipient_address, hex::encode(validator_pk.clone()) + fee_recipient_address, + hex::encode(validator_pk.clone()) ); let validator_store = { let node_ = node.read().await; @@ -1089,9 +1098,12 @@ pub async fn set_validator_fee_recipient( }; match validator_store { Some(validator_store) => { - validator_store. - set_fee_recipient_for_validator( - &BlsPublicKey::deserialize(&validator_pk).unwrap(), fee_recipient_address).await; + validator_store + .set_fee_recipient_for_validator( + &BlsPublicKey::deserialize(&validator_pk).unwrap(), + fee_recipient_address, + ) + .await; Ok(()) } _ => Err(DvfError::ValidatorStoreNotReady), diff --git a/src/validation/account_utils/validator_definitions.rs b/src/validation/account_utils/validator_definitions.rs index 453c25d0..a1d6e92e 100644 --- a/src/validation/account_utils/validator_definitions.rs +++ b/src/validation/account_utils/validator_definitions.rs @@ -88,7 +88,7 @@ pub enum SigningDefinition { #[serde(skip_serializing_if = "Option::is_none")] voting_keystore_password: Option, }, - /// A validator that defers to a Web3Signer HTTP server for signing. + /// A validator that defers to a Web3Signer HTTP server for signing. /// /// https://github.com/ConsenSys/web3signer #[serde(rename = "web3signer")] diff --git a/src/validation/attestation_service.rs b/src/validation/attestation_service.rs index bf8991a7..1296272b 100644 --- a/src/validation/attestation_service.rs +++ b/src/validation/attestation_service.rs @@ -12,17 +12,14 @@ use crate::validation::{ use environment::RuntimeContext; use futures::executor::block_on; use futures::future::join_all; -use slog::{crit, error, info, trace, warn, debug}; +use slog::{crit, debug, error, info, trace, warn}; use slot_clock::SlotClock; use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; use tokio::time::{sleep, sleep_until, Duration, Instant}; use tree_hash::TreeHash; -use types::{ - Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, - Slot, -}; +use types::{Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Slot}; /// Builds an `AttestationService`. pub struct AttestationServiceBuilder { @@ -294,17 +291,21 @@ impl AttestationService { // Then download, sign and publish a `SignedAggregateAndProof` for each // validator that is elected to aggregate for this `slot` and // `committee_index`. - self.produce_and_publish_aggregates(&attestation_data, committee_index, &validator_duties) - .await - .map_err(move |e| { - crit!( - log, - "Error during attestation routine"; - "error" => format!("{:?}", e), - "committee_index" => committee_index, - "slot" => slot.as_u64(), - ) - })?; + self.produce_and_publish_aggregates( + &attestation_data, + committee_index, + &validator_duties, + ) + .await + .map_err(move |e| { + crit!( + log, + "Error during attestation routine"; + "error" => format!("{:?}", e), + "committee_index" => committee_index, + "slot" => slot.as_u64(), + ) + })?; } Ok(()) @@ -554,7 +555,6 @@ impl AttestationService { .spec .fork_name_at_slot::(attestation_data.slot); - let aggregated_attestation = &self .beacon_nodes .first_success( diff --git a/src/validation/block_service.rs b/src/validation/block_service.rs index ab929f3e..9c9d58e3 100644 --- a/src/validation/block_service.rs +++ b/src/validation/block_service.rs @@ -1,4 +1,5 @@ //! Reference: lighthouse/validator_client/block_service.rs +use crate::node::dvfcore::BlockType as DvfBlockType; use crate::validation::beacon_node_fallback::{Error as FallbackError, Errors}; use crate::validation::{ beacon_node_fallback::{ApiTopic, BeaconNodeFallback, OfflineOnFailure, RequireSynced}, @@ -21,10 +22,9 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; use types::{ - BlindedBeaconBlock, BlockType, EthSpec, Graffiti, PublicKeyBytes, SignedBlindedBeaconBlock, GRAFFITI_BYTES_LEN, - Slot, + BlindedBeaconBlock, BlockType, EthSpec, Graffiti, PublicKeyBytes, SignedBlindedBeaconBlock, + Slot, GRAFFITI_BYTES_LEN, }; -use crate::node::dvfcore::BlockType as DvfBlockType; #[derive(Debug)] pub enum BlockError { @@ -125,8 +125,8 @@ impl BlockServiceBuilder { .context .ok_or("Cannot build BlockService without runtime_context")?, proposer_nodes: self.proposer_nodes, - graffiti: self.graffiti, - graffiti_file: self.graffiti_file, + _graffiti: self.graffiti, + _graffiti_file: self.graffiti_file, }), }) } @@ -214,8 +214,8 @@ pub struct Inner { beacon_nodes: Arc>, proposer_nodes: Option>>, context: RuntimeContext, - graffiti: Option, - graffiti_file: Option, + _graffiti: Option, + _graffiti_file: Option, } /// Attempts to produce attestations for any block producer(s) at the start of the epoch. @@ -349,7 +349,7 @@ impl BlockService { "block service", ) } - + Ok(()) } @@ -512,7 +512,7 @@ impl BlockService { .request_proposers_last( RequireSynced::No, OfflineOnFailure::Yes, - |beacon_node| async move { + |beacon_node| async move { let _get_timer = metrics::start_timer_vec( &metrics::BLOCK_SERVICE_TIMES, &[metrics::BEACON_BLOCK_HTTP_GET], @@ -634,7 +634,8 @@ impl BlockService { // Apply per validator configuration first. let validator_builder_boost_factor = self .validator_store - .determine_validator_builder_boost_factor(validator_pubkey).await; + .determine_validator_builder_boost_factor(validator_pubkey) + .await; // Fallback to process-wide configuration if needed. let maybe_builder_boost_factor = validator_builder_boost_factor.or_else(|| { diff --git a/src/validation/cli.rs b/src/validation/cli.rs index 319f7639..ff6e569e 100644 --- a/src/validation/cli.rs +++ b/src/validation/cli.rs @@ -434,5 +434,4 @@ pub fn cli_app() -> Command { .help_heading(FLAG_HEADER) .display_order(0) ) - } diff --git a/src/validation/config.rs b/src/validation/config.rs index 3d9c8430..82f1c0d4 100644 --- a/src/validation/config.rs +++ b/src/validation/config.rs @@ -1,6 +1,6 @@ use crate::node::config::{NodeConfig, API_ADDRESS}; use crate::node::contract::{ - DEFAULT_TRANSPORT_URL, NETWORK_CONTRACT, REGISTRY_CONTRACT, SELF_OPERATOR_ID, EXTRA_CONTRACT + DEFAULT_TRANSPORT_URL, EXTRA_CONTRACT, NETWORK_CONTRACT, REGISTRY_CONTRACT, SELF_OPERATOR_ID, }; use crate::validation::beacon_node_fallback::ApiTopic; use crate::validation::graffiti_file::GraffitiFile; @@ -382,8 +382,9 @@ impl Config { config.builder_boost_factor = parse_optional(cli_args, "builder-boost-factor")?; - config.dvf_node_config.builder_boost_factor = parse_optional(cli_args, "builder-boost-factor")?; - + config.dvf_node_config.builder_boost_factor = + parse_optional(cli_args, "builder-boost-factor")?; + config.validator_registration_batch_size = parse_required(cli_args, "validator-registration-batch-size")?; if config.validator_registration_batch_size == 0 { diff --git a/src/validation/generic_operator_committee.rs b/src/validation/generic_operator_committee.rs index fa6f86c1..f61929ac 100644 --- a/src/validation/generic_operator_committee.rs +++ b/src/validation/generic_operator_committee.rs @@ -2,7 +2,7 @@ use crate::utils::error::DvfError; use crate::validation::operator::TOperator; use async_trait::async_trait; use std::sync::Arc; -use tokio::sync::mpsc::Receiver; +// use tokio::sync::mpsc::Receiver; use tokio::sync::RwLock; use types::{Hash256, PublicKey, Signature}; /// Operator committee for a validator. @@ -13,11 +13,11 @@ pub trait TOperatorCommittee: Send { validator_id: u64, validator_public_key: PublicKey, t: usize, - rx_consensus: Receiver, + // rx_consensus: Receiver, ) -> Self; fn validator_id(&self) -> u64; async fn add_operator(&mut self, operator_id: u64, operator: Arc>); - async fn consensus(&self, msg: Hash256) -> Result<(), DvfError>; + // async fn consensus(&self, msg: Hash256) -> Result<(), DvfError>; async fn consensus_on_duty(&self, data: &[u8]) -> Result<(), DvfError>; async fn sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError>; async fn get_leader(&self, nonce: u64) -> u64; @@ -39,14 +39,14 @@ where validator_id: u64, validator_public_key: PublicKey, threshold: usize, - rx_consensus: Receiver, + // rx_consensus: Receiver, ) -> Self { Self { cmt: TOperatorCommittee::new( validator_id, validator_public_key, threshold, - rx_consensus, + // rx_consensus, ), } } diff --git a/src/validation/http_api/keystores.rs b/src/validation/http_api/keystores.rs index 7e3e78a8..1efb0baa 100644 --- a/src/validation/http_api/keystores.rs +++ b/src/validation/http_api/keystores.rs @@ -1,13 +1,10 @@ //! Implementation of the standard keystore management API. use crate::validation::account_utils::ZeroizeString; -use crate::validation::{signing_method::SigningMethod, - ValidatorStore, -}; +use crate::validation::{signing_method::SigningMethod, ValidatorStore}; use eth2::lighthouse_vc::std_types::{ - ImportKeystoreStatus, - ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr, KeystoreJsonStr, - ListKeystoresResponse, SingleKeystoreResponse, Status, - }; + ImportKeystoreStatus, ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr, + KeystoreJsonStr, ListKeystoresResponse, SingleKeystoreResponse, Status, +}; use eth2_keystore::Keystore; use futures::executor::block_on; use slog::{info, warn, Logger}; @@ -209,9 +206,9 @@ fn _import_single_keystore( None, None, None, - None + None, )) .map_err(|e| format!("failed to initialize validator: {:?}", e))?; Ok(ImportKeystoreStatus::Imported) -} \ No newline at end of file +} diff --git a/src/validation/http_api/mod.rs b/src/validation/http_api/mod.rs index d806313f..113042ee 100644 --- a/src/validation/http_api/mod.rs +++ b/src/validation/http_api/mod.rs @@ -1,18 +1,20 @@ mod api_secret; mod create_validator; +mod graffiti; mod keystores; mod remotekeys; mod tests; -mod graffiti; use crate::validation::{GraffitiFile, ValidatorStore}; use eth2::lighthouse_vc::{ std_types::AuthResponse, - types::{self as api_types, GenericResponse, GetFeeRecipientResponse, GetGasLimitResponse, Graffiti, PublicKey, - PublicKeyBytes}, + types::{ + self as api_types, GenericResponse, GetFeeRecipientResponse, GetGasLimitResponse, Graffiti, + PublicKey, PublicKeyBytes, + }, }; -use logging::SSELoggingComponents; use lighthouse_version::version_with_platform; +use logging::SSELoggingComponents; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use slog::{crit, info, warn, Logger}; @@ -181,23 +183,23 @@ pub fn serve( ) }) }); - - let inner_spec = Arc::new(ctx.spec.clone()); - let spec_filter = warp::any().map(move || inner_spec.clone()); - - let api_token_path_inner = api_token_path.clone(); - let api_token_path_filter = warp::any().map(move || api_token_path_inner.clone()); - - // Create a `warp` filter that provides access to local system information. - let system_info = Arc::new(RwLock::new(sysinfo::System::new())); - { - // grab write access for initialisation - let mut system_info = system_info.write(); - system_info.refresh_disks_list(); - system_info.refresh_networks_list(); - } // end lock - - let system_info_filter = + + let inner_spec = Arc::new(ctx.spec.clone()); + let spec_filter = warp::any().map(move || inner_spec.clone()); + + let api_token_path_inner = api_token_path.clone(); + let api_token_path_filter = warp::any().map(move || api_token_path_inner.clone()); + + // Create a `warp` filter that provides access to local system information. + let system_info = Arc::new(RwLock::new(sysinfo::System::new())); + { + // grab write access for initialisation + let mut system_info = system_info.write(); + system_info.refresh_disks_list(); + system_info.refresh_networks_list(); + } // end lock + + let system_info_filter = warp::any() .map(move || system_info.clone()) .map(|sysinfo: Arc>| { @@ -214,8 +216,8 @@ pub fn serve( sysinfo }); - let app_start = std::time::Instant::now(); - let app_start_filter = warp::any().map(move || app_start); + let app_start = std::time::Instant::now(); + let app_start_filter = warp::any().map(move || app_start); // GET lighthouse/version let get_node_version = warp::path("lighthouse") @@ -259,28 +261,29 @@ pub fn serve( .and(warp::path::end()) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) - .then(|validator_store: Arc>, task_executor: TaskExecutor| { - blocking_json_task(move || { - if let Some(handle) = task_executor.handle() { - let validators = handle.block_on(validator_store - .initialized_validators() - .read()) - .validator_definitions() - .iter() - .map(|def| api_types::ValidatorData { - enabled: def.enabled, - description: def.description.clone(), - voting_pubkey: PublicKeyBytes::from(&def.voting_public_key), - }) - .collect::>(); - Ok(api_types::GenericResponse::from(validators)) - } else { - Err(warp_utils::reject::custom_server_error( - "Lighthouse shutting down".into(), - )) - } - }) - }); + .then( + |validator_store: Arc>, task_executor: TaskExecutor| { + blocking_json_task(move || { + if let Some(handle) = task_executor.handle() { + let validators = handle + .block_on(validator_store.initialized_validators().read()) + .validator_definitions() + .iter() + .map(|def| api_types::ValidatorData { + enabled: def.enabled, + description: def.description.clone(), + voting_pubkey: PublicKeyBytes::from(&def.voting_public_key), + }) + .collect::>(); + Ok(api_types::GenericResponse::from(validators)) + } else { + Err(warp_utils::reject::custom_server_error( + "Lighthouse shutting down".into(), + )) + } + }) + }, + ); // GET lighthouse/validators/{validator_pubkey} let get_lighthouse_validators_pubkey = warp::path("lighthouse") @@ -290,12 +293,13 @@ pub fn serve( .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .then( - |validator_pubkey: PublicKey, validator_store: Arc>, task_executor: TaskExecutor| { + |validator_pubkey: PublicKey, + validator_store: Arc>, + task_executor: TaskExecutor| { blocking_json_task(move || { if let Some(handle) = task_executor.handle() { - let validator = handle.block_on(validator_store - .initialized_validators() - .read()) + let validator = handle + .block_on(validator_store.initialized_validators().read()) .validator_definitions() .iter() .find(|def| def.voting_public_key == validator_pubkey) @@ -315,7 +319,7 @@ pub fn serve( Err(warp_utils::reject::custom_server_error( "Lighthouse shutting down".into(), )) - } + } }) }, ); @@ -363,20 +367,27 @@ pub fn serve( .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .then( - |validator_pubkey: PublicKey, validator_store: Arc>, task_executor: TaskExecutor| { + |validator_pubkey: PublicKey, + validator_store: Arc>, + task_executor: TaskExecutor| { blocking_json_task(move || { if let Some(handle) = task_executor.handle() { - if handle.block_on(validator_store - .initialized_validators() - .read()).is_enabled(&validator_pubkey) - .is_none() { + if handle + .block_on(validator_store.initialized_validators().read()) + .is_enabled(&validator_pubkey) + .is_none() + { return Err(warp_utils::reject::custom_not_found(format!( "no validator found with pubkey {:?}", validator_pubkey ))); } - handle.block_on(validator_store - .get_fee_recipient(&PublicKeyBytes::from(&validator_pubkey))).map(|fee_recipient| { + handle + .block_on( + validator_store + .get_fee_recipient(&PublicKeyBytes::from(&validator_pubkey)), + ) + .map(|fee_recipient| { GenericResponse::from(GetFeeRecipientResponse { pubkey: PublicKeyBytes::from(validator_pubkey.clone()), ethaddress: fee_recipient, @@ -387,7 +398,7 @@ pub fn serve( "no fee recipient set".to_string(), ) }) - } else { + } else { Err(warp_utils::reject::custom_server_error( "Lighthouse shutting down".into(), )) @@ -444,34 +455,33 @@ pub fn serve( .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .then( - |validator_pubkey: PublicKey, validator_store: Arc>, task_executor: TaskExecutor| { - + |validator_pubkey: PublicKey, + validator_store: Arc>, + task_executor: TaskExecutor| { blocking_json_task(move || { if let Some(handle) = task_executor.handle() { - if handle.block_on(validator_store - .initialized_validators() - .read()) - .is_enabled(&validator_pubkey) - .is_none() - { - return Err(warp_utils::reject::custom_not_found(format!( - "no validator found with pubkey {:?}", - validator_pubkey - ))); - } - Ok(GenericResponse::from(GetGasLimitResponse { - pubkey: PublicKeyBytes::from(validator_pubkey.clone()), - gas_limit: handle.block_on(validator_store - .get_gas_limit(&PublicKeyBytes::from(&validator_pubkey))), - })) - } - else { + if handle + .block_on(validator_store.initialized_validators().read()) + .is_enabled(&validator_pubkey) + .is_none() + { + return Err(warp_utils::reject::custom_not_found(format!( + "no validator found with pubkey {:?}", + validator_pubkey + ))); + } + Ok(GenericResponse::from(GetGasLimitResponse { + pubkey: PublicKeyBytes::from(validator_pubkey.clone()), + gas_limit: handle.block_on( + validator_store + .get_gas_limit(&PublicKeyBytes::from(&validator_pubkey)), + ), + })) + } else { Err(warp_utils::reject::custom_server_error( "Lighthouse shutting down".into(), )) } - - }) }, ); @@ -497,21 +507,20 @@ pub fn serve( // // When adding a route, don't forget to add it to the `routes_with_invalid_auth` tests! .and( - warp::get() - .and( - get_node_version - .or(get_lighthouse_health) - .or(get_lighthouse_spec) - .or(get_lighthouse_validators) - .or(get_lighthouse_validators_pubkey) - .or(get_lighthouse_ui_health) - .or(get_fee_recipient) - .or(get_gas_limit) - // .or(get_graffiti) - .or(get_std_keystores) - .or(get_std_remotekeys) - .recover(warp_utils::reject::handle_rejection), - ) + warp::get().and( + get_node_version + .or(get_lighthouse_health) + .or(get_lighthouse_spec) + .or(get_lighthouse_validators) + .or(get_lighthouse_validators_pubkey) + .or(get_lighthouse_ui_health) + .or(get_fee_recipient) + .or(get_gas_limit) + // .or(get_graffiti) + .or(get_std_keystores) + .or(get_std_remotekeys) + .recover(warp_utils::reject::handle_rejection), + ), ) // The auth route and logs are the only routes that are allowed to be accessed without the API token. .or(warp::get().and(get_auth)) diff --git a/src/validation/http_api/remotekeys.rs b/src/validation/http_api/remotekeys.rs index 04136379..dd9494cc 100644 --- a/src/validation/http_api/remotekeys.rs +++ b/src/validation/http_api/remotekeys.rs @@ -1,6 +1,6 @@ //! Implementation of the standard remotekey management API. use crate::validation::account_utils::validator_definitions::{ - SigningDefinition, ValidatorDefinition, Web3SignerDefinition + SigningDefinition, ValidatorDefinition, Web3SignerDefinition, }; use crate::validation::{initialized_validators::Error, InitializedValidators, ValidatorStore}; use eth2::lighthouse_vc::std_types::{ @@ -40,7 +40,7 @@ pub fn list( url: url.clone(), readonly: false, }) - }, + } // [Zico]TODO: to be revised SigningDefinition::DistributedKeystore { .. } => None, } @@ -67,8 +67,12 @@ pub fn _import( for remotekey in request.remote_keys { let status = if let Some(handle) = task_executor.handle() { // Import the keystore. - match _import_single_remotekey(remotekey.pubkey, remotekey.url, &validator_store, handle) - { + match _import_single_remotekey( + remotekey.pubkey, + remotekey.url, + &validator_store, + handle, + ) { Ok(status) => Status::ok(status), Err(e) => { warn!( diff --git a/src/validation/impls/hotstuff.rs b/src/validation/impls/hotstuff.rs index 05f5cd89..71a4c7c5 100644 --- a/src/validation/impls/hotstuff.rs +++ b/src/validation/impls/hotstuff.rs @@ -4,13 +4,13 @@ use crate::validation::{generic_operator_committee::TOperatorCommittee, operator use async_trait::async_trait; use bls::{Hash256, PublicKey, Signature}; use futures::future::join_all; -use log::{debug, info}; +use log::info; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::mpsc::Receiver; -use tokio::sync::Notify; use tokio::sync::RwLock; -use tokio::task::JoinHandle; +// use tokio::sync::mpsc::Receiver; +// use tokio::sync::Notify; +// use tokio::task::JoinHandle; /// Provides the externally-facing operator committee type. pub mod types { @@ -23,14 +23,14 @@ pub struct HotstuffOperatorCommittee { validator_public_key: PublicKey, operators: RwLock>>>, threshold_: usize, - consensus_notifications: Arc>>>, - thread_handle: JoinHandle<()>, + // consensus_notifications: Arc>>>, + // thread_handle: JoinHandle<()>, } impl Drop for HotstuffOperatorCommittee { fn drop(&mut self) { info!("Shutting down hotstuff operator committee"); - self.thread_handle.abort(); + // self.thread_handle.abort(); } } @@ -40,40 +40,40 @@ impl TOperatorCommittee for HotstuffOperatorCommittee { validator_id: u64, validator_public_key: PublicKey, t: usize, - mut rx_consensus: Receiver, + // mut rx_consensus: Receiver, ) -> Self { - let consensus_notifications: Arc>>> = - Arc::new(RwLock::new(HashMap::default())); - let consensus_notifications_clone = consensus_notifications.clone(); - let thread_handle = tokio::spawn(async move { - loop { - match rx_consensus.recv().await { - Some(value) => { - debug!("Consensus achieved for msg {}", value); - let notes = consensus_notifications_clone.write().await; - if let Some(notify) = notes.get(&value) { - notify.notify_one(); - } - // else { - // let notify = Arc::new(Notify::new()); - // notify.notify_one(); - // notes.insert(value, notify); - // } - } - None => { - return; - } - } - } - }); + // let consensus_notifications: Arc>>> = + // Arc::new(RwLock::new(HashMap::default())); + // let consensus_notifications_clone = consensus_notifications.clone(); + // let thread_handle = tokio::spawn(async move { + // loop { + // match rx_consensus.recv().await { + // Some(value) => { + // debug!("Consensus achieved for msg {}", value); + // let notes = consensus_notifications_clone.write().await; + // if let Some(notify) = notes.get(&value) { + // notify.notify_one(); + // } + // // else { + // // let notify = Arc::new(Notify::new()); + // // notify.notify_one(); + // // notes.insert(value, notify); + // // } + // } + // None => { + // return; + // } + // } + // } + // }); Self { validator_id, validator_public_key, operators: <_>::default(), threshold_: t, - consensus_notifications, - thread_handle, + // consensus_notifications, + // thread_handle, } } @@ -104,35 +104,33 @@ impl TOperatorCommittee for HotstuffOperatorCommittee { ids.iter().position(|&id| id == op_id).unwrap() } - async fn consensus(&self, msg: Hash256) -> Result<(), DvfError> { - let notify = { - let mut notes = self.consensus_notifications.write().await; - if let Some(notify) = notes.get(&msg) { - notify.clone() - } else { - let notify = Arc::new(Notify::new()); - notes.insert(msg, notify.clone()); - notify - } - }; - - let operators = self.operators.read().await; - for operator in operators.values() { - operator.read().await.propose(msg).await; - } - notify.notified().await; - let mut notes = self.consensus_notifications.write().await; - notes.remove(&msg); - Ok(()) - } + // async fn consensus(&self, msg: Hash256) -> Result<(), DvfError> { + // let notify = { + // let mut notes = self.consensus_notifications.write().await; + // if let Some(notify) = notes.get(&msg) { + // notify.clone() + // } else { + // let notify = Arc::new(Notify::new()); + // notes.insert(msg, notify.clone()); + // notify + // } + // }; + + // let operators = self.operators.read().await; + // for operator in operators.values() { + // operator.read().await.propose(msg).await; + // } + // notify.notified().await; + // let mut notes = self.consensus_notifications.write().await; + // notes.remove(&msg); + // Ok(()) + // } async fn consensus_on_duty(&self, data: &[u8]) -> Result<(), DvfError> { let operators = &self.operators.read().await; let signing_futs = operators.keys().map(|operator_id| async move { let operator = operators.get(operator_id).unwrap().read().await; - operator - .consensus_on_duty(data) - .await + operator.consensus_on_duty(data).await }); let _ = join_all(signing_futs).await; Ok(()) @@ -170,7 +168,6 @@ impl TOperatorCommittee for HotstuffOperatorCommittee { Ok((sig, ids)) } - fn get_validator_pk(&self) -> PublicKey { self.validator_public_key.clone() } diff --git a/src/validation/initialized_validators.rs b/src/validation/initialized_validators.rs index 4748b8d9..b32c7391 100644 --- a/src/validation/initialized_validators.rs +++ b/src/validation/initialized_validators.rs @@ -215,7 +215,7 @@ impl InitializedValidator { key_cache: &mut KeyCache, key_stores: &mut HashMap, node: Option>>>, - slashing_protection: SlashingDatabase + slashing_protection: SlashingDatabase, ) -> Result { if !def.enabled { return Err(Error::UnableToInitializeDisabledValidator); @@ -390,9 +390,14 @@ impl InitializedValidator { let committee_def = OperatorCommitteeDefinition::from_file(committee_def_path) .map_err(Error::UnableToParseCommitteeDefinition)?; let validator_public_key = committee_def.validator_public_key.clone(); - let signer = DvfSigner::spawn(node.unwrap(), voting_keypair, committee_def, slashing_protection) - .await - .map_err(|e| Error::DvfError(format!("{:?}", e)))?; + let signer = DvfSigner::spawn( + node.unwrap(), + voting_keypair, + committee_def, + slashing_protection, + ) + .await + .map_err(|e| Error::DvfError(format!("{:?}", e)))?; SigningMethod::DistributedKeystore { voting_keystore_share_path, @@ -496,7 +501,7 @@ pub struct InitializedValidators { /// For logging via `slog`. log: Logger, /// For slashing protection in dvf signer - slashing_protection: SlashingDatabase + slashing_protection: SlashingDatabase, } impl InitializedValidators { @@ -506,7 +511,7 @@ impl InitializedValidators { validators_dir: PathBuf, node: Option>>>, log: Logger, - slashing_protection: SlashingDatabase + slashing_protection: SlashingDatabase, ) -> Result { let mut this = Self { validators_dir, @@ -514,7 +519,7 @@ impl InitializedValidators { validators: HashMap::::default(), node, log, - slashing_protection + slashing_protection, }; this.update_validators().await?; Ok(this) @@ -1271,7 +1276,7 @@ impl InitializedValidators { &mut key_stores, //&mut committee_cache, None, - self.slashing_protection.clone() + self.slashing_protection.clone(), ) .await { @@ -1323,7 +1328,7 @@ impl InitializedValidators { &mut key_stores, //&mut committee_cache, None, - self.slashing_protection.clone() + self.slashing_protection.clone(), ) .await { @@ -1376,7 +1381,7 @@ impl InitializedValidators { &mut key_stores, //&mut committee_cache, self.node.clone(), - self.slashing_protection.clone() + self.slashing_protection.clone(), ) .await { diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 1f618978..95688ffb 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -256,7 +256,6 @@ impl ProductionValidatorClient { .await .map_err(|e| format!("Dvf node creation failed: {}", e))?; - // Initialize slashing protection before initializing validators // // Create the slashing database if there are no validators, even if @@ -285,13 +284,16 @@ impl ProductionValidatorClient { config.validator_dir.clone(), Some(node.clone()), log.clone(), - slashing_protection.clone() + slashing_protection.clone(), ) .await .map_err(|e| format!("Unable to initialize validators: {:?}", e))?; let voting_pubkeys: Vec<_> = validators.iter_voting_pubkeys().collect(); - let pubkeys: Vec<_> = validators.iter_voting_pubkeys().map(|p| p.clone() ).collect(); + let pubkeys: Vec<_> = validators + .iter_voting_pubkeys() + .map(|p| p.clone()) + .collect(); info!( log, "Initialized validators"; @@ -307,8 +309,6 @@ impl ProductionValidatorClient { ); } - - // Check validator registration with slashing protection, or auto-register all validators. if config.init_slashing_protection { slashing_protection diff --git a/src/validation/operator.rs b/src/validation/operator.rs index 475b142f..75d68ff7 100644 --- a/src/validation/operator.rs +++ b/src/validation/operator.rs @@ -3,14 +3,14 @@ use std::sync::Arc; use std::time::Duration; use crate::node::config::{ - base_to_consensus_addr, base_to_mempool_addr, base_to_signature_addr, base_to_transaction_addr, - is_addr_invalid, base_to_duties_addr + base_to_consensus_addr, base_to_duties_addr, base_to_mempool_addr, base_to_signature_addr, + base_to_transaction_addr, is_addr_invalid, }; use crate::utils::error::DvfError; use async_trait::async_trait; use bytes::Bytes; use downcast_rs::DowncastSync; -use log::{debug, warn, info}; +use log::{debug, info, warn}; use network::{DvfMessage, ReliableSender, SimpleSender, VERSION}; use tokio::time::{sleep_until, timeout, Instant}; use types::{Hash256, Keypair, PublicKey, Signature}; @@ -83,7 +83,7 @@ impl TOperator for LocalOperator { } async fn consensus_on_duty(&self, _msg: &[u8]) { - return ; + return; } } @@ -194,7 +194,7 @@ impl TOperator for RemoteOperator { // skip this function quickly if is_addr_invalid(self.base_address()) { warn!("invalid socket address"); - return ; + return; } let n_try: u64 = 1; let timeout_mill: u64 = 800; @@ -214,9 +214,14 @@ impl TOperator for RemoteOperator { let result = timeout(Duration::from_millis(timeout_mill), receiver).await; match result { Ok(output) => match output { - Ok(data) => { - info!("Received consensus response from [{}/{}]: {:?}", self.operator_id, self.validator_id, std::str::from_utf8(&data)); - }, + Ok(data) => { + info!( + "Received consensus response from [{}/{}]: {:?}", + self.operator_id, + self.validator_id, + std::str::from_utf8(&data) + ); + } Err(_) => { warn!("recv is interrupted."); } diff --git a/src/validation/operator_committees.rs b/src/validation/operator_committees.rs index c0a7a539..2a8e5391 100644 --- a/src/validation/operator_committees.rs +++ b/src/validation/operator_committees.rs @@ -2,27 +2,25 @@ use crate::node::config::invalid_addr; use crate::validation::operator::RemoteOperator; use crate::validation::operator_committee_definitions::OperatorCommitteeDefinition; use crate::validation::OperatorCommittee; -use crate::DEFAULT_CHANNEL_CAPACITY; -use hsutils::monitored_channel::{MonitoredChannel, MonitoredSender}; use std::sync::Arc; use tokio::sync::RwLock; -use types::Hash256; +// use types::Hash256; +// use crate::DEFAULT_CHANNEL_CAPACITY; +// use hsutils::monitored_channel::{MonitoredChannel, MonitoredSender}; impl OperatorCommittee { - pub async fn from_definition( - def: OperatorCommitteeDefinition, - ) -> (Self, MonitoredSender) { - let (tx, rx) = MonitoredChannel::new( - DEFAULT_CHANNEL_CAPACITY, - format!("{}-dvf-op-committee", def.validator_id), - "debug", - ); + pub async fn from_definition(def: OperatorCommitteeDefinition) -> Self { + // let (tx, rx) = MonitoredChannel::new( + // DEFAULT_CHANNEL_CAPACITY, + // format!("{}-dvf-op-committee", def.validator_id), + // "debug", + // ); let mut committee = Self::new( def.validator_id, def.validator_public_key.clone(), def.threshold as usize, - rx, + // rx, ); for i in 0..(def.total as usize) { let addr = def.base_socket_addresses[i].unwrap_or(invalid_addr()); @@ -36,6 +34,6 @@ impl OperatorCommittee { .add_operator(def.operator_ids[i], Arc::new(RwLock::new(operator))) .await; } - (committee, tx) + committee } } diff --git a/src/validation/signing_method.rs b/src/validation/signing_method.rs index 402d67a1..c2fae802 100644 --- a/src/validation/signing_method.rs +++ b/src/validation/signing_method.rs @@ -7,7 +7,7 @@ //! - Via a distributed operator committee use crate::node::config::{API_ADDRESS, COLLECT_PERFORMANCE_URL}; -use crate::node::dvfcore::{DvfSigner, DvfType, BlockType}; +use crate::node::dvfcore::{BlockType, DvfSigner, DvfType}; use crate::node::utils::{request_to_web_server, DvfPerformanceRequest, SignDigest}; use crate::validation::eth2_keystore_share::keystore_share::KeystoreShare; use crate::validation::http_metrics::metrics; @@ -135,31 +135,42 @@ impl SigningContext { impl SigningMethod { pub async fn is_leader(&self, epoch: Epoch) -> bool { match self { - SigningMethod::DistributedKeystore {dvf_signer, .. } => { + SigningMethod::DistributedKeystore { dvf_signer, .. } => { dvf_signer.is_aggregator(epoch.as_u64()).await - }, - _ => { - false } + _ => false, } } - pub async fn distributed_consensus_attestation(&self, domain_hash: Hash256, attestation_data: &AttestationData, ) { + pub async fn distributed_consensus_attestation( + &self, + domain_hash: Hash256, + attestation_data: &AttestationData, + ) { match self { - SigningMethod::DistributedKeystore {dvf_signer, .. } => { + SigningMethod::DistributedKeystore { dvf_signer, .. } => { let data = serde_json::to_string(attestation_data).unwrap(); - dvf_signer.consensus_on_duty(domain_hash, DvfType::Attester, data.as_bytes()).await; - }, + dvf_signer + .consensus_on_duty(domain_hash, DvfType::Attester, data.as_bytes()) + .await; + } _ => {} } } - pub async fn distributed_consensus_block>(&self, domain_hash: Hash256, block: &BeaconBlock, block_type: BlockType) { + pub async fn distributed_consensus_block>( + &self, + domain_hash: Hash256, + block: &BeaconBlock, + block_type: BlockType, + ) { match self { - SigningMethod::DistributedKeystore {dvf_signer, .. } => { + SigningMethod::DistributedKeystore { dvf_signer, .. } => { let data = serde_json::to_string(block).unwrap(); - dvf_signer.consensus_on_duty(domain_hash, DvfType::Proposer(block_type), data.as_bytes()).await - }, + dvf_signer + .consensus_on_duty(domain_hash, DvfType::Proposer(block_type), data.as_bytes()) + .await + } _ => {} } } @@ -358,11 +369,7 @@ impl SigningMethod { (e.epoch.start_slot(T::slots_per_epoch()), "VA_EXIT", true) } }; - let is_aggregator = dvf_signer - .is_aggregator( - signing_epoch.as_u64(), - ) - .await; + let is_aggregator = dvf_signer.is_aggregator(signing_epoch.as_u64()).await; log::info!( "[Dvf {}/{}] Signing\t-\tSlot: {}.\tEpoch: {}.\tType: {}.\tRoot: {:?}. Is aggregator {}", dvf_signer.operator_id, diff --git a/src/validation/validator_store.rs b/src/validation/validator_store.rs index c92f1618..afe3eaeb 100644 --- a/src/validation/validator_store.rs +++ b/src/validation/validator_store.rs @@ -523,22 +523,25 @@ impl ValidatorStore { ) -> Option { let validator_prefer_builder_proposals = self .validators - .read().await + .read() + .await .prefer_builder_proposals(validator_pubkey); if matches!(validator_prefer_builder_proposals, Some(true)) { return Some(u64::MAX); } - let validator_prefer_builder_proposal = self.validators.read().await.builder_proposals(validator_pubkey); + let validator_prefer_builder_proposal = self + .validators + .read() + .await + .builder_proposals(validator_pubkey); self.validators - .read().await + .read() + .await .builder_boost_factor(validator_pubkey) .or_else(|| { - if matches!( - validator_prefer_builder_proposal, - Some(false) - ) { + if matches!(validator_prefer_builder_proposal, Some(false)) { return Some(0); } None @@ -577,7 +580,7 @@ impl ValidatorStore { validator_pubkey: PublicKeyBytes, block: BeaconBlock, current_slot: Slot, - block_type: BlockType + block_type: BlockType, ) -> Result, Error> { // Make sure the block slot is not higher than the current slot to avoid potential attacks. if block.slot() > current_slot { @@ -598,8 +601,8 @@ impl ValidatorStore { let domain_hash = signing_context.domain_hash(&self.spec); let signing_method = self - .doppelganger_checked_signing_method(validator_pubkey) - .await?; + .doppelganger_checked_signing_method(validator_pubkey) + .await?; if signing_method.is_leader(signing_epoch).await { // Check for slashing conditions. @@ -613,7 +616,9 @@ impl ValidatorStore { Ok(Safe::Valid) => { metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SUCCESS]); // distributed consensus - signing_method.distributed_consensus_block(domain_hash, &block, block_type).await; + signing_method + .distributed_consensus_block(domain_hash, &block, block_type) + .await; let signature = signing_method .get_signature::( SignableMessage::BeaconBlock(&block), @@ -639,7 +644,10 @@ impl ValidatorStore { "msg" => "Carefully consider running with --init-slashing-protection (see --help)", "public_key" => format!("{:?}", pk) ); - metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::UNREGISTERED]); + metrics::inc_counter_vec( + &metrics::SIGNED_BLOCKS_TOTAL, + &[metrics::UNREGISTERED], + ); Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) } Err(e) => { @@ -673,7 +681,9 @@ impl ValidatorStore { } // Get the signing method and check doppelganger protection. - let signing_method = self.doppelganger_checked_signing_method(validator_pubkey).await?; + let signing_method = self + .doppelganger_checked_signing_method(validator_pubkey) + .await?; // Checking for slashing conditions. let signing_epoch = attestation.data().target.epoch; @@ -682,7 +692,7 @@ impl ValidatorStore { if signing_method.is_leader(signing_epoch).await { let slashing_status = if signing_method - .requires_local_slashing_protection(self.enable_web3signer_slashing_protection) + .requires_local_slashing_protection(self.enable_web3signer_slashing_protection) { self.slashing_protection.check_and_insert_attestation( &validator_pubkey, @@ -690,12 +700,14 @@ impl ValidatorStore { domain_hash, ) } else { - Ok(Safe::Valid) + Ok(Safe::Valid) }; match slashing_status { // We can safely sign this attestation. Ok(Safe::Valid) => { - signing_method.distributed_consensus_attestation(domain_hash, attestation.data()).await; + signing_method + .distributed_consensus_attestation(domain_hash, attestation.data()) + .await; let signature = signing_method .get_signature::>( SignableMessage::AttestationData(&attestation.data()), @@ -708,7 +720,10 @@ impl ValidatorStore { .add_signature(&signature, validator_committee_position) .map_err(Error::UnableToSignAttestation)?; - metrics::inc_counter_vec(&metrics::SIGNED_ATTESTATIONS_TOTAL, &[metrics::SUCCESS]); + metrics::inc_counter_vec( + &metrics::SIGNED_ATTESTATIONS_TOTAL, + &[metrics::SUCCESS], + ); Ok(()) } @@ -750,13 +765,9 @@ impl ValidatorStore { Err(Error::Slashable(e)) } } - } - else { + } else { return Err(Error::UnableToSign(SigningError::NotLeader)); } - - - } pub async fn sign_voluntary_exit( From d7f109f058d77bcddad1fb3eaca913183bcbc44e Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Thu, 5 Sep 2024 23:55:04 +0000 Subject: [PATCH 18/43] remove unused code --- src/validation/mod.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 95688ffb..1b1a467a 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -279,7 +279,7 @@ impl ProductionValidatorClient { }) }?; - let mut validators = InitializedValidators::from_definitions( + let validators = InitializedValidators::from_definitions( validator_defs, config.validator_dir.clone(), Some(node.clone()), @@ -290,10 +290,6 @@ impl ProductionValidatorClient { .map_err(|e| format!("Unable to initialize validators: {:?}", e))?; let voting_pubkeys: Vec<_> = validators.iter_voting_pubkeys().collect(); - let pubkeys: Vec<_> = validators - .iter_voting_pubkeys() - .map(|p| p.clone()) - .collect(); info!( log, "Initialized validators"; @@ -328,10 +324,6 @@ impl ProductionValidatorClient { })?; } - for pk in pubkeys { - let _ = validators.enable_keystore(&pk.decompress().unwrap()).await; - } - let last_beacon_node_index = config .beacon_nodes .len() From aafe456bc1dbccac646ee071f231f8a15e15a897 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Fri, 6 Sep 2024 00:02:34 +0000 Subject: [PATCH 19/43] retry to request sig --- src/validation/operator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/validation/operator.rs b/src/validation/operator.rs index 75d68ff7..a3667e87 100644 --- a/src/validation/operator.rs +++ b/src/validation/operator.rs @@ -121,9 +121,9 @@ impl TOperator for RemoteOperator { return Err(DvfError::SocketAddrUnknown); } - let n_try: u64 = 1; + let n_try: u64 = 3; let timeout_mill: u64 = 600; - let sleep_mill: u64 = 300; + let sleep_mill: u64 = 200; let dvf_message = DvfMessage { version: VERSION, validator_id: self.validator_id, From ac4be3d3a8afae324b3082d83490a43b439454d7 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Fri, 6 Sep 2024 02:57:43 +0000 Subject: [PATCH 20/43] optimize activate --- src/node/node.rs | 40 +++++++++++++++++++------------ src/validation/validator_store.rs | 4 ++++ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/node/node.rs b/src/node/node.rs index 4cc8431e..5278349d 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -700,15 +700,20 @@ pub async fn activate_validator( match validator_store { Some(validator_store) => { - validator_store - .start_validator_keystore(&validator_pk) - .await; - info!("[VA {}] validator {} activated", validator_id, validator_pk); - set_int_gauge( - &metrics::DVT_VC_BALANCE_USED_UP, - &[&validator_pk.as_hex_string()], - BALANCE_STILL_AVAILABLE, - ); + match validator_store.is_active(&validator_pk).await { + Some(active) => { + if !active { + validator_store.start_validator_keystore(&validator_pk).await; + info!("[VA {}] validator {} activated", validator_id, validator_pk); + set_int_gauge( + &metrics::DVT_VC_BALANCE_USED_UP, + &[&validator_pk.as_hex_string()], + BALANCE_STILL_AVAILABLE, + ); + } + } + None => {} + } Ok(()) } _ => { @@ -769,13 +774,18 @@ pub async fn stop_validator( let node_ = node.read().await; node_.validator_store.clone() }; - - cleanup_handler(node.clone(), validator_id).await; + match validator_store { - Some(validator_store) => { - validator_store.stop_validator_keystore(&validator_pk).await; - info!("[VA {}] stopped validator {}", validator_id, validator_pk); - } + Some(validator_store) => match validator_store.is_active(&validator_pk).await { + Some(active) => { + if active { + cleanup_handler(node.clone(), validator_id).await; + validator_store.stop_validator_keystore(&validator_pk).await; + info!("[VA {}] stopped validator {}", validator_id, validator_pk); + } + } + None => {} + }, _ => { return Err(format!( "[VA {}] failed to stop validator {}. Error: no validator store is set. please wait, please wait", diff --git a/src/validation/validator_store.rs b/src/validation/validator_store.rs index afe3eaeb..e306252a 100644 --- a/src/validation/validator_store.rs +++ b/src/validation/validator_store.rs @@ -1200,4 +1200,8 @@ impl ValidatorStore { } } } + + pub async fn is_active(&self, pubkey: &PublicKey) -> Option { + self.validators.read().await.is_enabled(pubkey) + } } From 689e1aee5044997a706eaae4090f275eacd52041 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Fri, 6 Sep 2024 07:02:29 +0000 Subject: [PATCH 21/43] update time parameters --- src/node/node.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/node/node.rs b/src/node/node.rs index 5278349d..91ce77a7 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -207,7 +207,7 @@ impl Node { pub fn process_contract_command(node: Arc>>, db: Database) { tokio::spawn(async move { tokio::time::sleep(Duration::from_secs(30)).await; - let mut query_interval = tokio::time::interval(Duration::from_secs(6)); + let mut query_interval = tokio::time::interval(Duration::from_secs(1)); loop { query_interval.tick().await; match db.get_contract_command().await { @@ -260,7 +260,6 @@ impl Node { } } ContractCommand::ActivateValidator(validator) => { - info!("ActivateValidator"); let va_id = validator.id; let validator_pubkey = hex::encode(validator.public_key.clone()); match activate_validator(node.clone(), validator).await { @@ -275,7 +274,6 @@ impl Node { } } ContractCommand::StopValidator(validator) => { - info!("StopValidator"); let va_id = validator.id; let validator_pubkey = hex::encode(validator.public_key.clone()); match stop_validator(node.clone(), validator).await { @@ -689,10 +687,6 @@ pub async fn activate_validator( let validator_id = validator.id; let validator_pk = BlsPublicKey::deserialize(&validator.public_key) .map_err(|e| format!("Unable to deserialize validator public key: {:?}", e))?; - info!( - "[VA {}] activating validator {}...", - validator_id, validator_pk - ); let validator_store = { let node_ = node.read().await; node_.validator_store.clone() @@ -732,10 +726,6 @@ pub async fn remove_validator( let validator_id = validator.id; let validator_pk = BlsPublicKey::deserialize(&validator.public_key) .map_err(|e| format!("[VA {}] Deserialize error ({:?})", validator_id, e))?; - info!( - "[VA {}] removing validator {}...", - validator_id, validator_pk - ); let (validator_dir, secret_dir, validator_store) = { let node_ = node.read().await; let validator_dir = node_.config.validator_dir.clone(); @@ -766,10 +756,6 @@ pub async fn stop_validator( let validator_id = validator.id; let validator_pk = BlsPublicKey::deserialize(&validator.public_key) .map_err(|e| format!("[VA {}] Deserialize error ({:?})", validator_id, e))?; - info!( - "[VA {}] stopping validator {}...", - validator_id, validator_pk - ); let validator_store = { let node_ = node.read().await; node_.validator_store.clone() From a97631ba547ad9307d1de9bbe829a3cee732a45f Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Fri, 6 Sep 2024 08:19:12 +0000 Subject: [PATCH 22/43] fix store lock --- src/node/dvfcore.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index dbfb3a05..7fb8484b 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -583,8 +583,8 @@ impl DvfCore { _committee: HotstuffCommittee, _keypair: Keypair, _tx_consensus: MonitoredSender, - _store: Store, - _exit: exit_future::Exit, + store: Store, + exit: exit_future::Exit, ) { // let node = node.read().await; @@ -652,6 +652,14 @@ impl DvfCore { // .run() // .await // }); + + tokio::spawn(async move { + tokio::select! { + () = exit => { + store.exit().await; + } + } + }); } // pub async fn run(&mut self) { From bbf140e219ed9bedc373862b6c7ec4d1a1ab837d Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Fri, 6 Sep 2024 11:47:28 +0000 Subject: [PATCH 23/43] fix stop error and ip --- src/bin/dvf_root_node.rs | 68 ++++++++++++------------- src/node/config.rs | 2 +- src/node/discovery.rs | 105 ++++++++++++++------------------------- src/node/dvfcore.rs | 26 ++++------ src/node/node.rs | 8 +-- 5 files changed, 86 insertions(+), 123 deletions(-) diff --git a/src/bin/dvf_root_node.rs b/src/bin/dvf_root_node.rs index 26265a0a..b06f58c0 100644 --- a/src/bin/dvf_root_node.rs +++ b/src/bin/dvf_root_node.rs @@ -3,7 +3,6 @@ use bytes::Bytes; use futures::prelude::*; use hsconfig::Export as _; use hsconfig::Secret; -use lighthouse_network::discv5::enr::EnrPublicKey; use lighthouse_network::discv5::{ enr::{CombinedKey, Enr}, ConfigBuilder, Discv5, Event, ListenConfig, @@ -13,12 +12,12 @@ use network::{MessageHandler, Receiver as NetworkReceiver, Writer as NetworkWrit use std::collections::HashMap; use std::error::Error; use std::fs; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::{Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; use store::Store; use tokio::sync::RwLock; - +use dvf::node::discovery::Discovery; pub const DEFAULT_SECRET_DIR: &str = "node_key.json"; pub const DEFAULT_STORE_DIR: &str = "boot_store"; pub const DEFAULT_ROOT_DIR: &str = ".lighthouse"; @@ -157,39 +156,40 @@ async fn main() -> Result<(), Box> { Some(event) = event_stream.recv() => { match event { Event::Discovered(enr) => { - if let Some(enr_ip) = enr.ip4() { - if let Some(discv_port) = enr.udp4() { - match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { - Some(port) => { - store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(enr_ip), port)).unwrap()).await; - } - None => {} - } - - } - } + Discovery::process_enr(&store, enr).await; + // if let Some(enr_ip) = enr.ip4() { + // if let Some(discv_port) = enr.udp4() { + // match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { + // Some(port) => { + // store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(enr_ip), port)).unwrap()).await; + // } + // None => {} + // } + + // } + // } }, Event::SessionEstablished(enr, _addr) => { - - if let Some(enr_ip) = enr.ip4() { - if let Some(discv_port) = enr.udp4() { - match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { - Some(port) => { - let socketaddr = SocketAddr::new(IpAddr::V4(enr_ip), port); - info!("A peer has established session: public key: {}, base addr: {:?}", - base64::encode(enr.public_key().encode()), socketaddr); - store.write(enr.public_key().encode(), bincode::serialize(&socketaddr).unwrap()).await; - } - None => {} - } - - } else { - let socketaddr = SocketAddr::new(IpAddr::V4(enr_ip), 26000); - info!("A peer has established session with default port: public key: {}, base addr: {:?}", - base64::encode(enr.public_key().encode()), socketaddr); - store.write(enr.public_key().encode(), bincode::serialize(&socketaddr).unwrap()).await; - } - } + Discovery::process_enr(&store, enr).await; + // if let Some(enr_ip) = enr.ip4() { + // if let Some(discv_port) = enr.udp4() { + // match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { + // Some(port) => { + // let socketaddr = SocketAddr::new(IpAddr::V4(enr_ip), port); + // info!("A peer has established session: public key: {}, base addr: {:?}", + // base64::encode(enr.public_key().encode()), socketaddr); + // store.write(enr.public_key().encode(), bincode::serialize(&socketaddr).unwrap()).await; + // } + // None => {} + // } + + // } else { + // let socketaddr = SocketAddr::new(IpAddr::V4(enr_ip), 26000); + // info!("A peer has established session with default port: public key: {}, base addr: {:?}", + // base64::encode(enr.public_key().encode()), socketaddr); + // store.write(enr.public_key().encode(), bincode::serialize(&socketaddr).unwrap()).await; + // } + // } }, Event::SocketUpdated(addr) => { info!("Event::SocketUpdated: local ENR IP address has been updated, addr:{}", addr); diff --git a/src/node/config.rs b/src/node/config.rs index 5c20e20a..10bb63f1 100644 --- a/src/node/config.rs +++ b/src/node/config.rs @@ -15,7 +15,7 @@ pub const NODE_KEY_FILENAME: &str = "node_key.json"; pub const NODE_KEY_HEX_FILENAME: &str = "node_key_hex.json"; pub const DB_FILENAME: &str = "dvf_node_db"; -pub const DEFAULT_BASE_PORT: u16 = 25_000; +pub const DEFAULT_BASE_PORT: u16 = 26_000; pub const TRANSACTION_PORT_OFFSET: u16 = 0; pub const MEMPOOL_PORT_OFFSET: u16 = 1; pub const CONSENSUS_PORT_OFFSET: u16 = 2; diff --git a/src/node/discovery.rs b/src/node/discovery.rs index 771283da..7a63e798 100644 --- a/src/node/discovery.rs +++ b/src/node/discovery.rs @@ -1,5 +1,4 @@ -use crate::node::config::DISCOVERY_PORT_OFFSET; -use crate::validation::http_metrics::metrics::{self, inc_counter}; +use crate::node::config::{DISCOVERY_PORT_OFFSET, DEFAULT_BASE_PORT}; use crate::DEFAULT_CHANNEL_CAPACITY; use bytes::Bytes; use dvf_version::VERSION; @@ -25,7 +24,7 @@ use tokio::sync::{mpsc, oneshot}; use tokio::task::JoinHandle; use tokio::time::{timeout, Interval}; pub const DEFAULT_DISCOVERY_IP_STORE: &str = "discovery_ip_store"; -pub const DISCOVER_HEARTBEAT_INTERVAL: u64 = 60; +pub const DISCOVER_HEARTBEAT_INTERVAL: u64 = 60 * 5; pub const DEFAULT_DISCOVERY_PORT: u16 = 26004; pub struct Discovery { secret: Secret, @@ -45,6 +44,34 @@ impl Drop for Discovery { } impl Discovery { + pub async fn process_enr(store: &Store, enr: Enr) { + // check seq in the enr + let pk = enr.public_key().encode(); + let seq = enr.seq(); + let mut seq_key = pk.clone(); + seq_key.append(&mut "seq number".as_bytes().to_vec()); + if store.read(seq_key.clone()).await.unwrap().map_or(true, |read_seq| { + let read_seq = u64::from_le_bytes(read_seq.try_into().unwrap()); + seq > read_seq + }) { + store.write(seq_key, seq.to_le_bytes().to_vec()).await; + if let Some(ip) = enr.ip4() { + let discv_port = match enr.udp4() { + Some(port) => match port.checked_sub(DISCOVERY_PORT_OFFSET) { + Some(p) => p, + None => { + error!("error happens when get port {}", port); + return ; + } + }, + None => DEFAULT_BASE_PORT + }; + store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), discv_port)).unwrap()).await; + info!("0x{} seq set to {}, socket address {}:{}", hex::encode(enr.public_key().encode()), seq, ip, discv_port); + } + } + } + pub async fn spawn( ip: IpAddr, udp_port: u16, @@ -79,14 +106,14 @@ impl Discovery { let local_enr = { let mut builder = Enr::builder(); builder.ip(ip); - if udp_port != DEFAULT_DISCOVERY_PORT { - builder.udp4(udp_port); + if udp_port != DEFAULT_BASE_PORT { + builder.udp4(udp_port.checked_add(DISCOVERY_PORT_OFFSET).unwrap()); } builder.seq(seq); builder.build(&enr_key).unwrap() }; let base_address = - SocketAddr::new(ip, udp_port.checked_sub(DISCOVERY_PORT_OFFSET).unwrap()); + SocketAddr::new(ip, udp_port); info!("Node ENR ip: {}, port: {}", ip, udp_port); info!("Node public key: {}", secret.name.encode_base64()); info!("Node id: {}", base64::encode(local_enr.node_id().raw())); @@ -95,7 +122,7 @@ impl Discovery { // default configuration without packet filtering let config = ConfigBuilder::new(ListenConfig::Ipv4 { ip: "0.0.0.0".parse().unwrap(), - port: udp_port, + port: udp_port.checked_add(DISCOVERY_PORT_OFFSET).unwrap(), }) .build(); @@ -133,19 +160,7 @@ impl Discovery { debug!("Find Node result succeeded: {} nodes", v.len()); // found a list of ENR's print their NodeIds for enr in v { - if let Some(ip) = enr.ip4() { - if let Some(discv_port) = enr.udp4() { - // update public key socket address - match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { - Some(port) => { - store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), port)).unwrap()).await; - set_metrics(&store, enr.public_key().encode()).await; - } - None => { } - } - - } - }; + Discovery::process_enr(&store, enr).await; }; } } @@ -154,32 +169,10 @@ impl Discovery { Some(event) = event_stream.recv() => { match event { Event::Discovered(enr) => { - if let Some(ip) = enr.ip4() { - // update public key ip - if let Some(discv_port) = enr.udp4() { - match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { - Some(port) => { - store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), port)).unwrap()).await; - set_metrics(&store, enr.public_key().encode()).await; - } - None => { } - } - } - }; + Discovery::process_enr(&store, enr).await; }, Event::SessionEstablished(enr, _addr) => { - if let Some(ip) = enr.ip4() { - // update public key ip - if let Some(discv_port) = enr.udp4() { - match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { - Some(port) => { - store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), port)).unwrap()).await; - set_metrics(&store, enr.public_key().encode()).await; - } - None => { } - } - } - }; + Discovery::process_enr(&store, enr).await; }, Event::SocketUpdated(addr) => { info!("Discv5Event::SocketUpdated: local ENR IP address has been updated, addr:{}", addr); @@ -188,7 +181,6 @@ impl Discovery { match v4addr.port().checked_sub(DISCOVERY_PORT_OFFSET) { Some(port) => { store.write(local_enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(v4addr.ip().clone()), port)).unwrap()).await; - set_metrics(&store, local_enr.public_key().encode()).await; } None => {} } @@ -213,9 +205,7 @@ impl Discovery { store: store_clone, boot_enrs, discv5_service_handle, - base_port: udp_port - .checked_sub(DISCOVERY_PORT_OFFSET) - .expect("overflow due to incorrect config"), + base_port: udp_port, }; // immediately initiate a discover request to annouce ourself @@ -398,25 +388,6 @@ impl Discovery { } } -async fn is_new_op(store: &Store, pk: Vec) -> bool { - match store.read(pk).await { - Ok(r) => match r { - Some(_) => false, - None => true, - }, - Err(e) => { - error!("Failed to read from node {}", e); - false - } - } -} - -async fn set_metrics(store: &Store, pk: Vec) { - if is_new_op(store, pk).await { - inc_counter(&metrics::DVT_VC_CONNECTED_NODES) - } -} - #[tokio::test] async fn test_query_boot() { let pk = base64::decode("A2trBAZoZsNEOrpqzXF4E2mv04IapvtdJ3kiPSZDyNAz").unwrap(); diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index 7fb8484b..e7d7011a 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -458,7 +458,7 @@ impl DvfSigner { .map_err(|e| DvfError::StoreError(format!("Failed to create store: {:?}", e)))?; let (signal, exit) = exit_future::signal(); - Node::spawn_committee_ip_monitor(node_para, committee_def.clone(), exit); + Node::spawn_committee_ip_monitor(node_para, committee_def.clone(), exit.clone()); node.signature_handler_map.write().await.insert( validator_id, @@ -486,17 +486,13 @@ impl DvfSigner { ); info!("Insert duties handler for validator: {}", validator_id); - // DvfCore::spawn( - // operator_id, - // node_para.clone(), - // committee_def.validator_id, - // hotstuff_committee, - // keypair.clone(), - // tx_consensus, - // store.clone(), - // exit.clone(), - // ) - // .await; + DvfCore::spawn( + operator_id, + committee_def.validator_id, + store.clone(), + exit.clone(), + ) + .await; Ok(Self { signal: Some(signal), @@ -576,13 +572,9 @@ unsafe impl Send for DvfCore {} unsafe impl Sync for DvfCore {} impl DvfCore { - pub async fn spawn( + pub async fn spawn( operator_id: u64, - _node: Arc>>, validator_id: u64, - _committee: HotstuffCommittee, - _keypair: Keypair, - _tx_consensus: MonitoredSender, store: Store, exit: exit_future::Exit, ) { diff --git a/src/node/node.rs b/src/node/node.rs index 91ce77a7..0683b5b1 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -5,7 +5,7 @@ use crate::exit::get_distributed_voluntary_exit; use crate::network::io_committee::{SecureNetIOChannel, SecureNetIOCommittee}; use crate::node::config::{ base_to_duties_addr, base_to_signature_addr, NodeConfig, API_ADDRESS, DB_FILENAME, - DISCOVERY_PORT_OFFSET, DKG_PORT_OFFSET, PRESTAKE_SIGNATURE_URL, STAKE_SIGNATURE_URL, + DKG_PORT_OFFSET, PRESTAKE_SIGNATURE_URL, STAKE_SIGNATURE_URL, VALIDATOR_PK_URL, }; use crate::node::contract::{ @@ -151,7 +151,7 @@ impl Node { let base_port = config.base_address.port(); let discovery = Discovery::spawn( self_ip, - base_port + DISCOVERY_PORT_OFFSET, + base_port, secret.clone(), config.boot_enrs.clone(), config.base_store_path.clone(), @@ -435,9 +435,9 @@ impl Node { let node_lock = node.read().await; // Query IP for each operator in this committee. If any of them changed, should restart the VA. let mut restart = false; + for i in 0..committee_def.node_public_keys.len() { - let boot_idx = rand::random::() % 2; - if let Some(addr) = node_lock.discovery.query_addr_from_boot(boot_idx, &committee_def.node_public_keys[i].0).await { + if let Some(addr) = node_lock.discovery.query_addr(&committee_def.node_public_keys[i].0).await { if let Some(socket) = committee_def.base_socket_addresses[i].as_mut() { if *socket != addr { info!("op id: {}, local committee definition address {}, queried result {}", committee_def.operator_ids[i], socket, addr); From 8a5436bef72ad7ae528496b33d7ca2140fe5bcff Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Fri, 6 Sep 2024 12:26:47 +0000 Subject: [PATCH 24/43] update log --- src/bin/dvf_root_node.rs | 30 ------------------------------ src/node/discovery.rs | 2 +- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/src/bin/dvf_root_node.rs b/src/bin/dvf_root_node.rs index b06f58c0..bd54442d 100644 --- a/src/bin/dvf_root_node.rs +++ b/src/bin/dvf_root_node.rs @@ -157,39 +157,9 @@ async fn main() -> Result<(), Box> { match event { Event::Discovered(enr) => { Discovery::process_enr(&store, enr).await; - // if let Some(enr_ip) = enr.ip4() { - // if let Some(discv_port) = enr.udp4() { - // match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { - // Some(port) => { - // store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(enr_ip), port)).unwrap()).await; - // } - // None => {} - // } - - // } - // } }, Event::SessionEstablished(enr, _addr) => { Discovery::process_enr(&store, enr).await; - // if let Some(enr_ip) = enr.ip4() { - // if let Some(discv_port) = enr.udp4() { - // match discv_port.checked_sub(DISCOVERY_PORT_OFFSET) { - // Some(port) => { - // let socketaddr = SocketAddr::new(IpAddr::V4(enr_ip), port); - // info!("A peer has established session: public key: {}, base addr: {:?}", - // base64::encode(enr.public_key().encode()), socketaddr); - // store.write(enr.public_key().encode(), bincode::serialize(&socketaddr).unwrap()).await; - // } - // None => {} - // } - - // } else { - // let socketaddr = SocketAddr::new(IpAddr::V4(enr_ip), 26000); - // info!("A peer has established session with default port: public key: {}, base addr: {:?}", - // base64::encode(enr.public_key().encode()), socketaddr); - // store.write(enr.public_key().encode(), bincode::serialize(&socketaddr).unwrap()).await; - // } - // } }, Event::SocketUpdated(addr) => { info!("Event::SocketUpdated: local ENR IP address has been updated, addr:{}", addr); diff --git a/src/node/discovery.rs b/src/node/discovery.rs index 7a63e798..e3cac58e 100644 --- a/src/node/discovery.rs +++ b/src/node/discovery.rs @@ -67,7 +67,7 @@ impl Discovery { None => DEFAULT_BASE_PORT }; store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), discv_port)).unwrap()).await; - info!("0x{} seq set to {}, socket address {}:{}", hex::encode(enr.public_key().encode()), seq, ip, discv_port); + info!("0x{} seq set to {}, socket address {}:{}", base64::encode(enr.public_key().encode()), seq, ip, discv_port); } } } From 1e2defbdaab752dcbae1fe59cf6dbabecc7dd891 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Fri, 6 Sep 2024 12:32:27 +0000 Subject: [PATCH 25/43] update log --- src/node/discovery.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/node/discovery.rs b/src/node/discovery.rs index e3cac58e..63ee6f9b 100644 --- a/src/node/discovery.rs +++ b/src/node/discovery.rs @@ -67,7 +67,7 @@ impl Discovery { None => DEFAULT_BASE_PORT }; store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), discv_port)).unwrap()).await; - info!("0x{} seq set to {}, socket address {}:{}", base64::encode(enr.public_key().encode()), seq, ip, discv_port); + info!("{} seq set to {}, socket address {}:{}", base64::encode(enr.public_key().encode()), seq, ip, discv_port); } } } From e242f350ea78c33a679dbfcfdec8ac05e3d5cf3c Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Mon, 16 Sep 2024 03:31:47 +0000 Subject: [PATCH 26/43] feat: check op active --- src/bin/dvf_root_node.rs | 2 +- src/node/config.rs | 8 + src/node/discovery.rs | 39 ++- src/node/dvfcore.rs | 332 ++++++++++++------- src/node/node.rs | 66 ++-- src/validation/generic_operator_committee.rs | 5 + src/validation/impls/hotstuff.rs | 7 + src/validation/operator.rs | 77 ++++- src/validation/signing_method.rs | 9 +- 9 files changed, 372 insertions(+), 173 deletions(-) diff --git a/src/bin/dvf_root_node.rs b/src/bin/dvf_root_node.rs index bd54442d..635ed91f 100644 --- a/src/bin/dvf_root_node.rs +++ b/src/bin/dvf_root_node.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use bytes::Bytes; +use dvf::node::discovery::Discovery; use futures::prelude::*; use hsconfig::Export as _; use hsconfig::Secret; @@ -17,7 +18,6 @@ use std::path::PathBuf; use std::sync::Arc; use store::Store; use tokio::sync::RwLock; -use dvf::node::discovery::Discovery; pub const DEFAULT_SECRET_DIR: &str = "node_key.json"; pub const DEFAULT_STORE_DIR: &str = "boot_store"; pub const DEFAULT_ROOT_DIR: &str = ".lighthouse"; diff --git a/src/node/config.rs b/src/node/config.rs index 10bb63f1..489ef04e 100644 --- a/src/node/config.rs +++ b/src/node/config.rs @@ -90,6 +90,14 @@ pub fn base_to_duties_addr(base_addr: SocketAddr) -> SocketAddr { } } +pub fn base_to_active_addr(base_addr: SocketAddr) -> SocketAddr { + if is_addr_invalid(base_addr) { + base_addr + } else { + SocketAddr::new(base_addr.ip(), base_addr.port() + MEMPOOL_PORT_OFFSET) + } +} + #[derive(Clone, Serialize, Deserialize)] pub struct NodeConfig { pub base_address: SocketAddr, diff --git a/src/node/discovery.rs b/src/node/discovery.rs index 63ee6f9b..bc1d3552 100644 --- a/src/node/discovery.rs +++ b/src/node/discovery.rs @@ -1,4 +1,4 @@ -use crate::node::config::{DISCOVERY_PORT_OFFSET, DEFAULT_BASE_PORT}; +use crate::node::config::{DEFAULT_BASE_PORT, DISCOVERY_PORT_OFFSET}; use crate::DEFAULT_CHANNEL_CAPACITY; use bytes::Bytes; use dvf_version::VERSION; @@ -50,24 +50,40 @@ impl Discovery { let seq = enr.seq(); let mut seq_key = pk.clone(); seq_key.append(&mut "seq number".as_bytes().to_vec()); - if store.read(seq_key.clone()).await.unwrap().map_or(true, |read_seq| { - let read_seq = u64::from_le_bytes(read_seq.try_into().unwrap()); - seq > read_seq - }) { + if store + .read(seq_key.clone()) + .await + .unwrap() + .map_or(true, |read_seq| { + let read_seq = u64::from_le_bytes(read_seq.try_into().unwrap()); + seq > read_seq + }) + { store.write(seq_key, seq.to_le_bytes().to_vec()).await; if let Some(ip) = enr.ip4() { let discv_port = match enr.udp4() { Some(port) => match port.checked_sub(DISCOVERY_PORT_OFFSET) { Some(p) => p, - None => { + None => { error!("error happens when get port {}", port); - return ; + return; } }, - None => DEFAULT_BASE_PORT + None => DEFAULT_BASE_PORT, }; - store.write(enr.public_key().encode(), bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), discv_port)).unwrap()).await; - info!("{} seq set to {}, socket address {}:{}", base64::encode(enr.public_key().encode()), seq, ip, discv_port); + store + .write( + enr.public_key().encode(), + bincode::serialize(&SocketAddr::new(IpAddr::V4(ip), discv_port)).unwrap(), + ) + .await; + info!( + "{} seq set to {}, socket address {}:{}", + base64::encode(enr.public_key().encode()), + seq, + ip, + discv_port + ); } } } @@ -112,8 +128,7 @@ impl Discovery { builder.seq(seq); builder.build(&enr_key).unwrap() }; - let base_address = - SocketAddr::new(ip, udp_port); + let base_address = SocketAddr::new(ip, udp_port); info!("Node ENR ip: {}, port: {}", ip, udp_port); info!("Node public key: {}", secret.name.encode_base64()); info!("Node id: {}", base64::encode(local_enr.node_id().raw())); diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index e7d7011a..a8322258 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -1,3 +1,7 @@ +use crate::node::config::{ + base_to_consensus_addr, base_to_mempool_addr, base_to_signature_addr, base_to_transaction_addr, + invalid_addr, +}; use crate::node::node::Node; use crate::node::utils::SignDigest; use crate::utils::error::DvfError; @@ -8,12 +12,14 @@ use crate::validation::OperatorCommittee; use async_trait::async_trait; use bls::{Hash256, PublicKey as BlsPublicKey, Signature}; use bytes::Bytes; +use consensus::Committee as ConsensusCommittee; use futures::SinkExt; use hsconfig::Committee as HotstuffCommittee; use hscrypto::Digest; use hsutils::monitored_channel::MonitoredSender; use keccak_hash::keccak; use log::{error, info, warn}; +use mempool::Committee as MempoolCommittee; use network::{MessageHandler, Writer}; use serde::{Deserialize, Serialize}; use slashing_protection::SlashingDatabase; @@ -58,11 +64,33 @@ pub enum BlockType { Full, } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum DutySafety { + Invalid, + Safe, + NotSafe, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct DutyConsensusResponse { + safety: DutySafety, + message: String, +} + #[derive(Clone)] pub struct DvfSignatureReceiverHandler { pub store: Store, } +async fn reply(writer: &mut Writer, safety: DutySafety, message: String) { + let response = DutyConsensusResponse { + safety: safety, + message: message, + }; + let serialzed_msg = bincode::serialize(&response).unwrap(); + let _ = writer.send(Bytes::from(serialzed_msg)); +} + #[async_trait] impl MessageHandler for DvfSignatureReceiverHandler { async fn dispatch(&self, writer: &mut Writer, message: Bytes) -> Result<(), Box> { @@ -85,8 +113,28 @@ impl MessageHandler for DvfSignatureReceiverHandler { error!("can't read database, {}", e) } } - // Give the change to schedule other tasks. - // tokio::task::yield_now().await; + Ok(()) + } +} + +#[derive(Clone)] +pub struct DvfActiveReceiverHandler { + pub store: Store, + pub keypair: Keypair, +} + +#[async_trait] +impl MessageHandler for DvfActiveReceiverHandler { + async fn dispatch(&self, writer: &mut Writer, message: Bytes) -> Result<(), Box> { + let data: Vec = message.slice(..).to_vec(); + if data.len() != 32 { + let _ = writer.send(Bytes::from("invalid message format, length must be 32")); + return Ok(()); + } + let msg = Hash256::from_slice(&data[..]); + let sig = self.keypair.sk.sign(msg); + let serialized_signature = bincode::serialize(&sig).unwrap(); + let _ = writer.send(Bytes::from(serialized_signature)); Ok(()) } } @@ -137,44 +185,53 @@ impl DvfDutyCheckHandler { Ok(Safe::Valid) => { let signable_msg = SignableMessage::BeaconBlock(&block); let signing_root = signable_msg.signing_root(domain_hash); - info!("valid block duty, local sign and store {}", &signing_root); + info!( + "valid proposal duty, local sign and store {}", + &signing_root + ); let sig = self.keypair.sk.sign(signing_root); let serialized_signature = bincode::serialize(&sig).unwrap(); // save to local db let key = signing_root.as_bytes().into(); self.store.write(key, serialized_signature).await; - let _ = writer - .send(Bytes::from(format!( - "successfully consensus proposal on {}", - signing_root - ))) - .await; + reply( + writer, + DutySafety::Safe, + format!("successfully consensus proposal on {}", signing_root), + ) + .await; } Ok(Safe::SameData) => { - let _ = writer - .send(Bytes::from("Skipping signing of previously signed block")) - .await; + reply( + writer, + DutySafety::Safe, + format!("skipping signing of previously signed block"), + ) + .await; info!("Skipping signing of previously signed block"); } Err(NotSafe::UnregisteredValidator(pk)) => { - let _ = writer - .send(Bytes::from(format!( + reply( + writer, + DutySafety::NotSafe, + format!( "Not signing block for unregistered validator public_key {:?}", pk - ))) - .await; + ), + ) + .await; warn!( - "Not signing block for unregistered validator public_key {}", - format!("{:?}", pk) + "Not signing block for unregistered validator public_key {:?}", + pk ); } Err(e) => { - let _ = writer - .send(Bytes::from(format!( - "Not signing slashable block error {:?}", - e - ))) - .await; + reply( + writer, + DutySafety::NotSafe, + format!("Not signing slashable block error {:?}", e), + ) + .await; error!("Not signing slashable block error {}", format!("{:?}", e)); } } @@ -187,10 +244,13 @@ impl MessageHandler for DvfDutyCheckHandler { let check_msg: DvfDutyCheckMessage = match bincode::deserialize(&message.slice(..)) { Ok(m) => m, Err(_) => { - let _ = writer - .send(Bytes::from("failed to deserialize duty check msg")) - .await; - error!("failed to deserialize duty check msg"); + reply( + writer, + DutySafety::Invalid, + format!("failed to deserialize duty consensus msg"), + ) + .await; + error!("failed to deserialize duty consensus msg"); return Ok(()); } }; @@ -199,7 +259,12 @@ impl MessageHandler for DvfDutyCheckHandler { Some(pk) => pk, None => { return { - let _ = writer.send(Bytes::from("failed to find operator")).await; + reply( + writer, + DutySafety::Invalid, + format!("failed to find operator"), + ) + .await; error!("failed to find operator"); Ok(()) } @@ -210,11 +275,14 @@ impl MessageHandler for DvfDutyCheckHandler { match hex::decode(s) { Ok(s) => { if s.len() != 64 { - let _ = writer - .send(Bytes::from( - "the length of the signature is not 64, ignore the message", - )) - .await; + reply( + writer, + DutySafety::Invalid, + format!( + "the length of the signature is not 64, ignore the message" + ), + ) + .await; error!("the length of the signature is not 64, ignore the message"); return Ok(()); } @@ -224,31 +292,36 @@ impl MessageHandler for DvfDutyCheckHandler { info!("successfully verified duty message"); } Err(_) => { - let _ = writer - .send(Bytes::from( - "failed to verify the signature of the duty message", - )) - .await; + reply( + writer, + DutySafety::Invalid, + format!("failed to verify the signature of the duty message"), + ) + .await; error!("failed to verify the signature of the duty message"); return Ok(()); } } } Err(_) => { - let _ = writer - .send(Bytes::from( - "failed to decode signature, ignore the message", - )) - .await; + reply( + writer, + DutySafety::Invalid, + format!("failed to decode signature, ignore the message"), + ) + .await; error!("failed to decode signature, ignore the message"); return Ok(()); } }; } None => { - let _ = writer - .send(Bytes::from("empty signature, ignore the message")) - .await; + reply( + writer, + DutySafety::Invalid, + format!("empty signature, ignore the message"), + ) + .await; error!("empty signature, ignore the message"); return Ok(()); } @@ -259,9 +332,12 @@ impl MessageHandler for DvfDutyCheckHandler { match serde_json::from_slice(&check_msg.data) { Ok(a) => a, Err(_) => { - let _ = writer - .send(Bytes::from("failed to deserialize attestation data")) - .await; + reply( + writer, + DutySafety::Invalid, + format!("failed to deserialize attestation data"), + ) + .await; error!("failed to deserialize attestation data"); return Ok(()); } @@ -285,35 +361,36 @@ impl MessageHandler for DvfDutyCheckHandler { // save to local db let key = signing_root.as_bytes().into(); self.store.write(key, serialized_signature).await; - let _ = writer - .send(Bytes::from(format!( - "successfully consensus attestation on {}", - &signing_root - ))) - .await; + reply( + writer, + DutySafety::Safe, + format!("successfully consensus attestation on {}", &signing_root), + ) + .await; } Ok(Safe::SameData) => { info!("Skipping signing of previously signed attestation"); - let _ = writer - .send(Bytes::from( - "Skipping signing of previously signed attestation", - )) - .await; + reply( + writer, + DutySafety::Safe, + format!("Skipping signing of previously signed attestation"), + ) + .await; } Err(NotSafe::UnregisteredValidator(pk)) => { - let _ = writer.send(Bytes::from(format!("Not signing attestation for unregistered validator public_key {:?}", pk))).await; + reply(writer, DutySafety::NotSafe, format!("Not signing attestation for unregistered validator public_key {:?}", pk)).await; warn!( "Not signing attestation for unregistered validator public_key {}", format!("{:?}", pk) ); } Err(e) => { - let _ = writer - .send(Bytes::from(format!( - "Not signing slashable attestation {:?}", - e - ))) - .await; + reply( + writer, + DutySafety::NotSafe, + format!("Not signing slashable attestation {:?}", e), + ) + .await; error!("Not signing slashable attestation {}", format!("{:?}", e)); } } @@ -325,11 +402,12 @@ impl MessageHandler for DvfDutyCheckHandler { match serde_json::from_slice(&check_msg.data) { Ok(b) => b, Err(_) => { - let _ = writer - .send(Bytes::from( - "failed to deserialize full proposal block", - )) - .await; + reply( + writer, + DutySafety::Invalid, + format!("failed to deserialize full proposal block"), + ) + .await; error!("failed to deserialize full proposal block"); return Ok(()); } @@ -341,11 +419,12 @@ impl MessageHandler for DvfDutyCheckHandler { match serde_json::from_slice(&check_msg.data) { Ok(b) => b, Err(_) => { - let _ = writer - .send(Bytes::from( - "failed to deserialize blinded proposal block", - )) - .await; + reply( + writer, + DutySafety::Invalid, + format!("failed to deserialize blinded proposal block"), + ) + .await; error!("failed to deserialize blinded proposal block"); return Ok(()); } @@ -411,44 +490,6 @@ impl DvfSigner { .add_operator(operator_id, local_operator) .await; - // Construct the committee for hotstuff protocol - // let epoch = 1; - // let stake = 1; - // let mempool_committee = MempoolCommittee::new( - // committee_def - // .node_public_keys - // .iter() - // .enumerate() - // .map(|(i, pk)| { - // let addr = committee_def.base_socket_addresses[i].unwrap_or(invalid_addr()); - // ( - // pk.clone(), - // stake, - // base_to_transaction_addr(addr), - // base_to_mempool_addr(addr), - // base_to_signature_addr(addr), - // ) - // }) - // .collect(), - // epoch, - // ); - // let consensus_committee = ConsensusCommittee::new( - // committee_def - // .node_public_keys - // .iter() - // .enumerate() - // .map(|(i, pk)| { - // let addr = committee_def.base_socket_addresses[i].unwrap_or(invalid_addr()); - // (pk.clone(), stake, base_to_consensus_addr(addr)) - // }) - // .collect(), - // epoch, - // ); - // let hotstuff_committee = HotstuffCommittee { - // mempool: mempool_committee, - // consensus: consensus_committee, - // }; - let store_path = node .config .base_store_path @@ -525,7 +566,14 @@ impl DvfSigner { pub async fn is_aggregator(&self, nonce: u64) -> bool { self.operator_committee.get_leader(nonce).await == self.operator_id - || self.operator_committee.get_leader(nonce + 1).await == self.operator_id + } + + pub async fn is_next_aggregator(&self, nonce: u64) -> bool { + self.operator_committee.get_leader(nonce + 1).await == self.operator_id + } + + pub async fn is_leader_active(&self, nonce: u64) -> bool { + self.operator_committee.is_leader_active(nonce).await } pub async fn consensus_on_duty(&self, domain_hash: Hash256, check_type: DvfType, data: &[u8]) { @@ -555,6 +603,49 @@ impl DvfSigner { pub fn operator_id(&self) -> u64 { self.operator_id } + + pub fn hotstuff_committee( + &self, + committee_def: OperatorCommitteeDefinition, + ) -> HotstuffCommittee { + // Construct the committee for hotstuff protocol + let epoch = 1; + let stake = 1; + let mempool_committee = MempoolCommittee::new( + committee_def + .node_public_keys + .iter() + .enumerate() + .map(|(i, pk)| { + let addr = committee_def.base_socket_addresses[i].unwrap_or(invalid_addr()); + ( + pk.clone(), + stake, + base_to_transaction_addr(addr), + base_to_mempool_addr(addr), + base_to_signature_addr(addr), + ) + }) + .collect(), + epoch, + ); + let consensus_committee = ConsensusCommittee::new( + committee_def + .node_public_keys + .iter() + .enumerate() + .map(|(i, pk)| { + let addr = committee_def.base_socket_addresses[i].unwrap_or(invalid_addr()); + (pk.clone(), stake, base_to_consensus_addr(addr)) + }) + .collect(), + epoch, + ); + HotstuffCommittee { + mempool: mempool_committee, + consensus: consensus_committee, + } + } } pub struct DvfCore { @@ -572,12 +663,7 @@ unsafe impl Send for DvfCore {} unsafe impl Sync for DvfCore {} impl DvfCore { - pub async fn spawn( - operator_id: u64, - validator_id: u64, - store: Store, - exit: exit_future::Exit, - ) { + pub async fn spawn(operator_id: u64, validator_id: u64, store: Store, exit: exit_future::Exit) { // let node = node.read().await; // let (tx_commit, rx_commit) = diff --git a/src/node/node.rs b/src/node/node.rs index 0683b5b1..a05fe1f7 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -4,9 +4,8 @@ use crate::deposit::get_distributed_deposit; use crate::exit::get_distributed_voluntary_exit; use crate::network::io_committee::{SecureNetIOChannel, SecureNetIOCommittee}; use crate::node::config::{ - base_to_duties_addr, base_to_signature_addr, NodeConfig, API_ADDRESS, DB_FILENAME, - DKG_PORT_OFFSET, PRESTAKE_SIGNATURE_URL, STAKE_SIGNATURE_URL, - VALIDATOR_PK_URL, + base_to_active_addr, base_to_duties_addr, base_to_signature_addr, NodeConfig, API_ADDRESS, + DB_FILENAME, DKG_PORT_OFFSET, PRESTAKE_SIGNATURE_URL, STAKE_SIGNATURE_URL, VALIDATOR_PK_URL, }; use crate::node::contract::{ Contract, ContractCommand, EncryptedSecretKeys, Initiator, InitiatorStoreRecord, OperatorIds, @@ -36,12 +35,10 @@ use crate::validation::{ validator_store::ValidatorStore, }; use bls::{Keypair as BlsKeypair, PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}; -use consensus::ConsensusReceiverHandler; use eth2_keystore::KeystoreBuilder; use hsconfig::Export as _; use hsconfig::{ConfigError, Secret}; use log::{error, info, warn}; -use mempool::{MempoolReceiverHandler, TxReceiverHandler}; use network::Receiver as NetworkReceiver; use slot_clock::SystemTimeSlotClock; use std::collections::HashMap; @@ -70,11 +67,12 @@ fn with_wildcard_ip(mut addr: SocketAddr) -> SocketAddr { pub struct Node { pub config: NodeConfig, pub secret: Secret, - pub tx_handler_map: Arc>>, - pub mempool_handler_map: Arc>>, - pub consensus_handler_map: Arc>>, + // pub tx_handler_map: Arc>>, + // pub mempool_handler_map: Arc>>, + // pub consensus_handler_map: Arc>>, pub signature_handler_map: Arc>>, pub duties_handler_map: Arc>>>, + pub active_handler_map: Arc>>>, pub validator_store: Option>>, pub discovery: Arc, pub db: Database, @@ -91,18 +89,25 @@ impl Node { create_node_key_hex_backup(config.node_key_hex_path.clone(), &secret)?; info!("node public key {}", secret.name.encode_base64()); - let tx_handler_map = Arc::new(RwLock::new(HashMap::new())); - let mempool_handler_map = Arc::new(RwLock::new(HashMap::new())); - let consensus_handler_map = Arc::new(RwLock::new(HashMap::new())); + // let tx_handler_map = Arc::new(RwLock::new(HashMap::new())); + // let mempool_handler_map = Arc::new(RwLock::new(HashMap::new())); + // let consensus_handler_map = Arc::new(RwLock::new(HashMap::new())); let signature_handler_map = Arc::new(RwLock::new(HashMap::new())); let duties_handler_map = Arc::new(RwLock::new(HashMap::new())); - + let active_handler_map = Arc::new(RwLock::new(HashMap::new())); let duties_address = with_wildcard_ip(base_to_duties_addr(config.base_address)); + let active_address = with_wildcard_ip(base_to_active_addr(config.base_address)); NetworkReceiver::spawn( duties_address, Arc::clone(&duties_handler_map), "duties consensus", ); + + NetworkReceiver::spawn( + active_address, + Arc::clone(&active_handler_map), + "active handler", + ); info!( "Node {} listening to duties consensus on {}", secret.name, duties_address @@ -170,11 +175,12 @@ impl Node { let node = Self { config, secret: secret.clone(), - tx_handler_map: Arc::clone(&tx_handler_map), - mempool_handler_map: Arc::clone(&mempool_handler_map), - consensus_handler_map: Arc::clone(&consensus_handler_map), + // tx_handler_map: Arc::clone(&tx_handler_map), + // mempool_handler_map: Arc::clone(&mempool_handler_map), + // consensus_handler_map: Arc::clone(&consensus_handler_map), signature_handler_map: Arc::clone(&signature_handler_map), - duties_handler_map: Arc::clone(&duties_handler_map), + duties_handler_map: duties_handler_map, + active_handler_map: active_handler_map, validator_store: None, discovery: Arc::new(discovery), db: db.clone(), @@ -435,7 +441,7 @@ impl Node { let node_lock = node.read().await; // Query IP for each operator in this committee. If any of them changed, should restart the VA. let mut restart = false; - + for i in 0..committee_def.node_public_keys.len() { if let Some(addr) = node_lock.discovery.query_addr(&committee_def.node_public_keys[i].0).await { if let Some(socket) = committee_def.base_socket_addresses[i].as_mut() { @@ -760,7 +766,7 @@ pub async fn stop_validator( let node_ = node.read().await; node_.validator_store.clone() }; - + match validator_store { Some(validator_store) => match validator_store.is_active(&validator_pk).await { Some(active) => { @@ -1108,22 +1114,24 @@ pub async fn set_validator_fee_recipient( pub async fn cleanup_handler(node: Arc>>, validator_id: u64) { let node_ = node.read().await; - let _ = node_.tx_handler_map.write().await.remove(&validator_id); - let _ = node_ - .mempool_handler_map - .write() - .await - .remove(&validator_id); - let _ = node_ - .consensus_handler_map - .write() - .await - .remove(&validator_id); + // let _ = node_.tx_handler_map.write().await.remove(&validator_id); + // let _ = node_ + // .mempool_handler_map + // .write() + // .await + // .remove(&validator_id); + // let _ = node_ + // .consensus_handler_map + // .write() + // .await + // .remove(&validator_id); let _ = node_ .signature_handler_map .write() .await .remove(&validator_id); + let _ = node_.duties_handler_map.write().await.remove(&validator_id); + let _ = node_.active_handler_map.write().await.remove(&validator_id); } pub async fn cleanup_keystore( diff --git a/src/validation/generic_operator_committee.rs b/src/validation/generic_operator_committee.rs index f61929ac..86267bdc 100644 --- a/src/validation/generic_operator_committee.rs +++ b/src/validation/generic_operator_committee.rs @@ -22,6 +22,7 @@ pub trait TOperatorCommittee: Send { async fn sign(&self, msg: Hash256) -> Result<(Signature, Vec), DvfError>; async fn get_leader(&self, nonce: u64) -> u64; async fn get_op_pos(&self, op_id: u64) -> usize; + async fn is_leader_active(&self, nonce: u64) -> bool; fn get_validator_pk(&self) -> PublicKey; fn threshold(&self) -> usize; } @@ -82,4 +83,8 @@ where pub async fn consensus_on_duty(&self, data: &[u8]) { let _ = self.cmt.consensus_on_duty(data).await; } + + pub async fn is_leader_active(&self, nonce: u64) -> bool { + self.cmt.is_leader_active(nonce).await + } } diff --git a/src/validation/impls/hotstuff.rs b/src/validation/impls/hotstuff.rs index 71a4c7c5..6b01d35b 100644 --- a/src/validation/impls/hotstuff.rs +++ b/src/validation/impls/hotstuff.rs @@ -171,4 +171,11 @@ impl TOperatorCommittee for HotstuffOperatorCommittee { fn get_validator_pk(&self) -> PublicKey { self.validator_public_key.clone() } + + async fn is_leader_active(&self, nonce: u64) -> bool { + let leader = self.get_leader(nonce).await; + let operators = &self.operators.read().await; + let operator = operators.get(&leader).unwrap().read().await; + operator.is_active().await + } } diff --git a/src/validation/operator.rs b/src/validation/operator.rs index a3667e87..1e07a6df 100644 --- a/src/validation/operator.rs +++ b/src/validation/operator.rs @@ -3,9 +3,10 @@ use std::sync::Arc; use std::time::Duration; use crate::node::config::{ - base_to_consensus_addr, base_to_duties_addr, base_to_mempool_addr, base_to_signature_addr, - base_to_transaction_addr, is_addr_invalid, + base_to_active_addr, base_to_consensus_addr, base_to_duties_addr, base_to_mempool_addr, + base_to_signature_addr, base_to_transaction_addr, is_addr_invalid, }; +use crate::node::dvfcore::DutyConsensusResponse; use crate::utils::error::DvfError; use async_trait::async_trait; use bytes::Bytes; @@ -14,7 +15,6 @@ use log::{debug, info, warn}; use network::{DvfMessage, ReliableSender, SimpleSender, VERSION}; use tokio::time::{sleep_until, timeout, Instant}; use types::{Hash256, Keypair, PublicKey, Signature}; - pub enum OperatorMessage {} #[async_trait] @@ -38,7 +38,11 @@ pub trait TOperator: DowncastSync + Sync + Send { fn duties_address(&self) -> SocketAddr { base_to_duties_addr(self.base_address()) } + fn active_address(&self) -> SocketAddr { + base_to_active_addr(self.base_address()) + } async fn consensus_on_duty(&self, msg: &[u8]); + async fn is_active(&self) -> bool; } impl_downcast!(sync TOperator); @@ -85,6 +89,10 @@ impl TOperator for LocalOperator { async fn consensus_on_duty(&self, _msg: &[u8]) { return; } + + async fn is_active(&self) -> bool { + true + } } impl LocalOperator { @@ -215,11 +223,15 @@ impl TOperator for RemoteOperator { match result { Ok(output) => match output { Ok(data) => { + let resp = match bincode::deserialize::(&data) { + Ok(r) => r, + Err(_) => { + return; + } + }; info!( "Received consensus response from [{}/{}]: {:?}", - self.operator_id, - self.validator_id, - std::str::from_utf8(&data) + self.operator_id, self.validator_id, resp ); } Err(_) => { @@ -239,6 +251,59 @@ impl TOperator for RemoteOperator { } } } + + async fn is_active(&self) -> bool { + // skip this function quickly + if is_addr_invalid(self.base_address()) { + warn!("invalid socket address"); + return false; + } + + let n_try: u64 = 1; + let timeout_mill: u64 = 200; + let sleep_mill: u64 = 300; + let random_hash = Hash256::random(); + let dvf_message = DvfMessage { + version: VERSION, + validator_id: self.validator_id, + message: random_hash.to_fixed_bytes().to_vec(), + }; + + let serialize_msg = bincode::serialize(&dvf_message).unwrap(); + for i in 0..n_try { + let receiver = self + .network + .send(self.active_address(), Bytes::from(serialize_msg.clone())) + .await; + let result = timeout(Duration::from_millis(timeout_mill), receiver).await; + match result { + Ok(output) => match output { + Ok(data) => match bincode::deserialize::(&data) { + Ok(sig) => { + return sig.verify(&self.operator_public_key, random_hash); + } + Err(_) => { + return false; + } + }, + Err(_) => { + warn!("recv is interrupted."); + } + }, + Err(e) => { + warn!( + "Retry from operator {}/{}, error: {}", + self.operator_id, self.validator_id, e + ); + } + } + if i < n_try - 1 { + let next_try_instant = Instant::now() + Duration::from_millis(sleep_mill); + sleep_until(next_try_instant).await; + } + } + false + } } impl RemoteOperator { diff --git a/src/validation/signing_method.rs b/src/validation/signing_method.rs index c2fae802..0d72dfa8 100644 --- a/src/validation/signing_method.rs +++ b/src/validation/signing_method.rs @@ -134,9 +134,14 @@ impl SigningContext { impl SigningMethod { pub async fn is_leader(&self, epoch: Epoch) -> bool { + let nonce = epoch.as_u64(); match self { SigningMethod::DistributedKeystore { dvf_signer, .. } => { - dvf_signer.is_aggregator(epoch.as_u64()).await + if dvf_signer.is_leader_active(nonce).await { + dvf_signer.is_aggregator(nonce).await + } else { + dvf_signer.is_next_aggregator(nonce).await + } } _ => false, } @@ -369,7 +374,7 @@ impl SigningMethod { (e.epoch.start_slot(T::slots_per_epoch()), "VA_EXIT", true) } }; - let is_aggregator = dvf_signer.is_aggregator(signing_epoch.as_u64()).await; + let is_aggregator = dvf_signer.is_aggregator(signing_epoch.as_u64()).await || dvf_signer.is_next_aggregator(signing_epoch.as_u64()).await; log::info!( "[Dvf {}/{}] Signing\t-\tSlot: {}.\tEpoch: {}.\tType: {}.\tRoot: {:?}. Is aggregator {}", dvf_signer.operator_id, From 466c2aa3e89f06d2ad287506b8bfc972878683ad Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Thu, 19 Sep 2024 07:11:34 +0000 Subject: [PATCH 27/43] add log --- src/validation/operator.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/validation/operator.rs b/src/validation/operator.rs index 1e07a6df..37c3177b 100644 --- a/src/validation/operator.rs +++ b/src/validation/operator.rs @@ -11,7 +11,7 @@ use crate::utils::error::DvfError; use async_trait::async_trait; use bytes::Bytes; use downcast_rs::DowncastSync; -use log::{debug, info, warn}; +use log::{debug, info, warn, error}; use network::{DvfMessage, ReliableSender, SimpleSender, VERSION}; use tokio::time::{sleep_until, timeout, Instant}; use types::{Hash256, Keypair, PublicKey, Signature}; @@ -157,7 +157,7 @@ impl TOperator for RemoteOperator { return Ok(bls_signature); } Err(_) => { - warn!( + debug!( "Deserialize failed from operator {}/{}, retry..., msg: {:?} received data: {:?}", self.operator_id, self.validator_id, msg, std::str::from_utf8(&data) ); @@ -225,7 +225,8 @@ impl TOperator for RemoteOperator { Ok(data) => { let resp = match bincode::deserialize::(&data) { Ok(r) => r, - Err(_) => { + Err(e) => { + error!("failed to deserialize response data, {:?}", e); return; } }; From f6a96fc07a51ff843af7cb28b1e90856bddddf3a Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Thu, 19 Sep 2024 07:42:36 +0000 Subject: [PATCH 28/43] add log --- src/node/dvfcore.rs | 9 ++++++++- src/node/node.rs | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index a8322258..193d9a68 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -526,7 +526,14 @@ impl DvfSigner { }, ); info!("Insert duties handler for validator: {}", validator_id); - + node.active_handler_map.write().await.insert( + validator_id, + DvfActiveReceiverHandler { + store: store.clone(), + keypair: keypair.clone(), + } + ); + info!("Insert active handler for validator: {}", validator_id); DvfCore::spawn( operator_id, committee_def.validator_id, diff --git a/src/node/node.rs b/src/node/node.rs index a05fe1f7..bc5b9d24 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -15,7 +15,7 @@ use crate::node::contract::{ use crate::node::{ db::{self, Database}, discovery::Discovery, - dvfcore::{DvfDutyCheckHandler, DvfSignatureReceiverHandler}, + dvfcore::{DvfDutyCheckHandler, DvfSignatureReceiverHandler, DvfActiveReceiverHandler}, status_report::StatusReport, utils::{ convert_address_to_withdraw_crendentials, request_to_web_server, DepositRequest, @@ -72,7 +72,7 @@ pub struct Node { // pub consensus_handler_map: Arc>>, pub signature_handler_map: Arc>>, pub duties_handler_map: Arc>>>, - pub active_handler_map: Arc>>>, + pub active_handler_map: Arc>>, pub validator_store: Option>>, pub discovery: Arc, pub db: Database, From 4aca52993545d144844b35de5accaebe23be5f6a Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Thu, 19 Sep 2024 09:42:28 +0000 Subject: [PATCH 29/43] fix reply --- src/node/dvfcore.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index 193d9a68..e83ec66e 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -88,7 +88,7 @@ async fn reply(writer: &mut Writer, safety: DutySafety, message: String) { message: message, }; let serialzed_msg = bincode::serialize(&response).unwrap(); - let _ = writer.send(Bytes::from(serialzed_msg)); + let _ = writer.send(Bytes::from(serialzed_msg)).await; } #[async_trait] @@ -134,7 +134,7 @@ impl MessageHandler for DvfActiveReceiverHandler { let msg = Hash256::from_slice(&data[..]); let sig = self.keypair.sk.sign(msg); let serialized_signature = bincode::serialize(&sig).unwrap(); - let _ = writer.send(Bytes::from(serialized_signature)); + let _ = writer.send(Bytes::from(serialized_signature)).await; Ok(()) } } From 27ab0382dffe5bf261b1a316bc32b3a4639fa90a Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Fri, 20 Sep 2024 15:55:41 +0000 Subject: [PATCH 30/43] update active method --- src/node/dvfcore.rs | 10 +++--- src/validation/operator.rs | 45 ++++++++++++++++++++------- src/validation/operator_committees.rs | 1 + src/validation/signing_method.rs | 2 +- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index e83ec66e..303eaf02 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -15,7 +15,7 @@ use bytes::Bytes; use consensus::Committee as ConsensusCommittee; use futures::SinkExt; use hsconfig::Committee as HotstuffCommittee; -use hscrypto::Digest; +use hscrypto::{Digest, SecretKey as HotstuffSecretKey, Signature as HotstuffSignature}; use hsutils::monitored_channel::MonitoredSender; use keccak_hash::keccak; use log::{error, info, warn}; @@ -120,7 +120,7 @@ impl MessageHandler for DvfSignatureReceiverHandler { #[derive(Clone)] pub struct DvfActiveReceiverHandler { pub store: Store, - pub keypair: Keypair, + pub secret: HotstuffSecretKey, } #[async_trait] @@ -131,8 +131,8 @@ impl MessageHandler for DvfActiveReceiverHandler { let _ = writer.send(Bytes::from("invalid message format, length must be 32")); return Ok(()); } - let msg = Hash256::from_slice(&data[..]); - let sig = self.keypair.sk.sign(msg); + let msg: [u8; 32] = data.try_into().unwrap(); + let sig = HotstuffSignature::new(&Digest::from(&msg), &self.secret); let serialized_signature = bincode::serialize(&sig).unwrap(); let _ = writer.send(Bytes::from(serialized_signature)).await; Ok(()) @@ -530,7 +530,7 @@ impl DvfSigner { validator_id, DvfActiveReceiverHandler { store: store.clone(), - keypair: keypair.clone(), + secret: node_secret.clone(), } ); info!("Insert active handler for validator: {}", validator_id); diff --git a/src/validation/operator.rs b/src/validation/operator.rs index 37c3177b..a9a59540 100644 --- a/src/validation/operator.rs +++ b/src/validation/operator.rs @@ -15,6 +15,7 @@ use log::{debug, info, warn, error}; use network::{DvfMessage, ReliableSender, SimpleSender, VERSION}; use tokio::time::{sleep_until, timeout, Instant}; use types::{Hash256, Keypair, PublicKey, Signature}; +use hscrypto::{PublicKey as HotstuffPublicKey, Signature as HotstuffSignature, Digest}; pub enum OperatorMessage {} #[async_trait] @@ -115,7 +116,8 @@ impl LocalOperator { pub struct RemoteOperator { pub validator_id: u64, pub operator_id: u64, - pub operator_public_key: PublicKey, + pub operator_node_pk: HotstuffPublicKey, + pub operator_shared_pk: PublicKey, // pub signature_address: SocketAddr, pub base_address: SocketAddr, network: ReliableSender, @@ -189,7 +191,7 @@ impl TOperator for RemoteOperator { } fn public_key(&self) -> PublicKey { - self.operator_public_key.clone() + self.operator_shared_pk.clone() } async fn propose(&self, _msg: Hash256) {} @@ -261,13 +263,14 @@ impl TOperator for RemoteOperator { } let n_try: u64 = 1; - let timeout_mill: u64 = 200; + let timeout_mill: u64 = 600; let sleep_mill: u64 = 300; let random_hash = Hash256::random(); + let msg = random_hash.to_fixed_bytes(); let dvf_message = DvfMessage { version: VERSION, validator_id: self.validator_id, - message: random_hash.to_fixed_bytes().to_vec(), + message: msg.to_vec(), }; let serialize_msg = bincode::serialize(&dvf_message).unwrap(); @@ -279,12 +282,29 @@ impl TOperator for RemoteOperator { let result = timeout(Duration::from_millis(timeout_mill), receiver).await; match result { Ok(output) => match output { - Ok(data) => match bincode::deserialize::(&data) { + Ok(data) => match bincode::deserialize::(&data) { Ok(sig) => { - return sig.verify(&self.operator_public_key, random_hash); + match sig.verify(&Digest::from(&msg), &self.operator_node_pk) { + Ok(_) => { + info!( + "[{}/{}] is active", + self.operator_id, self.validator_id + ); + return true; + }, + Err(_) => { + warn!( + "[{}/{}] is not active!", + self.operator_id, self.validator_id + ); + } + } } Err(_) => { - return false; + warn!( + "[{}/{}] deserialize signature failed!", + self.operator_id, self.validator_id + ); } }, Err(_) => { @@ -311,13 +331,15 @@ impl RemoteOperator { pub fn new( validator_id: u64, operator_id: u64, - operator_public_key: PublicKey, + operator_node_pk: HotstuffPublicKey, + operator_shared_pk: PublicKey, base_address: SocketAddr, ) -> Self { Self { validator_id, operator_id, - operator_public_key, + operator_node_pk, + operator_shared_pk, base_address, network: ReliableSender::new(), } @@ -332,11 +354,12 @@ async fn remote_operator_test() { logger.init(); let validator_id = 1888062277302860207; let operator_id = 3; - let operator_public_key = PublicKey::deserialize(&hex::decode("86b85f1340b60b7f0c0fc73ef9ca59ce6ea8efc82c8d8d6590d2bf4fc34c9936779090932f19484b6a6942eb93d5e1c5").unwrap()).unwrap(); + let operator_shared_pk = PublicKey::deserialize(&hex::decode("86b85f1340b60b7f0c0fc73ef9ca59ce6ea8efc82c8d8d6590d2bf4fc34c9936779090932f19484b6a6942eb93d5e1c5").unwrap()).unwrap(); let remote_operator = RemoteOperator::new( validator_id, operator_id, - operator_public_key, + HotstuffPublicKey::default(), + operator_shared_pk, "13.228.88.177:26000".parse().unwrap(), ); remote_operator diff --git a/src/validation/operator_committees.rs b/src/validation/operator_committees.rs index 2a8e5391..889b4f2f 100644 --- a/src/validation/operator_committees.rs +++ b/src/validation/operator_committees.rs @@ -27,6 +27,7 @@ impl OperatorCommittee { let operator = RemoteOperator::new( def.validator_id, def.operator_ids[i], + def.node_public_keys[i].clone(), def.operator_public_keys[i].clone(), addr, ); diff --git a/src/validation/signing_method.rs b/src/validation/signing_method.rs index 0d72dfa8..d7e06db6 100644 --- a/src/validation/signing_method.rs +++ b/src/validation/signing_method.rs @@ -388,7 +388,7 @@ impl SigningMethod { // Following LocalKeystore, if the code logic reaches here, then it has already passed all checks of this duty, and // it is safe (from this operator's point of view) to sign it locally. - dvf_signer.local_sign_and_store(signing_root).await; + // dvf_signer.local_sign_and_store(signing_root).await; if !only_aggregator || (only_aggregator && is_aggregator) { log::debug!("[Dvf {}/{}] Leader trying to achieve duty consensus and aggregate duty signatures", dvf_signer.operator_id, From 8ba70f39e3f03fbc434b84e1c0e83f22b19162dd Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Mon, 23 Sep 2024 03:03:12 +0000 Subject: [PATCH 31/43] remove extract contract address --- docker-compose-operator-mev.yml | 2 +- docker-compose-operator.yml | 2 +- src/node/contract.rs | 4 ++-- src/validation/cli.rs | 22 +++++++++++----------- src/validation/config.rs | 8 ++++---- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docker-compose-operator-mev.yml b/docker-compose-operator-mev.yml index c9b86c68..dd1370a8 100644 --- a/docker-compose-operator-mev.yml +++ b/docker-compose-operator-mev.yml @@ -90,7 +90,7 @@ services: - /bin/sh - -c - | - dvf validator_client --builder-proposals --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --extra-contract=${EXTRA_CONTRACT_ADDRESS} --base-port=26000 2>&1 + dvf validator_client --builder-proposals --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --base-port=26000 2>&1 expose: - "26000" - "26001" diff --git a/docker-compose-operator.yml b/docker-compose-operator.yml index b7468d78..b153d02a 100644 --- a/docker-compose-operator.yml +++ b/docker-compose-operator.yml @@ -82,7 +82,7 @@ services: - /bin/sh - -c - | - dvf validator_client --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --extra-contract=${EXTRA_CONTRACT_ADDRESS} --base-port=26000 2>&1 + dvf validator_client --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --base-port=26000 2>&1 expose: - "26000" - "26001" diff --git a/src/node/contract.rs b/src/node/contract.rs index c0ec45fb..d6d1243a 100644 --- a/src/node/contract.rs +++ b/src/node/contract.rs @@ -38,7 +38,7 @@ pub static SELF_OPERATOR_ID: OnceCell = OnceCell::const_new(); pub static DEFAULT_TRANSPORT_URL: OnceCell = OnceCell::const_new(); pub static REGISTRY_CONTRACT: OnceCell = OnceCell::const_new(); pub static NETWORK_CONTRACT: OnceCell = OnceCell::const_new(); -pub static EXTRA_CONTRACT: OnceCell = OnceCell::const_new(); +// pub static EXTRA_CONTRACT: OnceCell = OnceCell::const_new(); pub static DATABASE: OnceCell = OnceCell::const_new(); const QUERY_LOGS_INTERVAL: u64 = 60; const QUERY_BLOCK_INTERVAL: u64 = 500; @@ -455,7 +455,7 @@ impl Contract { let va_filter_builder = FilterBuilder::default() .address(vec![ Address::from_slice(&hex::decode(NETWORK_CONTRACT.get().unwrap()).unwrap()), - Address::from_slice(&hex::decode(EXTRA_CONTRACT.get().unwrap()).unwrap()), + // Address::from_slice(&hex::decode(EXTRA_CONTRACT.get().unwrap()).unwrap()), ]) .topics( Some(vec![va_reg_topic, va_rm_topic, fee_receipient_set_topic]), diff --git a/src/validation/cli.rs b/src/validation/cli.rs index ff6e569e..aa437ee7 100644 --- a/src/validation/cli.rs +++ b/src/validation/cli.rs @@ -154,17 +154,17 @@ pub fn cli_app() -> Command { .display_order(0) .required(true) ) - .arg( - Arg::new("extra-contract") - .long("extra-contract") - .value_name("EXTRA_CONTRACT") - .help( - "This is the address of extra contract" - ) - .action(ArgAction::Set) - .display_order(0) - .required(true) - ) + // .arg( + // Arg::new("extra-contract") + // .long("extra-contract") + // .value_name("EXTRA_CONTRACT") + // .help( + // "This is the address of extra contract" + // ) + // .action(ArgAction::Set) + // .display_order(0) + // .required(true) + // ) .arg( Arg::new("init-slashing-protection") .long("init-slashing-protection") diff --git a/src/validation/config.rs b/src/validation/config.rs index 82f1c0d4..da5230f4 100644 --- a/src/validation/config.rs +++ b/src/validation/config.rs @@ -1,6 +1,6 @@ use crate::node::config::{NodeConfig, API_ADDRESS}; use crate::node::contract::{ - DEFAULT_TRANSPORT_URL, EXTRA_CONTRACT, NETWORK_CONTRACT, REGISTRY_CONTRACT, SELF_OPERATOR_ID, + DEFAULT_TRANSPORT_URL, NETWORK_CONTRACT, REGISTRY_CONTRACT, SELF_OPERATOR_ID, }; use crate::validation::beacon_node_fallback::ApiTopic; use crate::validation::graffiti_file::GraffitiFile; @@ -163,9 +163,9 @@ impl Config { info!(log, "read network contract"; "network-contract" => &network_contract); NETWORK_CONTRACT.set(network_contract).unwrap(); - let extra_contract: String = parse_required(cli_args, "extra-contract")?; - info!(log, "read extra contract"; "extra-contract" => &extra_contract); - EXTRA_CONTRACT.set(extra_contract).unwrap(); + // let extra_contract: String = parse_required(cli_args, "extra-contract")?; + // info!(log, "read extra contract"; "extra-contract" => &extra_contract); + // EXTRA_CONTRACT.set(extra_contract).unwrap(); let self_ip: Ipv4Addr = parse_required(cli_args, "ip")?; info!(log, "read node ip"; "ip" => &self_ip.to_string()); From 7ef2c52861a929c66baf008fa0b0a569ab63116e Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Mon, 23 Sep 2024 06:34:24 +0000 Subject: [PATCH 32/43] revert remove local sign --- src/validation/signing_method.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation/signing_method.rs b/src/validation/signing_method.rs index d7e06db6..0d72dfa8 100644 --- a/src/validation/signing_method.rs +++ b/src/validation/signing_method.rs @@ -388,7 +388,7 @@ impl SigningMethod { // Following LocalKeystore, if the code logic reaches here, then it has already passed all checks of this duty, and // it is safe (from this operator's point of view) to sign it locally. - // dvf_signer.local_sign_and_store(signing_root).await; + dvf_signer.local_sign_and_store(signing_root).await; if !only_aggregator || (only_aggregator && is_aggregator) { log::debug!("[Dvf {}/{}] Leader trying to achieve duty consensus and aggregate duty signatures", dvf_signer.operator_id, From bef313a7e1558e06db14c5fc4ad5cd336e6e0085 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Sun, 29 Sep 2024 03:51:31 +0000 Subject: [PATCH 33/43] bump version to 1.3.4 --- common/dvf_version/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/dvf_version/src/lib.rs b/common/dvf_version/src/lib.rs index bb86a1c3..b03dab13 100644 --- a/common/dvf_version/src/lib.rs +++ b/common/dvf_version/src/lib.rs @@ -6,6 +6,6 @@ pub const ROOT_VERSION: u64 = 1; /// Up to 1 million pub const MAJOR_VERSION: u64 = 3; /// Up to 1 million -pub const MINOR_VERSION: u64 = 2; +pub const MINOR_VERSION: u64 = 4; pub static VERSION: u64 = ROOT_VERSION * 1000_000_000_000 + MAJOR_VERSION * 1000_000 + MINOR_VERSION; \ No newline at end of file From 2ed7057af2776ceca6580f34596a60516aa5ebb5 Mon Sep 17 00:00:00 2001 From: mlbones666 <127071266+mlbones666@users.noreply.github.com> Date: Sun, 6 Oct 2024 17:58:13 +0800 Subject: [PATCH 34/43] update mainnet info --- .env.example | 23 +++++------ .../workflows/{ci_staging.yml => ci_main.yml} | 7 +++- docker-compose-operator-mev.yml | 8 ++-- docker-compose-operator.yml | 7 ++-- ...ng.md => safestake-running-a-boot-node.md} | 4 +- ... => safestake-running-an-operator-node.md} | 40 +++++++++---------- 6 files changed, 45 insertions(+), 44 deletions(-) rename .github/workflows/{ci_staging.yml => ci_main.yml} (95%) rename docs/{safestake-running-a-boot-node-on-going.md => safestake-running-a-boot-node.md} (94%) rename docs/{safestake-running-an-operator-node-on-going.md => safestake-running-an-operator-node.md} (89%) diff --git a/.env.example b/.env.example index 205f80b5..7fe9e86e 100644 --- a/.env.example +++ b/.env.example @@ -1,18 +1,17 @@ -GETH_NETWORK=holesky -NETHERMIND_NETWORK=holesky -BESU_NETWORK=holesky -ERIGON_NETWORK=holesky -LIGHTHOUSE_NETWORK=holesky -OPERATOR_NETWORK=holesky -IMAGE_TAG=v3.2.3-testnet -REGISTRY_CONTRACT_ADDRESS=7a7b5F1943C502a0fC398D99299D08A48b50e2c9 -NETWORK_CONTRACT_ADDRESS=fA30b23f44b9556d49479C5177039DDA77506609 -EXTRA_CONTRACT_ADDRESS=0xb3b13e5FdBE358A3dB89e04951d409c1c5222051 -API_SERVER=https://api-testnet-holesky.safestake.xyz/api/op/ +GETH_NETWORK=mainnet +NETHERMIND_NETWORK=mainnet +BESU_NETWORK=mainnet +ERIGON_NETWORK=mainnet +LIGHTHOUSE_NETWORK=mainnet +OPERATOR_NETWORK=mainnet +IMAGE_TAG=v3.3.0-mainnet +REGISTRY_CONTRACT_ADDRESS=1a1f82f0365571A0b06df0992FC4D1BCc5Fdc6aD +NETWORK_CONTRACT_ADDRESS=829f3c089fE315FCB2BC9506B237BB56b7c3335B +API_SERVER=https://api-node.safestake.xyz/api/op/ # different chain has different ttd TTD=10790000 # separated by ',' for multiple relays, such as MEV_BOOST_RELAYS=xxx,xxx,xxx -MEV_BOOST_RELAYS=https://0xafa4c6985aa049fb79dd37010438cfebeb0f2bd42b115b89dd678dab0670c1de38da0c4e9138c9290a398ecd9a0b3110@boost-relay-holesky.flashbots.net +MEV_BOOST_RELAYS=https://0xa15b52576bcbf1072f4a011c0f99f9fb6c66f3e1ff321f11f461d15e31b1cb359caa092c71bbded0bae5b5ea401aab7e@aestus.live,https://0xa7ab7a996c8584251c8f925da3170bdfd6ebc75d50f5ddc4050a6fdc77f2a3b5fce2cc750d0865e05d7228af97d69561@agnostic-relay.net,https://0x8b5d2e73e2a3a55c6c87b8b6eb92e0149a125c852751db1422fa951e42a09b82c142c3ea98d0d9930b056a3bc9896b8f@bloxroute.max-profit.blxrbdn.com,https://0xb0b07cd0abef743db4260b0ed50619cf6ad4d82064cb4fbec9d3ec530f7c5e6793d9f286c4e082c0244ffb9f2658fe88@bloxroute.regulated.blxrbdn.com,https://0xb3ee7afcf27f1f1259ac1787876318c6584ee353097a50ed84f51a1f21a323b3736f271a895c7ce918c038e4265918be@relay.edennetwork.io,https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net,https://0x98650451ba02064f7b000f5768cf0cf4d4e492317d82871bdc87ef841a0743f69f0f1eea11168503240ac35d101c9135@mainnet-relay.securerpc.com,https://0xa1559ace749633b997cb3fdacffb890aeebdb0f5a3b6aaa7eeeaf1a38af0a8fe88b9e4b1f61f236d2e64d95733327a62@relay.ultrasound.money #gas limit. [default: 30,000,000] GAS_LIMIT_INTEGER=30000000 WS_URL= diff --git a/.github/workflows/ci_staging.yml b/.github/workflows/ci_main.yml similarity index 95% rename from .github/workflows/ci_staging.yml rename to .github/workflows/ci_main.yml index 03bd694b..badbbf8b 100644 --- a/.github/workflows/ci_staging.yml +++ b/.github/workflows/ci_main.yml @@ -11,8 +11,8 @@ name: Publish Docker image on: push: - branches: - - staging + tags: + - '**-mainnet' jobs: push_to_registry: @@ -33,6 +33,9 @@ jobs: uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 with: images: parastate/dvf-operator + flavor: | + latest=false + - name: Checkout submodules run: git submodule update --init --recursive diff --git a/docker-compose-operator-mev.yml b/docker-compose-operator-mev.yml index dd1370a8..f07f5cce 100644 --- a/docker-compose-operator-mev.yml +++ b/docker-compose-operator-mev.yml @@ -1,7 +1,7 @@ version: '3' services: geth: - image: ethereum/client-go:v1.14.7 + image: ethereum/client-go:v1.14.11 pull_policy: always restart: unless-stopped network_mode: "host" @@ -37,7 +37,7 @@ services: - "30303" mev-boost: - image: flashbots/mev-boost:1.6.1a3 + image: flashbots/mev-boost:1.8 network_mode: "host" pull_policy: always restart: unless-stopped @@ -46,7 +46,7 @@ services: - "18550" lighthouse: network_mode: "host" - image: sigp/lighthouse:v5.2.1 + image: sigp/lighthouse:v5.3.0 pull_policy: always restart: unless-stopped volumes: @@ -72,7 +72,7 @@ services: "--subscribe-all-subnets", "--import-all-attestations", "--validator-monitor-auto", - "--checkpoint-sync-url=https://checkpoint-sync.holesky.ethpandaops.io", + "--checkpoint-sync-url=https://sync-mainnet.beaconcha.in", ] expose: - "5052" diff --git a/docker-compose-operator.yml b/docker-compose-operator.yml index b153d02a..d72e996e 100644 --- a/docker-compose-operator.yml +++ b/docker-compose-operator.yml @@ -1,7 +1,7 @@ version: '3' services: geth: - image: ethereum/client-go:v1.14.7 + image: ethereum/client-go:v1.14.11 pull_policy: always restart: unless-stopped network_mode: "host" @@ -38,7 +38,7 @@ services: lighthouse: network_mode: "host" - image: sigp/lighthouse:v5.2.1 + image: sigp/lighthouse:v5.3.0 pull_policy: always restart: unless-stopped volumes: @@ -63,7 +63,7 @@ services: "--subscribe-all-subnets", "--import-all-attestations", "--validator-monitor-auto", - "--checkpoint-sync-url=https://checkpoint-sync.holesky.ethpandaops.io", + "--checkpoint-sync-url=https://sync-mainnet.beaconcha.in", ] expose: - "5052" @@ -77,7 +77,6 @@ services: image: parastate/dvf-operator:${IMAGE_TAG} pull_policy: always restart: unless-stopped - #such as : lighthouse vc --builder-proposals/private-tx-proposals command: - /bin/sh - -c diff --git a/docs/safestake-running-a-boot-node-on-going.md b/docs/safestake-running-a-boot-node.md similarity index 94% rename from docs/safestake-running-a-boot-node-on-going.md rename to docs/safestake-running-a-boot-node.md index 1e5d7a65..d76a24cf 100644 --- a/docs/safestake-running-a-boot-node-on-going.md +++ b/docs/safestake-running-a-boot-node.md @@ -1,9 +1,9 @@ -# SafeStake: Running a Boot Node (on going) +# SafeStake: Running a Boot Node **Updates happen frequently! Our** [**Github**](https://github.com/ParaState/SafeStakeOperator) **always has the latest operator node resources and setup instructions.** -***NOTE: This tutorial is meant for SafeStake admininistrator. You DON'T need to read this if you are a user who wants to setup an operator node. Instead, you should read the [tutorial for operator node](./safestake-running-an-operator-node-on-going.md).*** +***NOTE: This tutorial is meant for SafeStake admininistrator. You DON'T need to read this if you are a user who wants to setup an operator node. Instead, you should read the [tutorial for operator node](./safestake-running-an-operator-node.md).*** ## **Boot Node Service** diff --git a/docs/safestake-running-an-operator-node-on-going.md b/docs/safestake-running-an-operator-node.md similarity index 89% rename from docs/safestake-running-an-operator-node-on-going.md rename to docs/safestake-running-an-operator-node.md index cfd2446a..422b151e 100644 --- a/docs/safestake-running-an-operator-node-on-going.md +++ b/docs/safestake-running-an-operator-node.md @@ -13,7 +13,7 @@ _**Updates happen frequently! Our**_ [_**Github**_](https://github.com/ParaState * (Standalone Mode Recommend) * CPU: 16 * Memory: 32G - * Disk: 600GB + * Disk: 2TB * (Light Mode Recommend) * CPU: 2 * Memory: 4G @@ -65,7 +65,7 @@ Log in to your host cloud service provider, open the following firewall inbound | Inbound/Ingress | TCP | 26005 | 0.0.0.0/0 | DKG port, which will listen only when DKG is triggered. By default, the port won't listen.| -#### 2. SSH Login to your server ([jumpserver](https://www.jumpserver.org/) recommand) +#### 2. SSH Login to your server (recommand [jumpserver](https://www.jumpserver.org/)) #### 3. Install Docker and Docker compose @@ -109,19 +109,19 @@ NOTE: This step is to provide a quick way to setup and run the execution client ```bash cd dvf cp .env.example .env -sudo docker compose -f docker-compose-operator.yml up geth -d +sudo docker compose -f docker-compose-operator-mev.yml up geth -d # OR, if you use Nethermind/Besu/Erigon: -# sudo docker compose -f docker-compose-operator.yml up nethermind -d -# sudo docker compose -f docker-compose-operator.yml up besu -d -# sudo docker compose -f docker-compose-operator.yml up erigon -d -sudo docker compose -f docker-compose-operator.yml up lighthouse -d +# sudo docker compose -f docker-compose-operator-mev.yml up nethermind -d +# sudo docker compose -f docker-compose-operator-mev.yml up besu -d +# sudo docker compose -f docker-compose-operator-mev.yml up erigon -d +sudo docker compose -f docker-compose-operator-mev.yml up lighthouse -d ``` NOTE: Remember to open the `5052` firewall port for this host Syncing data may take several hours. You can use the command to see the latest logs of lighthouse to check if the data is synced: ```bash -sudo docker compose -f docker-compose-operator.yml logs -f --tail 10 lighthouse +sudo docker compose -f docker-compose-operator-mev.yml logs -f --tail 10 lighthouse ``` Once the data is synced, you will see output like below: @@ -163,7 +163,7 @@ BEACON_NODE_ENDPOINT=http://12.102.103.1:5052 #### 10. Generate a registration public and private key ```bash -sudo docker compose -f docker-compose-operator.yml up dvf_key_tool +sudo docker compose -f docker-compose-operator-mev.yml up dvf_key_tool ``` Output: ``` @@ -171,14 +171,14 @@ Output: dvf-dvf_key_tool-1 | INFO: node public key AtzozvDHiWUpO+oJph2ikv+EyBN5pdBXsfgZqLi0+Yqd dvf-dvf_key_tool-1 exited with code 0 ``` -Save the public key, which will be used later. Or you can find the public key in the "name" field of the file `/data/operator/v1/holesky/node_key.json` +Save the public key, which will be used later. Or you can find the public key in the "name" field of the file `/data/operator/v1/mainnet/node_key.json` -#### 11. Go to [SafeStake website](https://holesky.safestake.xyz/): +#### 11. Go to [SafeStake website](https://eth.safestake.xyz/): * Click "Join As Operator".
-* Select a wallet where you have enough holesky testnet token to pay minimum fee to sign a transaction. +* Select a wallet where you have enough ETH to pay minimum fee to sign a transaction.
@@ -205,12 +205,12 @@ OPERATOR_ID= #The Operator ID is the ID you receive after registering the operat ``` #### 13. (Optional) Customize the base port -You are able to change the ports that will be exposed in case the default ports 26000-26005 conflict with the ports you are using. In the file `docker-compose-operator.yml`, change `--base-port=26000` to the port you want in the operator's start command. +You are able to change the ports that will be exposed in case the default ports 26000-26005 conflict with the ports you are using. In the file `docker-compose-operator-mev.yml`, change `--base-port=26000` to the port you want in the operator's start command. #### 14. Start operator service ```bash -sudo docker compose -f docker-compose-operator.yml up --force-recreate -d operator +sudo docker compose -f docker-compose-operator-mev.yml up --force-recreate -d operator ``` @@ -224,7 +224,7 @@ sudo docker compose -f docker-compose-operator.yml up --force-recreate -d operat You can always view your public key in case you forget it with the command: ``` -sudo docker compose -f docker-compose-operator.yml logs -f operator | grep "node public key" +sudo docker compose -f docker-compose-operator-mev.yml logs -f operator | grep "node public key" ``` output @@ -236,20 +236,20 @@ It is a good practice to back up your operator private key file > **Keep it safe and put it in a safe place!** ``` -/data/operator/v1/holesky/node_key.json +/data/operator/v1/mainnet/node_key.json ``` **`Your SafeStake Operator Node is now configured`** -then you may go to [SafeStake website](https://holesky.safestake.xyz/) to register a validator and then choose your operator. +then you may go to [SafeStake website](https://eth.safestake.xyz/) to register a validator and then choose your operator. ## Backup and Migration If you are using our default settings, all data other than configration files is stored in the folder `/data`. It is possible for Geth/Nethermind/Besu/Erigon and lighthouse to resync data in a new machine. For operator, it is important to always backup and copy the folder `/data/operator/` to the new machine before you start operator in the new machine. -Some description of the folders and files under `/data/operator/v1/holesky/`: +Some description of the folders and files under `/data/operator/v1/mainnet/`: ``` -── holesky +── mainnet ├── contract_record.yml # record the current synced block number ├── dvf_node_db # hotstuff consensus files ├── node_key.json # operator's public and private key @@ -265,7 +265,7 @@ graph TD; B --> |Yes| D[check if the following errors \nshown in the log of first 100 lines]; D --> |?| E[Wrong scheme: https]; E --> |solution| F["WS_URL in .env file should be set\n beginning with ws:// or wss:// instead of https://"]; - F --> G[change the block number in the file\n /data/operator/v1/holesky/contract_record.yml to \na block number before the registration of the validator]; + F --> G[change the block number in the file\n /data/operator/v1/mainnet/contract_record.yml to \na block number before the registration of the validator]; G --> H[restart operator]; D --> |?| K["Failed to connect to {ip}:26000"]; K --> |solution| L[need to open the port 26000 to the internet,\n also carefully check if other firewall rules shown\n in the doc are set correctly in your server]; From 9d8b5d091e440524437ea617489146ae98e82070 Mon Sep 17 00:00:00 2001 From: mlbones666 <127071266+mlbones666@users.noreply.github.com> Date: Sun, 6 Oct 2024 18:46:56 +0800 Subject: [PATCH 35/43] fix doc and params --- docker-compose-operator-mev.yml | 1 - docker-compose-operator.yml | 1 - docs/safestake-running-an-operator-node.md | 3 ++- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docker-compose-operator-mev.yml b/docker-compose-operator-mev.yml index f07f5cce..83733771 100644 --- a/docker-compose-operator-mev.yml +++ b/docker-compose-operator-mev.yml @@ -65,7 +65,6 @@ services: "--http-address=0.0.0.0", "--http-port=5052", "--staking", - "--http-allow-sync-stalled", "--execution-endpoint=http://127.0.0.1:8551", "--metrics", "--metrics-address=0.0.0.0", diff --git a/docker-compose-operator.yml b/docker-compose-operator.yml index d72e996e..8bae598e 100644 --- a/docker-compose-operator.yml +++ b/docker-compose-operator.yml @@ -56,7 +56,6 @@ services: "--http-address=0.0.0.0", "--http-port=5052", "--staking", - "--http-allow-sync-stalled", "--execution-endpoint=http://127.0.0.1:8551", "--metrics", "--metrics-address=0.0.0.0", diff --git a/docs/safestake-running-an-operator-node.md b/docs/safestake-running-an-operator-node.md index 422b151e..1bc57e81 100644 --- a/docs/safestake-running-an-operator-node.md +++ b/docs/safestake-running-an-operator-node.md @@ -1,4 +1,4 @@ -# SafeStake: Running an Operator Node (on going) +# SafeStake: Running an Operator Node _**Updates happen frequently! Our**_ [_**Github**_](https://github.com/ParaState/SafeStakeOperator) _**always has the latest operator node resources and setup instructions.**_ @@ -114,6 +114,7 @@ sudo docker compose -f docker-compose-operator-mev.yml up geth -d # sudo docker compose -f docker-compose-operator-mev.yml up nethermind -d # sudo docker compose -f docker-compose-operator-mev.yml up besu -d # sudo docker compose -f docker-compose-operator-mev.yml up erigon -d +sudo docker compose -f docker-compose-operator-mev.yml up mev-boost -d sudo docker compose -f docker-compose-operator-mev.yml up lighthouse -d ``` From a5535b70da2fbe36dbfc8b53c2141c653241f844 Mon Sep 17 00:00:00 2001 From: mlbones666 <127071266+mlbones666@users.noreply.github.com> Date: Sun, 6 Oct 2024 20:35:22 +0800 Subject: [PATCH 36/43] remove on-going in doc --- README.md | 2 +- docs/SUMMARY.md | 2 +- docs/safestake-ecosystem.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9ef904ae..b5b2b106 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ In the SafeStake ecosystem, there is no need for stakers to deploy validators as ### Run a SafeStake Operator Node -Please refer to the step-by-step instructions [here](docs/safestake-running-an-operator-node-on-going.md). +Please refer to the step-by-step instructions [here](docs/safestake-running-an-operator-node.md). ## SafeStake White Paper diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index ab3f3765..51d33e85 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -4,5 +4,5 @@ * [SafeStake: Ecosystem](safestake-ecosystem.md) * [SafeStake: Tech Stack](safestake-tech-stack.md) * [SafeStake: Tokenomics (ongoing)](safestake-tokenomics-ongoing.md) -* [SafeStake: Running an Operator Node (on going)](safestake-running-an-operator-node-on-going.md) +* [SafeStake: Running an Operator Node (on going)](safestake-running-an-operator-node.md) * [Distributed Key Generation](safestake-tech-dkg.md) diff --git a/docs/safestake-ecosystem.md b/docs/safestake-ecosystem.md index cd733b2e..38ef37d3 100644 --- a/docs/safestake-ecosystem.md +++ b/docs/safestake-ecosystem.md @@ -33,7 +33,7 @@ The SafeStake framework employs committees of non-trusting tech-savvy operators It is worth noting that in other staking infrastructure provider where similar initializer operators specify the execution layer address that collects priority fees and MEV extraction, oracles are used to monitor and help prevent malicious operators from stealing those fees. SafeStake's design is simplified and more effective. The validator private key is decentralized so no single operator determines the execution layer address, preventing them from stealing the funds. -To `run` an operator node on the SafeStake network, refer to the instructions [here](safestake-running-an-operator-node-on-going.md). +To `run` an operator node on the SafeStake network, refer to the instructions [here](safestake-running-an-operator-node.md). ### SafeStake Service Provider / ParaState DAO From 952f6b08983e00870f1fda1ab73eedd7029a6c8a Mon Sep 17 00:00:00 2001 From: mlbones666 <127071266+mlbones666@users.noreply.github.com> Date: Sun, 6 Oct 2024 20:53:31 +0800 Subject: [PATCH 37/43] remove 5/7 --- docs/imgs/committe-size.png | Bin 118035 -> 132943 bytes docs/safestake-running-an-operator-node.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/imgs/committe-size.png b/docs/imgs/committe-size.png index cb25b93a9b659248f170b7011fd34c12c4b4c4d5..7e15ebe94c1613057d8bb9d310eb5f6974023fe9 100644 GIT binary patch literal 132943 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
### Remove From c2dcf1696a91aa418e82aee013c64309466af9e6 Mon Sep 17 00:00:00 2001 From: mlbones666 <127071266+mlbones666@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:34:07 +0800 Subject: [PATCH 38/43] change version to v3.4.0 --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 7fe9e86e..6ab78003 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ BESU_NETWORK=mainnet ERIGON_NETWORK=mainnet LIGHTHOUSE_NETWORK=mainnet OPERATOR_NETWORK=mainnet -IMAGE_TAG=v3.3.0-mainnet +IMAGE_TAG=v3.4.0-mainnet REGISTRY_CONTRACT_ADDRESS=1a1f82f0365571A0b06df0992FC4D1BCc5Fdc6aD NETWORK_CONTRACT_ADDRESS=829f3c089fE315FCB2BC9506B237BB56b7c3335B API_SERVER=https://api-node.safestake.xyz/api/op/ From 6139ddc308f64fb9b1f9e067e90acee715d19d96 Mon Sep 17 00:00:00 2001 From: mlbones666 <127071266+mlbones666@users.noreply.github.com> Date: Fri, 11 Oct 2024 00:05:47 +0800 Subject: [PATCH 39/43] update doc for mev-boost --- docs/safestake-running-an-operator-node.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/safestake-running-an-operator-node.md b/docs/safestake-running-an-operator-node.md index 23075610..932aaa8c 100644 --- a/docs/safestake-running-an-operator-node.md +++ b/docs/safestake-running-an-operator-node.md @@ -103,7 +103,7 @@ openssl rand -hex 32 | tr -d "\n" | sudo tee /data/jwt/jwtsecret git clone --recurse-submodules https://github.com/ParaState/SafeStakeOperator.git dvf ``` -#### 8. Running Geth/Nethermind/Besu/Erigon & Lighthouse Service +#### 8. Running Geth/Nethermind/Besu/Erigon & mev-boost & Lighthouse Service NOTE: This step is to provide a quick way to setup and run the execution client and consensus client. If you already have a node running execution client and consensus client, you can skip this step. ```bash @@ -115,6 +115,7 @@ sudo docker compose -f docker-compose-operator-mev.yml up geth -d # sudo docker compose -f docker-compose-operator-mev.yml up besu -d # sudo docker compose -f docker-compose-operator-mev.yml up erigon -d sudo docker compose -f docker-compose-operator-mev.yml up mev-boost -d +# in .env, set MEV_ENDPOINT to the url of mev-boost, e.g. http://127.0.0.1:18550 then start lighthouse sudo docker compose -f docker-compose-operator-mev.yml up lighthouse -d ``` From ff7bbefe3ad254dc5c6a68012dd2dfaedac95f74 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Tue, 15 Oct 2024 09:31:08 +0000 Subject: [PATCH 40/43] add monitor tool --- src/bin/dvf_monitor_tool.rs | 97 +++++++++++++++++++++++++++++++++++++ src/node/config.rs | 2 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/bin/dvf_monitor_tool.rs diff --git a/src/bin/dvf_monitor_tool.rs b/src/bin/dvf_monitor_tool.rs new file mode 100644 index 00000000..db690f50 --- /dev/null +++ b/src/bin/dvf_monitor_tool.rs @@ -0,0 +1,97 @@ +use network::{DvfMessage, ReliableSender}; +use dvf_version::VERSION; +use std::fs::File; +use dvf::node::config::{BOOT_ENRS_CONFIG_FILE, base_to_signature_addr, base_to_duties_addr, base_to_active_addr}; +use lighthouse_network::discv5::enr::{Enr, CombinedKey}; +use std::net::{IpAddr, SocketAddr}; +use bytes::Bytes; +use log::{error, info}; +use std::net::TcpStream; + +fn all_equal(array: &[T]) -> bool { + if let Some(first) = array.first() { + array.iter().all(|x| x == first) + } else { + true + } +} + +async fn query_socket_address_from_boot(enrs: Vec>, op_pk: Vec) -> Vec { + // query socket address of the operator + let dvf_message = DvfMessage { + version: VERSION, + validator_id: 0, + message: op_pk.clone(), + }; + let serialized_msg = bincode::serialize(&dvf_message).unwrap(); + let boot_socketaddrs: Vec = enrs.iter().map(|e| { + SocketAddr::new( + IpAddr::V4(e.ip4().expect("boot enr ip should not be empty")), + e.udp4().expect("boot enr port should not be empty")) + }).collect(); + let network_sender = ReliableSender::new(); + let mut result = Vec::new(); + for addr in boot_socketaddrs { + match network_sender + .send(addr, Bytes::from(serialized_msg.clone())) + .await.await { + Ok(data) => { + let op_base_socketaddr = bincode::deserialize::(&data).unwrap(); + result.push(op_base_socketaddr); + }, + Err(e) => { + error!("failed to query op socket addr from boot {}", e) + } + } + } + result +} + +fn check_all_ports(base_addr: SocketAddr) -> bool { + TcpStream::connect(base_to_signature_addr(base_addr)).is_ok() && + TcpStream::connect(base_to_duties_addr(base_addr)).is_ok() && + TcpStream::connect(base_to_active_addr(base_addr)).is_ok() +} + +#[tokio::main] +async fn main() { + let mut logger = + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")); + logger.format_timestamp_millis(); + logger.init(); + log::info!("------dvf_monitor_tool------"); + + let op_pk_str = std::env::args() + .nth(1) + .expect("ERROR: there is no valid operator public key argument"); + + let op_pk = hex::decode(&op_pk_str[2..]).expect("decode failed, op pk should be in hex format"); + + let file = File::options() + .read(true) + .write(false) + .create(false) + .open(BOOT_ENRS_CONFIG_FILE) + .expect( + format!( + "Unable to open the boot enrs config file: {:?}", + BOOT_ENRS_CONFIG_FILE + ) + .as_str(), + ); + let boot_enrs: Vec> = + serde_yaml::from_reader(file).expect("Unable to parse boot enr"); + + let op_addrs = query_socket_address_from_boot(boot_enrs, op_pk).await; + if op_addrs.is_empty() { + panic!("failed to query op socket address from boot node"); + } + if !all_equal(&op_addrs) { + panic!("the op addresses from different boot nodes are not same {:?}", op_addrs) + } + let base_addr = op_addrs.first().unwrap(); + if !check_all_ports(base_addr.clone()) { + panic!("ports checking failed") + } + info!("op {} is ready", op_pk_str); +} \ No newline at end of file diff --git a/src/node/config.rs b/src/node/config.rs index 489ef04e..e08a0bec 100644 --- a/src/node/config.rs +++ b/src/node/config.rs @@ -30,7 +30,7 @@ pub const VALIDATOR_PK_URL: &str = "validator_pk"; pub const PRESTAKE_SIGNATURE_URL: &str = "prestake_signature"; pub const STAKE_SIGNATURE_URL: &str = "stake_signature"; pub const TOPIC_NODE_INFO: &str = "dvf/topic_node_info"; -const BOOT_ENRS_CONFIG_FILE: &str = "boot_config/boot_enrs.yaml"; +pub const BOOT_ENRS_CONFIG_FILE: &str = "boot_config/boot_enrs.yaml"; lazy_static! { // [Issue] SocketAddr::new is not yet a const fn in stable release. From dbc3fa40c9ae2047dae883ccad1ba4de9ef02569 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Sun, 27 Oct 2024 14:51:52 +0000 Subject: [PATCH 41/43] add config contract --- .env.example | 1 + contract_config/configs.yml | 2 +- docker-compose-operator-mev.yml | 2 +- docker-compose-operator.yml | 2 +- src/bin/dvf_monitor_tool.rs | 1 + src/node/contract.rs | 60 +++++++--------------- src/node/db.rs | 90 +++++++++++++++++++++++++++++++++ src/node/node.rs | 2 + src/validation/cli.rs | 22 ++++---- src/validation/config.rs | 8 +-- 10 files changed, 130 insertions(+), 60 deletions(-) diff --git a/.env.example b/.env.example index 6ab78003..e63c5d06 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,7 @@ OPERATOR_NETWORK=mainnet IMAGE_TAG=v3.4.0-mainnet REGISTRY_CONTRACT_ADDRESS=1a1f82f0365571A0b06df0992FC4D1BCc5Fdc6aD NETWORK_CONTRACT_ADDRESS=829f3c089fE315FCB2BC9506B237BB56b7c3335B +CONFIG_CONTRACT_ADDRESS=1EFB8c90381695584CcB117388Bba897b71e0635 API_SERVER=https://api-node.safestake.xyz/api/op/ # different chain has different ttd TTD=10790000 diff --git a/contract_config/configs.yml b/contract_config/configs.yml index bf62d92f..2e22bdb8 100644 --- a/contract_config/configs.yml +++ b/contract_config/configs.yml @@ -5,6 +5,6 @@ initiator_registration_topic: 3ed0a993c042af686c0f93773269df3a1874729d2b4fc3f71f initiator_minipool_created_topic: d37d31a5a66d534ce3b071e3ee6cf8b7d36a6dd20aa4f08e9cec322b27bd7704 initiator_minipool_ready_topic: c474edb44e2d7e6c7f20261d61afed24712bcbd6a0799ae5b0786626c47da63c initiator_removal_topic: 34ecedfa430a18df6cda4bc2313d74d224c6742df8e6a98eca2fda96b69a050d -fee_recipient_set_topic: a2ae9b7eef58d6d2779721289e4e6fecb49b0134e40d787b3bed6743ed325497 +fee_recipient_set_topic: 05d899f2c039ff7edfb413f1f4fe82889c2efaace7f08d7ac5aebad905b27ced safestake_network_abi_path: contract_config/SafeStakeNetwork.json safestake_registry_abi_path: contract_config/SafeStakeRegistry.json diff --git a/docker-compose-operator-mev.yml b/docker-compose-operator-mev.yml index 83733771..04587edd 100644 --- a/docker-compose-operator-mev.yml +++ b/docker-compose-operator-mev.yml @@ -89,7 +89,7 @@ services: - /bin/sh - -c - | - dvf validator_client --builder-proposals --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --base-port=26000 2>&1 + dvf validator_client --builder-proposals --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --config-contract=${CONFIG_CONTRACT_ADDRESS} --base-port=26000 2>&1 expose: - "26000" - "26001" diff --git a/docker-compose-operator.yml b/docker-compose-operator.yml index 8bae598e..26efde82 100644 --- a/docker-compose-operator.yml +++ b/docker-compose-operator.yml @@ -80,7 +80,7 @@ services: - /bin/sh - -c - | - dvf validator_client --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --base-port=26000 2>&1 + dvf validator_client --metrics --debug-level=info --network=${OPERATOR_NETWORK} --beacon-nodes=${BEACON_NODE_ENDPOINT} --api=${API_SERVER} --ws-url=${WS_URL} --ip=${NODE_IP} --id=${OPERATOR_ID} --registry-contract=${REGISTRY_CONTRACT_ADDRESS} --network-contract=${NETWORK_CONTRACT_ADDRESS} --config-contract=${CONFIG_CONTRACT_ADDRESS} --base-port=26000 2>&1 expose: - "26000" - "26001" diff --git a/src/bin/dvf_monitor_tool.rs b/src/bin/dvf_monitor_tool.rs index db690f50..12caa812 100644 --- a/src/bin/dvf_monitor_tool.rs +++ b/src/bin/dvf_monitor_tool.rs @@ -83,6 +83,7 @@ async fn main() { serde_yaml::from_reader(file).expect("Unable to parse boot enr"); let op_addrs = query_socket_address_from_boot(boot_enrs, op_pk).await; + info!("node addre {:?}", op_addrs); if op_addrs.is_empty() { panic!("failed to query op socket address from boot node"); } diff --git a/src/node/contract.rs b/src/node/contract.rs index d6d1243a..b3e6b8b3 100644 --- a/src/node/contract.rs +++ b/src/node/contract.rs @@ -38,7 +38,7 @@ pub static SELF_OPERATOR_ID: OnceCell = OnceCell::const_new(); pub static DEFAULT_TRANSPORT_URL: OnceCell = OnceCell::const_new(); pub static REGISTRY_CONTRACT: OnceCell = OnceCell::const_new(); pub static NETWORK_CONTRACT: OnceCell = OnceCell::const_new(); -// pub static EXTRA_CONTRACT: OnceCell = OnceCell::const_new(); +pub static CONFIG_CONTRACT: OnceCell = OnceCell::const_new(); pub static DATABASE: OnceCell = OnceCell::const_new(); const QUERY_LOGS_INTERVAL: u64 = 60; const QUERY_BLOCK_INTERVAL: u64 = 500; @@ -455,7 +455,7 @@ impl Contract { let va_filter_builder = FilterBuilder::default() .address(vec![ Address::from_slice(&hex::decode(NETWORK_CONTRACT.get().unwrap()).unwrap()), - // Address::from_slice(&hex::decode(EXTRA_CONTRACT.get().unwrap()).unwrap()), + Address::from_slice(&hex::decode(CONFIG_CONTRACT.get().unwrap()).unwrap()), ]) .topics( Some(vec![va_reg_topic, va_rm_topic, fee_receipient_set_topic]), @@ -805,10 +805,16 @@ pub async fn process_validator_registration( .into_iter() .map(|s| base64::decode(s).unwrap()) .collect(); + let fee_recipient_address = match db.query_owner_fee_recipient(address.clone()).await.map_err(|_| { + ContractError::DatabaseError + })? { + Some(a) => a, + None => address + }; //send command to node let validator = Validator { id: validator_id, - owner_address: address, + owner_address: fee_recipient_address, public_key: va_pk.try_into().unwrap(), releated_operators: op_ids, active: true, @@ -1173,20 +1179,10 @@ pub async fn process_fee_recipient_set(raw_log: Log, db: &Database) -> Result<() indexed: true, }, EventParam { - name: "pubkey".to_string(), - kind: ParamType::Bytes, - indexed: false, - }, - EventParam { - name: "feeReceiptAddress".to_string(), + name: "newAddress".to_string(), kind: ParamType::Address, indexed: true, }, - EventParam { - name: "updateCount".to_string(), - kind: ParamType::Uint(32), - indexed: false, - }, ], anonymous: false, }; @@ -1201,39 +1197,19 @@ pub async fn process_fee_recipient_set(raw_log: Log, db: &Database) -> Result<() .clone() .into_address() .ok_or(ContractError::LogParseError)?; - let pubkey = log.params[1] - .value - .clone() - .into_bytes() - .ok_or(ContractError::LogParseError)?; - let fee_recipient_address = log.params[2] + let fee_recipient_address = log.params[1] .value .clone() .into_address() .ok_or(ContractError::LogParseError)?; - if pubkey.iter().all(|&x| x == 0) { - // public key is zero - for v in db.query_validator_by_address(owner).await.unwrap().iter() { - let cmd = ContractCommand::SetFeeRecipient(v.public_key.clone(), fee_recipient_address); - db.insert_contract_command(v.id, serde_json::to_string(&cmd).unwrap()) - .await; - } - } else { - match db - .query_validator_by_public_key(hex::encode(pubkey.clone())) - .await - .unwrap() - { - Some(v) => { - let cmd = ContractCommand::SetFeeRecipient(pubkey, fee_recipient_address); - db.insert_contract_command(v.id, serde_json::to_string(&cmd).unwrap()) - .await; - } - None => { - info!("set fee recipient not releated to this operator"); - } - } + db.upsert_owner_fee_recipient(owner, fee_recipient_address).await; + + // public key is zero + for v in db.query_validator_by_address(owner).await.unwrap().iter() { + let cmd = ContractCommand::SetFeeRecipient(v.public_key.clone(), fee_recipient_address); + db.insert_contract_command(v.id, serde_json::to_string(&cmd).unwrap()) + .await; } Ok(()) diff --git a/src/node/db.rs b/src/node/db.rs index 0f6309cf..6cb7ef3b 100644 --- a/src/node/db.rs +++ b/src/node/db.rs @@ -41,6 +41,8 @@ pub enum DbCommand { QueryInitiatorStore(u32, oneshot::Sender>>), QueryAllValidatorPublicKeys(oneshot::Sender>>), QueryValidatorRegistrationTimestamp(String, oneshot::Sender>), + UpsertOwnerFeeRecipient(Address, Address), + QueryOwnerFeeRecipient(Address, oneshot::Sender>>) } #[derive(Clone, Debug)] @@ -134,6 +136,11 @@ impl Database { CONSTRAINT initiator_store_constraint FOREIGN KEY (record_id) REFERENCES initiator_store_record(id) ON DELETE CASCADE )"; + let create_owner_fee_recipient_sql = "CREATE TABLE IF NOT EXISTS owner_fee_recipient( + owner CHARACTER(40) NOT NULL PRIMARY KEY, + fee_recipient CHARACTER(40) NOT NULL + )"; + conn.execute(create_operators_sql, [])?; conn.execute(create_validators_sql, [])?; conn.execute(create_releation_sql, [])?; @@ -145,6 +152,7 @@ impl Database { conn.execute(create_initiator_store_sql, [])?; conn.execute(create_initiator_store_oppk_sql, [])?; conn.execute(create_validators_registration_timestamp_sql, [])?; + conn.execute(create_owner_fee_recipient_sql, [])?; let (tx, mut rx) = channel(1000); tokio::spawn(async move { @@ -242,6 +250,13 @@ impl Database { query_validator_registration_timestamp(&mut conn, &public_key); let _ = sender.send(response); } + DbCommand::UpsertOwnerFeeRecipient(owner, fee_recipient) => { + upsert_owner_fee_recipient(&mut conn, owner, fee_recipient); + } + DbCommand::QueryOwnerFeeRecipient(owner, sender) => { + let response = query_owner_fee_recipient(&mut conn, owner); + let _ = sender.send(response); + } } } }); @@ -617,6 +632,37 @@ impl Database { .await .expect("Failed to receive reply of query validators registration timestamp from db") } + + pub async fn upsert_owner_fee_recipient( + &self, + owner: Address, + fee_recipient: Address + ) { + + if let Err(e) = self + .channel + .send(DbCommand::UpsertOwnerFeeRecipient(owner, fee_recipient)) + .await + { + panic!("Failed to send enable validator command to store: {}", e); + } + } + + pub async fn query_owner_fee_recipient( + &self, + owner: Address, + ) -> DbResult>{ + let (sender, receiver) = oneshot::channel(); + if let Err(e) = self.channel.send(DbCommand::QueryOwnerFeeRecipient(owner, sender)).await { + panic!( + "Failed to send query fee recipient address to store: {}", + e + ); + } + receiver + .await + .expect("Failed to receive reply of query fee recipient address from db") + } } fn insert_operator(conn: &Connection, operator: Operator) { @@ -1209,6 +1255,50 @@ pub fn query_all_validator_publickeys(conn: &Connection) -> DbResult Ok(public_keys) } +pub fn upsert_owner_fee_recipient(conn: &Connection, owner: Address, fee_recipient: Address) { + let owner = format!("{0:0x}", owner); + let fee_recipient = format!("{0:0x}", fee_recipient); + if let Err(e) = conn.execute("insert into owner_fee_recipient(owner, fee_recipient) values(?1, ?2) ON conflict(owner) do update set fee_recipient = (?3)", params![owner, fee_recipient, fee_recipient]) { + error!("Can't insert into owner fee recipient, error: {}", e); + } +} + +pub fn query_owner_fee_recipient(conn: &Connection, owner: Address) -> DbResult> { + let owner = format!("{0:0x}", owner); + match conn.prepare("select fee_recipient from owner_fee_recipient where owner = (?)") { + Ok(mut stmt) => { + let mut rows = stmt.query([owner])?; + while let Some(row) = rows.next()? { + let fee_recipient: String = row.get(0)?; + return Ok(Some(Address::from_slice(&hex::decode(&fee_recipient).unwrap()))); + } + return Ok(None); + } + Err(e) => { + error!("Can't prepare statement {}", e); + return Err(e); + } + } +} + +#[tokio::test] +async fn test_fee_recipient() { + let mut logger = + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")); + logger.format_timestamp_millis(); + logger.init(); + let _ = Database::new("/tmp/test.db").unwrap(); + let mut conn = Connection::open("/tmp/test.db").unwrap(); + let owner = Address::random(); + let fee_recipient = Address::random(); + let new_fee_recipient = Address::random(); + assert_eq!(query_owner_fee_recipient(&conn, owner), Ok(None)); + upsert_owner_fee_recipient(&conn, owner, fee_recipient); + assert_eq!(query_owner_fee_recipient(&conn, owner), Ok(Some(fee_recipient))); + upsert_owner_fee_recipient(&conn, owner, new_fee_recipient); + assert_eq!(query_owner_fee_recipient(&conn, owner), Ok(Some(new_fee_recipient))); +} + #[tokio::test] async fn test_database() { use crate::crypto::ThresholdSignature; diff --git a/src/node/node.rs b/src/node/node.rs index bc5b9d24..95f76708 100644 --- a/src/node/node.rs +++ b/src/node/node.rs @@ -651,6 +651,8 @@ pub async fn add_validator( default_keystore_share_path(&keystore_share, validator_dir.clone()); let voting_keystore_share_password_path = default_keystore_share_password_path(&keystore_share, secret_dir.clone()); + + match &node.validator_store { Some(validator_store) => { let _ = validator_store diff --git a/src/validation/cli.rs b/src/validation/cli.rs index aa437ee7..20b07d13 100644 --- a/src/validation/cli.rs +++ b/src/validation/cli.rs @@ -154,17 +154,17 @@ pub fn cli_app() -> Command { .display_order(0) .required(true) ) - // .arg( - // Arg::new("extra-contract") - // .long("extra-contract") - // .value_name("EXTRA_CONTRACT") - // .help( - // "This is the address of extra contract" - // ) - // .action(ArgAction::Set) - // .display_order(0) - // .required(true) - // ) + .arg( + Arg::new("config-contract") + .long("config-contract") + .value_name("CONFIG_CONTRACT") + .help( + "This is the address of config contract" + ) + .action(ArgAction::Set) + .display_order(0) + .required(true) + ) .arg( Arg::new("init-slashing-protection") .long("init-slashing-protection") diff --git a/src/validation/config.rs b/src/validation/config.rs index da5230f4..a2d66d24 100644 --- a/src/validation/config.rs +++ b/src/validation/config.rs @@ -1,6 +1,6 @@ use crate::node::config::{NodeConfig, API_ADDRESS}; use crate::node::contract::{ - DEFAULT_TRANSPORT_URL, NETWORK_CONTRACT, REGISTRY_CONTRACT, SELF_OPERATOR_ID, + DEFAULT_TRANSPORT_URL, NETWORK_CONTRACT, REGISTRY_CONTRACT, SELF_OPERATOR_ID, CONFIG_CONTRACT }; use crate::validation::beacon_node_fallback::ApiTopic; use crate::validation::graffiti_file::GraffitiFile; @@ -163,9 +163,9 @@ impl Config { info!(log, "read network contract"; "network-contract" => &network_contract); NETWORK_CONTRACT.set(network_contract).unwrap(); - // let extra_contract: String = parse_required(cli_args, "extra-contract")?; - // info!(log, "read extra contract"; "extra-contract" => &extra_contract); - // EXTRA_CONTRACT.set(extra_contract).unwrap(); + let config_contract: String = parse_required(cli_args, "config-contract")?; + info!(log, "read config contract"; "config-contract" => &config_contract); + CONFIG_CONTRACT.set(config_contract).unwrap(); let self_ip: Ipv4Addr = parse_required(cli_args, "ip")?; info!(log, "read node ip"; "ip" => &self_ip.to_string()); From 0ed34c13401f3d1e2d988f0f63e084c0a82c40c0 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Mon, 28 Oct 2024 15:40:44 +0000 Subject: [PATCH 42/43] verify fee recipient address --- src/node/db.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++- src/node/dvfcore.rs | 30 ++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/node/db.rs b/src/node/db.rs index 6cb7ef3b..b1899fa5 100644 --- a/src/node/db.rs +++ b/src/node/db.rs @@ -42,7 +42,8 @@ pub enum DbCommand { QueryAllValidatorPublicKeys(oneshot::Sender>>), QueryValidatorRegistrationTimestamp(String, oneshot::Sender>), UpsertOwnerFeeRecipient(Address, Address), - QueryOwnerFeeRecipient(Address, oneshot::Sender>>) + QueryOwnerFeeRecipient(Address, oneshot::Sender>>), + CheckValidatorFeeRecipient(Vec, Address, oneshot::Sender>) } #[derive(Clone, Debug)] @@ -257,6 +258,10 @@ impl Database { let response = query_owner_fee_recipient(&mut conn, owner); let _ = sender.send(response); } + DbCommand::CheckValidatorFeeRecipient(pubkey, fee_recipient, sender) => { + let response = check_validator_fee_recipient(&mut conn, pubkey, fee_recipient); + let _ = sender.send(response); + } } } }); @@ -663,6 +668,23 @@ impl Database { .await .expect("Failed to receive reply of query fee recipient address from db") } + + pub async fn check_validator_fee_recipient( + &self, + pubkey: Vec, + fee_recipient: Address + ) -> DbResult { + let (sender, receiver) = oneshot::channel(); + if let Err(e) = self.channel.send(DbCommand::CheckValidatorFeeRecipient(pubkey, fee_recipient, sender)).await { + panic!( + "Failed to send check validator fee recipient address to store: {}", + e + ); + } + receiver + .await + .expect("Failed to receive reply of check validator fee recipient address from db") + } } fn insert_operator(conn: &Connection, operator: Operator) { @@ -1281,6 +1303,38 @@ pub fn query_owner_fee_recipient(conn: &Connection, owner: Address) -> DbResult< } } +pub fn check_validator_fee_recipient(conn: &Connection, pubkey: Vec, fee_recipient: Address) -> DbResult { + let pk = hex::encode(pubkey); + match conn.prepare("select owner_fee_recipient.fee_recipient from validators join owner_fee_recipient on validators.owner_address = owner_fee_recipient.owner where validators.public_key = (?)") { + Ok(mut stmt) => { + let mut rows = stmt.query([pk.clone()])?; + while let Some(row) = rows.next()? { + let res: String = row.get(0)?; + return Ok(res == format!("{0:0x}", fee_recipient)); + } + } + Err(e) => { + error!("Can't prepare statement {}", e); + return Err(e); + } + } + match conn.prepare("select owner_address from validators where public_key = (?)") { + Ok(mut stmt) => { + let mut rows = stmt.query([pk])?; + while let Some(row) = rows.next()? { + let res: String = row.get(0)?; + return Ok(res == format!("{0:0x}", fee_recipient)); + } + } + Err(e) => { + error!("Can't prepare statement {}", e); + return Err(e); + } + } + + Ok(false) +} + #[tokio::test] async fn test_fee_recipient() { let mut logger = @@ -1290,13 +1344,29 @@ async fn test_fee_recipient() { let _ = Database::new("/tmp/test.db").unwrap(); let mut conn = Connection::open("/tmp/test.db").unwrap(); let owner = Address::random(); + let mut rng = rand::thread_rng(); + let mut dest = [0u8; 48]; + rng.fill_bytes(&mut dest); + let pubkey = dest.to_vec(); + let validator = Validator { + id: 1, + owner_address: owner.clone(), + public_key: pubkey.clone(), + releated_operators: vec![], + active: true + }; + insert_validator(&mut conn, validator, 0); + assert_eq!(check_validator_fee_recipient(&conn, pubkey.clone(), owner).unwrap(), true); let fee_recipient = Address::random(); let new_fee_recipient = Address::random(); + assert_eq!(check_validator_fee_recipient(&conn, pubkey.clone(), fee_recipient).unwrap(), false); assert_eq!(query_owner_fee_recipient(&conn, owner), Ok(None)); upsert_owner_fee_recipient(&conn, owner, fee_recipient); + assert_eq!(check_validator_fee_recipient(&conn, pubkey.clone(), fee_recipient).unwrap(), true); assert_eq!(query_owner_fee_recipient(&conn, owner), Ok(Some(fee_recipient))); upsert_owner_fee_recipient(&conn, owner, new_fee_recipient); assert_eq!(query_owner_fee_recipient(&conn, owner), Ok(Some(new_fee_recipient))); + assert_eq!(check_validator_fee_recipient(&conn, pubkey.clone(), new_fee_recipient).unwrap(), true); } #[tokio::test] diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index 303eaf02..8f61316e 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -33,8 +33,10 @@ use store::Store; use tokio::sync::RwLock; use types::{ AbstractExecPayload, AttestationData, BeaconBlock, BlindedPayload, EthSpec, FullPayload, - Keypair, + Keypair, ExecPayload }; +use crate::node::db::Database; + #[derive(Serialize, Deserialize, Clone)] pub struct DvfInfo { pub validator_id: u64, @@ -147,6 +149,7 @@ pub struct DvfDutyCheckMessage { pub data: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub sign_hex: Option, + pub pubkey: Vec } impl DvfDutyCheckMessage { @@ -167,6 +170,7 @@ pub struct DvfDutyCheckHandler { pub validator_pk: BlsPublicKey, pub operator_pks: HashMap, pub keypair: Keypair, + pub db: Database, _phantom: PhantomData, } @@ -412,6 +416,17 @@ impl MessageHandler for DvfDutyCheckHandler { return Ok(()); } }; + let fee_recipient = block.body().execution_payload().unwrap().fee_recipient(); + if self.db.check_validator_fee_recipient(check_msg.pubkey, fee_recipient).await.unwrap() { + reply( + writer, + DutySafety::Invalid, + format!("fee recipient is not consistent"), + ) + .await; + error!("fee recipient is not consistent"); + return Ok(()); + } self.sign_block(writer, block, check_msg.domain_hash).await; } BlockType::Blinded => { @@ -429,6 +444,17 @@ impl MessageHandler for DvfDutyCheckHandler { return Ok(()); } }; + let fee_recipient = block.body().execution_payload().unwrap().fee_recipient(); + if self.db.check_validator_fee_recipient(check_msg.pubkey, fee_recipient).await.unwrap() { + reply( + writer, + DutySafety::Invalid, + format!("fee recipient is not consistent"), + ) + .await; + error!("fee recipient is not consistent"); + return Ok(()); + } self.sign_block(writer, block, check_msg.domain_hash).await; } }; @@ -522,6 +548,7 @@ impl DvfSigner { validator_pk: operator_committee.get_validator_pk(), operator_pks, keypair: keypair.clone(), + db: node.db.clone(), _phantom: PhantomData, }, ); @@ -590,6 +617,7 @@ impl DvfSigner { check_type, data: data.to_vec(), sign_hex: None, + pubkey: self.validator_public_key().serialize().to_vec() }; match msg.sign_digest(&self.node_secret) { Ok(sign_hex) => msg.sign_hex = Some(sign_hex), From 7c63ebec96e6f2291ff6b34c7747288efb9cf107 Mon Sep 17 00:00:00 2001 From: jiangjianlin Date: Tue, 29 Oct 2024 00:24:40 +0000 Subject: [PATCH 43/43] fix fee recipient --- src/node/db.rs | 1 + src/node/dvfcore.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/node/db.rs b/src/node/db.rs index b1899fa5..6152e4c9 100644 --- a/src/node/db.rs +++ b/src/node/db.rs @@ -1337,6 +1337,7 @@ pub fn check_validator_fee_recipient(conn: &Connection, pubkey: Vec, fee_rec #[tokio::test] async fn test_fee_recipient() { + use rand::RngCore; let mut logger = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")); logger.format_timestamp_millis(); diff --git a/src/node/dvfcore.rs b/src/node/dvfcore.rs index 8f61316e..f2b6676c 100644 --- a/src/node/dvfcore.rs +++ b/src/node/dvfcore.rs @@ -417,7 +417,7 @@ impl MessageHandler for DvfDutyCheckHandler { } }; let fee_recipient = block.body().execution_payload().unwrap().fee_recipient(); - if self.db.check_validator_fee_recipient(check_msg.pubkey, fee_recipient).await.unwrap() { + if !self.db.check_validator_fee_recipient(check_msg.pubkey, fee_recipient).await.unwrap() { reply( writer, DutySafety::Invalid, @@ -445,7 +445,7 @@ impl MessageHandler for DvfDutyCheckHandler { } }; let fee_recipient = block.body().execution_payload().unwrap().fee_recipient(); - if self.db.check_validator_fee_recipient(check_msg.pubkey, fee_recipient).await.unwrap() { + if !self.db.check_validator_fee_recipient(check_msg.pubkey, fee_recipient).await.unwrap() { reply( writer, DutySafety::Invalid,