From 73fdbfc11243156d89584a64b9c0156efae4668e Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 15 Jan 2025 16:06:39 +0000 Subject: [PATCH 01/45] remove ssv_types dependency on qbft --- Cargo.lock | 2 +- anchor/common/qbft/Cargo.toml | 1 + anchor/common/qbft/src/lib.rs | 11 ++--------- anchor/common/ssv_types/Cargo.toml | 1 - anchor/common/ssv_types/src/message.rs | 16 ++++++++++++---- anchor/qbft_manager/src/lib.rs | 11 ++++++----- 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c6794d1..7ae0db6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5619,6 +5619,7 @@ version = "0.1.0" dependencies = [ "derive_more 1.0.0", "indexmap", + "ssv_types", "tracing", "tracing-subscriber", ] @@ -6866,7 +6867,6 @@ dependencies = [ "base64 0.22.1", "derive_more 1.0.0", "openssl", - "qbft", "rusqlite", "tree_hash", "tree_hash_derive", diff --git a/anchor/common/qbft/Cargo.toml b/anchor/common/qbft/Cargo.toml index 9bab2134..d13cfe29 100644 --- a/anchor/common/qbft/Cargo.toml +++ b/anchor/common/qbft/Cargo.toml @@ -8,6 +8,7 @@ edition = { workspace = true } derive_more = { workspace = true } indexmap = { workspace = true } tracing = { workspace = true } +ssv_types = { workspace = true } [dev-dependencies] tracing-subscriber = { workspace = true } diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 8f6a7cbc..69bc736c 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -1,8 +1,5 @@ pub use config::{Config, ConfigBuilder}; -use std::cmp::Eq; use std::collections::{HashMap, HashSet}; -use std::fmt::Debug; -use std::hash::Hash; use tracing::{debug, error, warn}; pub use validation::{validate_consensus_data, ValidatedData, ValidationError}; @@ -12,6 +9,8 @@ pub use types::{ Message, OperatorId, Round, }; +use ssv_types::message::Data; + mod config; mod error; mod types; @@ -22,12 +21,6 @@ mod tests; type RoundChangeMap = HashMap>>; -pub trait Data: Debug + Clone { - type Hash: Debug + Clone + Eq + Hash; - - fn hash(&self) -> Self::Hash; -} - /// The structure that defines the Quorum Based Fault Tolerance (QBFT) instance. /// /// This builds and runs an entire QBFT process until it completes. It can complete either diff --git a/anchor/common/ssv_types/Cargo.toml b/anchor/common/ssv_types/Cargo.toml index 178d465c..33bb2ff5 100644 --- a/anchor/common/ssv_types/Cargo.toml +++ b/anchor/common/ssv_types/Cargo.toml @@ -8,7 +8,6 @@ authors = ["Sigma Prime "] base64 = { workspace = true } derive_more = { workspace = true } openssl = { workspace = true } -qbft = { workspace = true } rusqlite = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } diff --git a/anchor/common/ssv_types/src/message.rs b/anchor/common/ssv_types/src/message.rs index a60d9e6f..0b7fabee 100644 --- a/anchor/common/ssv_types/src/message.rs +++ b/anchor/common/ssv_types/src/message.rs @@ -7,10 +7,18 @@ use types::{ AggregateAndProof, BeaconBlock, BlindedBeaconBlock, Checkpoint, CommitteeIndex, EthSpec, Hash256, PublicKeyBytes, Signature, Slot, SyncCommitteeContribution, VariableList, }; +use std::fmt::Debug; +use std::hash::Hash; // todo - dear reader, this mainly serves as plain translation of the types found in the go code // there are a lot of byte[] there, and that got confusing, below should be more readable. // it needs some work to actually serialize to the same stuff on wire, and I feel like we can name // the fields better +// +pub trait Data: Debug + Clone { + type Hash: Debug + Clone + Eq + Hash; + + fn hash(&self) -> Self::Hash; +} #[derive(Clone, Debug)] pub struct SignedSsvMessage { @@ -24,7 +32,7 @@ pub struct SignedSsvMessage { pub struct SsvMessage { pub msg_type: MsgType, pub msg_id: MsgId, - pub data: Data, + pub data: SsvData, } #[derive(Clone, Debug)] @@ -34,7 +42,7 @@ pub enum MsgType { } #[derive(Clone, Debug)] -pub enum Data { +pub enum SsvData { QbftMessage(QbftMessage), PartialSignatureMessage(PartialSignatureMessage), } @@ -80,7 +88,7 @@ pub struct ValidatorConsensusData { pub data_ssz: Box>, } -impl qbft::Data for ValidatorConsensusData { +impl Data for ValidatorConsensusData { type Hash = Hash256; fn hash(&self) -> Self::Hash { @@ -181,7 +189,7 @@ pub struct BeaconVote { pub target: Checkpoint, } -impl qbft::Data for BeaconVote { +impl Data for BeaconVote { type Hash = Hash256; fn hash(&self) -> Self::Hash { diff --git a/anchor/qbft_manager/src/lib.rs b/anchor/qbft_manager/src/lib.rs index 6f960a0a..d4429bcc 100644 --- a/anchor/qbft_manager/src/lib.rs +++ b/anchor/qbft_manager/src/lib.rs @@ -5,6 +5,7 @@ use qbft::{ Message as NetworkMessage, OperatorId as QbftOperatorId, }; use slot_clock::SlotClock; +use ssv_types::message::Data; use ssv_types::message::{BeaconVote, ValidatorConsensusData}; use ssv_types::{Cluster, ClusterId, OperatorId}; use std::fmt::Debug; @@ -47,13 +48,13 @@ pub enum ValidatorDutyKind { } #[derive(Debug)] -pub struct QbftMessage { +pub struct QbftMessage { pub kind: QbftMessageKind, pub drop_on_finish: DropOnFinish, } #[derive(Debug)] -pub enum QbftMessageKind { +pub enum QbftMessageKind { Initialize { initial: D, config: qbft::Config, @@ -169,7 +170,7 @@ impl QbftManager { } pub trait QbftDecidable: - qbft::Data + Send + 'static + Data + Send + 'static { type Id: Hash + Eq + Send; @@ -220,7 +221,7 @@ impl QbftDecidable for BeaconVote { } } -enum QbftInstance)> { +enum QbftInstance)> { Uninitialized { // todo: proooobably limit this message_buffer: Vec>, @@ -235,7 +236,7 @@ enum QbftInstance)> { }, } -async fn qbft_instance(mut rx: UnboundedReceiver>) { +async fn qbft_instance(mut rx: UnboundedReceiver>) { let mut instance = QbftInstance::Uninitialized { message_buffer: Vec::new(), }; From 7f42ade89a0da96091b57e3e1329a04e9d4241a4 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 15 Jan 2025 18:19:53 +0000 Subject: [PATCH 02/45] fmt and update --- Cargo.lock | 82 +++++++++++++------------- anchor/common/ssv_types/src/message.rs | 6 +- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ae0db6c..e74c9b28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "alloy-core" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0713007d14d88a6edb8e248cddab783b698dbb954a28b8eee4bab21cfb7e578" +checksum = "648275bb59110f88cc5fa9a176845e52a554ebfebac2d21220bcda8c9220f797" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -184,9 +184,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44e3b98c37b3218924cd1d2a8570666b89662be54e5b182643855f783ea68b33" +checksum = "bc9138f4f0912793642d453523c3116bd5d9e11de73b70177aa7cb3e94b98ad2" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -253,9 +253,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "731ea743b3d843bc657e120fb1d1e9cc94f5dab8107e35a82125a63e6420a102" +checksum = "24acd2f5ba97c7a320e67217274bc81fe3c3174b8e6144ec875d9d54e760e278" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -315,9 +315,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788bb18e8f61d5d9340b52143f27771daf7e1dccbaf2741621d2493f9debf52e" +checksum = "ec878088ec6283ce1e90d280316aadd3d6ce3de06ff63d68953c855e7e447e92" dependencies = [ "alloy-rlp", "arbitrary", @@ -507,9 +507,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07b74d48661ab2e4b50bb5950d74dbff5e61dd8ed03bb822281b706d54ebacb" +checksum = "8d039d267aa5cbb7732fa6ce1fd9b5e9e29368f580f80ba9d7a8450c794de4b2" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -521,9 +521,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19cc9c7f20b90f9be1a8f71a3d8e283a43745137b0837b1a1cb13159d37cad72" +checksum = "620ae5eee30ee7216a38027dec34e0585c55099f827f92f50d11e3d2d3a4a954" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", @@ -540,9 +540,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-input" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713b7e6dfe1cb2f55c80fb05fd22ed085a1b4e48217611365ed0ae598a74c6ac" +checksum = "ad9f7d057e00f8c5994e4ff4492b76532c51ead39353aa2ed63f8c50c0f4d52e" dependencies = [ "alloy-json-abi", "const-hex", @@ -557,9 +557,9 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eda2711ab2e1fb517fc6e2ffa9728c9a232e296d16810810e6957b781a1b8bc" +checksum = "74e60b084fe1aef8acecda2743ff2d93c18ff3eb67a2d3b12f62582a1e66ef5e" dependencies = [ "serde", "winnow", @@ -567,9 +567,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b478bc9c0c4737a04cd976accde4df7eba0bdc0d90ad6ff43d58bc93cf79c1" +checksum = "c1382302752cd751efd275f4d6ef65877ddf61e0e6f5ac84ef4302b79a33a31a" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -1068,9 +1068,9 @@ dependencies = [ [[package]] name = "auto_impl" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" +checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73" dependencies = [ "proc-macro2", "quote", @@ -1236,9 +1236,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bitvec" @@ -1973,15 +1973,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" [[package]] name = "data-encoding-macro" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1559b6cba622276d6d63706db152618eeb15b89b3e4041446b05876e352e639" +checksum = "5b16d9d0d88a5273d830dac8b78ceb217ffc9b1d5404e5597a3542515329405b" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -1989,12 +1989,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332d754c0af53bc87c108fed664d121ecf59207ec4196041f04d6ab9002ad33f" +checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.96", ] [[package]] @@ -4378,7 +4378,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "libc", ] @@ -4536,9 +4536,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "logging" @@ -5080,7 +5080,7 @@ version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "cfg-if", "foreign-types", "libc", @@ -5551,7 +5551,7 @@ checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.7.0", + "bitflags 2.8.0", "lazy_static", "num-traits", "rand", @@ -5833,7 +5833,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", ] [[package]] @@ -6216,7 +6216,7 @@ version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -6423,7 +6423,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "core-foundation", "core-foundation-sys", "libc", @@ -7013,9 +7013,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e89d8bf2768d277f40573c83a02a099e96d96dd3104e13ea676194e61ac4b0" +checksum = "b84e4d83a0a6704561302b917a932484e1cae2d8c6354c64be8b7bac1c1fe057" dependencies = [ "paste", "proc-macro2", @@ -7066,7 +7066,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "core-foundation", "system-configuration-sys 0.6.0", ] @@ -7442,7 +7442,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.8.0", "bytes", "http 1.2.0", "pin-project-lite", diff --git a/anchor/common/ssv_types/src/message.rs b/anchor/common/ssv_types/src/message.rs index 0b7fabee..0ef1322a 100644 --- a/anchor/common/ssv_types/src/message.rs +++ b/anchor/common/ssv_types/src/message.rs @@ -1,5 +1,7 @@ use crate::msgid::MsgId; use crate::{OperatorId, ValidatorIndex}; +use std::fmt::Debug; +use std::hash::Hash; use tree_hash::{PackedEncoding, TreeHash, TreeHashType}; use tree_hash_derive::TreeHash; use types::typenum::U13; @@ -7,13 +9,11 @@ use types::{ AggregateAndProof, BeaconBlock, BlindedBeaconBlock, Checkpoint, CommitteeIndex, EthSpec, Hash256, PublicKeyBytes, Signature, Slot, SyncCommitteeContribution, VariableList, }; -use std::fmt::Debug; -use std::hash::Hash; // todo - dear reader, this mainly serves as plain translation of the types found in the go code // there are a lot of byte[] there, and that got confusing, below should be more readable. // it needs some work to actually serialize to the same stuff on wire, and I feel like we can name // the fields better -// + pub trait Data: Debug + Clone { type Hash: Debug + Clone + Eq + Hash; From b01b25a777803fe3118df6ae7c963e7ba37eccdc Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 15 Jan 2025 18:25:30 +0000 Subject: [PATCH 03/45] sort --- anchor/common/qbft/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anchor/common/qbft/Cargo.toml b/anchor/common/qbft/Cargo.toml index d13cfe29..d45345c9 100644 --- a/anchor/common/qbft/Cargo.toml +++ b/anchor/common/qbft/Cargo.toml @@ -7,8 +7,8 @@ edition = { workspace = true } [dependencies] derive_more = { workspace = true } indexmap = { workspace = true } -tracing = { workspace = true } ssv_types = { workspace = true } +tracing = { workspace = true } [dev-dependencies] tracing-subscriber = { workspace = true } From c7dd3a1097bf29f0f6eedd01c689c4aa45db8b0f Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Fri, 17 Jan 2025 21:36:01 +0000 Subject: [PATCH 04/45] bubblegum v1 message change --- Cargo.lock | 4 + Cargo.toml | 2 + anchor/common/qbft/Cargo.toml | 1 + anchor/common/qbft/src/config.rs | 3 +- anchor/common/qbft/src/lib.rs | 679 +++++++++--------- anchor/common/qbft/src/msg_container.rs | 79 ++ .../qbft/src/{types.rs => qbft_types.rs} | 50 +- anchor/common/qbft/src/tests.rs | 4 +- anchor/common/qbft/src/validation.rs | 6 +- anchor/common/ssv_types/Cargo.toml | 2 + anchor/common/ssv_types/src/message.rs | 140 +++- anchor/common/ssv_types/src/msgid.rs | 2 +- anchor/qbft_manager/src/lib.rs | 115 ++- anchor/validator_store/Cargo.toml | 1 + anchor/validator_store/src/lib.rs | 29 +- 15 files changed, 693 insertions(+), 424 deletions(-) create mode 100644 anchor/common/qbft/src/msg_container.rs rename anchor/common/qbft/src/{types.rs => qbft_types.rs} (76%) diff --git a/Cargo.lock b/Cargo.lock index e74c9b28..013afcf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -657,6 +657,7 @@ dependencies = [ "beacon_node_fallback", "dashmap", "eth2", + "ethereum_ssz", "futures", "parking_lot", "qbft", @@ -5622,6 +5623,7 @@ dependencies = [ "ssv_types", "tracing", "tracing-subscriber", + "types", ] [[package]] @@ -6866,6 +6868,8 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "derive_more 1.0.0", + "ethereum_ssz", + "ethereum_ssz_derive", "openssl", "rusqlite", "tree_hash", diff --git a/Cargo.toml b/Cargo.toml index 9d729a3e..442272cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,8 @@ validator_metrics = { git = "https://github.com/sigp/lighthouse", branch = "anch validator_services = { git = "https://github.com/sigp/lighthouse", branch = "anchor" } validator_store = { git = "https://github.com/sigp/lighthouse", branch = "anchor" } version = { path = "anchor/common/version" } +ethereum_ssz = "0.7" +ethereum_ssz_derive = "0.7.0" [profile.maxperf] inherits = "release" diff --git a/anchor/common/qbft/Cargo.toml b/anchor/common/qbft/Cargo.toml index d45345c9..b817ce3c 100644 --- a/anchor/common/qbft/Cargo.toml +++ b/anchor/common/qbft/Cargo.toml @@ -9,6 +9,7 @@ derive_more = { workspace = true } indexmap = { workspace = true } ssv_types = { workspace = true } tracing = { workspace = true } +types = { workspace = true } [dev-dependencies] tracing-subscriber = { workspace = true } diff --git a/anchor/common/qbft/src/config.rs b/anchor/common/qbft/src/config.rs index f8b8c94c..3ff7b025 100644 --- a/anchor/common/qbft/src/config.rs +++ b/anchor/common/qbft/src/config.rs @@ -1,5 +1,6 @@ use super::error::ConfigBuilderError; -use crate::types::{DefaultLeaderFunction, InstanceHeight, LeaderFunction, OperatorId, Round}; +use crate::qbft_types::{DefaultLeaderFunction, InstanceHeight, LeaderFunction, Round}; +use ssv_types::OperatorId; use indexmap::IndexSet; use std::fmt::Debug; use std::time::Duration; diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 69bc736c..ad27f2b7 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -1,26 +1,33 @@ -pub use config::{Config, ConfigBuilder}; -use std::collections::{HashMap, HashSet}; +use crate::msg_container::MessageContainer; +use ssv_types::message::{ + Data, MsgType, QbftMessage, QbftMessageType, SsvMessage, UnsignedSsvMessage, +}; +use ssv_types::msgid::MsgId; +use ssv_types::OperatorId; +use std::collections::HashMap; use tracing::{debug, error, warn}; -pub use validation::{validate_consensus_data, ValidatedData, ValidationError}; +use types::Hash256; +// Re-Exports for Manager +pub use config::{Config, ConfigBuilder}; pub use error::ConfigBuilderError; -pub use types::{ +pub use qbft_types::Message; +pub use qbft_types::WrappedQbftMessage; +pub use qbft_types::{ Completed, ConsensusData, DefaultLeaderFunction, InstanceHeight, InstanceState, LeaderFunction, - Message, OperatorId, Round, + Round, }; - -use ssv_types::message::Data; +pub use validation::{validate_consensus_data, ValidatedData, ValidationError}; mod config; mod error; -mod types; +mod msg_container; +mod qbft_types; mod validation; #[cfg(test)] mod tests; -type RoundChangeMap = HashMap>>; - /// The structure that defines the Quorum Based Fault Tolerance (QBFT) instance. /// /// This builds and runs an entire QBFT process until it completes. It can complete either @@ -30,82 +37,91 @@ pub struct Qbft where F: LeaderFunction + Clone, D: Data, - S: FnMut(Message), + S: FnMut(Message), { /// The initial configuration used to establish this instance of QBFT. config: Config, - /// Initial data that we will propose if we are the leader. - start_data: D::Hash, + /// The identification of this QBFT instance + identifier: MsgId, /// The instance height acts as an ID for the current instance and helps distinguish it from /// other instances. instance_height: InstanceHeight, + + /// Hash of the start data + start_data_hash: D::Hash, + /// Initial data that we will propose if we are the leader. + start_data: D, + /// All of the data that we have seen + data: HashMap, /// The current round this instance state is in.a current_round: Round, - /// All the data - data: HashMap>, - /// If we have come to consensus in a previous round this is set here. - past_consensus: HashMap, - /// The messages received this round that we have collected to reach quorum. - prepare_messages: HashMap>>, - commit_messages: HashMap>>, - /// Stores the round change messages. The second hashmap stores optional past consensus - /// data for each round change message. - round_change_messages: HashMap>, - send_message: S, /// The current state of the instance state: InstanceState, - /// The completed value, if any + /// If this QBFT instance has been completed, the completed value completed: Option>, + + // Message containers + propose_container: MessageContainer, + prepare_container: MessageContainer, + commit_container: MessageContainer, + round_change_container: MessageContainer, + + // Current round state + proposal_accepted_for_current_round: Option, + last_prepared_round: Option, + last_prepared_value: Option, + + // Network sender + send_message: S, } impl Qbft where F: LeaderFunction + Clone, - D: Data, - S: FnMut(Message), + D: Data, + S: FnMut(Message), { - pub fn new(config: Config, start_data: ValidatedData, send_message: S) -> Self { - let estimated_map_size = config.committee_members().len(); - - let mut data = HashMap::with_capacity(2); - let start_data_hash = start_data.data.hash(); - data.insert(start_data_hash.clone(), start_data); + // Construct a new QBFT Instance and start the first round + pub fn new(config: Config, start_data: D, send_message: S) -> Self { + let instance_height = *config.instance_height(); + let current_round = config.round(); + let quorum_size = config.quorum_size(); let mut qbft = Qbft { - current_round: config.round(), - instance_height: *config.instance_height(), config, - start_data: start_data_hash, - data, - past_consensus: HashMap::with_capacity(2), - prepare_messages: HashMap::with_capacity(estimated_map_size), - commit_messages: HashMap::with_capacity(estimated_map_size), - round_change_messages: HashMap::with_capacity(estimated_map_size), - send_message, + identifier: MsgId([0; 56]), + instance_height, + + start_data_hash: start_data.hash(), + start_data, + data: HashMap::new(), + current_round, state: InstanceState::AwaitingProposal, completed: None, + + propose_container: MessageContainer::new(quorum_size), + prepare_container: MessageContainer::new(quorum_size), + commit_container: MessageContainer::new(quorum_size), + round_change_container: MessageContainer::new(quorum_size), + + proposal_accepted_for_current_round: None, + last_prepared_round: None, + last_prepared_value: None, + + send_message, }; qbft.start_round(); qbft } pub fn start_data_hash(&self) -> &D::Hash { - &self.start_data + &self.start_data_hash } pub fn config(&self) -> &Config { &self.config } - /// Once we have achieved consensus on a PREPARE round, we add the data to mapping to match - /// against later. - fn insert_consensus(&mut self, round: Round, hash: D::Hash) { - debug!(round = *round, ?hash, "Reached prepare consensus"); - if let Some(past_data) = self.past_consensus.insert(round, hash.clone()) { - warn!(round = *round, ?hash, past_data = ?past_data, "Adding duplicate consensus data"); - } - } - /// Shifts this instance into a new round> fn set_round(&mut self, new_round: Round) { self.current_round.set(new_round); @@ -127,6 +143,53 @@ where self.config.committee_members().contains(operator_id) } + // Perform base QBFT relevant message verification. This verfiication is applicable to all QBFT + // message types + fn validate_message(&self, wrapped_msg: &WrappedQbftMessage) -> bool { + // Validate the wrapped message. This will validate the SignedSsvMessage and the QbftMessage + if !wrapped_msg.validate() { + warn!("Message validation unsuccessful"); + return false; + } + + // Ensure that this message is for the correct round + let current_round = self.current_round.get(); + if (wrapped_msg.qbft_message.round < current_round as u64) + || (current_round > self.config.max_rounds()) + { + warn!( + propose_round = wrapped_msg.qbft_message.round, + current_round = *self.current_round, + "Message received for a invalid round" + ); + return false; + } + + // Make sure there is only one signer + if wrapped_msg.signed_message.operator_ids.len() != 1 { + warn!( + num_signers = wrapped_msg.signed_message.operator_ids.len(), + "Propose message only allows one signer" + ); + return false; + } + + // Make sure we are at the correct instance height + if wrapped_msg.qbft_message.height != *self.instance_height as u64 { + warn!( + expected_instance = *self.instance_height, + "Message received for the wrong instance" + ); + return false; + } + + // Verify full data integrity + // TODO!(). Compare the roots of the instance data and the message data + + // Success! Message is well formed + true + } + /// Justify the round change quorum /// In order to justify a round change quorum, we find the maximum round of the quorum set that /// had achieved a past consensus. If we have also seen consensus on this round for the @@ -134,7 +197,8 @@ where /// If there is no past consensus data in the round change quorum or we disagree with quorum set /// this function will return None, and we obtain the data as if we were beginning this /// instance. - fn justify_round_change_quorum(&self) -> Option<&D::Hash> { + fn justify_round_change_quorum(&self) -> Option<&D> { + /* // If we have messages for the current round if let Some(new_round_messages) = self.round_change_messages.get(&self.current_round) { // If we have a quorum @@ -157,6 +221,7 @@ where } } } + */ None } @@ -164,10 +229,6 @@ where fn start_round(&mut self) { debug!(round = *self.current_round, "Starting new round"); - // Remove round change messages that would be for previous rounds - self.round_change_messages - .retain(|&round, _value| round >= self.current_round); - // Initialise the instance state for the round self.state = InstanceState::AwaitingProposal; @@ -175,8 +236,10 @@ where if self.check_leader(&self.config.operator_id()) { // We are the leader debug!("Current leader"); - // Check justification of round change quorum - let hash = if let Some(validated_data) = self.justify_round_change_quorum() { + + // Check justification of round change quorum. If there is a justification, we will use + // that data. Otherwise, use the initial state data + let data = if let Some(validated_data) = self.justify_round_change_quorum() { debug!( old_data = ?validated_data, "Using consensus data from a previous round"); @@ -185,240 +248,175 @@ where debug!("Using initialised data"); &self.start_data }; - if let Some(data) = self.data.get(hash).cloned() { - self.send_proposal(data); - } else { - error!("Unable to find data for known hash") - } - } - } - /// message must be authenticated by the caller! - pub fn receive(&mut self, msg: Message) { - match msg { - Message::Propose(operator_id, consensus_data) => { - self.received_propose(operator_id, consensus_data); - } - Message::Prepare(operator_id, consensus_data) => { - self.received_prepare(operator_id, consensus_data); - } - Message::Commit(operator_id, consensus_data) => { - self.received_commit(operator_id, consensus_data); - } - Message::RoundChange(operator_id, round, consensus_data) => { - self.received_round_change(operator_id, round, consensus_data); - } + // Send the initial proposal + self.send_proposal(data.clone()); } } - /// We have received a proposal message - fn received_propose(&mut self, operator_id: OperatorId, consensus_data: ConsensusData) { - // Check if proposal is from the leader we expect - if !self.check_leader(&operator_id) { - warn!(from = *operator_id, "PROPOSE message from non-leader"); + // Receive a new message from the network + pub fn receive(&mut self, wrapped_msg: WrappedQbftMessage) { + // Perform base qbft releveant verification on the message + if !self.validate_message(&wrapped_msg) { return; } + + // We know where is only one signer, so the first (and only) operator in the signed message + // is the sender + let operator_id = wrapped_msg + .signed_message + .operator_ids + .first() + .expect("Confirmed to exist in validation"); + // Check that this operator is in our committee - if !self.check_committee(&operator_id) { + if !self.check_committee(operator_id) { warn!( - from = *operator_id, + from = ?operator_id, "PROPOSE message from non-committee operator" ); return; } - // Check that we are awaiting a proposal + let msg_round: Round = wrapped_msg.qbft_message.round.into(); + + // All basic verification successful! Dispatch to the correct handler + match wrapped_msg.qbft_message.qbft_message_type { + QbftMessageType::Proposal => { + self.received_propose(*operator_id, msg_round, wrapped_msg) + } + QbftMessageType::Prepare => self.received_prepare(*operator_id, msg_round, wrapped_msg), + QbftMessageType::Commit => self.received_commit(*operator_id, msg_round, wrapped_msg), + QbftMessageType::RoundChange => { + self.received_round_change(*operator_id, msg_round, wrapped_msg) + } + } + } + + // We have received a new Proposal messaage + fn received_propose( + &mut self, + operator_id: OperatorId, + round: Round, + wrapped_msg: WrappedQbftMessage, + ) { + // Make sure that we are actually waiting for a proposal if !matches!(self.state, InstanceState::AwaitingProposal) { - warn!(from=*operator_id, ?self.state, "PROPOSE message while in invalid state"); + warn!(from=?operator_id, ?self.state, "PROPOSE message while in invalid state"); return; } - // Ensure that this message is for the correct round - if self.current_round != consensus_data.round { - warn!( - from = *operator_id, - current_round = *self.current_round, - propose_round = *consensus_data.round, - "PROPOSE message received for the wrong round" - ); + + // Check if proposal is from the leader we expect + if !self.check_leader(&operator_id) { + warn!(from = ?operator_id, "PROPOSE message from non-leader"); return; } - // Validate the data - let Ok(consensus_data) = validate_consensus_data(consensus_data) else { - warn!( - from = *operator_id, - current_round = *self.current_round, - "PROPOSE message is invalid" - ); - return; - }; + debug!(from = ?operator_id, "PROPOSE received"); - debug!(from = *operator_id, "PROPOSE received"); - - let hash = consensus_data.data.data.hash(); - // Justify the proposal by checking the round changes - if let Some(justified_data) = self.justify_round_change_quorum() { - if *justified_data != hash { - // The data doesn't match the justified value we expect. Drop the message - warn!( - from = *operator_id, - ?consensus_data, - ?justified_data, - "PROPOSE message isn't justified" - ); - return; - } + // Store the received propse message + if !self + .propose_container + .add_message(round, operator_id, &wrapped_msg) + { + warn!(from = ?operator_id, "PROPOSE message is a duplicate") + } + + // Extract and store the proposed data + // todo!() add back in the justified check + if let Some(data) = wrapped_msg.signed_message.full_data { + /* + let hash = data.hash(); + self.data.insert(hash, data); + + // Accept the proposal and move to prepare phase + self.proposal_accepted_for_current_round = Some(wrapped_msg); + self.state = InstanceState::Prepare; + + // Send prepare message + self.send_prepare(hash); + */ } - self.data.insert(hash.clone(), consensus_data.data); - self.send_prepare(hash); + + /* + // Check if we have seen anything justified + if let Some(justified_data) = self.justify_round_change_quorum() { + if *justified_data != hash { + // The data doesn't match the justified value we expect. Drop the message + warn!( + from = ?operator_id, + "PROPOSE message isn't justified" + ); + // return + } + // todo!() anything else here? + } + */ + + // Insert the data and send off a prepare signaling that we are okay with this data + // self.data.insert(hash.clone(), data); + // self.send_prepare(hash); } /// We have received a prepare message fn received_prepare( &mut self, operator_id: OperatorId, - consensus_data: ConsensusData, + round: Round, + wrapped_msg: WrappedQbftMessage, ) { - // Check that this operator is in our committee - if !self.check_committee(&operator_id) { - warn!( - from = *operator_id, - "PREPARE message from non-committee operator" - ); - return; - } - // Check that we are in the correct state if (self.state as u8) >= (InstanceState::SentRoundChange as u8) { - warn!(from=*operator_id, ?self.state, "PREPARE message while in invalid state"); + warn!(from=?operator_id, ?self.state, "PREPARE message while in invalid state"); return; } - // Ensure that this message is for the correct round - if self.current_round != consensus_data.round { - warn!( - from = *operator_id, - current_round = *self.current_round, - propose_round = *consensus_data.round, - "PREPARE message received for the wrong round" - ); - return; - } - - // Validate the data - let Ok(consensus_data) = validate_consensus_data(consensus_data) else { - warn!( - from = *operator_id, - current_round = *self.current_round, - "PREPARE message is invalid" - ); - return; - }; - - debug!(from = *operator_id, "PREPARE received"); + debug!(from = ?operator_id, "PREPARE received"); // Store the prepare message if !self - .prepare_messages - .entry(consensus_data.round) - .or_default() - .entry(consensus_data.data.data) - .or_default() - .insert(operator_id) + .prepare_container + .add_message(round, operator_id, &wrapped_msg) { - warn!(from = *operator_id, "PREPARE message is a duplicate") - }; - - // Check if we have reached quorum, if so send commit messages and store the fact that we - // have reached consensus on this quorum. - let mut update_data = None; - if let Some(prepare_messages) = self.prepare_messages.get(&self.current_round) { - // Check the quorum size - if let Some((data, operators)) = prepare_messages - .iter() - .max_by_key(|(_data, operators)| operators.len()) - { - if operators.len() >= self.config.quorum_size() - && matches!(self.state, InstanceState::Prepare) - { - // We reached quorum on this data - update_data = Some(data.clone()); - } - } + warn!(from = ?operator_id, "PREPARE message is a duplicate") } - // Send the data - if let Some(data) = update_data { - self.send_commit(data.clone()); - self.insert_consensus(self.current_round, data); + // Check if we have reached quorum, if so send the commit message + if let Some(hash) = self.prepare_container.has_quorum(round) { + if matches!(self.state, InstanceState::Prepare) { + self.send_commit(hash); + } } } - ///We have received a commit message - fn received_commit(&mut self, operator_id: OperatorId, consensus_data: ConsensusData) { - // Check that this operator is in our committee - if !self.check_committee(&operator_id) { - warn!( - from = *operator_id, - "COMMIT message from non-committee operator" - ); - return; - } - - // Check that we are awaiting a proposal + /// We have received a commit message + fn received_commit( + &mut self, + operator_id: OperatorId, + round: Round, + wrapped_msg: WrappedQbftMessage, + ) { + // Make sure that we are in the correct state if (self.state as u8) >= (InstanceState::SentRoundChange as u8) { warn!(from=*operator_id, ?self.state, "COMMIT message while in invalid state"); return; } - // Ensure that this message is for the correct round - if self.current_round != consensus_data.round { - warn!( - from = *operator_id, - current_round = *self.current_round, - propose_round = *consensus_data.round, - "COMMIT message received for the wrong round" - ); - return; - } - - // Validate the data - let Ok(consensus_data) = validate_consensus_data(consensus_data) else { - warn!( - from = *operator_id, - current_round = *self.current_round, - "COMMIT message is invalid" - ); - return; - }; - - debug!(from = *operator_id, "COMMIT received"); + debug!(from = ?operator_id, "COMMIT received"); // Store the received commit message if !self - .commit_messages - .entry(self.current_round) - .or_default() - .entry(consensus_data.data.data) - .or_default() - .insert(operator_id) + .commit_container + .add_message(round, operator_id, &wrapped_msg) { - warn!(from = *operator_id, "Received duplicate commit"); + warn!(from = ?operator_id, "COMMIT message is a duplicate") } - // Check if we have reached quorum - if let Some(commit_messages) = self.commit_messages.get(&self.current_round) { - // Check the quorum size - if let Some((data, operators)) = commit_messages - .iter() - .max_by_key(|(_data, operators)| operators.len()) - { - if operators.len() >= self.config.quorum_size() - && matches!(self.state, InstanceState::Commit) - { - self.completed = Some(Completed::Success(data.clone())); - self.state = InstanceState::Complete; - } + // Check if we have a commit quorum + if let Some(_data) = self.prepare_container.has_quorum(round) { + if matches!(self.state, InstanceState::Commit) { + //self.completed = Some(Completed::Success(data.clone())); + self.state = InstanceState::Complete; } } } @@ -428,76 +426,31 @@ where &mut self, operator_id: OperatorId, round: Round, - maybe_past_consensus_data: Option>, + wrapped_msg: WrappedQbftMessage, ) { - // Check that this operator is in our committee - if !self.check_committee(&operator_id) { - warn!( - from = *operator_id, - "ROUNDCHANGE message from non-committee operator" - ); - return; - } - - // Check that we are awaiting a proposal - // NOTE: THis is not necessary, but putting it here as these functions can be grouped for - // later + // Make sure we are in the correct state if (self.state as u8) >= (InstanceState::Complete as u8) { warn!(from=*operator_id, ?self.state, "ROUNDCHANGE message while in invalid state"); return; } - // Ensure that this message is for the correct round - if round < self.current_round || round.get() > self.config.max_rounds() { - warn!( - from = *operator_id, - current_round = *self.current_round, - propose_round = *round, - max_rounds = self.config.max_rounds(), - "ROUNDCHANGE message received for the wrong round" - ); - return; - } + debug!(from = ?operator_id, "ROUNDCHANGE received"); - // Validate the data, if it exists - /* let maybe_past_consensus_data = match maybe_past_consensus_data { - Some(consensus_data) => { - let Ok(consensus_data) = validate_consensus_data(consensus_data) else { - warn!( - from = *operator_id, - current_round = *self.current_round, - "ROUNDCHANGE message is invalid" - ); - return; - }; - Some(consensus_data) - } - None => None, - }; */ - - debug!(from = *operator_id, "ROUNDCHANGE received"); - - // Store the round change message, for the round the message references - if self - .round_change_messages - .entry(round) - .or_default() - .insert(operator_id, maybe_past_consensus_data.clone()) - .is_some() + // Store the round changed message + if !self + .round_change_container + .add_message(round, operator_id, &wrapped_msg) { - warn!(from = *operator_id, "ROUNDCHANGE duplicate request",); + warn!(from = ?operator_id, "ROUNDCHANGE message is a duplicate") } // There are two cases to check here - // 1. If we receive f+1 round change messages, we need to send our own round-change message - // 2. If we have received a quorum of round change messages, we need to start a new round + // 1. If we have received a quorum of round change messages, we need to start a new round + // 2. If we receive f+1 round change messages, we need to send our own round-change message // Check if we have any messages for the suggested round - if let Some(new_round_messages) = self.round_change_messages.get(&round) { - // Check the quorum size - if new_round_messages.len() >= self.config.quorum_size() - && matches!(self.state, InstanceState::SentRoundChange) - { + if let Some(hash) = self.round_change_container.has_quorum(round) { + if matches!(self.state, InstanceState::SentRoundChange) { // 1. If we have reached a quorum for this round, advance to that round. debug!( operator_id = ?self.config.operator_id(), @@ -505,15 +458,19 @@ where "Round change quorum reached" ); self.set_round(round); - } else if new_round_messages.len() > self.config.get_f() - && !(matches!(self.state, InstanceState::SentRoundChange)) - { - // 2. We have seen 2f + 1 messtages for this round. - self.send_round_change(round); + } else { + let num_messages_for_round = + self.round_change_container.num_messages_for_round(round); + if num_messages_for_round > self.config.get_f() + && !(matches!(self.state, InstanceState::SentRoundChange)) + { + self.send_round_change(hash); + } } } } + // End the current round and move to the next one, if possible. pub fn end_round(&mut self) { debug!(round = *self.current_round, "Incrementing round"); let Some(next_round) = self.current_round.next() else { @@ -526,68 +483,92 @@ where self.completed = Some(Completed::TimedOut); return; } - self.send_round_change(next_round); + + // Get the data to send with the round change? + //self.send_round_change(next_round); // Start a new round self.set_round(next_round); } - // Send message functions - fn send_proposal(&mut self, data: ValidatedData) { - let consensus_data = ConsensusData { - round: self.current_round, - data: data.data, + // Construct a new unsigned message. This will be passed to the processor to be signed and then + // send on the network + // Helper: Create unsigned message + fn new_unsigned_message( + &self, + msg_type: QbftMessageType, + data_hash: D::Hash, + ) -> UnsignedSsvMessage { + // Create the QBFT message + let _qbft_mesage = QbftMessage { + qbft_message_type: msg_type, + height: *self.instance_height as u64, + round: self.current_round.get() as u64, + identifier: self.identifier.clone(), + root: data_hash as Hash256, + data_round: 0, // Not used in MVP + round_change_justification: vec![], // Empty for MVP + prepare_justification: vec![], // Empty for MVP }; - let operator_id = self.config.operator_id(); - (self.send_message)(Message::Propose(operator_id, consensus_data.clone())); - self.received_propose(operator_id, consensus_data); - } - fn send_prepare(&mut self, data: D::Hash) { - self.state = InstanceState::Prepare; - debug!(?self.state, "State Changed"); - let consensus_data = ConsensusData { - round: self.current_round, - data, + let _ssv_message = SsvMessage { + msg_type: MsgType::SsvConsensusMsgType, + msg_id: self.identifier.clone(), + data: vec![], // this should by qbft_serialized }; - let operator_id = self.config.operator_id(); - (self.send_message)(Message::Prepare(operator_id, consensus_data.clone())); - self.received_prepare(operator_id, consensus_data); + + // Wrap in unsigned SSV message + UnsignedSsvMessage { + ssv_message: SsvMessage { + msg_type: MsgType::SsvConsensusMsgType, + msg_id: self.identifier.clone(), // This should be properly generated + data: vec![], // this should be ssv_message.serialize() + }, + full_data: None, //Some(self.data.get(&data_hash).unwrap().clone()), // Include the actual data + } } - fn send_commit(&mut self, data: D::Hash) { - self.state = InstanceState::Commit; - debug!(?self.state, "State changed"); - let consensus_data = ConsensusData { - round: self.current_round, - data, - }; + // Send a new qbft proposal message + fn send_proposal(&mut self, data: D) { + // Store the data we're proposing + let hash = data.hash(); + self.data.insert(hash, data.clone()); + + // Construct a unsigned proposal + let unsigned_msg = self.new_unsigned_message(QbftMessageType::Proposal, hash); + let operator_id = self.config.operator_id(); - (self.send_message)(Message::Commit(operator_id, consensus_data.clone())); - self.received_commit(operator_id, consensus_data) + (self.send_message)(Message::Propose(operator_id, unsigned_msg.clone())); } - fn send_round_change(&mut self, round: Round) { - self.state = InstanceState::SentRoundChange; - debug!(state = ?self.state, "New State"); + // Send a new qbft prepare message + fn send_prepare(&mut self, data_hash: D::Hash) { + // Only send prepare if we've seen this data + if !self.data.contains_key(&data_hash) { + warn!("Attempted to prepare unknown data"); + return; + } - // Get the maximum round we have come to consensus on - let best_consensus = self - .past_consensus - .iter() - .max_by_key(|(&round, _v)| *round) - .map(|(&round, data)| ConsensusData { - round, - data: data.clone(), - }); + // Construct unsigned prepare + let unsigned_msg = self.new_unsigned_message(QbftMessageType::Prepare, data_hash); let operator_id = self.config.operator_id(); - (self.send_message)(Message::RoundChange( - operator_id, - round, - best_consensus.clone(), - )); + (self.send_message)(Message::Prepare(operator_id, unsigned_msg.clone())); + } + + fn send_commit(&mut self, data_hash: D::Hash) { + // Construct unsigned commit + let unsigned_msg = self.new_unsigned_message(QbftMessageType::Commit, data_hash); - self.received_round_change(operator_id, round, best_consensus); + let operator_id = self.config.operator_id(); + (self.send_message)(Message::Commit(operator_id, unsigned_msg.clone())); + } + + fn send_round_change(&mut self, data_hash: D::Hash) { + // Construct unsigned round change + let unsigned_msg = self.new_unsigned_message(QbftMessageType::RoundChange, data_hash); + + let operator_id = self.config.operator_id(); + (self.send_message)(Message::RoundChange(operator_id, unsigned_msg.clone())); } pub fn completed(&self) -> Option> { @@ -600,7 +581,7 @@ where if data.is_none() { error!("could not find finished data"); } - data.map(|data| Completed::Success(data.data)) + data.map(|data| Completed::Success(data)) } }) } diff --git a/anchor/common/qbft/src/msg_container.rs b/anchor/common/qbft/src/msg_container.rs new file mode 100644 index 00000000..fd0d38cd --- /dev/null +++ b/anchor/common/qbft/src/msg_container.rs @@ -0,0 +1,79 @@ +use crate::Round; +use ssv_types::message::Data; +use ssv_types::OperatorId; +use std::collections::{HashMap, HashSet}; +use types::Hash256; + +/// Message container with strong typing and validation +#[derive(Default)] +pub struct MessageContainer { + /// Messages indexed by round and then by sender + messages: HashMap>, + /// Track unique values per round + values_by_round: HashMap>, + /// The quorum size for the qbft instance + quorum_size: usize, +} + +impl> MessageContainer { + pub fn new(quorum_size: usize) -> Self { + Self { + quorum_size, + messages: HashMap::new(), + values_by_round: HashMap::new(), + } + } + + pub fn add_message(&mut self, round: Round, sender: OperatorId, msg: &M) -> bool { + // Check if we already have a message from this sender for this round + if self + .messages + .get(&round) + .and_then(|msgs| msgs.get(&sender)) + .is_some() + { + return false; // Duplicate + } + + // Add message and track its value + self.messages + .entry(round) + .or_default() + .insert(sender, msg.clone()); + + self.values_by_round + .entry(round) + .or_default() + .insert(msg.hash()); + + true + } + + pub fn has_quorum(&self, round: Round) -> Option { + let round_messages = self.messages.get(&round)?; + + // Count occurrences of each value + let mut value_counts: HashMap = HashMap::new(); + for msg in round_messages.values() { + *value_counts.entry(msg.hash()).or_default() += 1; + } + + // Find any value that has reached quorum + value_counts + .into_iter() + .find(|(_, count)| *count >= self.quorum_size) + .map(|(value, _)| value) + } + + // Count messages for this round + pub fn num_messages_for_round(&self, round: Round) -> usize{ + self.messages.get(&round).map(|msgs| msgs.len()).unwrap_or(0) + } + + pub fn get_messages_for_value(&self, round: Round, value: Hash256) -> Vec<&M> { + self.messages + .get(&round) + .map(|msgs| msgs.values().filter(|msg| msg.hash() == value).collect()) + .unwrap_or_default() + } +} diff --git a/anchor/common/qbft/src/types.rs b/anchor/common/qbft/src/qbft_types.rs similarity index 76% rename from anchor/common/qbft/src/types.rs rename to anchor/common/qbft/src/qbft_types.rs index 87d329ee..fe306ec4 100644 --- a/anchor/common/qbft/src/types.rs +++ b/anchor/common/qbft/src/qbft_types.rs @@ -1,8 +1,10 @@ //! A collection of types used by the QBFT modules use crate::validation::ValidatedData; -use crate::Data; use derive_more::{Deref, From}; use indexmap::IndexSet; +use ssv_types::message::{Data, QbftMessage, SignedSsvMessage, UnsignedSsvMessage}; +use ssv_types::OperatorId; +use types::Hash256; use std::cmp::Eq; use std::fmt::Debug; use std::hash::Hash; @@ -40,10 +42,42 @@ impl LeaderFunction for DefaultLeaderFunction { } } +// Wrapped qbft message is a wrapper around both a signed ssv message, and the underlying qbft +// message. +#[derive(Debug, Clone)] +pub struct WrappedQbftMessage { + pub signed_message: SignedSsvMessage, + pub qbft_message: QbftMessage, +} + + +impl WrappedQbftMessage { + // Validate that the message is well formed + pub fn validate(&self) -> bool{ + self.signed_message.validate() && self.qbft_message.validate() + } +} + +impl Data for WrappedQbftMessage { + type Hash = Hash256; + + fn hash(&self) -> Self::Hash { + self.qbft_message.root + } + +} + /// This represents an individual round, these change on regular time intervals #[derive(Clone, Copy, Debug, Deref, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Round(NonZeroUsize); +impl From for Round { + fn from(round: u64) -> Round { + todo!() + } +} + + impl Default for Round { fn default() -> Self { // rounds are indexed starting at 1 @@ -64,8 +98,8 @@ impl Round { } /// The operator that is participating in the consensus instance. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, From, Deref)] -pub struct OperatorId(usize); +//#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, From, Deref)] +//pub struct OperatorId(u64); /// The instance height behaves like an "ID" for the QBFT instance. It is used to uniquely identify /// different instances, that have the same operator id. @@ -90,15 +124,15 @@ pub enum InstanceState { /// Generic Data trait to allow for future implementations of the QBFT module // Messages that can be received from the message_in channel #[derive(Debug, Clone)] -pub enum Message { +pub enum Message { /// A PROPOSE message to be sent on the network. - Propose(OperatorId, ConsensusData), + Propose(OperatorId, UnsignedSsvMessage), /// A PREPARE message to be sent on the network. - Prepare(OperatorId, ConsensusData), + Prepare(OperatorId, UnsignedSsvMessage), /// A commit message to be sent on the network. - Commit(OperatorId, ConsensusData), + Commit(OperatorId, UnsignedSsvMessage), /// Round change message received from network - RoundChange(OperatorId, Round, Option>), + RoundChange(OperatorId, UnsignedSsvMessage), } /// Type definitions for the allowable messages diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index 2d0361d5..400dfc72 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -8,8 +8,9 @@ use std::cell::RefCell; use std::collections::VecDeque; use std::rc::Rc; use tracing_subscriber::filter::EnvFilter; -use types::DefaultLeaderFunction; +use qbft_types::DefaultLeaderFunction; +/* // HELPER FUNCTIONS FOR TESTS /// Enable debug logging for tests @@ -134,3 +135,4 @@ fn test_basic_committee() { // Wait until consensus is reached or all the instances have ended test_instance.wait_until_end(); } +*/ diff --git a/anchor/common/qbft/src/validation.rs b/anchor/common/qbft/src/validation.rs index d6cbdd9c..7f3b082c 100644 --- a/anchor/common/qbft/src/validation.rs +++ b/anchor/common/qbft/src/validation.rs @@ -1,6 +1,7 @@ //! Validation for data function -use crate::types::ConsensusData; +use crate::qbft_types::ConsensusData; +use types::EthSpec; /// The list of possible validation errors that can occur #[derive(Debug)] @@ -23,10 +24,13 @@ pub fn validate_data(data: D) -> Result, ValidationError> { pub fn validate_consensus_data( consensus_data: ConsensusData, ) -> Result>, ValidationError> { + todo!() + /* let round = consensus_data.round; let validated_data = validate_data(consensus_data.data)?; Ok(ConsensusData { round, data: validated_data, }) + */ } diff --git a/anchor/common/ssv_types/Cargo.toml b/anchor/common/ssv_types/Cargo.toml index 33bb2ff5..9e4c1f1b 100644 --- a/anchor/common/ssv_types/Cargo.toml +++ b/anchor/common/ssv_types/Cargo.toml @@ -12,3 +12,5 @@ rusqlite = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } types = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } diff --git a/anchor/common/ssv_types/src/message.rs b/anchor/common/ssv_types/src/message.rs index 0ef1322a..77e56ca1 100644 --- a/anchor/common/ssv_types/src/message.rs +++ b/anchor/common/ssv_types/src/message.rs @@ -1,5 +1,7 @@ use crate::msgid::MsgId; use crate::{OperatorId, ValidatorIndex}; +use ssz_derive::{Decode, Encode}; +use std::collections::HashSet; use std::fmt::Debug; use std::hash::Hash; use tree_hash::{PackedEncoding, TreeHash, TreeHashType}; @@ -21,18 +23,64 @@ pub trait Data: Debug + Clone { } #[derive(Clone, Debug)] -pub struct SignedSsvMessage { +pub struct SignedSsvMessage { pub signatures: Vec<[u8; 256]>, pub operator_ids: Vec, - pub ssv_message: SsvMessage, - pub full_data: Option>, + pub ssv_message: SsvMessage, + pub full_data: Option, // should this just be serialized??? +} + + +#[derive(Debug, Clone)] +pub struct UnsignedSsvMessage { + pub ssv_message: SsvMessage, + pub full_data: Option, +} + + +impl SignedSsvMessage { + // Validate the signed message + pub fn validate(&self) -> bool { + // OperatorID must have at least one element + if self.operator_ids.is_empty() { + return false; + } + + // Note: Len Signers & Operators will only be > 1 after commit aggregation + + // Any OperatorID must not be 0 + if self.operator_ids.iter().any(|&id| *id == 0) { + return false; + } + + // The number of signatures and OperatorIDs must be the same + if self.operator_ids.len() != self.signatures.len() { + return false; + } + + // No duplicate signers + let mut seen_ids = HashSet::with_capacity(self.operator_ids.len()); + for &id in &self.operator_ids { + if !seen_ids.insert(id) { + return false; + } + } + true + } + + pub fn get_consensus_data(&self) -> Option { + if let Some(FullData::ValidatorConsensusData(data)) = &self.full_data { + return Some(data.clone()); + } + None + } } #[derive(Clone, Debug)] -pub struct SsvMessage { +pub struct SsvMessage { pub msg_type: MsgType, pub msg_id: MsgId, - pub data: SsvData, + pub data: Vec, // Underlying type is SSVData } #[derive(Clone, Debug)] @@ -42,29 +90,41 @@ pub enum MsgType { } #[derive(Clone, Debug)] -pub enum SsvData { - QbftMessage(QbftMessage), +pub enum SsvData { + QbftMessage(QbftMessage), PartialSignatureMessage(PartialSignatureMessage), } #[derive(Clone, Debug)] -pub struct QbftMessage { +pub struct QbftMessage { pub qbft_message_type: QbftMessageType, pub height: u64, pub round: u64, pub identifier: MsgId, pub root: Hash256, - pub round_change_justification: Vec>, // always without full_data - pub prepare_justification: Vec>, // always without full_data + // The last round that obtained a prepare quorum + pub data_round: u64, + pub round_change_justification: Vec, // always without full_data + pub prepare_justification: Vec, // always without full_data } -#[derive(Clone, Debug)] +impl QbftMessage { + pub fn validate(&self) -> bool { + // todo!() what other identification? + if self.qbft_message_type > QbftMessageType::RoundChange { + return false; + } + true + } +} + +#[derive(Clone, Debug, PartialEq, PartialOrd)] pub enum QbftMessageType { - ProposalMsgType, - PrepareMsgType, - CommitMsgType, - RoundChangeMsgType, + Proposal = 0, + Prepare, + Commit, + RoundChange, } #[derive(Clone, Debug)] @@ -73,22 +133,38 @@ pub struct PartialSignatureMessage { pub signing_root: Hash256, pub signer: OperatorId, pub validator_index: ValidatorIndex, + // todo!() test this out + pub full_data: Option, } #[derive(Clone, Debug)] -pub enum FullData { - ValidatorConsensusData(ValidatorConsensusData), +pub enum FullData { + ValidatorConsensusData(ValidatorConsensusData), BeaconVote(BeaconVote), } +impl Data for FullData { + type Hash = Hash256; + + fn hash(&self) -> Self::Hash { + match self { + FullData::ValidatorConsensusData(d) => d.hash(), + FullData::BeaconVote(d) => d.hash() + } + } +} + +#[derive(Clone, Debug)] +pub struct SszBytes(pub Vec); + #[derive(Clone, Debug, TreeHash)] -pub struct ValidatorConsensusData { +pub struct ValidatorConsensusData { pub duty: ValidatorDuty, pub version: DataVersion, - pub data_ssz: Box>, + pub data_ssz: SszBytes, } -impl Data for ValidatorConsensusData { +impl Data for ValidatorConsensusData { type Hash = Hash256; fn hash(&self) -> Self::Hash { @@ -96,6 +172,25 @@ impl Data for ValidatorConsensusData { } } +impl TreeHash for SszBytes { + fn tree_hash_type() -> TreeHashType { + TreeHashType::List + } + + fn tree_hash_packed_encoding(&self) -> PackedEncoding { + todo!() + } + + fn tree_hash_packing_factor() -> usize { + 1 + } + + fn tree_hash_root(&self) -> tree_hash::Hash256 { + todo!() + //tree_hash::Hash256::from_slice(&tree_hash::merkle_root(&self.0.into(), 1)) + } +} + #[derive(Clone, Debug, TreeHash)] pub struct ValidatorDuty { pub r#type: BeaconRole, @@ -167,8 +262,9 @@ impl TreeHash for DataVersion { } } -#[derive(Clone, Debug, TreeHash)] +#[derive(Clone, Debug, TreeHash, Encode)] #[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] pub enum DataSsz { AggregateAndProof(AggregateAndProof), BlindedBeaconBlock(BlindedBeaconBlock), @@ -176,7 +272,7 @@ pub enum DataSsz { Contributions(VariableList, U13>), } -#[derive(Clone, Debug, TreeHash)] +#[derive(Clone, Debug, TreeHash, Encode)] pub struct Contribution { pub selection_proof_sig: Signature, pub contribution: SyncCommitteeContribution, diff --git a/anchor/common/ssv_types/src/msgid.rs b/anchor/common/ssv_types/src/msgid.rs index 5a9e3c83..a31c1359 100644 --- a/anchor/common/ssv_types/src/msgid.rs +++ b/anchor/common/ssv_types/src/msgid.rs @@ -30,7 +30,7 @@ pub enum Executor { } #[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct MsgId([u8; 56]); +pub struct MsgId(pub [u8; 56]); // pub for testing impl MsgId { pub fn new(domain: &Domain, role: Role, duty_executor: &Executor) -> Self { diff --git a/anchor/qbft_manager/src/lib.rs b/anchor/qbft_manager/src/lib.rs index d4429bcc..35926859 100644 --- a/anchor/qbft_manager/src/lib.rs +++ b/anchor/qbft_manager/src/lib.rs @@ -2,11 +2,12 @@ use dashmap::DashMap; use processor::{DropOnFinish, Senders, WorkItem}; use qbft::{ Completed, ConfigBuilder, ConfigBuilderError, DefaultLeaderFunction, InstanceHeight, - Message as NetworkMessage, OperatorId as QbftOperatorId, + WrappedQbftMessage, Message }; use slot_clock::SlotClock; use ssv_types::message::Data; -use ssv_types::message::{BeaconVote, ValidatorConsensusData}; +use ssv_types::message::{BeaconVote, SignedSsvMessage, ValidatorConsensusData}; +use ssv_types::OperatorId as QbftOperatorId; use ssv_types::{Cluster, ClusterId, OperatorId}; use std::fmt::Debug; use std::hash::Hash; @@ -24,15 +25,17 @@ const QBFT_INSTANCE_NAME: &str = "qbft_instance"; const QBFT_MESSAGE_NAME: &str = "qbft_message"; const QBFT_CLEANER_NAME: &str = "qbft_cleaner"; -/// number of slots to keep before the current slot +/// Number of slots to keep before the current slot const QBFT_RETAIN_SLOTS: u64 = 1; +// Unique Identifier for a Cluster and its corresponding QBFT instance #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct CommitteeInstanceId { pub committee: ClusterId, pub instance_height: InstanceHeight, } +// Unique Identifier for a validator instance #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct ValidatorInstanceId { pub validator: PublicKeyBytes, @@ -40,6 +43,7 @@ pub struct ValidatorInstanceId { pub instance_height: InstanceHeight, } +// Type of validator duty that is being voted one #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum ValidatorDutyKind { Proposal, @@ -47,35 +51,50 @@ pub enum ValidatorDutyKind { SyncCommitteeAggregator, } +// Message that is passed around the QbftManager #[derive(Debug)] -pub struct QbftMessage { +pub struct QbftMessage> { pub kind: QbftMessageKind, pub drop_on_finish: DropOnFinish, } +// Type of the QBFT Message #[derive(Debug)] -pub enum QbftMessageKind { +pub enum QbftMessageKind> { + // Initialize a new qbft instance with some initial data, + // the configuration for the instance, and a channel to send the final data on Initialize { initial: D, config: qbft::Config, on_completed: oneshot::Sender>, }, - NetworkMessage(NetworkMessage), + // A message received from the network. The network exchanges SignedSsvMessages, but after + // deserialziation we dermine the message is for the qbft instance and decode it into a wrapped + // qbft messsage consisting of the signed message and the qbft message + NetworkMessage(WrappedQbftMessage), } type Qbft = qbft::Qbft; +// Map from an identifier to a sender for the instance type Map = DashMap>>; -pub struct QbftManager { +// Top level QBFTManager structure +pub struct QbftManager { + // Senders to send work off to the central processor processor: Senders, + // OperatorID operator_id: QbftOperatorId, + // The slot clock for timing slot_clock: T, - validator_consensus_data_instances: Map>, + // All of the QBFT instances that are voting on validator consensus data + validator_consensus_data_instances: Map, + // All of the QBFT instances that are voting on beacon data beacon_vote_instances: Map, } -impl QbftManager { +impl QbftManager { + // Construct a new QBFT Manager pub fn new( processor: Senders, operator_id: OperatorId, @@ -83,12 +102,13 @@ impl QbftManager { ) -> Result, QbftError> { let manager = Arc::new(QbftManager { processor, - operator_id: (*operator_id as usize).into(), + operator_id, slot_clock, validator_consensus_data_instances: DashMap::new(), beacon_vote_instances: DashMap::new(), }); + // Start a long running task that will clean up old instances manager .processor .permitless @@ -97,28 +117,36 @@ impl QbftManager { Ok(manager) } - pub async fn decide_instance>( + // Decide a brand new qbft instance + pub async fn decide_instance>( &self, id: D::Id, initial: D, committee: &Cluster, ) -> Result, QbftError> { + // Tx/Rx pair to send and retrieve the final result let (result_sender, result_receiver) = oneshot::channel(); + + // General the qbft configuration let config = ConfigBuilder::new( self.operator_id, initial.instance_height(&id), committee .cluster_members .iter() - .map(|&m| (*m as usize).into()) + .map(|&m| m) // todo!() fixt his map .collect(), ); let config = config .with_quorum_size(committee.cluster_members.len() - committee.faulty as usize) .build()?; + + // Get or spawn a new qbft instance. This will return the sender that we can use to send + // new messages to the specific instance let sender = D::get_or_spawn_instance(self, id); self.processor.urgent_consensus.send_immediate( move |drop_on_finish: DropOnFinish| { + // A message to initialize this instance let _ = sender.send(QbftMessage { kind: QbftMessageKind::Initialize { initial, @@ -130,13 +158,16 @@ impl QbftManager { }, QBFT_MESSAGE_NAME, )?; + + // Await the final result Ok(result_receiver.await?) } - pub fn receive_data>( + // What is the purpose of this?? + pub fn receive_data>( &self, id: D::Id, - data: NetworkMessage, + data: WrappedQbftMessage, ) -> Result<(), QbftError> { let sender = D::get_or_spawn_instance(self, id); self.processor.urgent_consensus.send_immediate( @@ -151,6 +182,7 @@ impl QbftManager { Ok(()) } + // Long running cleaner that will remove instances that are no longer relevant async fn cleaner(self: Arc) { while !self.processor.permitless.is_closed() { sleep( @@ -169,21 +201,22 @@ impl QbftManager { } } -pub trait QbftDecidable: - Data + Send + 'static -{ +// Trait that describes any data that is able to be decided upon during a qbft instance +pub trait QbftDecidable: Data + Send + 'static { type Id: Hash + Eq + Send; - fn get_map(manager: &QbftManager) -> &Map; + fn get_map(manager: &QbftManager) -> &Map; fn get_or_spawn_instance( - manager: &QbftManager, + manager: &QbftManager, id: Self::Id, ) -> UnboundedSender> { let map = Self::get_map(manager); let ret = match map.entry(id) { dashmap::Entry::Occupied(entry) => entry.get().clone(), dashmap::Entry::Vacant(entry) => { + // There is not an instance running yet, store the sender and spawn a new instance + // with the reeiver let (tx, rx) = mpsc::unbounded_channel(); let tx = entry.insert(tx); let _ = manager @@ -199,9 +232,9 @@ pub trait QbftDecidable: fn instance_height(&self, id: &Self::Id) -> InstanceHeight; } -impl QbftDecidable for ValidatorConsensusData { +impl QbftDecidable for ValidatorConsensusData { type Id = ValidatorInstanceId; - fn get_map(manager: &QbftManager) -> &Map { + fn get_map(manager: &QbftManager) -> &Map { &manager.validator_consensus_data_instances } @@ -210,9 +243,9 @@ impl QbftDecidable for ValidatorConsen } } -impl QbftDecidable for BeaconVote { +impl QbftDecidable for BeaconVote { type Id = CommitteeInstanceId; - fn get_map(manager: &QbftManager) -> &Map { + fn get_map(manager: &QbftManager) -> &Map { &manager.beacon_vote_instances } @@ -221,27 +254,35 @@ impl QbftDecidable for BeaconVote { } } -enum QbftInstance)> { +// States that Qbft instance may be in +enum QbftInstance, S: FnMut(Message)> { + // The instance is uninitialized Uninitialized { // todo: proooobably limit this - message_buffer: Vec>, + // A buffer of message that are being send into the system. Qbft instace RECEIVES + // WrappedQBFTMessage, but sends out Message + message_buffer: Vec, }, + // The instance is initialized Initialized { qbft: Box>, round_end: Interval, on_completed: Vec>>, }, + // The instance has been decided Decided { value: Completed, }, } -async fn qbft_instance(mut rx: UnboundedReceiver>) { +async fn qbft_instance>(mut rx: UnboundedReceiver>) { + // Signal a new instance that is uninitialized let mut instance = QbftInstance::Uninitialized { message_buffer: Vec::new(), }; loop { + // recieve a new message for this instance let message = match &mut instance { QbftInstance::Uninitialized { .. } | QbftInstance::Decided { .. } => rx.recv().await, QbftInstance::Initialized { @@ -270,13 +311,12 @@ async fn qbft_instance(mut rx: UnboundedReceiver>) { on_completed, } => { instance = match instance { + // The instance is uninitialized and we have recieved a manager message to + // initialize it QbftInstance::Uninitialized { message_buffer } => { // todo: actually send messages somewhere - let mut instance = Box::new(Qbft::new( - config, - qbft::ValidatedData { data: initial }, - |_| {}, - )); + // Create a new instance and receive any buffered messages + let mut instance = Box::new(Qbft::new(config, initial, |_| {})); for message in message_buffer { instance.receive(message); } @@ -286,6 +326,9 @@ async fn qbft_instance(mut rx: UnboundedReceiver>) { on_completed: vec![on_completed], } } + // The instance is initialize and we received a manager message to initialize + // it, todo!() why does this happen, I think when this is trying to decide new + // data or something??? QbftInstance::Initialized { qbft, round_end, @@ -301,6 +344,8 @@ async fn qbft_instance(mut rx: UnboundedReceiver>) { on_completed: on_completed_vec, } } + // The instance has been decided! Send off the final result and mark the + // instance state as decided QbftInstance::Decided { value } => { if on_completed.send(value.clone()).is_err() { error!("could not send qbft result"); @@ -309,11 +354,15 @@ async fn qbft_instance(mut rx: UnboundedReceiver>) { } } } + // We got a new network message, this should be passed onto the instance QbftMessageKind::NetworkMessage(message) => match &mut instance { QbftInstance::Initialized { qbft: instance, .. } => { + // If the instance is already initialized, receive it in the instance right away instance.receive(message); } QbftInstance::Uninitialized { message_buffer } => { + // The instance has not been initialized yet, save it in the buffer to be + // received message_buffer.push(message); } QbftInstance::Decided { .. } => { @@ -346,6 +395,10 @@ async fn qbft_instance(mut rx: UnboundedReceiver>) { } } + + + + #[derive(Debug, Clone)] pub enum QbftError { QueueClosedError, diff --git a/anchor/validator_store/Cargo.toml b/anchor/validator_store/Cargo.toml index e0ed9732..38f0512e 100644 --- a/anchor/validator_store/Cargo.toml +++ b/anchor/validator_store/Cargo.toml @@ -18,6 +18,7 @@ signature_collector = { workspace = true } slashing_protection = { workspace = true } slot_clock = { workspace = true } ssv_types = { workspace = true } +ethereum_ssz = { workspace = true } task_executor = { workspace = true } tokio = { workspace = true, features = ["sync", "time"] } tracing = { workspace = true } diff --git a/anchor/validator_store/src/lib.rs b/anchor/validator_store/src/lib.rs index 4c12b566..a27d3107 100644 --- a/anchor/validator_store/src/lib.rs +++ b/anchor/validator_store/src/lib.rs @@ -1,5 +1,4 @@ pub mod sync_committee_service; - use dashmap::DashMap; use futures::future::join_all; use parking_lot::Mutex; @@ -11,6 +10,7 @@ use safe_arith::{ArithError, SafeArith}; use signature_collector::{CollectionError, SignatureCollectorManager, SignatureRequest}; use slashing_protection::{NotSafe, Safe, SlashingDatabase}; use slot_clock::SlotClock; +use ssv_types::message::SszBytes; use ssv_types::message::{ BeaconVote, Contribution, DataSsz, ValidatorConsensusData, ValidatorDuty, BEACON_ROLE_AGGREGATOR, BEACON_ROLE_PROPOSER, BEACON_ROLE_SYNC_COMMITTEE_CONTRIBUTION, @@ -18,7 +18,9 @@ use ssv_types::message::{ DATA_VERSION_PHASE0, DATA_VERSION_UNKNOWN, }; use ssv_types::{Cluster, OperatorId, ValidatorMetadata}; +use ssz::Encode; use std::fmt::Debug; +use std::marker::PhantomData; use std::sync::Arc; use tracing::{error, info, warn}; use types::attestation::Attestation; @@ -63,18 +65,19 @@ struct InitializedCluster { pub struct AnchorValidatorStore { clusters: DashMap, signature_collector: Arc, - qbft_manager: Arc>, + qbft_manager: Arc>, slashing_protection: SlashingDatabase, slashing_protection_last_prune: Mutex, spec: Arc, genesis_validators_root: Hash256, operator_id: OperatorId, + _eth_spec: PhantomData, } impl AnchorValidatorStore { pub fn new( signature_collector: Arc, - qbft_manager: Arc>, + qbft_manager: Arc>, slashing_protection: SlashingDatabase, spec: Arc, genesis_validators_root: Hash256, @@ -89,6 +92,7 @@ impl AnchorValidatorStore { spec, genesis_validators_root, operator_id, + _eth_spec: PhantomData, } } @@ -174,6 +178,7 @@ impl AnchorValidatorStore { } let cluster = self.cluster(validator_pubkey)?; + let wrapped_block = wrapper(block.clone()); // first, we have to get to consensus let completed = self @@ -204,7 +209,7 @@ impl AnchorValidatorStore { BeaconBlock::Deneb(_) => DATA_VERSION_DENEB, BeaconBlock::Electra(_) => DATA_VERSION_UNKNOWN, }, - data_ssz: Box::new(wrapper(block)), + data_ssz: SszBytes(wrapped_block.as_ssz_bytes()), }, &cluster.cluster, ) @@ -212,9 +217,9 @@ impl AnchorValidatorStore { .map_err(SpecificError::from)?; let data = match completed { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), - Completed::Success(data) => data, + Completed::Success(_data) => wrapped_block, }; - Ok(*data.data_ssz) + Ok(data) } async fn sign_abstract_block>( @@ -312,6 +317,7 @@ impl AnchorValidatorStore { Err(_) => return error(SpecificError::TooManySyncSubnetsToSign.into()), }; + let wrapped_contribution = DataSsz::Contributions(data); let completed = self .qbft_manager .decide_instance( @@ -333,17 +339,18 @@ impl AnchorValidatorStore { validator_sync_committee_indices: Default::default(), }, version: DATA_VERSION_PHASE0, - data_ssz: Box::new(DataSsz::Contributions(data)), + data_ssz: SszBytes(wrapped_contribution.as_ssz_bytes()), }, &cluster.cluster, ) .await; + // todo, handle this in a different way let data = match completed { Ok(Completed::Success(data)) => data, Ok(Completed::TimedOut) => return error(SpecificError::Timeout.into()), Err(err) => return error(SpecificError::QbftError(err).into()), }; - let data = match *data.data_ssz { + let data = match wrapped_contribution { DataSsz::Contributions(data) => data, _ => return error(SpecificError::InvalidQbftData.into()), }; @@ -670,6 +677,7 @@ impl ValidatorStore for AnchorValidatorStore { let message = AggregateAndProof::from_attestation(aggregator_index, aggregate, selection_proof); + let wrapped_aggregate_and_proof = DataSsz::AggregateAndProof(message.clone()); // first, we have to get to consensus let completed = self .qbft_manager @@ -693,17 +701,18 @@ impl ValidatorStore for AnchorValidatorStore { validator_sync_committee_indices: Default::default(), }, version: DATA_VERSION_PHASE0, - data_ssz: Box::new(DataSsz::AggregateAndProof(message)), + data_ssz: SszBytes(wrapped_aggregate_and_proof.as_ssz_bytes()), }, &cluster.cluster, ) .await .map_err(SpecificError::from)?; + // todo!() handle a better way let data = match completed { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, }; - let message = match *data.data_ssz { + let message = match wrapped_aggregate_and_proof { DataSsz::AggregateAndProof(message) => message, _ => return Err(Error::SpecificError(SpecificError::InvalidQbftData)), }; From 21e557d79196a8c049b8ecf36825ddd4d0606409 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Fri, 17 Jan 2025 22:13:35 +0000 Subject: [PATCH 05/45] justify quorum for round change --- anchor/common/qbft/src/lib.rs | 171 ++++++++++++++---------- anchor/common/qbft/src/msg_container.rs | 13 ++ anchor/common/ssv_types/src/message.rs | 14 +- 3 files changed, 124 insertions(+), 74 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index ad27f2b7..4230c479 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -25,6 +25,12 @@ mod msg_container; mod qbft_types; mod validation; +struct ConsensusRecord { + round: Round, + data: D, + prepare_messages: Vec, +} + #[cfg(test)] mod tests; @@ -71,6 +77,9 @@ where last_prepared_round: Option, last_prepared_value: Option, + /// Past prepare consensus that we have reached + past_consensus: HashMap>, + // Network sender send_message: S, } @@ -108,6 +117,8 @@ where last_prepared_round: None, last_prepared_value: None, + past_consensus: HashMap::new(), + send_message, }; qbft.start_round(); @@ -183,13 +194,25 @@ where return false; } - // Verify full data integrity - // TODO!(). Compare the roots of the instance data and the message data - // Success! Message is well formed true } + // Helper method to record consensus when we see it + fn record_consensus( + &mut self, + round: Round, + data: D, + prepare_messages: Vec, + ) { + let record = ConsensusRecord { + round, + data, + prepare_messages, + }; + self.past_consensus.insert(round, record); + } + /// Justify the round change quorum /// In order to justify a round change quorum, we find the maximum round of the quorum set that /// had achieved a past consensus. If we have also seen consensus on this round for the @@ -197,31 +220,41 @@ where /// If there is no past consensus data in the round change quorum or we disagree with quorum set /// this function will return None, and we obtain the data as if we were beginning this /// instance. - fn justify_round_change_quorum(&self) -> Option<&D> { - /* - // If we have messages for the current round - if let Some(new_round_messages) = self.round_change_messages.get(&self.current_round) { - // If we have a quorum - if new_round_messages.len() >= self.config.quorum_size() { - // Find the maximum round,value pair - let max_consensus_data = new_round_messages - .values() - .max_by_key(|maybe_past_consensus_data| { - maybe_past_consensus_data - .as_ref() - .map(|consensus_data| consensus_data.round) - .unwrap_or_default() - })? - .as_ref()?; - - // We a maximum, check to make sure we have seen quorum on this - let past_data = self.past_consensus.get(&max_consensus_data.round)?; - if past_data == &max_consensus_data.data { - return Some(past_data); - } + fn justify_round_change_quorum(&self) -> Option { + // Get all round change messages for the current round + let round_change_messages = self + .round_change_container + .get_messages_for_round(self.current_round); + + // If we don't have enough messages for quorum, we can't justify anything + if round_change_messages.len() < self.config.quorum_size() { + return None; + } + + // Find the highest round that any node claims reached preparation + let highest_prepared = round_change_messages + .iter() + .filter(|msg| msg.qbft_message.data_round != 0) // Only consider messages with prepared data + .max_by_key(|msg| msg.qbft_message.data_round); + + // If we found a message with prepared data + if let Some(highest_msg) = highest_prepared { + // Get the prepared data from the message + let prepared_data = highest_msg.signed_message.full_data.as_ref()?; + let prepared_round = Round::from(highest_msg.qbft_message.data_round); + + // Verify we have also seen this consensus + if let Some(our_record) = self.past_consensus.get(&prepared_round) { + // Verify the data matches what we saw + //todo!() figure out this compare + //if prepared_data == our_record.data { + // We agree with the prepared data - use it + // return Some(our_record.data.clone()); + //} } } - */ + + // No consensus found or we disagree - use initial data None } @@ -239,15 +272,9 @@ where // Check justification of round change quorum. If there is a justification, we will use // that data. Otherwise, use the initial state data - let data = if let Some(validated_data) = self.justify_round_change_quorum() { - debug!( - old_data = ?validated_data, - "Using consensus data from a previous round"); - validated_data - } else { - debug!("Using initialised data"); - &self.start_data - }; + let data = self + .justify_round_change_quorum() + .unwrap_or_else(|| self.start_data.clone()); // Send the initial proposal self.send_proposal(data.clone()); @@ -312,6 +339,29 @@ where return; } + // Round change justification validation for rounds after the first + if round > Round::default() { + //self.validate_round_change_justification(); + } + + // Validate the prepare justifications if they exist + if !wrapped_msg.qbft_message.prepare_justification.is_empty() { + //self.validate_prepare_justification(wrapped_msg)?; + } + + // Extract and validate the proposed data + let Some(data) = wrapped_msg.signed_message.full_data.clone() else { + warn!(from = ?operator_id, "No data was proposed"); + return; + }; + + // Verify data hash matches the root + let data_hash = data.hash(); + if data_hash != wrapped_msg.qbft_message.root { + warn!(from = ?operator_id, "Data roots do not match"); + return; + } + debug!(from = ?operator_id, "PROPOSE received"); // Store the received propse message @@ -322,40 +372,15 @@ where warn!(from = ?operator_id, "PROPOSE message is a duplicate") } - // Extract and store the proposed data - // todo!() add back in the justified check - if let Some(data) = wrapped_msg.signed_message.full_data { - /* - let hash = data.hash(); - self.data.insert(hash, data); - - // Accept the proposal and move to prepare phase - self.proposal_accepted_for_current_round = Some(wrapped_msg); - self.state = InstanceState::Prepare; + // Store the data + //self.data.insert(data_hash, data); - // Send prepare message - self.send_prepare(hash); - */ - } - - /* - // Check if we have seen anything justified - if let Some(justified_data) = self.justify_round_change_quorum() { - if *justified_data != hash { - // The data doesn't match the justified value we expect. Drop the message - warn!( - from = ?operator_id, - "PROPOSE message isn't justified" - ); - // return - } - // todo!() anything else here? - } - */ + // Update state + self.proposal_accepted_for_current_round = Some(wrapped_msg); + self.state = InstanceState::Prepare; - // Insert the data and send off a prepare signaling that we are okay with this data - // self.data.insert(hash.clone(), data); - // self.send_prepare(hash); + // Create and send prepare message + self.send_prepare(data_hash); } /// We have received a prepare message @@ -383,6 +408,18 @@ where // Check if we have reached quorum, if so send the commit message if let Some(hash) = self.prepare_container.has_quorum(round) { + // Record this prepare consensus. Fetch the data, all of its prepare messages, and then + // record the consensus for them + let data = self.data.get(&hash).expect("Data must exist"); + let prepares_for_data: Vec = self + .prepare_container + .get_messages_for_value(round, hash) + .into_iter() + .cloned() + .collect(); + self.record_consensus(round, data.clone(), prepares_for_data); + + // if we are in the correct state, also send the commit if matches!(self.state, InstanceState::Prepare) { self.send_commit(hash); } diff --git a/anchor/common/qbft/src/msg_container.rs b/anchor/common/qbft/src/msg_container.rs index fd0d38cd..4fd0c0cb 100644 --- a/anchor/common/qbft/src/msg_container.rs +++ b/anchor/common/qbft/src/msg_container.rs @@ -70,6 +70,19 @@ impl> MessageContainer { self.messages.get(&round).map(|msgs| msgs.len()).unwrap_or(0) } + // Gets all messages for a specific round + pub fn get_messages_for_round(&self, round: Round) -> Vec<&M> { + // If we have messages for this round in our container, return them all + // If not, return an empty vector + self.messages + .get(&round) + .map(|round_messages| { + // Convert the values of the HashMap into a Vec + round_messages.values().collect() + }) + .unwrap_or_default() + } + pub fn get_messages_for_value(&self, round: Round, value: Hash256) -> Vec<&M> { self.messages .get(&round) diff --git a/anchor/common/ssv_types/src/message.rs b/anchor/common/ssv_types/src/message.rs index 77e56ca1..9c21eda8 100644 --- a/anchor/common/ssv_types/src/message.rs +++ b/anchor/common/ssv_types/src/message.rs @@ -31,7 +31,7 @@ pub struct SignedSsvMessage { } -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] pub struct UnsignedSsvMessage { pub ssv_message: SsvMessage, pub full_data: Option, @@ -137,7 +137,7 @@ pub struct PartialSignatureMessage { pub full_data: Option, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum FullData { ValidatorConsensusData(ValidatorConsensusData), BeaconVote(BeaconVote), @@ -154,10 +154,10 @@ impl Data for FullData { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct SszBytes(pub Vec); -#[derive(Clone, Debug, TreeHash)] +#[derive(Clone, Debug, TreeHash ,PartialEq)] pub struct ValidatorConsensusData { pub duty: ValidatorDuty, pub version: DataVersion, @@ -191,7 +191,7 @@ impl TreeHash for SszBytes { } } -#[derive(Clone, Debug, TreeHash)] +#[derive(Clone, Debug, TreeHash, PartialEq)] pub struct ValidatorDuty { pub r#type: BeaconRole, pub pub_key: PublicKeyBytes, @@ -204,7 +204,7 @@ pub struct ValidatorDuty { pub validator_sync_committee_indices: VariableList, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct BeaconRole(u64); pub const BEACON_ROLE_ATTESTER: BeaconRole = BeaconRole(0); @@ -234,7 +234,7 @@ impl TreeHash for BeaconRole { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct DataVersion(u64); pub const DATA_VERSION_UNKNOWN: DataVersion = DataVersion(0); From d3818d7ee5d72aa105699138d5388d70f54e042f Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Mon, 20 Jan 2025 23:04:36 +0000 Subject: [PATCH 06/45] ssz data compatability --- Cargo.lock | 6 ++ Cargo.toml | 1 + anchor/common/qbft/Cargo.toml | 3 + anchor/common/qbft/src/lib.rs | 98 ++++++++++++++------------ anchor/common/ssv_types/Cargo.toml | 1 + anchor/common/ssv_types/src/message.rs | 19 ++++- anchor/qbft_manager/Cargo.toml | 2 + 7 files changed, 83 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21a5363f..3f7264c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5711,7 +5711,10 @@ name = "qbft" version = "0.1.0" dependencies = [ "derive_more 1.0.0", + "ethereum_ssz 0.7.1", + "ethereum_ssz_derive 0.7.1", "indexmap", + "sha2 0.10.8", "ssv_types", "tracing", "tracing-subscriber", @@ -5723,6 +5726,8 @@ name = "qbft_manager" version = "0.1.0" dependencies = [ "dashmap", + "ethereum_ssz 0.7.1", + "ethereum_ssz_derive 0.7.1", "processor", "qbft", "slot_clock", @@ -6965,6 +6970,7 @@ dependencies = [ "ethereum_ssz_derive 0.7.1", "openssl", "rusqlite", + "sha2 0.10.8", "tree_hash 0.8.0", "tree_hash_derive", "types", diff --git a/Cargo.toml b/Cargo.toml index ae7d092e..c28c90d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,7 @@ parking_lot = "0.12" reqwest = "0.12.12" rusqlite = "0.28.0" serde = { version = "1.0.208", features = ["derive"] } +sha2 = "0.10.8" strum = { version = "0.24", features = ["derive"] } tokio = { version = "1.39.2", features = [ "rt", diff --git a/anchor/common/qbft/Cargo.toml b/anchor/common/qbft/Cargo.toml index b817ce3c..471959db 100644 --- a/anchor/common/qbft/Cargo.toml +++ b/anchor/common/qbft/Cargo.toml @@ -10,6 +10,9 @@ indexmap = { workspace = true } ssv_types = { workspace = true } tracing = { workspace = true } types = { workspace = true } +sha2 = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } [dev-dependencies] tracing-subscriber = { workspace = true } diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 4230c479..1320be0a 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -4,6 +4,7 @@ use ssv_types::message::{ }; use ssv_types::msgid::MsgId; use ssv_types::OperatorId; +use ssz::Encode; use std::collections::HashMap; use tracing::{debug, error, warn}; use types::Hash256; @@ -25,9 +26,9 @@ mod msg_container; mod qbft_types; mod validation; -struct ConsensusRecord { +struct ConsensusRecord { round: Round, - data: D, + data_hash: Hash256, prepare_messages: Vec, } @@ -39,10 +40,13 @@ mod tests; /// This builds and runs an entire QBFT process until it completes. It can complete either /// successfully (i.e that it has successfully come to consensus, or through a timeout where enough /// round changes have elapsed before coming to consensus. +/// +/// The QBFT instance will recieve SignedSSVMessages from the network and it will construct +/// UnsignedSSVMessages to be signed and sent on the network. pub struct Qbft where F: LeaderFunction + Clone, - D: Data, + D: Data + Encode, S: FnMut(Message), { /// The initial configuration used to establish this instance of QBFT. @@ -58,7 +62,7 @@ where /// Initial data that we will propose if we are the leader. start_data: D, /// All of the data that we have seen - data: HashMap, + data: HashMap>, /// The current round this instance state is in.a current_round: Round, /// The current state of the instance @@ -78,7 +82,7 @@ where last_prepared_value: Option, /// Past prepare consensus that we have reached - past_consensus: HashMap>, + past_consensus: HashMap, // Network sender send_message: S, @@ -87,7 +91,7 @@ where impl Qbft where F: LeaderFunction + Clone, - D: Data, + D: Data + Encode, S: FnMut(Message), { // Construct a new QBFT Instance and start the first round @@ -125,15 +129,17 @@ where qbft } + // Hash of the start data pub fn start_data_hash(&self) -> &D::Hash { &self.start_data_hash } + /// Return a reference to the qbft configuration pub fn config(&self) -> &Config { &self.config } - /// Shifts this instance into a new round> + // Shifts this instance into a new round> fn set_round(&mut self, new_round: Round) { self.current_round.set(new_round); self.start_round(); @@ -202,12 +208,12 @@ where fn record_consensus( &mut self, round: Round, - data: D, + data_hash: Hash256, prepare_messages: Vec, ) { let record = ConsensusRecord { round, - data, + data_hash, prepare_messages, }; self.past_consensus.insert(round, record); @@ -220,7 +226,7 @@ where /// If there is no past consensus data in the round change quorum or we disagree with quorum set /// this function will return None, and we obtain the data as if we were beginning this /// instance. - fn justify_round_change_quorum(&self) -> Option { + fn justify_round_change_quorum(&self) -> Option<(Hash256, Vec)> { // Get all round change messages for the current round let round_change_messages = self .round_change_container @@ -240,17 +246,23 @@ where // If we found a message with prepared data if let Some(highest_msg) = highest_prepared { // Get the prepared data from the message - let prepared_data = highest_msg.signed_message.full_data.as_ref()?; + let prepared_data = highest_msg.signed_message.full_data.clone(); let prepared_round = Round::from(highest_msg.qbft_message.data_round); // Verify we have also seen this consensus if let Some(our_record) = self.past_consensus.get(&prepared_round) { + // We have seen consensus on the data, get the value + let our_data = self + .data + .get(&our_record.data_hash) + .expect("Data must exist") + .clone(); + // Verify the data matches what we saw - //todo!() figure out this compare - //if prepared_data == our_record.data { - // We agree with the prepared data - use it - // return Some(our_record.data.clone()); - //} + if prepared_data == our_data { + // We agree with the prepared data - use it + return Some((our_record.data_hash, our_data)); + } } } @@ -272,12 +284,12 @@ where // Check justification of round change quorum. If there is a justification, we will use // that data. Otherwise, use the initial state data - let data = self + let (data_hash, data) = self .justify_round_change_quorum() - .unwrap_or_else(|| self.start_data.clone()); + .unwrap_or_else(|| (self.start_data_hash, self.start_data.as_ssz_bytes())); // Send the initial proposal - self.send_proposal(data.clone()); + self.send_proposal(data_hash, data); } } @@ -349,14 +361,8 @@ where //self.validate_prepare_justification(wrapped_msg)?; } - // Extract and validate the proposed data - let Some(data) = wrapped_msg.signed_message.full_data.clone() else { - warn!(from = ?operator_id, "No data was proposed"); - return; - }; - - // Verify data hash matches the root - let data_hash = data.hash(); + // Verify that the fulldata matches the data root of the qbft message + let data_hash = wrapped_msg.signed_message.hash_fulldata(); if data_hash != wrapped_msg.qbft_message.root { warn!(from = ?operator_id, "Data roots do not match"); return; @@ -373,7 +379,8 @@ where } // Store the data - //self.data.insert(data_hash, data); + self.data + .insert(data_hash, wrapped_msg.signed_message.full_data.clone()); // Update state self.proposal_accepted_for_current_round = Some(wrapped_msg); @@ -408,21 +415,24 @@ where // Check if we have reached quorum, if so send the commit message if let Some(hash) = self.prepare_container.has_quorum(round) { - // Record this prepare consensus. Fetch the data, all of its prepare messages, and then + // Make sure we are in the correct state + if !matches!(self.state, InstanceState::Prepare) { + warn!(from=?operator_id, ?self.state, "Not in PREPARE state"); + return; + } + + // Record this prepare consensus. Fetch all of the preapre messages for the data and then // record the consensus for them - let data = self.data.get(&hash).expect("Data must exist"); let prepares_for_data: Vec = self .prepare_container .get_messages_for_value(round, hash) .into_iter() .cloned() .collect(); - self.record_consensus(round, data.clone(), prepares_for_data); + self.record_consensus(round, hash, prepares_for_data); - // if we are in the correct state, also send the commit - if matches!(self.state, InstanceState::Prepare) { - self.send_commit(hash); - } + // Send a commit message for the prepare quorum data + self.send_commit(hash); } } @@ -528,8 +538,7 @@ where } // Construct a new unsigned message. This will be passed to the processor to be signed and then - // send on the network - // Helper: Create unsigned message + // sent on the network fn new_unsigned_message( &self, msg_type: QbftMessageType, @@ -557,17 +566,16 @@ where UnsignedSsvMessage { ssv_message: SsvMessage { msg_type: MsgType::SsvConsensusMsgType, - msg_id: self.identifier.clone(), // This should be properly generated - data: vec![], // this should be ssv_message.serialize() + msg_id: self.identifier.clone(), + data: vec![], // this should be ssv_message.as_ssz_bytes() }, - full_data: None, //Some(self.data.get(&data_hash).unwrap().clone()), // Include the actual data + full_data: self.data.get(&data_hash).unwrap().clone(), } } // Send a new qbft proposal message - fn send_proposal(&mut self, data: D) { + fn send_proposal(&mut self, hash: D::Hash, data: Vec) { // Store the data we're proposing - let hash = data.hash(); self.data.insert(hash, data.clone()); // Construct a unsigned proposal @@ -592,6 +600,7 @@ where (self.send_message)(Message::Prepare(operator_id, unsigned_msg.clone())); } + // Send a new qbft commit message fn send_commit(&mut self, data_hash: D::Hash) { // Construct unsigned commit let unsigned_msg = self.new_unsigned_message(QbftMessageType::Commit, data_hash); @@ -600,6 +609,7 @@ where (self.send_message)(Message::Commit(operator_id, unsigned_msg.clone())); } + // Send a new qbft round change message fn send_round_change(&mut self, data_hash: D::Hash) { // Construct unsigned round change let unsigned_msg = self.new_unsigned_message(QbftMessageType::RoundChange, data_hash); @@ -608,7 +618,7 @@ where (self.send_message)(Message::RoundChange(operator_id, unsigned_msg.clone())); } - pub fn completed(&self) -> Option> { + pub fn completed(&self) -> Option>> { self.completed .clone() .and_then(|completed| match completed { @@ -618,7 +628,7 @@ where if data.is_none() { error!("could not find finished data"); } - data.map(|data| Completed::Success(data)) + data.map(Completed::Success) } }) } diff --git a/anchor/common/ssv_types/Cargo.toml b/anchor/common/ssv_types/Cargo.toml index 9e4c1f1b..95afe5bb 100644 --- a/anchor/common/ssv_types/Cargo.toml +++ b/anchor/common/ssv_types/Cargo.toml @@ -14,3 +14,4 @@ tree_hash_derive = { workspace = true } types = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +sha2 = { workspace = true } diff --git a/anchor/common/ssv_types/src/message.rs b/anchor/common/ssv_types/src/message.rs index 204c56d5..f77b1bb3 100644 --- a/anchor/common/ssv_types/src/message.rs +++ b/anchor/common/ssv_types/src/message.rs @@ -1,5 +1,6 @@ use crate::msgid::MsgId; use crate::{OperatorId, ValidatorIndex}; +use sha2::{Digest, Sha256}; use ssz_derive::{Decode, Encode}; use std::collections::HashSet; use std::fmt::Debug; @@ -26,14 +27,24 @@ pub trait Data: Debug + Clone { pub struct SignedSsvMessage { pub signatures: Vec<[u8; 256]>, pub operator_ids: Vec, - pub ssv_message: SsvMessage, - pub full_data: Option, // should this just be serialized??? + pub ssv_message: SsvMessage, // this is the ssv message, consensu/partial sign + pub full_data: Vec, // this is the underlying data +} + + +impl SignedSsvMessage { + pub fn hash_fulldata(&self) -> Hash256 { + let mut hasher = Sha256::new(); + hasher.update(self.full_data.clone()); + let hash: [u8; 32] = hasher.finalize().into(); + Hash256::from(hash) + } } #[derive(Clone, Debug)] pub struct UnsignedSsvMessage { pub ssv_message: SsvMessage, - pub full_data: Option, + pub full_data: Vec, } impl SignedSsvMessage { @@ -66,12 +77,14 @@ impl SignedSsvMessage { true } + /* pub fn get_consensus_data(&self) -> Option { if let Some(FullData::ValidatorConsensusData(data)) = &self.full_data { return Some(data.clone()); } None } + */ } #[derive(Clone, Debug)] diff --git a/anchor/qbft_manager/Cargo.toml b/anchor/qbft_manager/Cargo.toml index 6a0ab078..4317650e 100644 --- a/anchor/qbft_manager/Cargo.toml +++ b/anchor/qbft_manager/Cargo.toml @@ -13,3 +13,5 @@ ssv_types = { workspace = true } tokio = { workspace = true, features = ["sync"] } tracing = { workspace = true } types = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } From f768d3c5ce91a096b44c660d030c429a1dccaac8 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Tue, 21 Jan 2025 15:36:33 +0000 Subject: [PATCH 07/45] ssz, moved messages to types, extracted into consensus types --- Cargo.lock | 1 + anchor/common/qbft/src/lib.rs | 50 +- anchor/common/qbft/src/msg_container.rs | 2 +- anchor/common/qbft/src/qbft_types.rs | 19 +- anchor/common/ssv_types/Cargo.toml | 1 + anchor/common/ssv_types/src/cluster.rs | 21 + anchor/common/ssv_types/src/consensus.rs | 264 ++++++ anchor/common/ssv_types/src/lib.rs | 1 + anchor/common/ssv_types/src/message.rs | 796 +++++++++++++----- anchor/qbft_manager/src/lib.rs | 29 +- anchor/validator_store/src/lib.rs | 16 +- .../src/sync_committee_service.rs | 2 +- 12 files changed, 934 insertions(+), 268 deletions(-) create mode 100644 anchor/common/ssv_types/src/consensus.rs diff --git a/Cargo.lock b/Cargo.lock index 3f7264c2..2f697e65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6968,6 +6968,7 @@ dependencies = [ "derive_more 1.0.0", "ethereum_ssz 0.7.1", "ethereum_ssz_derive 0.7.1", + "hex", "openssl", "rusqlite", "sha2 0.10.8", diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 1320be0a..2dfe71c3 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -1,7 +1,6 @@ use crate::msg_container::MessageContainer; -use ssv_types::message::{ - Data, MsgType, QbftMessage, QbftMessageType, SsvMessage, UnsignedSsvMessage, -}; +use ssv_types::consensus::{Data, QbftMessage, QbftMessageType, UnsignedSSVMessage}; +use ssv_types::message::{MessageID, MsgType, SSVMessage}; use ssv_types::msgid::MsgId; use ssv_types::OperatorId; use ssz::Encode; @@ -183,9 +182,9 @@ where } // Make sure there is only one signer - if wrapped_msg.signed_message.operator_ids.len() != 1 { + if wrapped_msg.signed_message.operator_ids().len() != 1 { warn!( - num_signers = wrapped_msg.signed_message.operator_ids.len(), + num_signers = wrapped_msg.signed_message.operator_ids().len(), "Propose message only allows one signer" ); return false; @@ -246,7 +245,7 @@ where // If we found a message with prepared data if let Some(highest_msg) = highest_prepared { // Get the prepared data from the message - let prepared_data = highest_msg.signed_message.full_data.clone(); + let prepared_data = highest_msg.signed_message.full_data(); let prepared_round = Round::from(highest_msg.qbft_message.data_round); // Verify we have also seen this consensus @@ -304,12 +303,13 @@ where // is the sender let operator_id = wrapped_msg .signed_message - .operator_ids + .operator_ids() .first() .expect("Confirmed to exist in validation"); + let operator_id = OperatorId(*operator_id); // Check that this operator is in our committee - if !self.check_committee(operator_id) { + if !self.check_committee(&operator_id) { warn!( from = ?operator_id, "PROPOSE message from non-committee operator" @@ -322,12 +322,12 @@ where // All basic verification successful! Dispatch to the correct handler match wrapped_msg.qbft_message.qbft_message_type { QbftMessageType::Proposal => { - self.received_propose(*operator_id, msg_round, wrapped_msg) + self.received_propose(operator_id, msg_round, wrapped_msg) } - QbftMessageType::Prepare => self.received_prepare(*operator_id, msg_round, wrapped_msg), - QbftMessageType::Commit => self.received_commit(*operator_id, msg_round, wrapped_msg), + QbftMessageType::Prepare => self.received_prepare(operator_id, msg_round, wrapped_msg), + QbftMessageType::Commit => self.received_commit(operator_id, msg_round, wrapped_msg), QbftMessageType::RoundChange => { - self.received_round_change(*operator_id, msg_round, wrapped_msg) + self.received_round_change(operator_id, msg_round, wrapped_msg) } } } @@ -379,8 +379,10 @@ where } // Store the data - self.data - .insert(data_hash, wrapped_msg.signed_message.full_data.clone()); + self.data.insert( + data_hash, + wrapped_msg.signed_message.full_data().to_vec().clone(), + ); // Update state self.proposal_accepted_for_current_round = Some(wrapped_msg); @@ -543,7 +545,7 @@ where &self, msg_type: QbftMessageType, data_hash: D::Hash, - ) -> UnsignedSsvMessage { + ) -> UnsignedSSVMessage { // Create the QBFT message let _qbft_mesage = QbftMessage { qbft_message_type: msg_type, @@ -556,19 +558,15 @@ where prepare_justification: vec![], // Empty for MVP }; - let _ssv_message = SsvMessage { - msg_type: MsgType::SsvConsensusMsgType, - msg_id: self.identifier.clone(), - data: vec![], // this should by qbft_serialized - }; + let ssv_message = SSVMessage::new( + MsgType::SSVConsensusMsgType, + MessageID::new([0; 56]), + vec![], // this should be the qbft message ssz + ); // Wrap in unsigned SSV message - UnsignedSsvMessage { - ssv_message: SsvMessage { - msg_type: MsgType::SsvConsensusMsgType, - msg_id: self.identifier.clone(), - data: vec![], // this should be ssv_message.as_ssz_bytes() - }, + UnsignedSSVMessage { + ssv_message, full_data: self.data.get(&data_hash).unwrap().clone(), } } diff --git a/anchor/common/qbft/src/msg_container.rs b/anchor/common/qbft/src/msg_container.rs index 7b8f6598..914512fd 100644 --- a/anchor/common/qbft/src/msg_container.rs +++ b/anchor/common/qbft/src/msg_container.rs @@ -1,5 +1,5 @@ use crate::Round; -use ssv_types::message::Data; +use ssv_types::consensus::Data; use ssv_types::OperatorId; use std::collections::{HashMap, HashSet}; use types::Hash256; diff --git a/anchor/common/qbft/src/qbft_types.rs b/anchor/common/qbft/src/qbft_types.rs index 175501be..631c20c4 100644 --- a/anchor/common/qbft/src/qbft_types.rs +++ b/anchor/common/qbft/src/qbft_types.rs @@ -2,7 +2,8 @@ use crate::validation::ValidatedData; use derive_more::{Deref, From}; use indexmap::IndexSet; -use ssv_types::message::{Data, QbftMessage, SignedSsvMessage, UnsignedSsvMessage}; +use ssv_types::consensus::{Data, QbftMessage, UnsignedSSVMessage}; +use ssv_types::message::SignedSSVMessage; use ssv_types::OperatorId; use std::cmp::Eq; use std::fmt::Debug; @@ -46,7 +47,7 @@ impl LeaderFunction for DefaultLeaderFunction { // message. #[derive(Debug, Clone)] pub struct WrappedQbftMessage { - pub signed_message: SignedSsvMessage, + pub signed_message: SignedSSVMessage, pub qbft_message: QbftMessage, } @@ -70,7 +71,7 @@ impl Data for WrappedQbftMessage { pub struct Round(NonZeroUsize); impl From for Round { - fn from(round: u64) -> Round { + fn from(_round: u64) -> Round { todo!() } } @@ -94,10 +95,6 @@ impl Round { } } -/// The operator that is participating in the consensus instance. -//#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, From, Deref)] -//pub struct OperatorId(u64); - /// The instance height behaves like an "ID" for the QBFT instance. It is used to uniquely identify /// different instances, that have the same operator id. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, From, Deref)] @@ -125,13 +122,13 @@ pub enum InstanceState { #[derive(Debug, Clone)] pub enum Message { /// A PROPOSE message to be sent on the network. - Propose(OperatorId, UnsignedSsvMessage), + Propose(OperatorId, UnsignedSSVMessage), /// A PREPARE message to be sent on the network. - Prepare(OperatorId, UnsignedSsvMessage), + Prepare(OperatorId, UnsignedSSVMessage), /// A commit message to be sent on the network. - Commit(OperatorId, UnsignedSsvMessage), + Commit(OperatorId, UnsignedSSVMessage), /// Round change message received from network - RoundChange(OperatorId, UnsignedSsvMessage), + RoundChange(OperatorId, UnsignedSSVMessage), } /// Type definitions for the allowable messages diff --git a/anchor/common/ssv_types/Cargo.toml b/anchor/common/ssv_types/Cargo.toml index 95afe5bb..cdce6cde 100644 --- a/anchor/common/ssv_types/Cargo.toml +++ b/anchor/common/ssv_types/Cargo.toml @@ -15,3 +15,4 @@ types = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } sha2 = { workspace = true } +hex = { workspace = true } diff --git a/anchor/common/ssv_types/src/cluster.rs b/anchor/common/ssv_types/src/cluster.rs index ed21c884..0f71084f 100644 --- a/anchor/common/ssv_types/src/cluster.rs +++ b/anchor/common/ssv_types/src/cluster.rs @@ -2,6 +2,8 @@ use crate::OperatorId; use derive_more::{Deref, From}; use std::collections::HashSet; use types::{Address, Graffiti, PublicKey}; +use ssz::{Decode, DecodeError, Encode}; +use ssz_derive::{Decode, Encode}; /// Unique identifier for a cluster #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, From, Deref)] @@ -41,6 +43,25 @@ pub struct ClusterMember { #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, From, Deref)] pub struct ValidatorIndex(pub usize); + +impl Encode for ValidatorIndex { + fn is_ssz_fixed_len() -> bool { + todo!() + } + + fn ssz_append(&self, buf: &mut Vec) { + todo!() + } + + fn ssz_fixed_len() -> usize { + todo!() + } + + fn ssz_bytes_len(&self) -> usize { + todo!() + } +} + /// General Metadata about a Validator #[derive(Debug, Clone)] pub struct ValidatorMetadata { diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs new file mode 100644 index 00000000..7740e2ee --- /dev/null +++ b/anchor/common/ssv_types/src/consensus.rs @@ -0,0 +1,264 @@ +use crate::message::*; +use crate::msgid::MsgId; +use crate::{OperatorId, ValidatorIndex}; +use ssz_derive::{Decode, Encode}; +use ssz::{Decode, DecodeError, Encode}; +use std::fmt::Debug; +use std::hash::Hash; +use tree_hash::{PackedEncoding, TreeHash, TreeHashType}; +use tree_hash_derive::TreeHash; +use types::typenum::U13; +use types::{ + AggregateAndProof, BeaconBlock, BlindedBeaconBlock, Checkpoint, CommitteeIndex, EthSpec, + Hash256, PublicKeyBytes, Signature, Slot, SyncCommitteeContribution, VariableList, +}; + +// UnsignedSSVMessage +// ---------------------------------------------- +// | | +// | | +// SSVMessage FullData +// --------------------- ---------- +// | | ValidatorConsensusData/BeaconVote SSZ +// | | +// MsgType FullData +// --------- ----------- +// ConsensusMsg QBFTMessage SSZ +// PartialSigMsg PartialSignatureMessage SSZ + +pub trait Data: Debug + Clone { + type Hash: Debug + Clone + Eq + Hash; + fn hash(&self) -> Self::Hash; +} + +/// A SSV Message that has not been signed yet. +#[derive(Clone, Debug)] +pub struct UnsignedSSVMessage { + /// The SSV Message to be send. This is either a consensus message which contains a serialized + /// QbftMessage, or a partial signature message which contains a PartialSignatureMessage + pub ssv_message: SSVMessage, + /// If this is a consensus message, fulldata contains the beacon data that is being agreed upon. + /// Otherwise, it is empty. + pub full_data: Vec, +} + +/// A QBFT specific message +#[derive(Clone, Debug)] +pub struct QbftMessage { + pub qbft_message_type: QbftMessageType, + pub height: u64, + pub round: u64, + pub identifier: MsgId, + pub root: Hash256, + // The last round that obtained a prepare quorum + pub data_round: u64, + pub round_change_justification: Vec, // always without full_data + pub prepare_justification: Vec, // always without full_data +} + +impl QbftMessage { + /// Do QBFTMessage specific validation + pub fn validate(&self) -> bool { + // todo!() what other identification? + if self.qbft_message_type > QbftMessageType::RoundChange { + return false; + } + true + } +} + +/// Different states the QBFT Message may represent +#[derive(Clone, Debug, PartialEq, PartialOrd)] +pub enum QbftMessageType { + Proposal = 0, + Prepare, + Commit, + RoundChange, +} + +// A partial signature specific message +#[derive(Clone, Debug)] +pub struct PartialSignatureMessage { + pub partial_signature: Signature, + pub signing_root: Hash256, + pub signer: OperatorId, + pub validator_index: ValidatorIndex, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SszBytes(pub Vec); + +#[derive(Clone, Debug, PartialEq, Encode)] +pub struct ValidatorConsensusData { + pub duty: ValidatorDuty, + pub version: DataVersion, + pub data_ssz: Vec, +} + +impl Data for ValidatorConsensusData { + type Hash = Hash256; + + fn hash(&self) -> Self::Hash { + todo!() + //self.tree_hash_root() + } +} + +/* + +impl TreeHash for SszBytes { + fn tree_hash_type() -> TreeHashType { + TreeHashType::List + } + + fn tree_hash_packed_encoding(&self) -> PackedEncoding { + todo!() + } + + fn tree_hash_packing_factor() -> usize { + todo!() + } + + fn tree_hash_root(&self) -> tree_hash::Hash256 { + todo!() + } +} +*/ + +#[derive(Clone, Debug, TreeHash, PartialEq, Encode)] +pub struct ValidatorDuty { + pub r#type: BeaconRole, + pub pub_key: PublicKeyBytes, + pub slot: Slot, + pub validator_index: ValidatorIndex, + pub committee_index: CommitteeIndex, + pub committee_length: u64, + pub committees_at_slot: u64, + pub validator_committee_index: u64, + pub validator_sync_committee_indices: VariableList, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BeaconRole(u64); + +impl Encode for BeaconRole { + fn is_ssz_fixed_len() -> bool { + todo!() + } + + fn ssz_append(&self, buf: &mut Vec) { + todo!() + } + + fn ssz_fixed_len() -> usize { + todo!() + } + + fn ssz_bytes_len(&self) -> usize { + todo!() + } +} + +pub const BEACON_ROLE_ATTESTER: BeaconRole = BeaconRole(0); +pub const BEACON_ROLE_AGGREGATOR: BeaconRole = BeaconRole(1); +pub const BEACON_ROLE_PROPOSER: BeaconRole = BeaconRole(2); +pub const BEACON_ROLE_SYNC_COMMITTEE: BeaconRole = BeaconRole(3); +pub const BEACON_ROLE_SYNC_COMMITTEE_CONTRIBUTION: BeaconRole = BeaconRole(4); +pub const BEACON_ROLE_VALIDATOR_REGISTRATION: BeaconRole = BeaconRole(5); +pub const BEACON_ROLE_VOLUNTARY_EXIT: BeaconRole = BeaconRole(6); +pub const BEACON_ROLE_UNKNOWN: BeaconRole = BeaconRole(u64::MAX); + +impl TreeHash for BeaconRole { + fn tree_hash_type() -> TreeHashType { + u64::tree_hash_type() + } + + fn tree_hash_packed_encoding(&self) -> PackedEncoding { + self.0.tree_hash_packed_encoding() + } + + fn tree_hash_packing_factor() -> usize { + u64::tree_hash_packing_factor() + } + + fn tree_hash_root(&self) -> tree_hash::Hash256 { + self.0.tree_hash_root() + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct DataVersion(u64); + +impl Encode for DataVersion { + fn is_ssz_fixed_len() -> bool { + todo!() + } + + fn ssz_append(&self, buf: &mut Vec) { + todo!() + } + + fn ssz_fixed_len() -> usize { + todo!() + } + + fn ssz_bytes_len(&self) -> usize { + todo!() + } +} + +pub const DATA_VERSION_UNKNOWN: DataVersion = DataVersion(0); +pub const DATA_VERSION_PHASE0: DataVersion = DataVersion(1); +pub const DATA_VERSION_ALTAIR: DataVersion = DataVersion(2); +pub const DATA_VERSION_BELLATRIX: DataVersion = DataVersion(3); +pub const DATA_VERSION_CAPELLA: DataVersion = DataVersion(4); +pub const DATA_VERSION_DENEB: DataVersion = DataVersion(5); + +impl TreeHash for DataVersion { + fn tree_hash_type() -> TreeHashType { + u64::tree_hash_type() + } + + fn tree_hash_packed_encoding(&self) -> PackedEncoding { + self.0.tree_hash_packed_encoding() + } + + fn tree_hash_packing_factor() -> usize { + u64::tree_hash_packing_factor() + } + + fn tree_hash_root(&self) -> tree_hash::Hash256 { + self.0.tree_hash_root() + } +} + +#[derive(Clone, Debug, TreeHash, Encode)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] +pub enum DataSsz { + AggregateAndProof(AggregateAndProof), + BlindedBeaconBlock(BlindedBeaconBlock), + BeaconBlock(BeaconBlock), + Contributions(VariableList, U13>), +} + +#[derive(Clone, Debug, TreeHash, Encode)] +pub struct Contribution { + pub selection_proof_sig: Signature, + pub contribution: SyncCommitteeContribution, +} + +#[derive(Clone, Debug, TreeHash, PartialEq, Eq, Encode, Decode)] +pub struct BeaconVote { + pub block_root: Hash256, + pub source: Checkpoint, + pub target: Checkpoint, +} + +impl Data for BeaconVote { + type Hash = Hash256; + + fn hash(&self) -> Self::Hash { + self.tree_hash_root() + } +} diff --git a/anchor/common/ssv_types/src/lib.rs b/anchor/common/ssv_types/src/lib.rs index ac7cb2f5..e3b031e8 100644 --- a/anchor/common/ssv_types/src/lib.rs +++ b/anchor/common/ssv_types/src/lib.rs @@ -5,6 +5,7 @@ mod cluster; pub mod message; pub mod msgid; mod operator; +pub mod consensus; mod share; mod sql_conversions; mod util; diff --git a/anchor/common/ssv_types/src/message.rs b/anchor/common/ssv_types/src/message.rs index f77b1bb3..8334dc78 100644 --- a/anchor/common/ssv_types/src/message.rs +++ b/anchor/common/ssv_types/src/message.rs @@ -1,54 +1,316 @@ -use crate::msgid::MsgId; -use crate::{OperatorId, ValidatorIndex}; -use sha2::{Digest, Sha256}; +use ssz::{Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; +use std::fmt; use std::collections::HashSet; -use std::fmt::Debug; use std::hash::Hash; -use tree_hash::{PackedEncoding, TreeHash, TreeHashType}; -use tree_hash_derive::TreeHash; -use types::typenum::U13; -use types::{ - AggregateAndProof, BeaconBlock, BlindedBeaconBlock, Checkpoint, CommitteeIndex, EthSpec, - Hash256, PublicKeyBytes, Signature, Slot, SyncCommitteeContribution, VariableList, -}; -// todo - dear reader, this mainly serves as plain translation of the types found in the go code -// there are a lot of byte[] there, and that got confusing, below should be more readable. -// it needs some work to actually serialize to the same stuff on wire, and I feel like we can name -// the fields better - -pub trait Data: Debug + Clone { - type Hash: Debug + Clone + Eq + Hash; - - fn hash(&self) -> Self::Hash; +use sha2::{Digest, Sha256}; +use std::fmt::Debug; +use types::Hash256; + +const MESSAGE_ID_LEN: usize = 56; + +/// Represents a unique Message ID consisting of 56 bytes. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MessageID([u8; MESSAGE_ID_LEN]); + +impl MessageID { + /// Creates a new `MessageID` if the provided array is exactly 56 bytes. + /// + /// # Arguments + /// + /// * `id` - A 56-byte array representing the message ID. + /// + /// # Examples + /// + /// ``` + /// use network::types::ssv_message::MessageID; + /// let id = [0u8; 56]; + /// let message_id = MessageID::new(id); + /// ``` + pub fn new(id: [u8; MESSAGE_ID_LEN]) -> Self { + MessageID(id) + } + + /// Returns a reference to the underlying 56-byte array. + pub fn as_bytes(&self) -> &[u8; MESSAGE_ID_LEN] { + &self.0 + } +} + +impl Encode for MessageID { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.0); + } + + fn ssz_fixed_len() -> usize { + MESSAGE_ID_LEN + } + + fn ssz_bytes_len(&self) -> usize { + MESSAGE_ID_LEN + } +} + +impl Decode for MessageID { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + MESSAGE_ID_LEN + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.len() != MESSAGE_ID_LEN { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected: MESSAGE_ID_LEN, + }); + } + let mut id = [0u8; MESSAGE_ID_LEN]; + id.copy_from_slice(bytes); + Ok(MessageID(id)) + } +} + +impl fmt::Display for MessageID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let hex_str = hex::encode(self.0); + write!(f, "MessageID({})", hex_str) + } } -#[derive(Clone, Debug)] -pub struct SignedSsvMessage { - pub signatures: Vec<[u8; 256]>, - pub operator_ids: Vec, - pub ssv_message: SsvMessage, // this is the ssv message, consensu/partial sign - pub full_data: Vec, // this is the underlying data +/// Defines the types of messages with explicit discriminant values. +#[derive(Debug, Clone, PartialEq, Eq)] +#[repr(u64)] +pub enum MsgType { + SSVConsensusMsgType = 0, + SSVPartialSignatureMsgType = 1, } +impl TryFrom for MsgType { + type Error = DecodeError; + + fn try_from(value: u64) -> Result { + match value { + 0 => Ok(MsgType::SSVConsensusMsgType), + 1 => Ok(MsgType::SSVPartialSignatureMsgType), + _ => Err(DecodeError::NoMatchingVariant), + } + } +} + +const U64_SIZE: usize = 8; // u64 is 8 bytes + +impl Encode for MsgType { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_append(&self, buf: &mut Vec) { + let value: u64 = match self { + MsgType::SSVConsensusMsgType => 0, + MsgType::SSVPartialSignatureMsgType => 1, + }; + buf.extend_from_slice(&value.to_le_bytes()); + } + + fn ssz_fixed_len() -> usize { + U64_SIZE + } + + fn ssz_bytes_len(&self) -> usize { + U64_SIZE + } +} + +impl Decode for MsgType { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + U64_SIZE + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.len() != U64_SIZE { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected: U64_SIZE, + }); + } + let value = u64::from_le_bytes(bytes.try_into().unwrap()); + value.try_into() + } +} + +/// Represents an Operator ID as a 64-bit unsigned integer. +pub type OperatorID = u64; + +/// Represents an SSV Message with type, ID, and data. +#[derive(Encode, Decode, Debug, Clone, PartialEq, Eq)] +pub struct SSVMessage { + msg_type: MsgType, + msg_id: MessageID, // Fixed-size [u8; 56] + data: Vec, // Variable-length byte array +} + +impl SSVMessage { + /// Creates a new `SSVMessage`. + /// + /// # Arguments + /// + /// * `msg_type` - The type of the message. + /// * `msg_id` - The unique message ID. + /// * `data` - The message data. + /// + /// # Examples + /// + /// ``` + /// use network::types::ssv_message::{SSVMessage, MsgType, MessageID}; + /// let message_id = MessageID::new([0u8; 56]); + /// let msg = SSVMessage::new(MsgType::SSVConsensusMsgType, message_id, vec![1, 2, 3]); + /// ``` + pub fn new(msg_type: MsgType, msg_id: MessageID, data: Vec) -> Self { + SSVMessage { + msg_type, + msg_id, + data, + } + } + + /// Returns a reference to the message type. + pub fn msg_type(&self) -> &MsgType { + &self.msg_type + } + + /// Returns a reference to the message ID. + pub fn msg_id(&self) -> &MessageID { + &self.msg_id + } + + /// Returns a reference to the message data. + pub fn data(&self) -> &[u8] { + &self.data + } +} + +/// Represents a signed SSV Message with signatures, operator IDs, the message itself, and full data. +#[derive(Encode, Decode, Debug, Clone, PartialEq, Eq)] +pub struct SignedSSVMessage { + signatures: Vec>, // Vec of Vec, max 13 elements, each up to 256 bytes + operator_ids: Vec, // Vec of OperatorID (u64), max 13 elements + ssv_message: SSVMessage, // SSVMessage: Required field + full_data: Vec, // Variable-length byte array, max 4,194,532 bytes +} + +impl SignedSSVMessage { + /// Maximum allowed number of signatures and operator IDs. + pub const MAX_SIGNATURES: usize = 13; + /// Maximum allowed length for each signature in bytes. + pub const MAX_SIGNATURE_LENGTH: usize = 256; + /// Maximum allowed length for `full_data` in bytes. + pub const MAX_FULL_DATA_LENGTH: usize = 4_194_532; + + /// Creates a new `SignedSSVMessage` after validating constraints. + /// + /// # Arguments + /// + /// * `signatures` - A vector of signatures, each up to 256 bytes. + /// * `operator_ids` - A vector of operator IDs, maximum 13 elements. + /// * `ssv_message` - The SSV message. + /// * `full_data` - Full data, up to 4,194,532 bytes. + /// + /// # Errors + /// + /// Returns an `SSVMessageError` if any constraints are violated. + /// + /// # Examples + /// + /// ``` + /// use network::types::ssv_message::{SignedSSVMessage, SSVMessage, MsgType, MessageID}; + /// let ssv_msg = SSVMessage::new(MsgType::SSVConsensusMsgType, MessageID::new([0u8; 56]), vec![1,2,3]); + /// let signed_msg = SignedSSVMessage::new(vec![vec![0; 256]], vec![1], ssv_msg, vec![4,5,6]).unwrap(); + /// ``` + pub fn new( + signatures: Vec>, + operator_ids: Vec, + ssv_message: SSVMessage, + full_data: Vec, + ) -> Result { + if signatures.len() > Self::MAX_SIGNATURES { + return Err(SSVMessageError::TooManySignatures { + provided: signatures.len(), + max: Self::MAX_SIGNATURES, + }); + } -impl SignedSsvMessage { + for (i, sig) in signatures.iter().enumerate() { + if sig.len() > Self::MAX_SIGNATURE_LENGTH { + return Err(SSVMessageError::SignatureTooLong { + index: i, + length: sig.len(), + max: Self::MAX_SIGNATURE_LENGTH, + }); + } + } + + if operator_ids.len() > Self::MAX_SIGNATURES { + return Err(SSVMessageError::TooManyOperatorIDs { + provided: operator_ids.len(), + max: Self::MAX_SIGNATURES, + }); + } + + if full_data.len() > Self::MAX_FULL_DATA_LENGTH { + return Err(SSVMessageError::FullDataTooLong { + length: full_data.len(), + max: Self::MAX_FULL_DATA_LENGTH, + }); + } + + Ok(SignedSSVMessage { + signatures, + operator_ids, + ssv_message, + full_data, + }) + } + + /// Returns a reference to the signatures. + pub fn signatures(&self) -> &Vec> { + &self.signatures + } + + /// Returns a reference to the operator IDs. + pub fn operator_ids(&self) -> &Vec { + &self.operator_ids + } + + /// Returns a reference to the SSV message. + pub fn ssv_message(&self) -> &SSVMessage { + &self.ssv_message + } + + /// Returns a reference to the full data. + pub fn full_data(&self) -> &[u8] { + &self.full_data + } + + /// Returns a hash of the fulldata pub fn hash_fulldata(&self) -> Hash256 { let mut hasher = Sha256::new(); hasher.update(self.full_data.clone()); let hash: [u8; 32] = hasher.finalize().into(); Hash256::from(hash) } -} -#[derive(Clone, Debug)] -pub struct UnsignedSsvMessage { - pub ssv_message: SsvMessage, - pub full_data: Vec, -} - -impl SignedSsvMessage { - // Validate the signed message + // Validate the signed message to ensure that it is well formed for qbft processing pub fn validate(&self) -> bool { // OperatorID must have at least one element if self.operator_ids.is_empty() { @@ -58,7 +320,7 @@ impl SignedSsvMessage { // Note: Len Signers & Operators will only be > 1 after commit aggregation // Any OperatorID must not be 0 - if self.operator_ids.iter().any(|&id| *id == 0) { + if self.operator_ids.iter().any(|&id| id == 0) { return false; } @@ -77,229 +339,339 @@ impl SignedSsvMessage { true } - /* - pub fn get_consensus_data(&self) -> Option { - if let Some(FullData::ValidatorConsensusData(data)) = &self.full_data { - return Some(data.clone()); - } - None - } - */ -} - -#[derive(Clone, Debug)] -pub struct SsvMessage { - pub msg_type: MsgType, - pub msg_id: MsgId, - pub data: Vec, // Underlying type is SSVData -} - -#[derive(Clone, Debug)] -pub enum MsgType { - SsvConsensusMsgType, - SsvPartialSignatureMsgType, } -#[derive(Clone, Debug)] -pub enum SsvData { - QbftMessage(QbftMessage), - PartialSignatureMessage(PartialSignatureMessage), +/// Represents errors that can occur while creating or processing `SignedSSVMessage`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SSVMessageError { + /// Exceeded the maximum number of signatures. + TooManySignatures { provided: usize, max: usize }, + /// A signature exceeds the maximum allowed length. + SignatureTooLong { + index: usize, + length: usize, + max: usize, + }, + /// Exceeded the maximum number of operator IDs. + TooManyOperatorIDs { provided: usize, max: usize }, + /// `full_data` exceeds the maximum allowed length. + FullDataTooLong { length: usize, max: usize }, } -#[derive(Clone, Debug)] -pub struct QbftMessage { - pub qbft_message_type: QbftMessageType, - pub height: u64, - pub round: u64, - pub identifier: MsgId, - - pub root: Hash256, - // The last round that obtained a prepare quorum - pub data_round: u64, - pub round_change_justification: Vec, // always without full_data - pub prepare_justification: Vec, // always without full_data -} - -impl QbftMessage { - pub fn validate(&self) -> bool { - // todo!() what other identification? - if self.qbft_message_type > QbftMessageType::RoundChange { - return false; +impl fmt::Display for SSVMessageError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SSVMessageError::TooManySignatures { provided, max } => { + write!( + f, + "Too many signatures: provided {}, maximum allowed is {}.", + provided, max + ) + } + SSVMessageError::SignatureTooLong { index, length, max } => { + write!( + f, + "Signature at index {} is too long: {} bytes, maximum allowed is {} bytes.", + index, length, max + ) + } + SSVMessageError::TooManyOperatorIDs { provided, max } => { + write!( + f, + "Too many operator IDs: provided {}, maximum allowed is {}.", + provided, max + ) + } + SSVMessageError::FullDataTooLong { length, max } => { + write!( + f, + "Full data is too long: {} bytes, maximum allowed is {} bytes.", + length, max + ) + } } - true } } -#[derive(Clone, Debug, PartialEq, PartialOrd)] -pub enum QbftMessageType { - Proposal = 0, - Prepare, - Commit, - RoundChange, -} +impl std::error::Error for SSVMessageError {} -#[derive(Clone, Debug)] -pub struct PartialSignatureMessage { - pub partial_signature: Signature, - pub signing_root: Hash256, - pub signer: OperatorId, - pub validator_index: ValidatorIndex, - // todo!() test this out - pub full_data: Option, -} +#[cfg(test)] +mod tests { + use super::*; + use ssz::{Decode, Encode}; -#[derive(Clone, Debug, PartialEq)] -pub enum FullData { - ValidatorConsensusData(ValidatorConsensusData), - BeaconVote(BeaconVote), -} - -impl Data for FullData { - type Hash = Hash256; - - fn hash(&self) -> Self::Hash { - match self { - FullData::ValidatorConsensusData(d) => d.hash(), - FullData::BeaconVote(d) => d.hash(), - } + #[test] + fn test_message_id_creation() { + let id = [1u8; 56]; + let message_id = MessageID::new(id); + assert_eq!(message_id.as_bytes(), &id); } -} -#[derive(Clone, Debug, PartialEq)] -pub struct SszBytes(pub Vec); + #[test] + fn test_message_id_display() { + let id = [0xABu8; 56]; + let message_id = MessageID::new(id); + let display = format!("{}", message_id); + assert_eq!(display, format!("MessageID({})", "ab".repeat(56))); + } -#[derive(Clone, Debug, TreeHash, PartialEq)] -pub struct ValidatorConsensusData { - pub duty: ValidatorDuty, - pub version: DataVersion, - pub data_ssz: SszBytes, -} + #[test] + fn test_message_id_encode_decode() { + let id = [42u8; 56]; + let message_id = MessageID::new(id); + let encoded = message_id.as_ssz_bytes(); + assert_eq!(encoded.len(), 56); + let decoded = MessageID::from_ssz_bytes(&encoded).unwrap(); + assert_eq!(decoded, message_id); + } -impl Data for ValidatorConsensusData { - type Hash = Hash256; + #[test] + fn test_message_id_decode_invalid_length() { + let bytes = vec![0u8; 55]; // One byte short + let result = MessageID::from_ssz_bytes(&bytes); + assert!(matches!( + result, + Err(DecodeError::InvalidByteLength { + len: 55, + expected: 56 + }) + )); + } - fn hash(&self) -> Self::Hash { - self.tree_hash_root() + #[test] + fn test_msgtype_encode_decode() { + let msg_type = MsgType::SSVConsensusMsgType; + let encoded = msg_type.as_ssz_bytes(); + assert_eq!(encoded.len(), U64_SIZE); + let decoded = MsgType::from_ssz_bytes(&encoded).unwrap(); + assert_eq!(decoded, msg_type); + + let msg_type = MsgType::SSVPartialSignatureMsgType; + let encoded = msg_type.as_ssz_bytes(); + let decoded = MsgType::from_ssz_bytes(&encoded).unwrap(); + assert_eq!(decoded, msg_type); } -} -impl TreeHash for SszBytes { - fn tree_hash_type() -> TreeHashType { - TreeHashType::List + #[test] + fn test_msgtype_decode_invalid_variant() { + let invalid_value = 2u64.to_le_bytes(); + let result = MsgType::from_ssz_bytes(&invalid_value); + assert!(matches!(result, Err(DecodeError::NoMatchingVariant))); } - fn tree_hash_packed_encoding(&self) -> PackedEncoding { - todo!() + #[test] + fn test_ssv_message_encode_decode() { + let message_id = MessageID::new([7u8; 56]); + let ssv_msg = SSVMessage::new( + MsgType::SSVConsensusMsgType, + message_id.clone(), + vec![10, 20, 30], + ); + let encoded = ssv_msg.as_ssz_bytes(); + let decoded = SSVMessage::from_ssz_bytes(&encoded).unwrap(); + assert_eq!(decoded, ssv_msg); } - fn tree_hash_packing_factor() -> usize { - 1 + #[test] + fn test_signed_ssv_message_creation_valid() { + let message_id = MessageID::new([0u8; 56]); + let ssv_msg = SSVMessage::new( + MsgType::SSVPartialSignatureMsgType, + message_id, + vec![1, 2, 3], + ); + + let signatures = vec![vec![0u8; 256], vec![1u8; 100]]; + let operator_ids = vec![1, 2]; + let full_data = vec![255u8; 4_194_532]; + + let signed_msg = SignedSSVMessage::new( + signatures.clone(), + operator_ids.clone(), + ssv_msg.clone(), + full_data.clone(), + ); + + assert!(signed_msg.is_ok()); + + let signed_msg = signed_msg.unwrap(); + assert_eq!(signed_msg.signatures(), &signatures); + assert_eq!(signed_msg.operator_ids(), &operator_ids); + assert_eq!(signed_msg.ssv_message(), &ssv_msg); + assert_eq!(signed_msg.full_data(), &full_data); } - fn tree_hash_root(&self) -> tree_hash::Hash256 { - todo!() - //tree_hash::Hash256::from_slice(&tree_hash::merkle_root(&self.0.into(), 1)) + #[test] + fn test_signed_ssv_message_creation_too_many_signatures() { + let message_id = MessageID::new([0u8; 56]); + let ssv_msg = SSVMessage::new(MsgType::SSVConsensusMsgType, message_id, vec![]); + + let signatures = vec![vec![0u8; 256]; 14]; // Exceeds max of 13 + let operator_ids = vec![1; 13]; + let full_data = vec![]; + + let signed_msg = SignedSSVMessage::new(signatures, operator_ids, ssv_msg, full_data); + + assert!(matches!( + signed_msg, + Err(SSVMessageError::TooManySignatures { + provided: 14, + max: 13 + }) + )); } -} -#[derive(Clone, Debug, TreeHash, PartialEq)] -pub struct ValidatorDuty { - pub r#type: BeaconRole, - pub pub_key: PublicKeyBytes, - pub slot: Slot, - pub validator_index: ValidatorIndex, - pub committee_index: CommitteeIndex, - pub committee_length: u64, - pub committees_at_slot: u64, - pub validator_committee_index: u64, - pub validator_sync_committee_indices: VariableList, -} + #[test] + fn test_signed_ssv_message_creation_signature_too_long() { + let message_id = MessageID::new([0u8; 56]); + let ssv_msg = SSVMessage::new(MsgType::SSVConsensusMsgType, message_id, vec![]); + + let mut signatures = vec![vec![0u8; 256]]; + signatures.push(vec![1u8; 257]); // Exceeds max length -#[derive(Clone, Debug, PartialEq)] -pub struct BeaconRole(u64); + let operator_ids = vec![1, 2]; + let full_data = vec![]; -pub const BEACON_ROLE_ATTESTER: BeaconRole = BeaconRole(0); -pub const BEACON_ROLE_AGGREGATOR: BeaconRole = BeaconRole(1); -pub const BEACON_ROLE_PROPOSER: BeaconRole = BeaconRole(2); -pub const BEACON_ROLE_SYNC_COMMITTEE: BeaconRole = BeaconRole(3); -pub const BEACON_ROLE_SYNC_COMMITTEE_CONTRIBUTION: BeaconRole = BeaconRole(4); -pub const BEACON_ROLE_VALIDATOR_REGISTRATION: BeaconRole = BeaconRole(5); -pub const BEACON_ROLE_VOLUNTARY_EXIT: BeaconRole = BeaconRole(6); -pub const BEACON_ROLE_UNKNOWN: BeaconRole = BeaconRole(u64::MAX); + let signed_msg = SignedSSVMessage::new(signatures, operator_ids, ssv_msg, full_data); -impl TreeHash for BeaconRole { - fn tree_hash_type() -> TreeHashType { - u64::tree_hash_type() + assert!(matches!( + signed_msg, + Err(SSVMessageError::SignatureTooLong { + index: 1, + length: 257, + max: 256 + }) + )); } - fn tree_hash_packed_encoding(&self) -> PackedEncoding { - self.0.tree_hash_packed_encoding() + #[test] + fn test_signed_ssv_message_creation_too_many_operator_ids() { + let message_id = MessageID::new([0u8; 56]); + let ssv_msg = SSVMessage::new(MsgType::SSVPartialSignatureMsgType, message_id, vec![]); + + let signatures = vec![vec![0u8; 256]; 5]; + let operator_ids = vec![1u64; 14]; // Exceeds max of 13 + let full_data = vec![]; + + let signed_msg = SignedSSVMessage::new(signatures, operator_ids, ssv_msg, full_data); + + assert!(matches!( + signed_msg, + Err(SSVMessageError::TooManyOperatorIDs { + provided: 14, + max: 13 + }) + )); } - fn tree_hash_packing_factor() -> usize { - u64::tree_hash_packing_factor() + #[test] + fn test_signed_ssv_message_creation_full_data_too_long() { + let message_id = MessageID::new([0u8; 56]); + let ssv_msg = SSVMessage::new(MsgType::SSVConsensusMsgType, message_id, vec![]); + + let signatures = vec![vec![0u8; 256]]; + let operator_ids = vec![1]; + let full_data = vec![0u8; 4_194_533]; // Exceeds max + + let signed_msg = SignedSSVMessage::new(signatures, operator_ids, ssv_msg, full_data); + + assert!(matches!( + signed_msg, + Err(SSVMessageError::FullDataTooLong { + length: 4_194_533, + max: 4_194_532 + }) + )); } - fn tree_hash_root(&self) -> tree_hash::Hash256 { - self.0.tree_hash_root() + #[test] + fn test_signed_ssv_message_encode_decode() { + let message_id = MessageID::new([9u8; 56]); + let ssv_msg = SSVMessage::new( + MsgType::SSVConsensusMsgType, + message_id.clone(), + vec![100, 101, 102], + ); + + let signatures = vec![vec![10u8; 256], vec![20u8; 100]]; + let operator_ids = vec![1, 2]; + let full_data = vec![200u8; 1024]; + + let signed_msg = SignedSSVMessage::new( + signatures.clone(), + operator_ids.clone(), + ssv_msg.clone(), + full_data.clone(), + ) + .unwrap(); + + let encoded = signed_msg.as_ssz_bytes(); + let decoded = SignedSSVMessage::from_ssz_bytes(&encoded).unwrap(); + + assert_eq!(decoded, signed_msg); } -} -#[derive(Clone, Debug, PartialEq)] -pub struct DataVersion(u64); + #[test] + fn test_ssvmessage_encode_decode_empty_data() { + let message_id = MessageID::new([0u8; 56]); + let ssv_msg = SSVMessage::new(MsgType::SSVConsensusMsgType, message_id.clone(), vec![]); -pub const DATA_VERSION_UNKNOWN: DataVersion = DataVersion(0); -pub const DATA_VERSION_PHASE0: DataVersion = DataVersion(1); -pub const DATA_VERSION_ALTAIR: DataVersion = DataVersion(2); -pub const DATA_VERSION_BELLATRIX: DataVersion = DataVersion(3); -pub const DATA_VERSION_CAPELLA: DataVersion = DataVersion(4); -pub const DATA_VERSION_DENEB: DataVersion = DataVersion(5); + let encoded = ssv_msg.as_ssz_bytes(); + let decoded = SSVMessage::from_ssz_bytes(&encoded).unwrap(); -impl TreeHash for DataVersion { - fn tree_hash_type() -> TreeHashType { - u64::tree_hash_type() + assert_eq!(decoded, ssv_msg); } - fn tree_hash_packed_encoding(&self) -> PackedEncoding { - self.0.tree_hash_packed_encoding() + #[test] + fn test_ssvmessage_decode_invalid_length() { + let bytes = vec![0u8; 56 + 8 + 3 - 1]; // Missing one byte in data + let result = SSVMessage::from_ssz_bytes(&bytes); + assert!(result.is_err()); } - fn tree_hash_packing_factor() -> usize { - u64::tree_hash_packing_factor() + #[test] + fn test_msgtype_invalid_bytes_length() { + let bytes = vec![0u8; U64_SIZE - 1]; // One byte short + let result = MsgType::from_ssz_bytes(&bytes); + assert!(matches!( + result, + Err(DecodeError::InvalidByteLength { + len: 7, + expected: 8 + }) + )); } - fn tree_hash_root(&self) -> tree_hash::Hash256 { - self.0.tree_hash_root() - } -} + #[test] + fn test_full_data_max_length() { + let full_data = vec![0u8; SignedSSVMessage::MAX_FULL_DATA_LENGTH]; + let message_id = MessageID::new([0u8; 56]); + let ssv_msg = SSVMessage::new(MsgType::SSVConsensusMsgType, message_id, vec![]); + let signatures = vec![vec![0u8; 256]]; + let operator_ids = vec![1]; -#[derive(Clone, Debug, TreeHash, Encode)] -#[tree_hash(enum_behaviour = "transparent")] -#[ssz(enum_behaviour = "transparent")] -pub enum DataSsz { - AggregateAndProof(AggregateAndProof), - BlindedBeaconBlock(BlindedBeaconBlock), - BeaconBlock(BeaconBlock), - Contributions(VariableList, U13>), -} + let signed_msg = + SignedSSVMessage::new(signatures, operator_ids, ssv_msg, full_data.clone()); -#[derive(Clone, Debug, TreeHash, Encode)] -pub struct Contribution { - pub selection_proof_sig: Signature, - pub contribution: SyncCommitteeContribution, -} + assert!(signed_msg.is_ok()); -#[derive(Clone, Debug, TreeHash, PartialEq, Eq)] -pub struct BeaconVote { - pub block_root: Hash256, - pub source: Checkpoint, - pub target: Checkpoint, -} + let signed_msg = signed_msg.unwrap(); + assert_eq!(signed_msg.full_data(), &full_data); + } + + #[test] + fn test_full_data_exceeds_max_length() { + let full_data = vec![0u8; SignedSSVMessage::MAX_FULL_DATA_LENGTH + 1]; + let message_id = MessageID::new([0u8; 56]); + let ssv_msg = SSVMessage::new(MsgType::SSVConsensusMsgType, message_id, vec![]); + let signatures = vec![vec![0u8; 256]]; + let operator_ids = vec![1]; -impl Data for BeaconVote { - type Hash = Hash256; + let signed_msg = SignedSSVMessage::new(signatures, operator_ids, ssv_msg, full_data); - fn hash(&self) -> Self::Hash { - self.tree_hash_root() + assert!(matches!( + signed_msg, + Err(SSVMessageError::FullDataTooLong { length: _, max: _ }) + )); } } diff --git a/anchor/qbft_manager/src/lib.rs b/anchor/qbft_manager/src/lib.rs index f75b9eac..378dd6d7 100644 --- a/anchor/qbft_manager/src/lib.rs +++ b/anchor/qbft_manager/src/lib.rs @@ -5,10 +5,12 @@ use qbft::{ WrappedQbftMessage, }; use slot_clock::SlotClock; -use ssv_types::message::Data; -use ssv_types::message::{BeaconVote, SignedSsvMessage, ValidatorConsensusData}; +use ssv_types::consensus::{BeaconVote, Data, ValidatorConsensusData}; + +use ssv_types::message::SignedSSVMessage; use ssv_types::OperatorId as QbftOperatorId; use ssv_types::{Cluster, ClusterId, OperatorId}; +use ssz::Encode; use std::fmt::Debug; use std::hash::Hash; use std::sync::Arc; @@ -53,20 +55,20 @@ pub enum ValidatorDutyKind { // Message that is passed around the QbftManager #[derive(Debug)] -pub struct QbftMessage> { +pub struct QbftMessage + Encode> { pub kind: QbftMessageKind, pub drop_on_finish: DropOnFinish, } // Type of the QBFT Message #[derive(Debug)] -pub enum QbftMessageKind> { +pub enum QbftMessageKind + Encode> { // Initialize a new qbft instance with some initial data, // the configuration for the instance, and a channel to send the final data on Initialize { initial: D, config: qbft::Config, - on_completed: oneshot::Sender>, + on_completed: oneshot::Sender>>, }, // A message received from the network. The network exchanges SignedSsvMessages, but after // deserialziation we dermine the message is for the qbft instance and decode it into a wrapped @@ -123,7 +125,7 @@ impl QbftManager { id: D::Id, initial: D, committee: &Cluster, - ) -> Result, QbftError> { + ) -> Result>, QbftError> { // Tx/Rx pair to send and retrieve the final result let (result_sender, result_receiver) = oneshot::channel(); @@ -160,6 +162,7 @@ impl QbftManager { )?; // Await the final result + // todo!(), we recieve a vec here, we should deserialize back to D Ok(result_receiver.await?) } @@ -202,7 +205,9 @@ impl QbftManager { } // Trait that describes any data that is able to be decided upon during a qbft instance -pub trait QbftDecidable: Data + Send + 'static { +pub trait QbftDecidable: + Data + Encode + Send + 'static +{ type Id: Hash + Eq + Send; fn get_map(manager: &QbftManager) -> &Map; @@ -255,7 +260,7 @@ impl QbftDecidable for BeaconVote { } // States that Qbft instance may be in -enum QbftInstance, S: FnMut(Message)> { +enum QbftInstance + Encode, S: FnMut(Message)> { // The instance is uninitialized Uninitialized { // todo: proooobably limit this @@ -267,15 +272,17 @@ enum QbftInstance, S: FnMut(Message)> { Initialized { qbft: Box>, round_end: Interval, - on_completed: Vec>>, + on_completed: Vec>>>, }, // The instance has been decided Decided { - value: Completed, + value: Completed>, }, } -async fn qbft_instance>(mut rx: UnboundedReceiver>) { +async fn qbft_instance + Encode>( + mut rx: UnboundedReceiver>, +) { // Signal a new instance that is uninitialized let mut instance = QbftInstance::Uninitialized { message_buffer: Vec::new(), diff --git a/anchor/validator_store/src/lib.rs b/anchor/validator_store/src/lib.rs index 2824c7f7..7cd3a572 100644 --- a/anchor/validator_store/src/lib.rs +++ b/anchor/validator_store/src/lib.rs @@ -10,15 +10,15 @@ use safe_arith::{ArithError, SafeArith}; use signature_collector::{CollectionError, SignatureCollectorManager, SignatureRequest}; use slashing_protection::{NotSafe, Safe, SlashingDatabase}; use slot_clock::SlotClock; -use ssv_types::message::SszBytes; -use ssv_types::message::{ +use ssv_types::consensus::SszBytes; +use ssv_types::consensus::{ BeaconVote, Contribution, DataSsz, ValidatorConsensusData, ValidatorDuty, BEACON_ROLE_AGGREGATOR, BEACON_ROLE_PROPOSER, BEACON_ROLE_SYNC_COMMITTEE_CONTRIBUTION, DATA_VERSION_ALTAIR, DATA_VERSION_BELLATRIX, DATA_VERSION_CAPELLA, DATA_VERSION_DENEB, DATA_VERSION_PHASE0, DATA_VERSION_UNKNOWN, }; use ssv_types::{Cluster, OperatorId, ValidatorMetadata}; -use ssz::Encode; +use ssz::{Encode, Decode}; use std::fmt::Debug; use std::marker::PhantomData; use std::sync::Arc; @@ -216,7 +216,7 @@ impl AnchorValidatorStore { BeaconBlock::Deneb(_) => DATA_VERSION_DENEB, BeaconBlock::Electra(_) => DATA_VERSION_UNKNOWN, }, - data_ssz: SszBytes(wrapped_block.as_ssz_bytes()), + data_ssz: wrapped_block.as_ssz_bytes(), }, &cluster.cluster, ) @@ -281,6 +281,8 @@ impl AnchorValidatorStore { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, }; + let data = BeaconVote::from_ssz_bytes(&data).map_err(|_| Error::SpecificError(SpecificError::InvalidQbftData))?; + let domain = self.get_domain(epoch, Domain::SyncCommittee); let signing_root = data.block_root.signing_root(domain); @@ -346,7 +348,7 @@ impl AnchorValidatorStore { validator_sync_committee_indices: Default::default(), }, version: DATA_VERSION_PHASE0, - data_ssz: SszBytes(wrapped_contribution.as_ssz_bytes()), + data_ssz: wrapped_contribution.as_ssz_bytes(), }, &cluster.cluster, ) @@ -621,6 +623,8 @@ impl ValidatorStore for AnchorValidatorStore { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, }; + let data = BeaconVote::from_ssz_bytes(&data).map_err(|_| Error::SpecificError(SpecificError::InvalidQbftData))?; + attestation.data_mut().beacon_block_root = data.block_root; attestation.data_mut().source = data.source; attestation.data_mut().target = data.target; @@ -728,7 +732,7 @@ impl ValidatorStore for AnchorValidatorStore { validator_sync_committee_indices: Default::default(), }, version: DATA_VERSION_PHASE0, - data_ssz: SszBytes(wrapped_aggregate_and_proof.as_ssz_bytes()), + data_ssz: wrapped_aggregate_and_proof.as_ssz_bytes(), }, &cluster.cluster, ) diff --git a/anchor/validator_store/src/sync_committee_service.rs b/anchor/validator_store/src/sync_committee_service.rs index 2b167825..510802f6 100644 --- a/anchor/validator_store/src/sync_committee_service.rs +++ b/anchor/validator_store/src/sync_committee_service.rs @@ -7,7 +7,7 @@ use beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use futures::future::join_all; use futures::future::FutureExt; use slot_clock::SlotClock; -use ssv_types::message::BeaconVote; +use ssv_types::consensus::BeaconVote; use std::collections::HashMap; use std::ops::Deref; use std::sync::atomic::{AtomicBool, Ordering}; From a7af706cb66b5544da19deeab947734aa190924a Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Tue, 21 Jan 2025 16:35:57 +0000 Subject: [PATCH 08/45] remove warnings --- anchor/common/qbft/src/tests.rs | 3 ++- anchor/common/qbft/src/validation.rs | 6 ++---- anchor/common/ssv_types/src/cluster.rs | 6 +++--- anchor/common/ssv_types/src/consensus.rs | 6 +++--- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index 089946fe..17aed3af 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -2,15 +2,16 @@ //! //! These test individual components and also provide full end-to-end tests of the entire protocol. +/* use super::*; use crate::validation::{validate_data, ValidatedData}; use qbft_types::DefaultLeaderFunction; use std::cell::RefCell; +use ssz::{Encode, Decode}; use std::collections::{HashSet, VecDeque}; use std::rc::Rc; use tracing_subscriber::filter::EnvFilter; -/* // HELPER FUNCTIONS FOR TESTS /// Enable debug logging for tests diff --git a/anchor/common/qbft/src/validation.rs b/anchor/common/qbft/src/validation.rs index 7f3b082c..da366a50 100644 --- a/anchor/common/qbft/src/validation.rs +++ b/anchor/common/qbft/src/validation.rs @@ -1,7 +1,5 @@ //! Validation for data function - use crate::qbft_types::ConsensusData; -use types::EthSpec; /// The list of possible validation errors that can occur #[derive(Debug)] @@ -16,13 +14,13 @@ pub struct ValidatedData { } /// This verifies the data is correct an appropriate to use for consensus. -pub fn validate_data(data: D) -> Result, ValidationError> { +pub fn _validate_data(data: D) -> Result, ValidationError> { Ok(ValidatedData { data }) } // Validates consensus data pub fn validate_consensus_data( - consensus_data: ConsensusData, + _consensus_data: ConsensusData, ) -> Result>, ValidationError> { todo!() /* diff --git a/anchor/common/ssv_types/src/cluster.rs b/anchor/common/ssv_types/src/cluster.rs index 0f71084f..7b475517 100644 --- a/anchor/common/ssv_types/src/cluster.rs +++ b/anchor/common/ssv_types/src/cluster.rs @@ -2,8 +2,7 @@ use crate::OperatorId; use derive_more::{Deref, From}; use std::collections::HashSet; use types::{Address, Graffiti, PublicKey}; -use ssz::{Decode, DecodeError, Encode}; -use ssz_derive::{Decode, Encode}; +use ssz::Encode; /// Unique identifier for a cluster #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, From, Deref)] @@ -44,12 +43,13 @@ pub struct ClusterMember { pub struct ValidatorIndex(pub usize); +// Implement SSZ encoding and decoding for Validator Index impl Encode for ValidatorIndex { fn is_ssz_fixed_len() -> bool { todo!() } - fn ssz_append(&self, buf: &mut Vec) { + fn ssz_append(&self, _buf: &mut Vec) { todo!() } diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index 7740e2ee..5e408510 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -2,7 +2,7 @@ use crate::message::*; use crate::msgid::MsgId; use crate::{OperatorId, ValidatorIndex}; use ssz_derive::{Decode, Encode}; -use ssz::{Decode, DecodeError, Encode}; +use ssz::Encode; use std::fmt::Debug; use std::hash::Hash; use tree_hash::{PackedEncoding, TreeHash, TreeHashType}; @@ -146,7 +146,7 @@ impl Encode for BeaconRole { todo!() } - fn ssz_append(&self, buf: &mut Vec) { + fn ssz_append(&self, _buf: &mut Vec) { todo!() } @@ -194,7 +194,7 @@ impl Encode for DataVersion { todo!() } - fn ssz_append(&self, buf: &mut Vec) { + fn ssz_append(&self, _buf: &mut Vec) { todo!() } From 6ea0611ac1f936791f3a63e7b2ef40351ff56070 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Tue, 21 Jan 2025 17:22:31 +0000 Subject: [PATCH 09/45] formattings and implement ssz --- anchor/common/qbft/src/lib.rs | 4 +- anchor/common/qbft/src/msg_container.rs | 11 ++- anchor/common/qbft/src/qbft_types.rs | 4 +- anchor/common/qbft/src/validation.rs | 8 -- anchor/common/ssv_types/src/cluster.rs | 3 +- anchor/common/ssv_types/src/consensus.rs | 107 +++++++++++++++-------- anchor/common/ssv_types/src/lib.rs | 2 +- anchor/common/ssv_types/src/message.rs | 7 +- anchor/validator_store/src/lib.rs | 10 +-- 9 files changed, 90 insertions(+), 66 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 2dfe71c3..5cff4006 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -321,9 +321,7 @@ where // All basic verification successful! Dispatch to the correct handler match wrapped_msg.qbft_message.qbft_message_type { - QbftMessageType::Proposal => { - self.received_propose(operator_id, msg_round, wrapped_msg) - } + QbftMessageType::Proposal => self.received_propose(operator_id, msg_round, wrapped_msg), QbftMessageType::Prepare => self.received_prepare(operator_id, msg_round, wrapped_msg), QbftMessageType::Commit => self.received_commit(operator_id, msg_round, wrapped_msg), QbftMessageType::RoundChange => { diff --git a/anchor/common/qbft/src/msg_container.rs b/anchor/common/qbft/src/msg_container.rs index 914512fd..b917d0c5 100644 --- a/anchor/common/qbft/src/msg_container.rs +++ b/anchor/common/qbft/src/msg_container.rs @@ -16,6 +16,7 @@ pub struct MessageContainer { } impl> MessageContainer { + /// Construct a new MessageContainer with a specific quorum size pub fn new(quorum_size: usize) -> Self { Self { quorum_size, @@ -24,6 +25,7 @@ impl> MessageContainer { } } + // Add a new message to the container for the round pub fn add_message(&mut self, round: Round, sender: OperatorId, msg: &M) -> bool { // Check if we already have a message from this sender for this round if self @@ -32,7 +34,7 @@ impl> MessageContainer { .and_then(|msgs| msgs.get(&sender)) .is_some() { - return false; // Duplicate + return false; // Duplicate message } // Add message and track its value @@ -49,6 +51,8 @@ impl> MessageContainer { true } + // Check if we have a quorum of messages for the round. If so, return the hash of the value with + // the quorum pub fn has_quorum(&self, round: Round) -> Option { let round_messages = self.messages.get(&round)?; @@ -65,7 +69,7 @@ impl> MessageContainer { .map(|(value, _)| value) } - // Count messages for this round + /// Count the number of messages we have recieved for this round pub fn num_messages_for_round(&self, round: Round) -> usize { self.messages .get(&round) @@ -73,7 +77,7 @@ impl> MessageContainer { .unwrap_or(0) } - // Gets all messages for a specific round + /// Gets all messages for a specific round pub fn get_messages_for_round(&self, round: Round) -> Vec<&M> { // If we have messages for this round in our container, return them all // If not, return an empty vector @@ -86,6 +90,7 @@ impl> MessageContainer { .unwrap_or_default() } + /// Get all of the messages for the round and hash pub fn get_messages_for_value(&self, round: Round, value: Hash256) -> Vec<&M> { self.messages .get(&round) diff --git a/anchor/common/qbft/src/qbft_types.rs b/anchor/common/qbft/src/qbft_types.rs index 631c20c4..1fca2c84 100644 --- a/anchor/common/qbft/src/qbft_types.rs +++ b/anchor/common/qbft/src/qbft_types.rs @@ -71,8 +71,8 @@ impl Data for WrappedQbftMessage { pub struct Round(NonZeroUsize); impl From for Round { - fn from(_round: u64) -> Round { - todo!() + fn from(round: u64) -> Round { + Round(NonZeroUsize::new(round as usize).expect("round == 0")) } } diff --git a/anchor/common/qbft/src/validation.rs b/anchor/common/qbft/src/validation.rs index da366a50..6606d23d 100644 --- a/anchor/common/qbft/src/validation.rs +++ b/anchor/common/qbft/src/validation.rs @@ -23,12 +23,4 @@ pub fn validate_consensus_data( _consensus_data: ConsensusData, ) -> Result>, ValidationError> { todo!() - /* - let round = consensus_data.round; - let validated_data = validate_data(consensus_data.data)?; - Ok(ConsensusData { - round, - data: validated_data, - }) - */ } diff --git a/anchor/common/ssv_types/src/cluster.rs b/anchor/common/ssv_types/src/cluster.rs index 7b475517..14711914 100644 --- a/anchor/common/ssv_types/src/cluster.rs +++ b/anchor/common/ssv_types/src/cluster.rs @@ -1,8 +1,8 @@ use crate::OperatorId; use derive_more::{Deref, From}; +use ssz::Encode; use std::collections::HashSet; use types::{Address, Graffiti, PublicKey}; -use ssz::Encode; /// Unique identifier for a cluster #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, From, Deref)] @@ -42,7 +42,6 @@ pub struct ClusterMember { #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, From, Deref)] pub struct ValidatorIndex(pub usize); - // Implement SSZ encoding and decoding for Validator Index impl Encode for ValidatorIndex { fn is_ssz_fixed_len() -> bool { diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index 5e408510..39eea876 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -1,8 +1,9 @@ use crate::message::*; use crate::msgid::MsgId; use crate::{OperatorId, ValidatorIndex}; +use sha2::{Digest, Sha256}; +use ssz::{Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; -use ssz::Encode; use std::fmt::Debug; use std::hash::Hash; use tree_hash::{PackedEncoding, TreeHash, TreeHashType}; @@ -50,7 +51,6 @@ pub struct QbftMessage { pub round: u64, pub identifier: MsgId, pub root: Hash256, - // The last round that obtained a prepare quorum pub data_round: u64, pub round_change_justification: Vec, // always without full_data pub prepare_justification: Vec, // always without full_data @@ -59,10 +59,11 @@ pub struct QbftMessage { impl QbftMessage { /// Do QBFTMessage specific validation pub fn validate(&self) -> bool { - // todo!() what other identification? if self.qbft_message_type > QbftMessageType::RoundChange { return false; } + + // todo!(). Any other validation? true } } @@ -85,10 +86,8 @@ pub struct PartialSignatureMessage { pub validator_index: ValidatorIndex, } -#[derive(Clone, Debug, PartialEq)] -pub struct SszBytes(pub Vec); -#[derive(Clone, Debug, PartialEq, Encode)] +#[derive(Clone, Debug, PartialEq, Encode)] pub struct ValidatorConsensusData { pub duty: ValidatorDuty, pub version: DataVersion, @@ -99,31 +98,14 @@ impl Data for ValidatorConsensusData { type Hash = Hash256; fn hash(&self) -> Self::Hash { - todo!() - //self.tree_hash_root() - } -} - -/* + let bytes = self.as_ssz_bytes(); -impl TreeHash for SszBytes { - fn tree_hash_type() -> TreeHashType { - TreeHashType::List - } - - fn tree_hash_packed_encoding(&self) -> PackedEncoding { - todo!() - } - - fn tree_hash_packing_factor() -> usize { - todo!() - } - - fn tree_hash_root(&self) -> tree_hash::Hash256 { - todo!() + let mut hasher = Sha256::new(); + hasher.update(bytes); + let hash: [u8; 32] = hasher.finalize().into(); + Hash256::from(hash) } } -*/ #[derive(Clone, Debug, TreeHash, PartialEq, Encode)] pub struct ValidatorDuty { @@ -141,21 +123,45 @@ pub struct ValidatorDuty { #[derive(Clone, Debug, PartialEq)] pub struct BeaconRole(u64); +// BeaconRole SSZ implementation impl Encode for BeaconRole { fn is_ssz_fixed_len() -> bool { - todo!() + true } - fn ssz_append(&self, _buf: &mut Vec) { - todo!() + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.0.to_le_bytes()); } fn ssz_fixed_len() -> usize { - todo!() + 8 } fn ssz_bytes_len(&self) -> usize { - todo!() + 8 + } +} + +impl Decode for BeaconRole { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 8 + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 8 { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected: 8, + }); + } + + let mut array = [0u8; 8]; + array.copy_from_slice(bytes); + Ok(BeaconRole(u64::from_le_bytes(array))) } } @@ -189,21 +195,46 @@ impl TreeHash for BeaconRole { #[derive(Clone, Debug, PartialEq)] pub struct DataVersion(u64); +// DataVersion SSZ implementation impl Encode for DataVersion { fn is_ssz_fixed_len() -> bool { - todo!() + true } - fn ssz_append(&self, _buf: &mut Vec) { - todo!() + fn ssz_append(&self, buf: &mut Vec) { + // DataVersion is represented as u64 internally + buf.extend_from_slice(&self.0.to_le_bytes()); } fn ssz_fixed_len() -> usize { - todo!() + 8 // u64 size } fn ssz_bytes_len(&self) -> usize { - todo!() + 8 + } +} + +impl Decode for DataVersion { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_fixed_len() -> usize { + 8 // u64 size + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 8 { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected: 8, + }); + } + + let mut array = [0u8; 8]; + array.copy_from_slice(bytes); + Ok(DataVersion(u64::from_le_bytes(array))) } } diff --git a/anchor/common/ssv_types/src/lib.rs b/anchor/common/ssv_types/src/lib.rs index e3b031e8..31b8a990 100644 --- a/anchor/common/ssv_types/src/lib.rs +++ b/anchor/common/ssv_types/src/lib.rs @@ -2,10 +2,10 @@ pub use cluster::{Cluster, ClusterId, ClusterMember, ValidatorIndex, ValidatorMe pub use operator::{Operator, OperatorId}; pub use share::Share; mod cluster; +pub mod consensus; pub mod message; pub mod msgid; mod operator; -pub mod consensus; mod share; mod sql_conversions; mod util; diff --git a/anchor/common/ssv_types/src/message.rs b/anchor/common/ssv_types/src/message.rs index 8334dc78..a82bc75d 100644 --- a/anchor/common/ssv_types/src/message.rs +++ b/anchor/common/ssv_types/src/message.rs @@ -1,10 +1,10 @@ +use sha2::{Digest, Sha256}; use ssz::{Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; -use std::fmt; use std::collections::HashSet; -use std::hash::Hash; -use sha2::{Digest, Sha256}; +use std::fmt; use std::fmt::Debug; +use std::hash::Hash; use types::Hash256; const MESSAGE_ID_LEN: usize = 56; @@ -338,7 +338,6 @@ impl SignedSSVMessage { } true } - } /// Represents errors that can occur while creating or processing `SignedSSVMessage`. diff --git a/anchor/validator_store/src/lib.rs b/anchor/validator_store/src/lib.rs index 7cd3a572..a0510baf 100644 --- a/anchor/validator_store/src/lib.rs +++ b/anchor/validator_store/src/lib.rs @@ -10,7 +10,6 @@ use safe_arith::{ArithError, SafeArith}; use signature_collector::{CollectionError, SignatureCollectorManager, SignatureRequest}; use slashing_protection::{NotSafe, Safe, SlashingDatabase}; use slot_clock::SlotClock; -use ssv_types::consensus::SszBytes; use ssv_types::consensus::{ BeaconVote, Contribution, DataSsz, ValidatorConsensusData, ValidatorDuty, BEACON_ROLE_AGGREGATOR, BEACON_ROLE_PROPOSER, BEACON_ROLE_SYNC_COMMITTEE_CONTRIBUTION, @@ -18,7 +17,7 @@ use ssv_types::consensus::{ DATA_VERSION_PHASE0, DATA_VERSION_UNKNOWN, }; use ssv_types::{Cluster, OperatorId, ValidatorMetadata}; -use ssz::{Encode, Decode}; +use ssz::{Decode, Encode}; use std::fmt::Debug; use std::marker::PhantomData; use std::sync::Arc; @@ -281,8 +280,8 @@ impl AnchorValidatorStore { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, }; - let data = BeaconVote::from_ssz_bytes(&data).map_err(|_| Error::SpecificError(SpecificError::InvalidQbftData))?; - + let data = BeaconVote::from_ssz_bytes(&data) + .map_err(|_| Error::SpecificError(SpecificError::InvalidQbftData))?; let domain = self.get_domain(epoch, Domain::SyncCommittee); let signing_root = data.block_root.signing_root(domain); @@ -623,7 +622,8 @@ impl ValidatorStore for AnchorValidatorStore { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, }; - let data = BeaconVote::from_ssz_bytes(&data).map_err(|_| Error::SpecificError(SpecificError::InvalidQbftData))?; + let data = BeaconVote::from_ssz_bytes(&data) + .map_err(|_| Error::SpecificError(SpecificError::InvalidQbftData))?; attestation.data_mut().beacon_block_root = data.block_root; attestation.data_mut().source = data.source; From d7ddc24576d8b669ccad30850bafc60345ff29f4 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Tue, 21 Jan 2025 22:17:08 +0000 Subject: [PATCH 10/45] fix some features, fmt, sort --- Cargo.lock | 93 ++++++++++++------------ Cargo.toml | 4 +- anchor/common/qbft/Cargo.toml | 6 +- anchor/common/qbft/src/lib.rs | 50 ++++++------- anchor/common/ssv_types/Cargo.toml | 8 +- anchor/common/ssv_types/src/consensus.rs | 1 - anchor/qbft_manager/Cargo.toml | 4 +- anchor/qbft_manager/src/lib.rs | 12 +-- anchor/validator_store/Cargo.toml | 2 +- anchor/validator_store/src/lib.rs | 11 ++- 10 files changed, 91 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f697e65..dc462145 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,7 +1193,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "beacon_node_fallback" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "eth2", "futures", @@ -1283,7 +1283,7 @@ dependencies = [ [[package]] name = "bls" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "alloy-primitives", "arbitrary", @@ -1481,9 +1481,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", @@ -1491,9 +1491,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.26" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -1523,7 +1523,7 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clap_utils" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "alloy-primitives", "clap", @@ -1582,7 +1582,7 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compare_fields" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "itertools 0.10.5", ] @@ -1590,7 +1590,7 @@ dependencies = [ [[package]] name = "compare_fields_derive" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "quote", "syn 1.0.109", @@ -2139,7 +2139,7 @@ dependencies = [ [[package]] name = "directory" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "clap", "clap_utils", @@ -2417,7 +2417,7 @@ dependencies = [ [[package]] name = "eth2" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "derivative", "enr", @@ -2446,7 +2446,7 @@ dependencies = [ [[package]] name = "eth2_config" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "paste", "types", @@ -2455,7 +2455,7 @@ dependencies = [ [[package]] name = "eth2_interop_keypairs" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "bls", "ethereum_hashing", @@ -2468,7 +2468,7 @@ dependencies = [ [[package]] name = "eth2_key_derivation" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "bls", "num-bigint-dig", @@ -2480,7 +2480,7 @@ dependencies = [ [[package]] name = "eth2_keystore" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "aes 0.7.5", "bls", @@ -2502,7 +2502,7 @@ dependencies = [ [[package]] name = "eth2_network_config" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "bytes", "discv5", @@ -2752,7 +2752,7 @@ dependencies = [ [[package]] name = "filesystem" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "winapi", "windows-acl", @@ -2773,7 +2773,7 @@ dependencies = [ [[package]] name = "fixed_bytes" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "alloy-primitives", "safe_arith", @@ -3044,7 +3044,7 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "gossipsub" version = "0.5.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "async-channel", "asynchronous-codec", @@ -3073,7 +3073,7 @@ dependencies = [ [[package]] name = "graffiti_file" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "bls", "serde", @@ -3181,7 +3181,7 @@ dependencies = [ [[package]] name = "health_metrics" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "eth2", "metrics", @@ -3844,7 +3844,7 @@ dependencies = [ [[package]] name = "int_to_bytes" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "bytes", ] @@ -3974,7 +3974,7 @@ dependencies = [ [[package]] name = "kzg" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "arbitrary", "c-kzg", @@ -4530,7 +4530,7 @@ dependencies = [ [[package]] name = "lighthouse_network" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -4580,7 +4580,7 @@ dependencies = [ [[package]] name = "lighthouse_version" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "git-version", "target_info", @@ -4629,7 +4629,7 @@ checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "logging" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "chrono", "metrics", @@ -4669,7 +4669,7 @@ dependencies = [ [[package]] name = "lru_cache" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "fnv", ] @@ -4734,7 +4734,7 @@ dependencies = [ [[package]] name = "merkle_proof" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "alloy-primitives", "ethereum_hashing", @@ -4768,7 +4768,7 @@ dependencies = [ [[package]] name = "metrics" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "prometheus", ] @@ -4944,17 +4944,16 @@ dependencies = [ [[package]] name = "netlink-proto" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b33524dc0968bfad349684447bfce6db937a9ac3332a1fe60c0c5a5ce63f21" +checksum = "b2741a6c259755922e3ed29ebce3b299cc2160c4acae94b465b5938ab02c2bbe" dependencies = [ "bytes", "futures", "log", "netlink-packet-core", "netlink-sys", - "thiserror 1.0.69", - "tokio", + "thiserror 2.0.11", ] [[package]] @@ -5501,7 +5500,7 @@ dependencies = [ [[package]] name = "pretty_reqwest_error" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "reqwest 0.11.27", "sensitive_url", @@ -5670,7 +5669,7 @@ dependencies = [ [[package]] name = "proto_array" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "ethereum_ssz 0.7.1", "ethereum_ssz_derive 0.7.1", @@ -6434,7 +6433,7 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "safe_arith" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" [[package]] name = "salsa20" @@ -6572,7 +6571,7 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "sensitive_url" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "serde", "url", @@ -6770,7 +6769,7 @@ dependencies = [ [[package]] name = "slashing_protection" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "arbitrary", "ethereum_serde_utils 0.7.0", @@ -6889,7 +6888,7 @@ dependencies = [ [[package]] name = "slot_clock" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "metrics", "parking_lot", @@ -7102,7 +7101,7 @@ dependencies = [ [[package]] name = "swap_or_not_shuffle" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "alloy-primitives", "ethereum_hashing", @@ -7232,7 +7231,7 @@ checksum = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe" [[package]] name = "task_executor" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "async-channel", "futures", @@ -7282,7 +7281,7 @@ dependencies = [ [[package]] name = "test_random_derive" version = "0.2.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "quote", "syn 1.0.109", @@ -7756,7 +7755,7 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "types" version = "0.2.1" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -7919,7 +7918,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "unused_port" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "lru_cache", "parking_lot", @@ -7973,7 +7972,7 @@ dependencies = [ [[package]] name = "validator_metrics" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "metrics", ] @@ -7981,7 +7980,7 @@ dependencies = [ [[package]] name = "validator_services" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "beacon_node_fallback", "bls", @@ -8004,7 +8003,7 @@ dependencies = [ [[package]] name = "validator_store" version = "0.1.0" -source = "git+https://github.com/sigp/lighthouse?branch=anchor#997991f5513f22bb240816c2e2400cf6a1819a0c" +source = "git+https://github.com/sigp/lighthouse?branch=anchor#1a77f7a0609fc96d1cc2eb74da7fe90aef046352" dependencies = [ "slashing_protection", "types", diff --git a/Cargo.toml b/Cargo.toml index c28c90d4..0630a880 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,8 @@ derive_more = { version = "1.0.0", features = ["full"] } dirs = "5.0.1" discv5 = "0.9.0" either = "1.13.0" +ethereum_ssz = "0.7" +ethereum_ssz_derive = "0.7.0" futures = "0.3.30" hex = "0.4.3" hyper = "1.4" @@ -99,8 +101,6 @@ validator_metrics = { git = "https://github.com/sigp/lighthouse", branch = "anch validator_services = { git = "https://github.com/sigp/lighthouse", branch = "anchor" } validator_store = { git = "https://github.com/sigp/lighthouse", branch = "anchor" } version = { path = "anchor/common/version" } -ethereum_ssz = "0.7" -ethereum_ssz_derive = "0.7.0" [profile.maxperf] inherits = "release" diff --git a/anchor/common/qbft/Cargo.toml b/anchor/common/qbft/Cargo.toml index 471959db..6fab0fba 100644 --- a/anchor/common/qbft/Cargo.toml +++ b/anchor/common/qbft/Cargo.toml @@ -6,13 +6,13 @@ edition = { workspace = true } [dependencies] derive_more = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } indexmap = { workspace = true } +sha2 = { workspace = true } ssv_types = { workspace = true } tracing = { workspace = true } types = { workspace = true } -sha2 = { workspace = true } -ethereum_ssz = { workspace = true } -ethereum_ssz_derive = { workspace = true } [dev-dependencies] tracing-subscriber = { workspace = true } diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 5cff4006..0b377309 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -27,7 +27,7 @@ mod validation; struct ConsensusRecord { round: Round, - data_hash: Hash256, + hash: Hash256, prepare_messages: Vec, } @@ -203,21 +203,6 @@ where true } - // Helper method to record consensus when we see it - fn record_consensus( - &mut self, - round: Round, - data_hash: Hash256, - prepare_messages: Vec, - ) { - let record = ConsensusRecord { - round, - data_hash, - prepare_messages, - }; - self.past_consensus.insert(round, record); - } - /// Justify the round change quorum /// In order to justify a round change quorum, we find the maximum round of the quorum set that /// had achieved a past consensus. If we have also seen consensus on this round for the @@ -253,14 +238,14 @@ where // We have seen consensus on the data, get the value let our_data = self .data - .get(&our_record.data_hash) + .get(&our_record.hash) .expect("Data must exist") .clone(); // Verify the data matches what we saw if prepared_data == our_data { // We agree with the prepared data - use it - return Some((our_record.data_hash, our_data)); + return Some((our_record.hash, our_data)); } } } @@ -308,7 +293,7 @@ where .expect("Confirmed to exist in validation"); let operator_id = OperatorId(*operator_id); - // Check that this operator is in our committee + // Check that this sender is in our committee if !self.check_committee(&operator_id) { warn!( from = ?operator_id, @@ -316,7 +301,6 @@ where ); return; } - let msg_round: Round = wrapped_msg.qbft_message.round.into(); // All basic verification successful! Dispatch to the correct handler @@ -421,15 +405,24 @@ where return; } + // Move the state forward since we have a prepare quorum + self.state = InstanceState::Commit; + // Record this prepare consensus. Fetch all of the preapre messages for the data and then // record the consensus for them - let prepares_for_data: Vec = self + let prepare_messages: Vec = self .prepare_container .get_messages_for_value(round, hash) .into_iter() .cloned() .collect(); - self.record_consensus(round, hash, prepares_for_data); + + let record = ConsensusRecord { + round, + hash, + prepare_messages, + }; + self.past_consensus.insert(round, record); // Send a commit message for the prepare quorum data self.send_commit(hash); @@ -460,10 +453,12 @@ where } // Check if we have a commit quorum - if let Some(_data) = self.prepare_container.has_quorum(round) { + if let Some(hash) = self.prepare_container.has_quorum(round) { if matches!(self.state, InstanceState::Commit) { - //self.completed = Some(Completed::Success(data.clone())); + // We have come to consensus, mark ourself as completed and record the agreed upon + // value self.state = InstanceState::Complete; + self.completed = Some(Completed::Success(hash)); } } } @@ -532,7 +527,7 @@ where } // Get the data to send with the round change? - //self.send_round_change(next_round); + self.send_round_change(self.start_data_hash); // Start a new round self.set_round(next_round); } @@ -551,7 +546,7 @@ where round: self.current_round.get() as u64, identifier: self.identifier.clone(), root: data_hash as Hash256, - data_round: 0, // Not used in MVP + data_round: self.current_round.get() as u64, round_change_justification: vec![], // Empty for MVP prepare_justification: vec![], // Empty for MVP }; @@ -559,7 +554,7 @@ where let ssv_message = SSVMessage::new( MsgType::SSVConsensusMsgType, MessageID::new([0; 56]), - vec![], // this should be the qbft message ssz + vec![], // qbft_message.as_ssz_bytes() ); // Wrap in unsigned SSV message @@ -614,6 +609,7 @@ where (self.send_message)(Message::RoundChange(operator_id, unsigned_msg.clone())); } + /// Extract the data that the instance has come to consensus on pub fn completed(&self) -> Option>> { self.completed .clone() diff --git a/anchor/common/ssv_types/Cargo.toml b/anchor/common/ssv_types/Cargo.toml index cdce6cde..7a9e9861 100644 --- a/anchor/common/ssv_types/Cargo.toml +++ b/anchor/common/ssv_types/Cargo.toml @@ -7,12 +7,12 @@ authors = ["Sigma Prime "] [dependencies] base64 = { workspace = true } derive_more = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } +hex = { workspace = true } openssl = { workspace = true } rusqlite = { workspace = true } +sha2 = { workspace = true } tree_hash = { workspace = true } tree_hash_derive = { workspace = true } types = { workspace = true } -ethereum_ssz = { workspace = true } -ethereum_ssz_derive = { workspace = true } -sha2 = { workspace = true } -hex = { workspace = true } diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index 39eea876..307a8428 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -86,7 +86,6 @@ pub struct PartialSignatureMessage { pub validator_index: ValidatorIndex, } - #[derive(Clone, Debug, PartialEq, Encode)] pub struct ValidatorConsensusData { pub duty: ValidatorDuty, diff --git a/anchor/qbft_manager/Cargo.toml b/anchor/qbft_manager/Cargo.toml index 4317650e..42d7313f 100644 --- a/anchor/qbft_manager/Cargo.toml +++ b/anchor/qbft_manager/Cargo.toml @@ -6,6 +6,8 @@ edition = { workspace = true } [dependencies] dashmap = { workspace = true } +ethereum_ssz = { workspace = true } +ethereum_ssz_derive = { workspace = true } processor = { workspace = true } qbft = { workspace = true } slot_clock = { workspace = true } @@ -13,5 +15,3 @@ ssv_types = { workspace = true } tokio = { workspace = true, features = ["sync"] } tracing = { workspace = true } types = { workspace = true } -ethereum_ssz = { workspace = true } -ethereum_ssz_derive = { workspace = true } diff --git a/anchor/qbft_manager/src/lib.rs b/anchor/qbft_manager/src/lib.rs index 378dd6d7..84549152 100644 --- a/anchor/qbft_manager/src/lib.rs +++ b/anchor/qbft_manager/src/lib.rs @@ -7,7 +7,6 @@ use qbft::{ use slot_clock::SlotClock; use ssv_types::consensus::{BeaconVote, Data, ValidatorConsensusData}; -use ssv_types::message::SignedSSVMessage; use ssv_types::OperatorId as QbftOperatorId; use ssv_types::{Cluster, ClusterId, OperatorId}; use ssz::Encode; @@ -21,7 +20,7 @@ use tokio::sync::oneshot::error::RecvError; use tokio::sync::{mpsc, oneshot}; use tokio::time::{sleep, Interval}; use tracing::{error, warn}; -use types::{EthSpec, Hash256, PublicKeyBytes}; +use types::{Hash256, PublicKeyBytes}; const QBFT_INSTANCE_NAME: &str = "qbft_instance"; const QBFT_MESSAGE_NAME: &str = "qbft_message"; @@ -133,11 +132,7 @@ impl QbftManager { let config = ConfigBuilder::new( self.operator_id, initial.instance_height(&id), - committee - .cluster_members - .iter() - .map(|&m| m) // todo!() fixt his map - .collect(), + committee.cluster_members.iter().copied().collect(), ); let config = config .with_quorum_size(committee.cluster_members.len() - committee.faulty as usize) @@ -162,11 +157,10 @@ impl QbftManager { )?; // Await the final result - // todo!(), we recieve a vec here, we should deserialize back to D Ok(result_receiver.await?) } - // What is the purpose of this?? + /// Send a new network message to the instance pub fn receive_data>( &self, id: D::Id, diff --git a/anchor/validator_store/Cargo.toml b/anchor/validator_store/Cargo.toml index 38f0512e..6f819f05 100644 --- a/anchor/validator_store/Cargo.toml +++ b/anchor/validator_store/Cargo.toml @@ -9,6 +9,7 @@ rust-version = "1.81.0" beacon_node_fallback = { workspace = true } dashmap = { workspace = true } eth2 = { workspace = true } +ethereum_ssz = { workspace = true } futures = "0.3.31" parking_lot = { workspace = true } qbft = { workspace = true } @@ -18,7 +19,6 @@ signature_collector = { workspace = true } slashing_protection = { workspace = true } slot_clock = { workspace = true } ssv_types = { workspace = true } -ethereum_ssz = { workspace = true } task_executor = { workspace = true } tokio = { workspace = true, features = ["sync", "time"] } tracing = { workspace = true } diff --git a/anchor/validator_store/src/lib.rs b/anchor/validator_store/src/lib.rs index a0510baf..76e2895e 100644 --- a/anchor/validator_store/src/lib.rs +++ b/anchor/validator_store/src/lib.rs @@ -276,6 +276,8 @@ impl AnchorValidatorStore { ) .await .map_err(SpecificError::from)?; + + // Extract the completed value let data = match completed { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, @@ -352,8 +354,8 @@ impl AnchorValidatorStore { &cluster.cluster, ) .await; - // todo, handle this in a different way - let data = match completed { + // todo SSZ deser + let _data = match completed { Ok(Completed::Success(data)) => data, Ok(Completed::TimedOut) => return error(SpecificError::Timeout.into()), Err(err) => return error(SpecificError::QbftError(err).into()), @@ -738,11 +740,12 @@ impl ValidatorStore for AnchorValidatorStore { ) .await .map_err(SpecificError::from)?; - // todo!() handle a better way - let data = match completed { + // todo SSZ deser + let _data = match completed { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, }; + let message = match wrapped_aggregate_and_proof { DataSsz::AggregateAndProof(message) => message, _ => return Err(Error::SpecificError(SpecificError::InvalidQbftData)), From 86d059194a53b92de6b1aa6d046c796fabb84042 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 22 Jan 2025 15:17:56 +0000 Subject: [PATCH 11/45] round change fix and prepare consensus recording --- anchor/common/qbft/src/lib.rs | 58 ++++++++++--------------- anchor/common/qbft/src/msg_container.rs | 8 ---- anchor/validator_store/src/lib.rs | 3 +- 3 files changed, 26 insertions(+), 43 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 0b377309..f7f430c6 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -25,12 +25,6 @@ mod msg_container; mod qbft_types; mod validation; -struct ConsensusRecord { - round: Round, - hash: Hash256, - prepare_messages: Vec, -} - #[cfg(test)] mod tests; @@ -81,7 +75,7 @@ where last_prepared_value: Option, /// Past prepare consensus that we have reached - past_consensus: HashMap, + past_consensus: HashMap, // Network sender send_message: S, @@ -234,18 +228,14 @@ where let prepared_round = Round::from(highest_msg.qbft_message.data_round); // Verify we have also seen this consensus - if let Some(our_record) = self.past_consensus.get(&prepared_round) { + if let Some(hash) = self.past_consensus.get(&prepared_round) { // We have seen consensus on the data, get the value - let our_data = self - .data - .get(&our_record.hash) - .expect("Data must exist") - .clone(); + let our_data = self.data.get(hash).expect("Data must exist").clone(); // Verify the data matches what we saw if prepared_data == our_data { // We agree with the prepared data - use it - return Some((our_record.hash, our_data)); + return Some((*hash, our_data)); } } } @@ -408,21 +398,14 @@ where // Move the state forward since we have a prepare quorum self.state = InstanceState::Commit; - // Record this prepare consensus. Fetch all of the preapre messages for the data and then - // record the consensus for them - let prepare_messages: Vec = self - .prepare_container - .get_messages_for_value(round, hash) - .into_iter() - .cloned() - .collect(); - - let record = ConsensusRecord { - round, - hash, - prepare_messages, - }; - self.past_consensus.insert(round, record); + // Record this prepare consensus + // todo!() may need to record all of the prepare messages for the hash and save that + // too, used for justifications + self.past_consensus.insert(round, hash); + + // Record as last prepared value and round + self.last_prepared_value = Some(hash); + self.last_prepared_round = Some(self.current_round); // Send a commit message for the prepare quorum data self.send_commit(hash); @@ -499,6 +482,7 @@ where round = *round, "Round change quorum reached" ); + self.set_round(round); } else { let num_messages_for_round = @@ -520,16 +504,20 @@ where self.completed = Some(Completed::TimedOut); return; }; + if next_round.get() > self.config.max_rounds() { self.state = InstanceState::Complete; self.completed = Some(Completed::TimedOut); return; } - // Get the data to send with the round change? - self.send_round_change(self.start_data_hash); // Start a new round - self.set_round(next_round); + self.current_round.set(next_round); + // Check if we have a prepared value, if so we want to send a round change proposing the + // value. Else, send a blank hash + let hash = self.last_prepared_value.unwrap_or_default(); + self.send_round_change(hash); + self.start_round(); } // Construct a new unsigned message. This will be passed to the processor to be signed and then @@ -546,7 +534,9 @@ where round: self.current_round.get() as u64, identifier: self.identifier.clone(), root: data_hash as Hash256, - data_round: self.current_round.get() as u64, + data_round: self + .last_prepared_round + .map_or(0, |round| round.get() as u64), round_change_justification: vec![], // Empty for MVP prepare_justification: vec![], // Empty for MVP }; @@ -560,7 +550,7 @@ where // Wrap in unsigned SSV message UnsignedSSVMessage { ssv_message, - full_data: self.data.get(&data_hash).unwrap().clone(), + full_data: self.data.get(&data_hash).unwrap_or(&Vec::new()).clone(), } } diff --git a/anchor/common/qbft/src/msg_container.rs b/anchor/common/qbft/src/msg_container.rs index b917d0c5..3760192a 100644 --- a/anchor/common/qbft/src/msg_container.rs +++ b/anchor/common/qbft/src/msg_container.rs @@ -89,12 +89,4 @@ impl> MessageContainer { }) .unwrap_or_default() } - - /// Get all of the messages for the round and hash - pub fn get_messages_for_value(&self, round: Round, value: Hash256) -> Vec<&M> { - self.messages - .get(&round) - .map(|msgs| msgs.values().filter(|msg| msg.hash() == value).collect()) - .unwrap_or_default() - } } diff --git a/anchor/validator_store/src/lib.rs b/anchor/validator_store/src/lib.rs index 76e2895e..9d4ab741 100644 --- a/anchor/validator_store/src/lib.rs +++ b/anchor/validator_store/src/lib.rs @@ -214,6 +214,7 @@ impl AnchorValidatorStore { BeaconBlock::Capella(_) => DATA_VERSION_CAPELLA, BeaconBlock::Deneb(_) => DATA_VERSION_DENEB, BeaconBlock::Electra(_) => DATA_VERSION_UNKNOWN, + BeaconBlock::Fulu(_) => DATA_VERSION_UNKNOWN, // todo! fulu is now upstream }, data_ssz: wrapped_block.as_ssz_bytes(), }, @@ -741,7 +742,7 @@ impl ValidatorStore for AnchorValidatorStore { .await .map_err(SpecificError::from)?; // todo SSZ deser - let _data = match completed { + let data = match completed { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, }; From 9358829628047ef8bdab532b3d404287616028c7 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 22 Jan 2025 16:50:25 +0000 Subject: [PATCH 12/45] qbft ssz and start test integration --- anchor/common/qbft/src/lib.rs | 9 +- anchor/common/qbft/src/tests.rs | 119 +++++++++++++++++------ anchor/common/ssv_types/src/consensus.rs | 71 +++++++++++++- anchor/validator_store/src/lib.rs | 2 +- 4 files changed, 161 insertions(+), 40 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index f7f430c6..3d2cebde 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -1,7 +1,6 @@ use crate::msg_container::MessageContainer; use ssv_types::consensus::{Data, QbftMessage, QbftMessageType, UnsignedSSVMessage}; use ssv_types::message::{MessageID, MsgType, SSVMessage}; -use ssv_types::msgid::MsgId; use ssv_types::OperatorId; use ssz::Encode; use std::collections::HashMap; @@ -45,7 +44,7 @@ where /// The initial configuration used to establish this instance of QBFT. config: Config, /// The identification of this QBFT instance - identifier: MsgId, + identifier: MessageID, /// The instance height acts as an ID for the current instance and helps distinguish it from /// other instances. instance_height: InstanceHeight, @@ -95,7 +94,7 @@ where let mut qbft = Qbft { config, - identifier: MsgId([0; 56]), + identifier: MessageID::new([0; 56]), instance_height, start_data_hash: start_data.hash(), @@ -528,7 +527,7 @@ where data_hash: D::Hash, ) -> UnsignedSSVMessage { // Create the QBFT message - let _qbft_mesage = QbftMessage { + let qbft_message = QbftMessage { qbft_message_type: msg_type, height: *self.instance_height as u64, round: self.current_round.get() as u64, @@ -544,7 +543,7 @@ where let ssv_message = SSVMessage::new( MsgType::SSVConsensusMsgType, MessageID::new([0; 56]), - vec![], // qbft_message.as_ssz_bytes() + qbft_message.as_ssz_bytes(), ); // Wrap in unsigned SSV message diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index 17aed3af..af72acca 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -2,21 +2,82 @@ //! //! These test individual components and also provide full end-to-end tests of the entire protocol. -/* use super::*; -use crate::validation::{validate_data, ValidatedData}; use qbft_types::DefaultLeaderFunction; +use ssv_types::consensus::UnsignedSSVMessage; +use ssv_types::message::SignedSSVMessage; +use ssv_types::OperatorId; +use ssz::Encode; use std::cell::RefCell; -use ssz::{Encode, Decode}; use std::collections::{HashSet, VecDeque}; use std::rc::Rc; use tracing_subscriber::filter::EnvFilter; +use types::Hash256; // HELPER FUNCTIONS FOR TESTS /// Enable debug logging for tests const ENABLE_TEST_LOGGING: bool = true; +/// Test data structure that implements the Data trait +#[derive(Debug, Clone, Default)] +struct TestData(usize); + +impl Encode for TestData { + fn is_ssz_fixed_len() -> bool { + true + } + + fn ssz_append(&self, buf: &mut Vec) { + buf.extend_from_slice(&self.0.to_le_bytes()); + } + + fn ssz_fixed_len() -> usize { + std::mem::size_of::() + } + + fn ssz_bytes_len(&self) -> usize { + std::mem::size_of::() + } +} + +impl Data for TestData { + type Hash = Hash256; + + fn hash(&self) -> Self::Hash { + let mut result = [0u8; 32]; + let bytes = self.0.to_le_bytes(); + result[..bytes.len()].copy_from_slice(&bytes); + Hash256::from(result) + } +} + +fn convert_unsigned_to_wrapped( + msg: UnsignedSSVMessage, + operator_id: OperatorId, +) -> WrappedQbftMessage { + // Create a signed message containing just this operator + let _signed_message = SignedSSVMessage::new( + vec![vec![0; 96]], // Test signature of 96 bytes + vec![*operator_id], + msg.ssv_message, + msg.full_data, + ) + .expect("Should create signed message"); + + /* + // Parse the QBFT message from the SSV message data + let qbft_message = QbftMessage::from_ssz_bytes(&msg.ssv_message.data()) + .expect("Should decode QBFT message"); + + WrappedQbftMessage { + signed_message, + qbft_message, + } + */ + todo!() +} + /// A struct to help build and initialise a test of running instances struct TestQBFTCommitteeBuilder { /// The configuration to use for all the instances. @@ -39,9 +100,9 @@ impl Default for TestQBFTCommitteeBuilder { impl TestQBFTCommitteeBuilder { /// Consumes self and runs a test scenario. This returns a [`TestQBFTCommittee`] which /// represents a running quorum. - pub fn run(self, data: D) -> TestQBFTCommittee)> + pub fn run(self, data: D) -> TestQBFTCommittee where - D: Default + Data, + D: Default + Data + Encode, { if ENABLE_TEST_LOGGING { let env_filter = EnvFilter::new("debug"); @@ -50,21 +111,17 @@ impl TestQBFTCommitteeBuilder { .with_env_filter(env_filter) .init(); } - - // Validate the data - let validated_data = validate_data(data).unwrap(); - - construct_and_run_committee(self.config, validated_data) + construct_and_run_committee(self.config, data) } } /// A testing structure representing a committee of running instances #[allow(clippy::type_complexity)] -struct TestQBFTCommittee)> { - msg_queue: Rc)>>>, +struct TestQBFTCommittee + 'static + Encode, S: FnMut(Message)> { + msg_queue: Rc>>, instances: HashMap>, // All of the instances that are currently active, allows us to stop/restart instances by - // controlling the messages being send and received + // controlling the messages being sent and received active_instances: HashSet, } @@ -72,10 +129,10 @@ struct TestQBFTCommittee)> { /// /// This will create instances and spawn them in a task and return the sender/receiver channels for /// all created instances. -fn construct_and_run_committee( +fn construct_and_run_committee + Default + 'static + Encode>( mut config: ConfigBuilder, - validated_data: ValidatedData, -) -> TestQBFTCommittee)> { + validated_data: D, +) -> TestQBFTCommittee { // The ID of a committee is just an integer in [0,committee_size) let msg_queue = Rc::new(RefCell::new(VecDeque::new())); @@ -84,7 +141,7 @@ fn construct_and_run_committee( for id in 0..config.committee_members().len() { let msg_queue = Rc::clone(&msg_queue); - let id = OperatorId::from(id); + let id = OperatorId::from(id as u64); // Creates a new instance config = config.with_operator_id(id); let instance = Qbft::new( @@ -103,7 +160,7 @@ fn construct_and_run_committee( } } -impl)> TestQBFTCommittee { +impl + Encode, S: FnMut(Message)> TestQBFTCommittee { fn wait_until_end(mut self) -> i32 { loop { let msg = self.msg_queue.borrow_mut().pop_front(); @@ -113,7 +170,7 @@ impl)> TestQBFTCommittee { for id in self.active_instances.iter() { let instance = self.instances.get_mut(id).expect("Instance exists"); // Check if this instance just reached consensus - if matches!(instance.completed(), Some(Completed::Success(_))) { + if matches!(instance.completed, Some(Completed::Success(_))) { num_consensus += 1; } } @@ -124,7 +181,16 @@ impl)> TestQBFTCommittee { for id in self.active_instances.iter() { if *id != sender { let instance = self.instances.get_mut(id).expect("Instance exists"); - instance.receive(msg.clone()); + // get the unsigned message and the sender + let (_, unsigned) = match msg { + Message::Propose(o, ref u) + | Message::Prepare(o, ref u) + | Message::Commit(o, ref u) + | Message::RoundChange(o, ref u) => (o, u), + }; + + let wrapped = convert_unsigned_to_wrapped(unsigned.clone(), sender); + instance.receive(wrapped); } } } @@ -141,17 +207,6 @@ impl)> TestQBFTCommittee { } } -#[derive(Debug, Copy, Clone, Default)] -struct TestData(usize); - -impl Data for TestData { - type Hash = usize; - - fn hash(&self) -> Self::Hash { - self.0 - } -} - #[test] // Construct and run a test committee fn test_basic_committee() { @@ -188,6 +243,7 @@ fn test_node_recovery() { assert_eq!(num_consensus, 5); // Should reach full consensus after recovery } +/* #[test] fn test_duplicate_proposals() { let mut test_instance = TestQBFTCommitteeBuilder::default().run(TestData(42)); @@ -201,6 +257,7 @@ fn test_duplicate_proposals() { }, ); + // Send the same message multiple times for id in 0..5 { let instance = test_instance diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index 307a8428..f6080f0e 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -1,5 +1,4 @@ use crate::message::*; -use crate::msgid::MsgId; use crate::{OperatorId, ValidatorIndex}; use sha2::{Digest, Sha256}; use ssz::{Decode, DecodeError, Encode}; @@ -44,12 +43,12 @@ pub struct UnsignedSSVMessage { } /// A QBFT specific message -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Encode)] pub struct QbftMessage { pub qbft_message_type: QbftMessageType, pub height: u64, pub round: u64, - pub identifier: MsgId, + pub identifier: MessageID, pub root: Hash256, pub data_round: u64, pub round_change_justification: Vec, // always without full_data @@ -77,6 +76,72 @@ pub enum QbftMessageType { RoundChange, } +impl Encode for QbftMessageType { + // QbftMessageType is represented as a fixed-length u64 + fn is_ssz_fixed_len() -> bool { + true + } + + // Append the bytes representation of the enum variant + fn ssz_append(&self, buf: &mut Vec) { + // Convert enum variant to u64 and append bytes + let value: u64 = match self { + QbftMessageType::Proposal => 0, + QbftMessageType::Prepare => 1, + QbftMessageType::Commit => 2, + QbftMessageType::RoundChange => 3, + }; + buf.extend_from_slice(&value.to_le_bytes()); + } + + // Fixed length is 8 bytes (size of u64) + fn ssz_fixed_len() -> usize { + 8 + } + + // Actual length is always 8 bytes + fn ssz_bytes_len(&self) -> usize { + 8 + } +} + +impl Decode for QbftMessageType { + // QbftMessageType is always fixed length + fn is_ssz_fixed_len() -> bool { + true + } + + // Fixed length is 8 bytes (size of u64) + fn ssz_fixed_len() -> usize { + 8 + } + + // Convert bytes back into enum variant + fn from_ssz_bytes(bytes: &[u8]) -> Result { + // Verify we have exactly 8 bytes + if bytes.len() != 8 { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected: 8, + }); + } + + // Convert bytes to u64 + let mut array = [0u8; 8]; + array.copy_from_slice(bytes); + let value = u64::from_le_bytes(array); + + // Convert value back to enum variant + match value { + 0 => Ok(QbftMessageType::Proposal), + 1 => Ok(QbftMessageType::Prepare), + 2 => Ok(QbftMessageType::Commit), + 3 => Ok(QbftMessageType::RoundChange), + _ => Err(DecodeError::NoMatchingVariant), + } + } +} + // A partial signature specific message #[derive(Clone, Debug)] pub struct PartialSignatureMessage { diff --git a/anchor/validator_store/src/lib.rs b/anchor/validator_store/src/lib.rs index 9d4ab741..86f63651 100644 --- a/anchor/validator_store/src/lib.rs +++ b/anchor/validator_store/src/lib.rs @@ -742,7 +742,7 @@ impl ValidatorStore for AnchorValidatorStore { .await .map_err(SpecificError::from)?; // todo SSZ deser - let data = match completed { + let _data = match completed { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, }; From 6c35627289277372b344c10ef5add0e9a9b39b16 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 22 Jan 2025 18:31:04 +0000 Subject: [PATCH 13/45] bugfixes with hashing --- anchor/common/qbft/src/lib.rs | 40 ++++++++++++++++++------ anchor/common/qbft/src/tests.rs | 27 ++++++++-------- anchor/common/ssv_types/src/consensus.rs | 2 +- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 3d2cebde..466765ef 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -6,6 +6,7 @@ use ssz::Encode; use std::collections::HashMap; use tracing::{debug, error, warn}; use types::Hash256; +use sha2::{Digest, Sha256}; // Re-Exports for Manager pub use config::{Config, ConfigBuilder}; @@ -92,12 +93,18 @@ where let current_round = config.round(); let quorum_size = config.quorum_size(); + let start_data_ssz = start_data.as_ssz_bytes(); + let mut hasher = Sha256::new(); + hasher.update(start_data_ssz); + let hash: [u8; 32] = hasher.finalize().into(); + let start_data_hash = Hash256::from(hash); + let mut qbft = Qbft { config, identifier: MessageID::new([0; 56]), instance_height, - start_data_hash: start_data.hash(), + start_data_hash, start_data, data: HashMap::new(), current_round, @@ -253,7 +260,6 @@ where // Check if we are the leader if self.check_leader(&self.config.operator_id()) { // We are the leader - debug!("Current leader"); // Check justification of round change quorum. If there is a justification, we will use // that data. Otherwise, use the initial state data @@ -261,8 +267,14 @@ where .justify_round_change_quorum() .unwrap_or_else(|| (self.start_data_hash, self.start_data.as_ssz_bytes())); - // Send the initial proposal + debug!(operator_id = ?self.config.operator_id(), hash = ?data_hash, data = ?data, "Current leader proposing data"); + + // Send the initial proposal and then the following prepare self.send_proposal(data_hash, data); + self.send_prepare(data_hash); + + // Since we are the leader and send the proposal, switch to prepare state + self.state = InstanceState::Prepare; } } @@ -339,7 +351,7 @@ where return; } - debug!(from = ?operator_id, "PROPOSE received"); + debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "PROPOSE received"); // Store the received propse message if !self @@ -356,11 +368,12 @@ where ); // Update state - self.proposal_accepted_for_current_round = Some(wrapped_msg); + self.proposal_accepted_for_current_round = Some(wrapped_msg.clone()); self.state = InstanceState::Prepare; + debug!(in = ?self.config.operator_id(), state = ?self.state, "State updated to PREPARE"); // Create and send prepare message - self.send_prepare(data_hash); + self.send_prepare(wrapped_msg.qbft_message.root); } /// We have received a prepare message @@ -376,7 +389,7 @@ where return; } - debug!(from = ?operator_id, "PREPARE received"); + debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "PREPARE received"); // Store the prepare message if !self @@ -394,8 +407,10 @@ where return; } + // Move the state forward since we have a prepare quorum self.state = InstanceState::Commit; + debug!(in = ?self.config.operator_id(), state = ?self.state, "Reached a PREPARE consensus. State updated to COMMIT"); // Record this prepare consensus // todo!() may need to record all of the prepare messages for the hash and save that @@ -418,13 +433,19 @@ where round: Round, wrapped_msg: WrappedQbftMessage, ) { + + // If we are already done, ignore + if self.completed.is_some() { + return; + } + // Make sure that we are in the correct state if (self.state as u8) >= (InstanceState::SentRoundChange as u8) { warn!(from=*operator_id, ?self.state, "COMMIT message while in invalid state"); return; } - debug!(from = ?operator_id, "COMMIT received"); + debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "COMMIT received"); // Store the received commit message if !self @@ -441,6 +462,7 @@ where // value self.state = InstanceState::Complete; self.completed = Some(Completed::Success(hash)); + debug!(in = ?self.config.operator_id(), state = ?self.state, "Reached a COMMIT consensus. Success!"); } } } @@ -458,7 +480,7 @@ where return; } - debug!(from = ?operator_id, "ROUNDCHANGE received"); + debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "ROUNDCHANGE received"); // Store the round changed message if !self diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index af72acca..b123b68c 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -3,11 +3,12 @@ //! These test individual components and also provide full end-to-end tests of the entire protocol. use super::*; +use sha2::{Digest, Sha256}; use qbft_types::DefaultLeaderFunction; use ssv_types::consensus::UnsignedSSVMessage; use ssv_types::message::SignedSSVMessage; use ssv_types::OperatorId; -use ssz::Encode; +use ssz::{Encode, Decode}; use std::cell::RefCell; use std::collections::{HashSet, VecDeque}; use std::rc::Rc; @@ -45,10 +46,10 @@ impl Data for TestData { type Hash = Hash256; fn hash(&self) -> Self::Hash { - let mut result = [0u8; 32]; - let bytes = self.0.to_le_bytes(); - result[..bytes.len()].copy_from_slice(&bytes); - Hash256::from(result) + let mut hasher = Sha256::new(); + hasher.update(self.0.to_le_bytes()); + let hash: [u8; 32] = hasher.finalize().into(); + Hash256::from(hash) } } @@ -57,25 +58,22 @@ fn convert_unsigned_to_wrapped( operator_id: OperatorId, ) -> WrappedQbftMessage { // Create a signed message containing just this operator - let _signed_message = SignedSSVMessage::new( + let signed_message = SignedSSVMessage::new( vec![vec![0; 96]], // Test signature of 96 bytes vec![*operator_id], - msg.ssv_message, + msg.ssv_message.clone(), msg.full_data, ) .expect("Should create signed message"); - /* // Parse the QBFT message from the SSV message data - let qbft_message = QbftMessage::from_ssz_bytes(&msg.ssv_message.data()) + let qbft_message = QbftMessage::from_ssz_bytes(msg.ssv_message.data()) .expect("Should decode QBFT message"); WrappedQbftMessage { signed_message, qbft_message, } - */ - todo!() } /// A struct to help build and initialise a test of running instances @@ -88,9 +86,10 @@ impl Default for TestQBFTCommitteeBuilder { fn default() -> Self { TestQBFTCommitteeBuilder { config: ConfigBuilder::new( - 0.into(), + 1.into(), InstanceHeight::default(), - (0..5).map(OperatorId::from).collect(), + (1..6).map(OperatorId::from).collect(), + ), } } @@ -139,7 +138,7 @@ fn construct_and_run_committee + Default + 'static + Enc let mut instances = HashMap::with_capacity(config.committee_members().len()); let mut active_instances = HashSet::new(); - for id in 0..config.committee_members().len() { + for id in 1..config.committee_members().len() + 1 { let msg_queue = Rc::clone(&msg_queue); let id = OperatorId::from(id as u64); // Creates a new instance diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index f6080f0e..25d374dd 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -43,7 +43,7 @@ pub struct UnsignedSSVMessage { } /// A QBFT specific message -#[derive(Clone, Debug, Encode)] +#[derive(Clone, Debug, Encode, Decode)] pub struct QbftMessage { pub qbft_message_type: QbftMessageType, pub height: u64, From f204b20410ef61b6b82a55946b3487ba4acdd582 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 22 Jan 2025 20:57:25 +0000 Subject: [PATCH 14/45] fmt --- anchor/common/qbft/src/lib.rs | 4 +--- anchor/common/qbft/src/tests.rs | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 466765ef..182d3c58 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -1,4 +1,5 @@ use crate::msg_container::MessageContainer; +use sha2::{Digest, Sha256}; use ssv_types::consensus::{Data, QbftMessage, QbftMessageType, UnsignedSSVMessage}; use ssv_types::message::{MessageID, MsgType, SSVMessage}; use ssv_types::OperatorId; @@ -6,7 +7,6 @@ use ssz::Encode; use std::collections::HashMap; use tracing::{debug, error, warn}; use types::Hash256; -use sha2::{Digest, Sha256}; // Re-Exports for Manager pub use config::{Config, ConfigBuilder}; @@ -407,7 +407,6 @@ where return; } - // Move the state forward since we have a prepare quorum self.state = InstanceState::Commit; debug!(in = ?self.config.operator_id(), state = ?self.state, "Reached a PREPARE consensus. State updated to COMMIT"); @@ -433,7 +432,6 @@ where round: Round, wrapped_msg: WrappedQbftMessage, ) { - // If we are already done, ignore if self.completed.is_some() { return; diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index b123b68c..be043a14 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -3,12 +3,12 @@ //! These test individual components and also provide full end-to-end tests of the entire protocol. use super::*; -use sha2::{Digest, Sha256}; use qbft_types::DefaultLeaderFunction; +use sha2::{Digest, Sha256}; use ssv_types::consensus::UnsignedSSVMessage; use ssv_types::message::SignedSSVMessage; use ssv_types::OperatorId; -use ssz::{Encode, Decode}; +use ssz::{Decode, Encode}; use std::cell::RefCell; use std::collections::{HashSet, VecDeque}; use std::rc::Rc; @@ -67,8 +67,8 @@ fn convert_unsigned_to_wrapped( .expect("Should create signed message"); // Parse the QBFT message from the SSV message data - let qbft_message = QbftMessage::from_ssz_bytes(msg.ssv_message.data()) - .expect("Should decode QBFT message"); + let qbft_message = + QbftMessage::from_ssz_bytes(msg.ssv_message.data()).expect("Should decode QBFT message"); WrappedQbftMessage { signed_message, @@ -89,7 +89,6 @@ impl Default for TestQBFTCommitteeBuilder { 1.into(), InstanceHeight::default(), (1..6).map(OperatorId::from).collect(), - ), } } From 38364747fd63f4faab9638ceef2dc62fff9170c5 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 22 Jan 2025 21:15:06 +0000 Subject: [PATCH 15/45] adjust test to send to self --- Cargo.lock | 30 +++---- anchor/common/qbft/src/lib.rs | 6 +- anchor/common/qbft/src/tests.rs | 146 ++++---------------------------- 3 files changed, 33 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc462145..552ff7bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,9 +123,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a725039ef382d1b6b4e2ebcb15b1efff6cde9af48c47a1bdce6fb67b9456c34b" +checksum = "4ab9d1367c6ffb90c93fb4a9a4989530aa85112438c6f73a734067255d348469" dependencies = [ "alloy-primitives", "num_enum", @@ -976,7 +976,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 0.38.43", + "rustix 0.38.44", "slab", "tracing", "windows-sys 0.59.0", @@ -1770,9 +1770,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-bigint" @@ -2210,9 +2210,9 @@ dependencies = [ [[package]] name = "discv5" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898d136ecb64116ec68aecf14d889bd30f8b1fe0c19e262953f7388dbe77052e" +checksum = "c4b4e7798d2ff74e29cee344dc490af947ae657d6ab5273dde35d58ce06a4d71" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -3880,13 +3880,13 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5454,7 +5454,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.43", + "rustix 0.38.44", "tracing", "windows-sys 0.59.0", ] @@ -6310,9 +6310,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.43" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.8.0", "errno", @@ -7253,7 +7253,7 @@ dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 0.38.43", + "rustix 0.38.44", "windows-sys 0.59.0", ] @@ -7274,7 +7274,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ - "rustix 0.38.43", + "rustix 0.38.44", "windows-sys 0.59.0", ] diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 182d3c58..19910b53 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -324,13 +324,13 @@ where ) { // Make sure that we are actually waiting for a proposal if !matches!(self.state, InstanceState::AwaitingProposal) { - warn!(from=?operator_id, ?self.state, "PROPOSE message while in invalid state"); + warn!(from=?operator_id, self=?self.config.operator_id(), ?self.state, "PROPOSE message while in invalid state"); return; } // Check if proposal is from the leader we expect if !self.check_leader(&operator_id) { - warn!(from = ?operator_id, "PROPOSE message from non-leader"); + warn!(from = ?operator_id, self=?self.config.operator_id(), "PROPOSE message from non-leader"); return; } @@ -347,7 +347,7 @@ where // Verify that the fulldata matches the data root of the qbft message let data_hash = wrapped_msg.signed_message.hash_fulldata(); if data_hash != wrapped_msg.qbft_message.root { - warn!(from = ?operator_id, "Data roots do not match"); + warn!(from = ?operator_id, self=?self.config.operator_id(), "Data roots do not match"); return; } diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index be043a14..07c5186a 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -177,19 +177,19 @@ impl + Encode, S: FnMut(Message)> TestQBFTComm // Only recieve messages for active instances for id in self.active_instances.iter() { - if *id != sender { - let instance = self.instances.get_mut(id).expect("Instance exists"); - // get the unsigned message and the sender - let (_, unsigned) = match msg { - Message::Propose(o, ref u) - | Message::Prepare(o, ref u) - | Message::Commit(o, ref u) - | Message::RoundChange(o, ref u) => (o, u), - }; - - let wrapped = convert_unsigned_to_wrapped(unsigned.clone(), sender); - instance.receive(wrapped); - } + // We do not make sure that id != sender since we want to loop back and recieve our + // own messages + let instance = self.instances.get_mut(id).expect("Instance exists"); + // get the unsigned message and the sender + let (_, unsigned) = match msg { + Message::Propose(o, ref u) + | Message::Prepare(o, ref u) + | Message::Commit(o, ref u) + | Message::RoundChange(o, ref u) => (o, u), + }; + + let wrapped = convert_unsigned_to_wrapped(unsigned.clone(), sender); + instance.receive(wrapped); } } } @@ -232,127 +232,11 @@ fn test_node_recovery() { let mut test_instance = TestQBFTCommitteeBuilder::default().run(TestData(42)); // Pause a node - test_instance.pause_instance(&OperatorId::from(0)); + test_instance.pause_instance(&OperatorId::from(2)); // Then restart it - test_instance.restart_instance(&OperatorId::from(0)); + test_instance.restart_instance(&OperatorId::from(2)); let num_consensus = test_instance.wait_until_end(); assert_eq!(num_consensus, 5); // Should reach full consensus after recovery } - -/* -#[test] -fn test_duplicate_proposals() { - let mut test_instance = TestQBFTCommitteeBuilder::default().run(TestData(42)); - - // Send duplicate propose messages - let msg = Message::Propose( - OperatorId::from(0), - ConsensusData { - round: Round::default(), - data: TestData(42), - }, - ); - - - // Send the same message multiple times - for id in 0..5 { - let instance = test_instance - .instances - .get_mut(&OperatorId::from(id)) - .unwrap(); - instance.receive(msg.clone()); - instance.receive(msg.clone()); - instance.receive(msg.clone()); - } - - let num_consensus = test_instance.wait_until_end(); - assert_eq!(num_consensus, 5); // Should still reach consensus despite duplicates -} - -#[test] -fn test_invalid_sender() { - let mut test_instance = TestQBFTCommitteeBuilder::default().run(TestData(42)); - - // Create a message from an invalid sender (operator id 10 which isn't in the committee) - let invalid_msg = Message::Propose( - OperatorId::from(10), - ConsensusData { - round: Round::default(), - data: TestData(42), - }, - ); - - // Send to a valid instance - let instance = test_instance - .instances - .get_mut(&OperatorId::from(0)) - .unwrap(); - instance.receive(invalid_msg); - - let num_consensus = test_instance.wait_until_end(); - assert_eq!(num_consensus, 5); // Should ignore invalid sender and still reach consensus -} - -#[test] -fn test_proposal_from_non_leader() { - let mut test_instance = TestQBFTCommitteeBuilder::default().run(TestData(42)); - - // Send proposal from non-leader (node 1) - let non_leader_msg = Message::Propose( - OperatorId::from(1), - ConsensusData { - round: Round::default(), - data: TestData(42), - }, - ); - - // Send to all instances - for instance in test_instance.instances.values_mut() { - instance.receive(non_leader_msg.clone()); - } - - let num_consensus = test_instance.wait_until_end(); - assert_eq!(num_consensus, 5); // Should ignore non-leader proposal and still reach consensus -} - -#[test] -fn test_invalid_round_messages() { - let mut test_instance = TestQBFTCommitteeBuilder::default().run(TestData(42)); - - // Create a message with an invalid round number - let future_round = Round::default().next().unwrap().next().unwrap(); // Round 3 - let invalid_round_msg = Message::Prepare( - OperatorId::from(0), - ConsensusData { - round: future_round, - data: 42, - }, - ); - - // Send to all instances - for instance in test_instance.instances.values_mut() { - instance.receive(invalid_round_msg.clone()); - } - - let num_consensus = test_instance.wait_until_end(); - assert_eq!(num_consensus, 5); // Should ignore invalid round messages and still reach consensus -} - -#[test] -fn test_round_change_timeout() { - let mut test_instance = TestQBFTCommitteeBuilder::default().run(TestData(42)); - - // Pause the leader node to force a round change - test_instance.pause_instance(&OperatorId::from(0)); - - // Manually trigger round changes in all instances - for instance in test_instance.instances.values_mut() { - instance.end_round(); - } - - let num_consensus = test_instance.wait_until_end(); - assert_eq!(num_consensus, 4); // Should reach consensus with new leader -} -*/ From bf93baa278bd0ec53ed9ea8e930e086f12a12313 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 22 Jan 2025 23:09:32 +0000 Subject: [PATCH 16/45] add identifier and decode --- anchor/common/qbft/src/lib.rs | 2 +- anchor/common/ssv_types/src/consensus.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 19910b53..1865be90 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -562,7 +562,7 @@ where let ssv_message = SSVMessage::new( MsgType::SSVConsensusMsgType, - MessageID::new([0; 56]), + self.identifier.clone(), qbft_message.as_ssz_bytes(), ); diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index 25d374dd..936cc4f0 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -337,7 +337,7 @@ pub enum DataSsz { Contributions(VariableList, U13>), } -#[derive(Clone, Debug, TreeHash, Encode)] +#[derive(Clone, Debug, TreeHash, Encode, Decode)] pub struct Contribution { pub selection_proof_sig: Signature, pub contribution: SyncCommitteeContribution, From 1cf7b88c2ea0c588a8b110ec40c21d58971b95fb Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Thu, 23 Jan 2025 15:24:34 +0000 Subject: [PATCH 17/45] data_ssz decoding, switch over to generic D --- anchor/common/qbft/src/lib.rs | 32 +++++--------- anchor/common/qbft/src/tests.rs | 26 ++++++++--- anchor/common/ssv_types/src/cluster.rs | 16 ++++++- anchor/common/ssv_types/src/consensus.rs | 37 +++++++++++++++- anchor/qbft_manager/src/lib.rs | 20 ++++----- anchor/validator_store/src/lib.rs | 56 ++++++++++++------------ 6 files changed, 120 insertions(+), 67 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 1865be90..53e46b89 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -3,7 +3,7 @@ use sha2::{Digest, Sha256}; use ssv_types::consensus::{Data, QbftMessage, QbftMessageType, UnsignedSSVMessage}; use ssv_types::message::{MessageID, MsgType, SSVMessage}; use ssv_types::OperatorId; -use ssz::Encode; +use ssz::{Decode, Encode}; use std::collections::HashMap; use tracing::{debug, error, warn}; use types::Hash256; @@ -39,7 +39,7 @@ mod tests; pub struct Qbft where F: LeaderFunction + Clone, - D: Data + Encode, + D: Data + Encode + Decode, S: FnMut(Message), { /// The initial configuration used to establish this instance of QBFT. @@ -55,7 +55,7 @@ where /// Initial data that we will propose if we are the leader. start_data: D, /// All of the data that we have seen - data: HashMap>, + data: HashMap, /// The current round this instance state is in.a current_round: Round, /// The current state of the instance @@ -84,7 +84,7 @@ where impl Qbft where F: LeaderFunction + Clone, - D: Data + Encode, + D: Data + Encode + Decode, S: FnMut(Message), { // Construct a new QBFT Instance and start the first round @@ -210,7 +210,7 @@ where /// If there is no past consensus data in the round change quorum or we disagree with quorum set /// this function will return None, and we obtain the data as if we were beginning this /// instance. - fn justify_round_change_quorum(&self) -> Option<(Hash256, Vec)> { + fn justify_round_change_quorum(&self) -> Option<(Hash256, D)> { // Get all round change messages for the current round let round_change_messages = self .round_change_container @@ -230,19 +230,13 @@ where // If we found a message with prepared data if let Some(highest_msg) = highest_prepared { // Get the prepared data from the message - let prepared_data = highest_msg.signed_message.full_data(); let prepared_round = Round::from(highest_msg.qbft_message.data_round); // Verify we have also seen this consensus if let Some(hash) = self.past_consensus.get(&prepared_round) { // We have seen consensus on the data, get the value let our_data = self.data.get(hash).expect("Data must exist").clone(); - - // Verify the data matches what we saw - if prepared_data == our_data { - // We agree with the prepared data - use it - return Some((*hash, our_data)); - } + return Some((*hash, our_data)); } } @@ -265,7 +259,7 @@ where // that data. Otherwise, use the initial state data let (data_hash, data) = self .justify_round_change_quorum() - .unwrap_or_else(|| (self.start_data_hash, self.start_data.as_ssz_bytes())); + .unwrap_or_else(|| (self.start_data_hash, self.start_data.clone())); debug!(operator_id = ?self.config.operator_id(), hash = ?data_hash, data = ?data, "Current leader proposing data"); @@ -361,11 +355,9 @@ where warn!(from = ?operator_id, "PROPOSE message is a duplicate") } + let data = D::from_ssz_bytes(wrapped_msg.signed_message.full_data()).unwrap(); // Store the data - self.data.insert( - data_hash, - wrapped_msg.signed_message.full_data().to_vec().clone(), - ); + self.data.insert(data_hash, data); // Update state self.proposal_accepted_for_current_round = Some(wrapped_msg.clone()); @@ -569,12 +561,12 @@ where // Wrap in unsigned SSV message UnsignedSSVMessage { ssv_message, - full_data: self.data.get(&data_hash).unwrap_or(&Vec::new()).clone(), + full_data: self.data.get(&data_hash).as_ssz_bytes(), } } // Send a new qbft proposal message - fn send_proposal(&mut self, hash: D::Hash, data: Vec) { + fn send_proposal(&mut self, hash: D::Hash, data: D) { // Store the data we're proposing self.data.insert(hash, data.clone()); @@ -619,7 +611,7 @@ where } /// Extract the data that the instance has come to consensus on - pub fn completed(&self) -> Option>> { + pub fn completed(&self) -> Option> { self.completed .clone() .and_then(|completed| match completed { diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index 07c5186a..7cad5270 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -8,7 +8,7 @@ use sha2::{Digest, Sha256}; use ssv_types::consensus::UnsignedSSVMessage; use ssv_types::message::SignedSSVMessage; use ssv_types::OperatorId; -use ssz::{Decode, Encode}; +use ssz::{Decode, Encode, DecodeError}; use std::cell::RefCell; use std::collections::{HashSet, VecDeque}; use std::rc::Rc; @@ -42,6 +42,22 @@ impl Encode for TestData { } } +impl Decode for TestData { + fn is_ssz_fixed_len() -> bool { + todo!() + } + + fn ssz_fixed_len() -> usize { + todo!() + } + + fn from_ssz_bytes(_bytes: &[u8]) -> Result { + todo!() + } +} + + + impl Data for TestData { type Hash = Hash256; @@ -100,7 +116,7 @@ impl TestQBFTCommitteeBuilder { /// represents a running quorum. pub fn run(self, data: D) -> TestQBFTCommittee where - D: Default + Data + Encode, + D: Default + Data + Encode + Decode, { if ENABLE_TEST_LOGGING { let env_filter = EnvFilter::new("debug"); @@ -115,7 +131,7 @@ impl TestQBFTCommitteeBuilder { /// A testing structure representing a committee of running instances #[allow(clippy::type_complexity)] -struct TestQBFTCommittee + 'static + Encode, S: FnMut(Message)> { +struct TestQBFTCommittee + 'static + Encode + Decode, S: FnMut(Message)> { msg_queue: Rc>>, instances: HashMap>, // All of the instances that are currently active, allows us to stop/restart instances by @@ -127,7 +143,7 @@ struct TestQBFTCommittee + 'static + Encode, S /// /// This will create instances and spawn them in a task and return the sender/receiver channels for /// all created instances. -fn construct_and_run_committee + Default + 'static + Encode>( +fn construct_and_run_committee + Default + 'static + Encode + Decode>( mut config: ConfigBuilder, validated_data: D, ) -> TestQBFTCommittee { @@ -158,7 +174,7 @@ fn construct_and_run_committee + Default + 'static + Enc } } -impl + Encode, S: FnMut(Message)> TestQBFTCommittee { +impl + Encode + Decode, S: FnMut(Message)> TestQBFTCommittee { fn wait_until_end(mut self) -> i32 { loop { let msg = self.msg_queue.borrow_mut().pop_front(); diff --git a/anchor/common/ssv_types/src/cluster.rs b/anchor/common/ssv_types/src/cluster.rs index 14711914..85ed29ee 100644 --- a/anchor/common/ssv_types/src/cluster.rs +++ b/anchor/common/ssv_types/src/cluster.rs @@ -1,6 +1,6 @@ use crate::OperatorId; use derive_more::{Deref, From}; -use ssz::Encode; +use ssz::{Decode, DecodeError, Encode}; use std::collections::HashSet; use types::{Address, Graffiti, PublicKey}; @@ -61,6 +61,20 @@ impl Encode for ValidatorIndex { } } +impl Decode for ValidatorIndex { + fn is_ssz_fixed_len() -> bool { + todo!() + } + + fn ssz_fixed_len() -> usize { + todo!() + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + todo!() + } +} + /// General Metadata about a Validator #[derive(Debug, Clone)] pub struct ValidatorMetadata { diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index 936cc4f0..10a8fe03 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -11,6 +11,7 @@ use types::typenum::U13; use types::{ AggregateAndProof, BeaconBlock, BlindedBeaconBlock, Checkpoint, CommitteeIndex, EthSpec, Hash256, PublicKeyBytes, Signature, Slot, SyncCommitteeContribution, VariableList, + AggregateAndProofBase, AggregateAndProofElectra }; // UnsignedSSVMessage @@ -151,7 +152,7 @@ pub struct PartialSignatureMessage { pub validator_index: ValidatorIndex, } -#[derive(Clone, Debug, PartialEq, Encode)] +#[derive(Clone, Debug, PartialEq, Encode, Decode)] pub struct ValidatorConsensusData { pub duty: ValidatorDuty, pub version: DataVersion, @@ -171,7 +172,7 @@ impl Data for ValidatorConsensusData { } } -#[derive(Clone, Debug, TreeHash, PartialEq, Encode)] +#[derive(Clone, Debug, TreeHash, PartialEq, Encode, Decode)] pub struct ValidatorDuty { pub r#type: BeaconRole, pub pub_key: PublicKeyBytes, @@ -337,6 +338,38 @@ pub enum DataSsz { Contributions(VariableList, U13>), } +impl DataSsz { + /// SSZ deserialization that tries all possible variants + pub fn from_ssz_bytes(bytes: &[u8]) -> Result { + // 1. Try BeaconBlock variants first (using fork-aware decoding) + if let Ok(block) = BeaconBlock::any_from_ssz_bytes(bytes) { + return Ok(Self::BeaconBlock(block)); + } + + // 2. Try BlindedBeaconBlock + if let Ok(blinded) = BlindedBeaconBlock::any_from_ssz_bytes(bytes) { + return Ok(Self::BlindedBeaconBlock(blinded)); + } + + // 3. Handle AggregateAndProof variants explicitly + if let Ok(base) = AggregateAndProofBase::::from_ssz_bytes(bytes) { + return Ok(Self::AggregateAndProof(AggregateAndProof::Base(base))); + } + if let Ok(electra) = AggregateAndProofElectra::::from_ssz_bytes(bytes) { + return Ok(Self::AggregateAndProof(AggregateAndProof::Electra(electra))); + } + + // 4. Try Contributions + if let Ok(contributions) = VariableList::, U13>::from_ssz_bytes(bytes) { + return Ok(Self::Contributions(contributions)); + } + + Err(ssz::DecodeError::BytesInvalid( + "Failed to decode as any DataSsz variant".into(), + )) + } +} + #[derive(Clone, Debug, TreeHash, Encode, Decode)] pub struct Contribution { pub selection_proof_sig: Signature, diff --git a/anchor/qbft_manager/src/lib.rs b/anchor/qbft_manager/src/lib.rs index 84549152..327e0089 100644 --- a/anchor/qbft_manager/src/lib.rs +++ b/anchor/qbft_manager/src/lib.rs @@ -9,7 +9,7 @@ use ssv_types::consensus::{BeaconVote, Data, ValidatorConsensusData}; use ssv_types::OperatorId as QbftOperatorId; use ssv_types::{Cluster, ClusterId, OperatorId}; -use ssz::Encode; +use ssz::{Encode, Decode}; use std::fmt::Debug; use std::hash::Hash; use std::sync::Arc; @@ -54,20 +54,20 @@ pub enum ValidatorDutyKind { // Message that is passed around the QbftManager #[derive(Debug)] -pub struct QbftMessage + Encode> { +pub struct QbftMessage + Encode + Decode> { pub kind: QbftMessageKind, pub drop_on_finish: DropOnFinish, } // Type of the QBFT Message #[derive(Debug)] -pub enum QbftMessageKind + Encode> { +pub enum QbftMessageKind + Encode + Decode> { // Initialize a new qbft instance with some initial data, // the configuration for the instance, and a channel to send the final data on Initialize { initial: D, config: qbft::Config, - on_completed: oneshot::Sender>>, + on_completed: oneshot::Sender>, }, // A message received from the network. The network exchanges SignedSsvMessages, but after // deserialziation we dermine the message is for the qbft instance and decode it into a wrapped @@ -124,7 +124,7 @@ impl QbftManager { id: D::Id, initial: D, committee: &Cluster, - ) -> Result>, QbftError> { + ) -> Result, QbftError> { // Tx/Rx pair to send and retrieve the final result let (result_sender, result_receiver) = oneshot::channel(); @@ -200,7 +200,7 @@ impl QbftManager { // Trait that describes any data that is able to be decided upon during a qbft instance pub trait QbftDecidable: - Data + Encode + Send + 'static + Data + Encode + Decode + Send + 'static { type Id: Hash + Eq + Send; @@ -254,7 +254,7 @@ impl QbftDecidable for BeaconVote { } // States that Qbft instance may be in -enum QbftInstance + Encode, S: FnMut(Message)> { +enum QbftInstance + Encode + Decode, S: FnMut(Message)> { // The instance is uninitialized Uninitialized { // todo: proooobably limit this @@ -266,15 +266,15 @@ enum QbftInstance + Encode, S: FnMut(Message)> { Initialized { qbft: Box>, round_end: Interval, - on_completed: Vec>>>, + on_completed: Vec>>, }, // The instance has been decided Decided { - value: Completed>, + value: Completed, }, } -async fn qbft_instance + Encode>( +async fn qbft_instance + Encode + Decode>( mut rx: UnboundedReceiver>, ) { // Signal a new instance that is uninitialized diff --git a/anchor/validator_store/src/lib.rs b/anchor/validator_store/src/lib.rs index 86f63651..c29b01bc 100644 --- a/anchor/validator_store/src/lib.rs +++ b/anchor/validator_store/src/lib.rs @@ -1,4 +1,5 @@ pub mod sync_committee_service; + use dashmap::DashMap; use futures::future::join_all; use parking_lot::Mutex; @@ -17,7 +18,7 @@ use ssv_types::consensus::{ DATA_VERSION_PHASE0, DATA_VERSION_UNKNOWN, }; use ssv_types::{Cluster, OperatorId, ValidatorMetadata}; -use ssz::{Decode, Encode}; +use ssz::Encode; use std::fmt::Debug; use std::marker::PhantomData; use std::sync::Arc; @@ -73,7 +74,7 @@ pub struct AnchorValidatorStore { spec: Arc, genesis_validators_root: Hash256, operator_id: OperatorId, - _eth_spec: PhantomData, + _ethspec: PhantomData, } impl AnchorValidatorStore { @@ -96,7 +97,7 @@ impl AnchorValidatorStore { spec, genesis_validators_root, operator_id, - _eth_spec: PhantomData, + _ethspec: PhantomData, } } @@ -184,7 +185,7 @@ impl AnchorValidatorStore { } let cluster = self.cluster(validator_pubkey)?; - let wrapped_block = wrapper(block.clone()); + let wrapped = wrapper(block.clone()); // first, we have to get to consensus let completed = self @@ -213,20 +214,23 @@ impl AnchorValidatorStore { BeaconBlock::Bellatrix(_) => DATA_VERSION_BELLATRIX, BeaconBlock::Capella(_) => DATA_VERSION_CAPELLA, BeaconBlock::Deneb(_) => DATA_VERSION_DENEB, - BeaconBlock::Electra(_) => DATA_VERSION_UNKNOWN, - BeaconBlock::Fulu(_) => DATA_VERSION_UNKNOWN, // todo! fulu is now upstream + _ => DATA_VERSION_UNKNOWN, }, - data_ssz: wrapped_block.as_ssz_bytes(), + data_ssz: wrapped.as_ssz_bytes(), }, &cluster.cluster, ) .await .map_err(SpecificError::from)?; - let data = match completed { + let completed_data = match completed { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), - Completed::Success(_data) => wrapped_block, + Completed::Success(data) => data, }; - Ok(data) + + let data_ssz = DataSsz::from_ssz_bytes(&completed_data.data_ssz) + .map_err(|_| Error::SpecificError(SpecificError::InvalidQbftData))?; + + Ok(data_ssz) } async fn sign_abstract_block>( @@ -277,14 +281,10 @@ impl AnchorValidatorStore { ) .await .map_err(SpecificError::from)?; - - // Extract the completed value let data = match completed { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, }; - let data = BeaconVote::from_ssz_bytes(&data) - .map_err(|_| Error::SpecificError(SpecificError::InvalidQbftData))?; let domain = self.get_domain(epoch, Domain::SyncCommittee); let signing_root = data.block_root.signing_root(domain); @@ -328,7 +328,6 @@ impl AnchorValidatorStore { Err(_) => return error(SpecificError::TooManySyncSubnetsToSign.into()), }; - let wrapped_contribution = DataSsz::Contributions(data); let completed = self .qbft_manager .decide_instance( @@ -350,19 +349,21 @@ impl AnchorValidatorStore { validator_sync_committee_indices: Default::default(), }, version: DATA_VERSION_PHASE0, - data_ssz: wrapped_contribution.as_ssz_bytes(), + data_ssz: DataSsz::Contributions(data).as_ssz_bytes(), }, &cluster.cluster, ) .await; - // todo SSZ deser - let _data = match completed { + let data = match completed { Ok(Completed::Success(data)) => data, Ok(Completed::TimedOut) => return error(SpecificError::Timeout.into()), Err(err) => return error(SpecificError::QbftError(err).into()), }; - let data = match wrapped_contribution { - DataSsz::Contributions(data) => data, + + let data_ssz = DataSsz::from_ssz_bytes(&data.data_ssz); + + let data = match data_ssz { + Ok(DataSsz::Contributions(data)) => data, _ => return error(SpecificError::InvalidQbftData.into()), }; @@ -625,9 +626,6 @@ impl ValidatorStore for AnchorValidatorStore { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, }; - let data = BeaconVote::from_ssz_bytes(&data) - .map_err(|_| Error::SpecificError(SpecificError::InvalidQbftData))?; - attestation.data_mut().beacon_block_root = data.block_root; attestation.data_mut().source = data.source; attestation.data_mut().target = data.target; @@ -711,7 +709,6 @@ impl ValidatorStore for AnchorValidatorStore { let message = AggregateAndProof::from_attestation(aggregator_index, aggregate, selection_proof); - let wrapped_aggregate_and_proof = DataSsz::AggregateAndProof(message.clone()); // first, we have to get to consensus let completed = self .qbft_manager @@ -735,20 +732,21 @@ impl ValidatorStore for AnchorValidatorStore { validator_sync_committee_indices: Default::default(), }, version: DATA_VERSION_PHASE0, - data_ssz: wrapped_aggregate_and_proof.as_ssz_bytes(), + data_ssz: DataSsz::AggregateAndProof(message).as_ssz_bytes(), }, &cluster.cluster, ) .await .map_err(SpecificError::from)?; - // todo SSZ deser - let _data = match completed { + let data = match completed { Completed::TimedOut => return Err(Error::SpecificError(SpecificError::Timeout)), Completed::Success(data) => data, }; - let message = match wrapped_aggregate_and_proof { - DataSsz::AggregateAndProof(message) => message, + let data_ssz = DataSsz::from_ssz_bytes(&data.data_ssz); + + let message = match data_ssz { + Ok(DataSsz::AggregateAndProof(message)) => message, _ => return Err(Error::SpecificError(SpecificError::InvalidQbftData)), }; From 7d671d7fbbfc54c3dbbfc6f056731f2ec34a2422 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Thu, 23 Jan 2025 17:17:00 +0000 Subject: [PATCH 18/45] bugfix and decode impl --- anchor/common/qbft/src/lib.rs | 22 +++++++++++--- anchor/common/qbft/src/tests.rs | 38 ++++++++++++++++-------- anchor/common/ssv_types/src/cluster.rs | 27 +++++++++++------ anchor/common/ssv_types/src/consensus.rs | 6 ++-- anchor/qbft_manager/src/lib.rs | 2 +- 5 files changed, 65 insertions(+), 30 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 53e46b89..73e2d15a 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -339,6 +339,8 @@ where } // Verify that the fulldata matches the data root of the qbft message + // Data + // data as ssz bytes then hashed let data_hash = wrapped_msg.signed_message.hash_fulldata(); if data_hash != wrapped_msg.qbft_message.root { warn!(from = ?operator_id, self=?self.config.operator_id(), "Data roots do not match"); @@ -352,12 +354,18 @@ where .propose_container .add_message(round, operator_id, &wrapped_msg) { - warn!(from = ?operator_id, "PROPOSE message is a duplicate") + warn!(from = ?operator_id, "PROPOSE message is a duplicate"); + return; } - let data = D::from_ssz_bytes(wrapped_msg.signed_message.full_data()).unwrap(); // Store the data - self.data.insert(data_hash, data); + if let Ok(data) = D::from_ssz_bytes(wrapped_msg.signed_message.full_data()) { + // Store the data + self.data.insert(data_hash, data); + } else { + debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "Failed to deserialize data"); + return; + } // Update state self.proposal_accepted_for_current_round = Some(wrapped_msg.clone()); @@ -558,10 +566,16 @@ where qbft_message.as_ssz_bytes(), ); + let full_data = if let Some(data) = self.data.get(&data_hash) { + data.as_ssz_bytes() + } else { + vec![] + }; + // Wrap in unsigned SSV message UnsignedSSVMessage { ssv_message, - full_data: self.data.get(&data_hash).as_ssz_bytes(), + full_data, } } diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index 7cad5270..f84d914b 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -8,7 +8,7 @@ use sha2::{Digest, Sha256}; use ssv_types::consensus::UnsignedSSVMessage; use ssv_types::message::SignedSSVMessage; use ssv_types::OperatorId; -use ssz::{Decode, Encode, DecodeError}; +use ssz::{Decode, DecodeError, Encode}; use std::cell::RefCell; use std::collections::{HashSet, VecDeque}; use std::rc::Rc; @@ -22,7 +22,7 @@ const ENABLE_TEST_LOGGING: bool = true; /// Test data structure that implements the Data trait #[derive(Debug, Clone, Default)] -struct TestData(usize); +struct TestData(u64); impl Encode for TestData { fn is_ssz_fixed_len() -> bool { @@ -30,34 +30,41 @@ impl Encode for TestData { } fn ssz_append(&self, buf: &mut Vec) { - buf.extend_from_slice(&self.0.to_le_bytes()); + let value = self.0; + println!("{:?}", value.to_le_bytes()); + buf.extend_from_slice(&value.to_le_bytes()); } fn ssz_fixed_len() -> usize { - std::mem::size_of::() + 8 // u64 size } fn ssz_bytes_len(&self) -> usize { - std::mem::size_of::() + 8 // u64 size } } impl Decode for TestData { fn is_ssz_fixed_len() -> bool { - todo!() + true } fn ssz_fixed_len() -> usize { - todo!() + 8 // u64 size } - fn from_ssz_bytes(_bytes: &[u8]) -> Result { - todo!() + fn from_ssz_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 8 { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected: 8, + }); + } + let value = u64::from_le_bytes(bytes.try_into().unwrap()); + Ok(TestData(value)) } } - - impl Data for TestData { type Hash = Hash256; @@ -131,7 +138,10 @@ impl TestQBFTCommitteeBuilder { /// A testing structure representing a committee of running instances #[allow(clippy::type_complexity)] -struct TestQBFTCommittee + 'static + Encode + Decode, S: FnMut(Message)> { +struct TestQBFTCommittee< + D: Default + Data + 'static + Encode + Decode, + S: FnMut(Message), +> { msg_queue: Rc>>, instances: HashMap>, // All of the instances that are currently active, allows us to stop/restart instances by @@ -174,7 +184,9 @@ fn construct_and_run_committee + Default + 'static + Enc } } -impl + Encode + Decode, S: FnMut(Message)> TestQBFTCommittee { +impl + Encode + Decode, S: FnMut(Message)> + TestQBFTCommittee +{ fn wait_until_end(mut self) -> i32 { loop { let msg = self.msg_queue.borrow_mut().pop_front(); diff --git a/anchor/common/ssv_types/src/cluster.rs b/anchor/common/ssv_types/src/cluster.rs index 85ed29ee..4deec55b 100644 --- a/anchor/common/ssv_types/src/cluster.rs +++ b/anchor/common/ssv_types/src/cluster.rs @@ -42,36 +42,45 @@ pub struct ClusterMember { #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, From, Deref)] pub struct ValidatorIndex(pub usize); -// Implement SSZ encoding and decoding for Validator Index impl Encode for ValidatorIndex { fn is_ssz_fixed_len() -> bool { - todo!() + true } - fn ssz_append(&self, _buf: &mut Vec) { - todo!() + fn ssz_append(&self, buf: &mut Vec) { + // Convert usize to u64 for consistent encoding across platforms + let value = self.0 as u64; + buf.extend_from_slice(&value.to_le_bytes()); } fn ssz_fixed_len() -> usize { - todo!() + 8 // Size of u64 in bytes } fn ssz_bytes_len(&self) -> usize { - todo!() + 8 // Size of u64 in bytes } } impl Decode for ValidatorIndex { fn is_ssz_fixed_len() -> bool { - todo!() + true } fn ssz_fixed_len() -> usize { - todo!() + 8 // Size of u64 in bytes } fn from_ssz_bytes(bytes: &[u8]) -> Result { - todo!() + if bytes.len() != 8 { + return Err(DecodeError::InvalidByteLength { + len: bytes.len(), + expected: 8, + }); + } + + let value = u64::from_le_bytes(bytes.try_into().unwrap()); + Ok(ValidatorIndex(value as usize)) } } diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index 10a8fe03..6854dcff 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -9,9 +9,9 @@ use tree_hash::{PackedEncoding, TreeHash, TreeHashType}; use tree_hash_derive::TreeHash; use types::typenum::U13; use types::{ - AggregateAndProof, BeaconBlock, BlindedBeaconBlock, Checkpoint, CommitteeIndex, EthSpec, - Hash256, PublicKeyBytes, Signature, Slot, SyncCommitteeContribution, VariableList, - AggregateAndProofBase, AggregateAndProofElectra + AggregateAndProof, AggregateAndProofBase, AggregateAndProofElectra, BeaconBlock, + BlindedBeaconBlock, Checkpoint, CommitteeIndex, EthSpec, Hash256, PublicKeyBytes, Signature, + Slot, SyncCommitteeContribution, VariableList, }; // UnsignedSSVMessage diff --git a/anchor/qbft_manager/src/lib.rs b/anchor/qbft_manager/src/lib.rs index 327e0089..c06377ec 100644 --- a/anchor/qbft_manager/src/lib.rs +++ b/anchor/qbft_manager/src/lib.rs @@ -9,7 +9,7 @@ use ssv_types::consensus::{BeaconVote, Data, ValidatorConsensusData}; use ssv_types::OperatorId as QbftOperatorId; use ssv_types::{Cluster, ClusterId, OperatorId}; -use ssz::{Encode, Decode}; +use ssz::{Decode, Encode}; use std::fmt::Debug; use std::hash::Hash; use std::sync::Arc; From 23d567719fd5e75d3dfff6644dd0bccb07ac8770 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Thu, 23 Jan 2025 17:17:26 +0000 Subject: [PATCH 19/45] remove print --- anchor/common/qbft/src/tests.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index f84d914b..ae939951 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -31,7 +31,6 @@ impl Encode for TestData { fn ssz_append(&self, buf: &mut Vec) { let value = self.0; - println!("{:?}", value.to_le_bytes()); buf.extend_from_slice(&value.to_le_bytes()); } From 6ea0abe50d72a0052d55c64cd961867a506e1277 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Thu, 23 Jan 2025 18:07:58 +0000 Subject: [PATCH 20/45] move to QbftData and supertype Decode --- anchor/common/qbft/src/lib.rs | 29 +++++++++--------------- anchor/common/qbft/src/msg_container.rs | 24 ++++++++++++-------- anchor/common/qbft/src/qbft_types.rs | 11 +-------- anchor/common/qbft/src/tests.rs | 15 ++++-------- anchor/common/ssv_types/src/consensus.rs | 6 ++--- anchor/qbft_manager/src/lib.rs | 17 +++++--------- 6 files changed, 41 insertions(+), 61 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 73e2d15a..5c971646 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -1,9 +1,8 @@ use crate::msg_container::MessageContainer; -use sha2::{Digest, Sha256}; -use ssv_types::consensus::{Data, QbftMessage, QbftMessageType, UnsignedSSVMessage}; +use ssv_types::consensus::{QbftData, QbftMessage, QbftMessageType, UnsignedSSVMessage}; use ssv_types::message::{MessageID, MsgType, SSVMessage}; use ssv_types::OperatorId; -use ssz::{Decode, Encode}; +use ssz::Encode; use std::collections::HashMap; use tracing::{debug, error, warn}; use types::Hash256; @@ -39,7 +38,7 @@ mod tests; pub struct Qbft where F: LeaderFunction + Clone, - D: Data + Encode + Decode, + D: QbftData, S: FnMut(Message), { /// The initial configuration used to establish this instance of QBFT. @@ -64,10 +63,10 @@ where completed: Option>, // Message containers - propose_container: MessageContainer, - prepare_container: MessageContainer, - commit_container: MessageContainer, - round_change_container: MessageContainer, + propose_container: MessageContainer, + prepare_container: MessageContainer, + commit_container: MessageContainer, + round_change_container: MessageContainer, // Current round state proposal_accepted_for_current_round: Option, @@ -84,7 +83,7 @@ where impl Qbft where F: LeaderFunction + Clone, - D: Data + Encode + Decode, + D: QbftData, S: FnMut(Message), { // Construct a new QBFT Instance and start the first round @@ -93,18 +92,12 @@ where let current_round = config.round(); let quorum_size = config.quorum_size(); - let start_data_ssz = start_data.as_ssz_bytes(); - let mut hasher = Sha256::new(); - hasher.update(start_data_ssz); - let hash: [u8; 32] = hasher.finalize().into(); - let start_data_hash = Hash256::from(hash); - let mut qbft = Qbft { config, identifier: MessageID::new([0; 56]), instance_height, - start_data_hash, + start_data_hash: start_data.hash(), start_data, data: HashMap::new(), current_round, @@ -210,7 +203,7 @@ where /// If there is no past consensus data in the round change quorum or we disagree with quorum set /// this function will return None, and we obtain the data as if we were beginning this /// instance. - fn justify_round_change_quorum(&self) -> Option<(Hash256, D)> { + fn justify_round_change_quorum(&self) -> Option<(D::Hash, D)> { // Get all round change messages for the current round let round_change_messages = self .round_change_container @@ -552,7 +545,7 @@ where height: *self.instance_height as u64, round: self.current_round.get() as u64, identifier: self.identifier.clone(), - root: data_hash as Hash256, + root: data_hash, data_round: self .last_prepared_round .map_or(0, |round| round.get() as u64), diff --git a/anchor/common/qbft/src/msg_container.rs b/anchor/common/qbft/src/msg_container.rs index 3760192a..61adc70c 100644 --- a/anchor/common/qbft/src/msg_container.rs +++ b/anchor/common/qbft/src/msg_container.rs @@ -1,21 +1,20 @@ -use crate::Round; -use ssv_types::consensus::Data; +use crate::{Round, WrappedQbftMessage}; use ssv_types::OperatorId; use std::collections::{HashMap, HashSet}; use types::Hash256; /// Message container with strong typing and validation #[derive(Default)] -pub struct MessageContainer { +pub struct MessageContainer { /// Messages indexed by round and then by sender - messages: HashMap>, + messages: HashMap>, /// Track unique values per round values_by_round: HashMap>, /// The quorum size for the qbft instance quorum_size: usize, } -impl> MessageContainer { +impl MessageContainer { /// Construct a new MessageContainer with a specific quorum size pub fn new(quorum_size: usize) -> Self { Self { @@ -26,7 +25,12 @@ impl> MessageContainer { } // Add a new message to the container for the round - pub fn add_message(&mut self, round: Round, sender: OperatorId, msg: &M) -> bool { + pub fn add_message( + &mut self, + round: Round, + sender: OperatorId, + msg: &WrappedQbftMessage, + ) -> bool { // Check if we already have a message from this sender for this round if self .messages @@ -46,7 +50,7 @@ impl> MessageContainer { self.values_by_round .entry(round) .or_default() - .insert(msg.hash()); + .insert(msg.signed_message.hash_fulldata()); true } @@ -59,7 +63,9 @@ impl> MessageContainer { // Count occurrences of each value let mut value_counts: HashMap = HashMap::new(); for msg in round_messages.values() { - *value_counts.entry(msg.hash()).or_default() += 1; + *value_counts + .entry(msg.signed_message.hash_fulldata()) + .or_default() += 1; } // Find any value that has reached quorum @@ -78,7 +84,7 @@ impl> MessageContainer { } /// Gets all messages for a specific round - pub fn get_messages_for_round(&self, round: Round) -> Vec<&M> { + pub fn get_messages_for_round(&self, round: Round) -> Vec<&WrappedQbftMessage> { // If we have messages for this round in our container, return them all // If not, return an empty vector self.messages diff --git a/anchor/common/qbft/src/qbft_types.rs b/anchor/common/qbft/src/qbft_types.rs index 1fca2c84..a8bafecc 100644 --- a/anchor/common/qbft/src/qbft_types.rs +++ b/anchor/common/qbft/src/qbft_types.rs @@ -2,14 +2,13 @@ use crate::validation::ValidatedData; use derive_more::{Deref, From}; use indexmap::IndexSet; -use ssv_types::consensus::{Data, QbftMessage, UnsignedSSVMessage}; +use ssv_types::consensus::{QbftMessage, UnsignedSSVMessage}; use ssv_types::message::SignedSSVMessage; use ssv_types::OperatorId; use std::cmp::Eq; use std::fmt::Debug; use std::hash::Hash; use std::num::NonZeroUsize; -use types::Hash256; /// Generic LeaderFunction trait to allow for future implementations of the QBFT module pub trait LeaderFunction { @@ -58,14 +57,6 @@ impl WrappedQbftMessage { } } -impl Data for WrappedQbftMessage { - type Hash = Hash256; - - fn hash(&self) -> Self::Hash { - self.qbft_message.root - } -} - /// This represents an individual round, these change on regular time intervals #[derive(Clone, Copy, Debug, Deref, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Round(NonZeroUsize); diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index ae939951..55c7c7bf 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -64,7 +64,7 @@ impl Decode for TestData { } } -impl Data for TestData { +impl QbftData for TestData { type Hash = Hash256; fn hash(&self) -> Self::Hash { @@ -122,7 +122,7 @@ impl TestQBFTCommitteeBuilder { /// represents a running quorum. pub fn run(self, data: D) -> TestQBFTCommittee where - D: Default + Data + Encode + Decode, + D: Default + QbftData, { if ENABLE_TEST_LOGGING { let env_filter = EnvFilter::new("debug"); @@ -137,10 +137,7 @@ impl TestQBFTCommitteeBuilder { /// A testing structure representing a committee of running instances #[allow(clippy::type_complexity)] -struct TestQBFTCommittee< - D: Default + Data + 'static + Encode + Decode, - S: FnMut(Message), -> { +struct TestQBFTCommittee, S: FnMut(Message)> { msg_queue: Rc>>, instances: HashMap>, // All of the instances that are currently active, allows us to stop/restart instances by @@ -152,7 +149,7 @@ struct TestQBFTCommittee< /// /// This will create instances and spawn them in a task and return the sender/receiver channels for /// all created instances. -fn construct_and_run_committee + Default + 'static + Encode + Decode>( +fn construct_and_run_committee>( mut config: ConfigBuilder, validated_data: D, ) -> TestQBFTCommittee { @@ -183,9 +180,7 @@ fn construct_and_run_committee + Default + 'static + Enc } } -impl + Encode + Decode, S: FnMut(Message)> - TestQBFTCommittee -{ +impl, S: FnMut(Message)> TestQBFTCommittee { fn wait_until_end(mut self) -> i32 { loop { let msg = self.msg_queue.borrow_mut().pop_front(); diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index 6854dcff..d9115c1e 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -27,7 +27,7 @@ use types::{ // ConsensusMsg QBFTMessage SSZ // PartialSigMsg PartialSignatureMessage SSZ -pub trait Data: Debug + Clone { +pub trait QbftData: Debug + Clone + Encode + Decode { type Hash: Debug + Clone + Eq + Hash; fn hash(&self) -> Self::Hash; } @@ -159,7 +159,7 @@ pub struct ValidatorConsensusData { pub data_ssz: Vec, } -impl Data for ValidatorConsensusData { +impl QbftData for ValidatorConsensusData { type Hash = Hash256; fn hash(&self) -> Self::Hash { @@ -383,7 +383,7 @@ pub struct BeaconVote { pub target: Checkpoint, } -impl Data for BeaconVote { +impl QbftData for BeaconVote { type Hash = Hash256; fn hash(&self) -> Self::Hash { diff --git a/anchor/qbft_manager/src/lib.rs b/anchor/qbft_manager/src/lib.rs index c06377ec..c7442ab2 100644 --- a/anchor/qbft_manager/src/lib.rs +++ b/anchor/qbft_manager/src/lib.rs @@ -5,11 +5,10 @@ use qbft::{ WrappedQbftMessage, }; use slot_clock::SlotClock; -use ssv_types::consensus::{BeaconVote, Data, ValidatorConsensusData}; +use ssv_types::consensus::{BeaconVote, QbftData, ValidatorConsensusData}; use ssv_types::OperatorId as QbftOperatorId; use ssv_types::{Cluster, ClusterId, OperatorId}; -use ssz::{Decode, Encode}; use std::fmt::Debug; use std::hash::Hash; use std::sync::Arc; @@ -54,14 +53,14 @@ pub enum ValidatorDutyKind { // Message that is passed around the QbftManager #[derive(Debug)] -pub struct QbftMessage + Encode + Decode> { +pub struct QbftMessage> { pub kind: QbftMessageKind, pub drop_on_finish: DropOnFinish, } // Type of the QBFT Message #[derive(Debug)] -pub enum QbftMessageKind + Encode + Decode> { +pub enum QbftMessageKind> { // Initialize a new qbft instance with some initial data, // the configuration for the instance, and a channel to send the final data on Initialize { @@ -199,9 +198,7 @@ impl QbftManager { } // Trait that describes any data that is able to be decided upon during a qbft instance -pub trait QbftDecidable: - Data + Encode + Decode + Send + 'static -{ +pub trait QbftDecidable: QbftData + Send + 'static { type Id: Hash + Eq + Send; fn get_map(manager: &QbftManager) -> &Map; @@ -254,7 +251,7 @@ impl QbftDecidable for BeaconVote { } // States that Qbft instance may be in -enum QbftInstance + Encode + Decode, S: FnMut(Message)> { +enum QbftInstance, S: FnMut(Message)> { // The instance is uninitialized Uninitialized { // todo: proooobably limit this @@ -274,9 +271,7 @@ enum QbftInstance + Encode + Decode, S: FnMut(Message)> }, } -async fn qbft_instance + Encode + Decode>( - mut rx: UnboundedReceiver>, -) { +async fn qbft_instance>(mut rx: UnboundedReceiver>) { // Signal a new instance that is uninitialized let mut instance = QbftInstance::Uninitialized { message_buffer: Vec::new(), From 5e7ead04b68c03a2e96578014357338fe091d0b6 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Thu, 23 Jan 2025 21:18:50 +0000 Subject: [PATCH 21/45] add data validation --- anchor/common/qbft/src/lib.rs | 26 ++++++++++++++++-------- anchor/common/qbft/src/tests.rs | 4 ++++ anchor/common/ssv_types/src/consensus.rs | 12 +++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 5c971646..6637f5c3 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -192,6 +192,20 @@ where return false; } + // Try to decode the data. If we can decode the data, then also validate it + let data = match D::from_ssz_bytes(wrapped_msg.signed_message.full_data()) { + Ok(data) => data, + _ => { + warn!(in = ?self.config.operator_id(), "Invalid data"); + return false; + } + }; + + if !data.validate() { + warn!(in = ?self.config.operator_id(), "Data failed validation"); + return false; + } + // Success! Message is well formed true } @@ -351,14 +365,10 @@ where return; } - // Store the data - if let Ok(data) = D::from_ssz_bytes(wrapped_msg.signed_message.full_data()) { - // Store the data - self.data.insert(data_hash, data); - } else { - debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "Failed to deserialize data"); - return; - } + // We have previously verified that this data is able to be de-serialized. Store it now + let data = D::from_ssz_bytes(wrapped_msg.signed_message.full_data()) + .expect("Data has already been validated"); + self.data.insert(data_hash, data); // Update state self.proposal_accepted_for_current_round = Some(wrapped_msg.clone()); diff --git a/anchor/common/qbft/src/tests.rs b/anchor/common/qbft/src/tests.rs index 55c7c7bf..9582a411 100644 --- a/anchor/common/qbft/src/tests.rs +++ b/anchor/common/qbft/src/tests.rs @@ -73,6 +73,10 @@ impl QbftData for TestData { let hash: [u8; 32] = hasher.finalize().into(); Hash256::from(hash) } + + fn validate(&self) -> bool { + true + } } fn convert_unsigned_to_wrapped( diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index d9115c1e..422ac216 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -29,7 +29,9 @@ use types::{ pub trait QbftData: Debug + Clone + Encode + Decode { type Hash: Debug + Clone + Eq + Hash; + fn hash(&self) -> Self::Hash; + fn validate(&self) -> bool; } /// A SSV Message that has not been signed yet. @@ -170,6 +172,11 @@ impl QbftData for ValidatorConsensusData { let hash: [u8; 32] = hasher.finalize().into(); Hash256::from(hash) } + + fn validate(&self) -> bool { + // todo!(). What does proper validation look like?? + true + } } #[derive(Clone, Debug, TreeHash, PartialEq, Encode, Decode)] @@ -389,4 +396,9 @@ impl QbftData for BeaconVote { fn hash(&self) -> Self::Hash { self.tree_hash_root() } + + fn validate(&self) -> bool { + // todo!(). What does proper validation look like?? + true + } } From feca81aec209ba4dbf5ccb7f9d39c1efb9baf02b Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Thu, 23 Jan 2025 22:14:45 +0000 Subject: [PATCH 22/45] misc cleanup --- anchor/common/qbft/src/lib.rs | 10 +++------ anchor/common/qbft/src/qbft_types.rs | 10 --------- anchor/common/qbft/src/validation.rs | 26 ------------------------ anchor/common/ssv_types/src/consensus.rs | 2 +- anchor/common/ssv_types/src/msgid.rs | 2 +- 5 files changed, 5 insertions(+), 45 deletions(-) delete mode 100644 anchor/common/qbft/src/validation.rs diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 6637f5c3..f19a62c8 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -16,13 +16,11 @@ pub use qbft_types::{ Completed, ConsensusData, DefaultLeaderFunction, InstanceHeight, InstanceState, LeaderFunction, Round, }; -pub use validation::{validate_consensus_data, ValidatedData, ValidationError}; mod config; mod error; mod msg_container; mod qbft_types; -mod validation; #[cfg(test)] mod tests; @@ -33,7 +31,7 @@ mod tests; /// successfully (i.e that it has successfully come to consensus, or through a timeout where enough /// round changes have elapsed before coming to consensus. /// -/// The QBFT instance will recieve SignedSSVMessages from the network and it will construct +/// The QBFT instance will recieve WrappedQbftMessages from the network and it will construct /// UnsignedSSVMessages to be signed and sent on the network. pub struct Qbft where @@ -247,7 +245,7 @@ where } } - // No consensus found or we disagree - use initial data + // No consensus found None } @@ -345,9 +343,7 @@ where //self.validate_prepare_justification(wrapped_msg)?; } - // Verify that the fulldata matches the data root of the qbft message - // Data - // data as ssz bytes then hashed + // Verify that the fulldata matches the data root of the qbft message data let data_hash = wrapped_msg.signed_message.hash_fulldata(); if data_hash != wrapped_msg.qbft_message.root { warn!(from = ?operator_id, self=?self.config.operator_id(), "Data roots do not match"); diff --git a/anchor/common/qbft/src/qbft_types.rs b/anchor/common/qbft/src/qbft_types.rs index a8bafecc..38114a64 100644 --- a/anchor/common/qbft/src/qbft_types.rs +++ b/anchor/common/qbft/src/qbft_types.rs @@ -1,5 +1,4 @@ //! A collection of types used by the QBFT modules -use crate::validation::ValidatedData; use derive_more::{Deref, From}; use indexmap::IndexSet; use ssv_types::consensus::{QbftMessage, UnsignedSSVMessage}; @@ -132,15 +131,6 @@ pub struct ConsensusData { pub data: D, } -impl From>> for ConsensusData { - fn from(value: ConsensusData>) -> Self { - ConsensusData { - round: value.round, - data: value.data.data, - } - } -} - #[derive(Debug, Clone)] /// The consensus instance has finished. pub enum Completed { diff --git a/anchor/common/qbft/src/validation.rs b/anchor/common/qbft/src/validation.rs deleted file mode 100644 index 6606d23d..00000000 --- a/anchor/common/qbft/src/validation.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Validation for data function -use crate::qbft_types::ConsensusData; - -/// The list of possible validation errors that can occur -#[derive(Debug)] -pub enum ValidationError { - Invalid, -} - -/// Data that has been validated by our validation function. -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct ValidatedData { - pub data: D, -} - -/// This verifies the data is correct an appropriate to use for consensus. -pub fn _validate_data(data: D) -> Result, ValidationError> { - Ok(ValidatedData { data }) -} - -// Validates consensus data -pub fn validate_consensus_data( - _consensus_data: ConsensusData, -) -> Result>, ValidationError> { - todo!() -} diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index 422ac216..4f82819f 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -348,7 +348,7 @@ pub enum DataSsz { impl DataSsz { /// SSZ deserialization that tries all possible variants pub fn from_ssz_bytes(bytes: &[u8]) -> Result { - // 1. Try BeaconBlock variants first (using fork-aware decoding) + // 1. Try BeaconBlock variants if let Ok(block) = BeaconBlock::any_from_ssz_bytes(bytes) { return Ok(Self::BeaconBlock(block)); } diff --git a/anchor/common/ssv_types/src/msgid.rs b/anchor/common/ssv_types/src/msgid.rs index a31c1359..5a9e3c83 100644 --- a/anchor/common/ssv_types/src/msgid.rs +++ b/anchor/common/ssv_types/src/msgid.rs @@ -30,7 +30,7 @@ pub enum Executor { } #[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct MsgId(pub [u8; 56]); // pub for testing +pub struct MsgId([u8; 56]); impl MsgId { pub fn new(domain: &Domain, role: Role, duty_executor: &Executor) -> Self { From 995dafa41b18992c3660602e684507de308a4abd Mon Sep 17 00:00:00 2001 From: Zachary Holme Date: Fri, 24 Jan 2025 07:29:49 -0600 Subject: [PATCH 23/45] fmt --- anchor/validator_store/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/anchor/validator_store/src/lib.rs b/anchor/validator_store/src/lib.rs index 117dcc39..98ac94fa 100644 --- a/anchor/validator_store/src/lib.rs +++ b/anchor/validator_store/src/lib.rs @@ -20,11 +20,11 @@ use ssv_types::consensus::{ DATA_VERSION_ALTAIR, DATA_VERSION_BELLATRIX, DATA_VERSION_CAPELLA, DATA_VERSION_DENEB, DATA_VERSION_PHASE0, DATA_VERSION_UNKNOWN, }; -use ssz::Encode; -use std::marker::PhantomData; use ssv_types::{Cluster, OperatorId, ValidatorIndex, ValidatorMetadata}; +use ssz::Encode; use std::collections::HashSet; use std::fmt::Debug; +use std::marker::PhantomData; use std::str::from_utf8; use std::sync::Arc; use std::time::Duration; From b2dec52c8ce06f3b1b925c5f27aacf1f9e5266d1 Mon Sep 17 00:00:00 2001 From: Zachary Holme Date: Fri, 24 Jan 2025 14:19:54 -0600 Subject: [PATCH 24/45] start on justifications --- anchor/common/qbft/src/lib.rs | 162 ++++++++++++++++++++++++++++++---- 1 file changed, 143 insertions(+), 19 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index f19a62c8..b51f1687 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -335,7 +335,7 @@ where // Round change justification validation for rounds after the first if round > Round::default() { - //self.validate_round_change_justification(); + self.validate_round_change_justification(&wrapped_msg); } // Validate the prepare justifications if they exist @@ -375,6 +375,69 @@ where self.send_prepare(wrapped_msg.qbft_message.root); } + fn validate_round_change_justification(&self, msg: &WrappedQbftMessage) -> bool { + let prepare_messages = msg.qbft_message.prepare_justification; + + if prepare_messages.len() < self.config().quorum_size() { + warn!( + messages = prepare_messages.len(), + required = self.config.quorum_size(), + "Not enough prepare justifications for quorum" + ); + return false; + } + + // Validate each prepare message + for prepare_msg in &prepare_messages { + // Message must be a PREPARE message + if prepare_msg.qbft_message.qbft_message_type != QbftMessageType::Prepare { + warn!("Justification contains non-PREPARE message"); + return false; + } + + // Must be for the claimed prepared round + if prepare_msg.qbft_message.round != prepared_round { + warn!( + msg_round = prepare_msg.qbft_message.round, + claimed_round = prepared_round, + "Prepare justification round mismatch" + ); + return false; + } + + // Must match the claimed prepared value + if prepare_msg.qbft_message.root != prepared_value { + warn!("Prepare justification value mismatch"); + return false; + } + + // Verify the prepare message is from a committee member + if !self.check_committee(&OperatorId( + *prepare_msg.signed_message.operator_ids().first().unwrap(), + )) { + warn!("Prepare justification from non-committee member"); + return false; + } + } + + // Verify we have unique signers for the quorum + let unique_signers: std::collections::HashSet<_> = prepare_messages + .iter() + .map(|msg| msg.signed_message.operator_ids().first().unwrap()) + .collect(); + + if unique_signers.len() < self.config.quorum_size() { + warn!( + unique_signers = unique_signers.len(), + required = self.config.quorum_size(), + "Not enough unique signers in prepare justifications" + ); + return false; + } + + true + } + /// We have received a prepare message fn received_prepare( &mut self, @@ -388,6 +451,12 @@ where return; } + // Make sure that we have accepted a proposal for this round + if self.proposal_accepted_for_current_round.is_none() { + warn!(from=?operator_id, ?self.state, "Have not accepted Proposal for current round yet"); + return; + } + debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "PREPARE received"); // Store the prepare message @@ -401,7 +470,9 @@ where // Check if we have reached quorum, if so send the commit message if let Some(hash) = self.prepare_container.has_quorum(round) { // Make sure we are in the correct state - if !matches!(self.state, InstanceState::Prepare) { + if !matches!(self.state, InstanceState::Prepare) + | !matches!(self.state, InstanceState::AwaitingProposal) + { warn!(from=?operator_id, ?self.state, "Not in PREPARE state"); return; } @@ -488,11 +559,10 @@ where } // There are two cases to check here - // 1. If we have received a quorum of round change messages, we need to start a new round - // 2. If we receive f+1 round change messages, we need to send our own round-change message - // Check if we have any messages for the suggested round - if let Some(hash) = self.round_change_container.has_quorum(round) { + // 1. If we have received a quorum of round change messages, we need to start a new round + // todo!() do we ignore this hash? + if let Some(_hash) = self.round_change_container.has_quorum(round) { if matches!(self.state, InstanceState::SentRoundChange) { // 1. If we have reached a quorum for this round, advance to that round. debug!( @@ -501,15 +571,28 @@ where "Round change quorum reached" ); + // The round change messages is round + 1, so this is the next round we want to use self.set_round(round); - } else { - let num_messages_for_round = - self.round_change_container.num_messages_for_round(round); - if num_messages_for_round > self.config.get_f() - && !(matches!(self.state, InstanceState::SentRoundChange)) - { - self.send_round_change(hash); - } + } + } else { + // 2. If we receive f+1 round change messages, we need to send our own round-change message + let num_messages_for_round = self.round_change_container.num_messages_for_round(round); + if num_messages_for_round > self.config.get_f() + && !(matches!(self.state, InstanceState::SentRoundChange)) + { + // send our own round change message + + // Set the state so SendRoundChange so we include Round + 1 in message + self.state = InstanceState::SentRoundChange; + + // Use the value from our last prepare if we have one + let value_to_propose = if let Some(prepared_value) = self.last_prepared_value { + prepared_value + } else { + self.start_data_hash + }; + + self.send_round_change(value_to_propose); } } } @@ -529,8 +612,9 @@ where return; } - // Start a new round - self.current_round.set(next_round); + // Set the state so SendRoundChange so we include Round + 1 in message + self.state = InstanceState::SentRoundChange; + // Check if we have a prepared value, if so we want to send a round change proposing the // value. Else, send a blank hash let hash = self.last_prepared_value.unwrap_or_default(); @@ -545,18 +629,55 @@ where msg_type: QbftMessageType, data_hash: D::Hash, ) -> UnsignedSSVMessage { + // if we are in a round change, use round + 1 and get the prepare justifications if needed + let (round, prepare_justification) = if matches!(self.state, InstanceState::SentRoundChange) + { + // If we are sending a round change and have a value that was prepared in the last + // round, we must include the prepare messages that prove that this value was actually + // prepared + let prepare_justification = if self.last_prepared_round.is_some() { + // Get all prepare messages for our last prepared value + self.prepare_container + .get_messages_for_round(self.last_prepared_round.expect("Value was prepared")) + .iter() + .filter(|msg| msg.qbft_message.root == data_hash) + .map(|msg| msg.signed_message.clone()) + .collect() + } else { + vec![] + }; + (self.current_round.get() as u64 + 1, prepare_justification) + } else { + (self.current_round.get() as u64, vec![]) + }; + + // if round > 1 and we are AwaitingProposal. Include round change justifications. We know + // that only the leader can send a message while in the AwaitingProposal state, so this must + // be a proposal message. We must include the round change messages that justify the choice + // of value + let round_change_justification = + if round > 0 && matches!(self.state, InstanceState::AwaitingProposal) { + self.round_change_container + .get_messages_for_round(self.current_round) + .iter() + .map(|msg| msg.signed_message.clone()) + .collect() + } else { + vec![] + }; + // Create the QBFT message let qbft_message = QbftMessage { qbft_message_type: msg_type, height: *self.instance_height as u64, - round: self.current_round.get() as u64, + round, identifier: self.identifier.clone(), root: data_hash, data_round: self .last_prepared_round .map_or(0, |round| round.get() as u64), - round_change_justification: vec![], // Empty for MVP - prepare_justification: vec![], // Empty for MVP + round_change_justification, + prepare_justification, }; let ssv_message = SSVMessage::new( @@ -619,6 +740,9 @@ where // Construct unsigned round change let unsigned_msg = self.new_unsigned_message(QbftMessageType::RoundChange, data_hash); + // forget that we accpeted a proposal + self.proposal_accepted_for_current_round = None; + let operator_id = self.config.operator_id(); (self.send_message)(Message::RoundChange(operator_id, unsigned_msg.clone())); } From 4d0cc26b4db8deb05255fb11b1610e024ff3194d Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Mon, 27 Jan 2025 16:14:35 +0000 Subject: [PATCH 25/45] initial justification system --- Cargo.lock | 52 ++++++++++---- anchor/common/qbft/src/lib.rs | 128 ++++++++++++++++++++++------------ 2 files changed, 119 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 955deace..f7825d50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1634,6 +1634,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1676,9 +1696,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -4900,9 +4920,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ "libc", "log", @@ -4954,9 +4974,9 @@ dependencies = [ [[package]] name = "netlink-proto" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2741a6c259755922e3ed29ebce3b299cc2160c4acae94b465b5938ab02c2bbe" +checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" dependencies = [ "bytes", "futures", @@ -5176,9 +5196,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -5263,28 +5283,30 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.6.12" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee" +checksum = "7e5fdb66425c73b3f87565b2c8bd70c27afdd41b5e14ab46f303873a7c50294e" dependencies = [ "arrayvec", "bitvec", "byte-slice-cast", + "const_format", "impl-trait-for-tuples", "parity-scale-codec-derive", + "rustversion", "serde", ] [[package]] name = "parity-scale-codec-derive" -version = "3.6.12" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d830939c76d294956402033aee57a6da7b438f2294eb94864c37b0569053a42c" +checksum = "40e3e3dbdd7e7f6a58a4bdb105f88a85ba6d52bac9c98c69276bce153abe9ab5" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.96", ] [[package]] @@ -6377,9 +6399,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" dependencies = [ "web-time", ] diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index b51f1687..dc77dd8f 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -2,8 +2,9 @@ use crate::msg_container::MessageContainer; use ssv_types::consensus::{QbftData, QbftMessage, QbftMessageType, UnsignedSSVMessage}; use ssv_types::message::{MessageID, MsgType, SSVMessage}; use ssv_types::OperatorId; -use ssz::Encode; +use ssz::{Decode, Encode}; use std::collections::HashMap; +use std::collections::HashSet; use tracing::{debug, error, warn}; use types::Hash256; @@ -376,65 +377,89 @@ where } fn validate_round_change_justification(&self, msg: &WrappedQbftMessage) -> bool { - let prepare_messages = msg.qbft_message.prepare_justification; - - if prepare_messages.len() < self.config().quorum_size() { - warn!( - messages = prepare_messages.len(), - required = self.config.quorum_size(), - "Not enough prepare justifications for quorum" - ); - return false; - } - - // Validate each prepare message - for prepare_msg in &prepare_messages { - // Message must be a PREPARE message - if prepare_msg.qbft_message.qbft_message_type != QbftMessageType::Prepare { - warn!("Justification contains non-PREPARE message"); + // Record if any of the round change messages have a value that was prepared + let mut previously_prepared = false; + let mut max_prepared_round = 0; + let mut max_prepared_value = None; + + for signed_round_change in &msg.qbft_message.round_change_justification { + let deser_round_change: QbftMessage = + QbftMessage::from_ssz_bytes(signed_round_change.ssv_message().data()).unwrap(); // todo!() get rid of this unwrap + + // Make sure this is actually a round change message + if !matches!( + deser_round_change.qbft_message_type, + QbftMessageType::RoundChange + ) { return false; } - // Must be for the claimed prepared round - if prepare_msg.qbft_message.round != prepared_round { - warn!( - msg_round = prepare_msg.qbft_message.round, - claimed_round = prepared_round, - "Prepare justification round mismatch" - ); + // Make sure it is for the correct height + if deser_round_change.height != *self.instance_height as u64 { return false; } - // Must match the claimed prepared value - if prepare_msg.qbft_message.root != prepared_value { - warn!("Prepare justification value mismatch"); + // Make sure this is for the correct round + if deser_round_change.round != self.current_round.get() as u64 { return false; } - // Verify the prepare message is from a committee member - if !self.check_committee(&OperatorId( - *prepare_msg.signed_message.operator_ids().first().unwrap(), - )) { - warn!("Prepare justification from non-committee member"); + // Make sure there is only one signer + if signed_round_change.operator_ids().len() != 1 { return false; } + + // If the data round != 0, that means we have prepared a value in previous rounds + if deser_round_change.data_round > 0 { + previously_prepared = true; + if deser_round_change.data_round > max_prepared_round { + max_prepared_round = deser_round_change.data_round; + max_prepared_value = Some(deser_round_change.root); + } + } } - // Verify we have unique signers for the quorum - let unique_signers: std::collections::HashSet<_> = prepare_messages - .iter() - .map(|msg| msg.signed_message.operator_ids().first().unwrap()) - .collect(); + if previously_prepared { + // Must have enough prepare messages for quorum + if msg.qbft_message.prepare_justification.len() < self.config.quorum_size() { + return false; + } - if unique_signers.len() < self.config.quorum_size() { - warn!( - unique_signers = unique_signers.len(), - required = self.config.quorum_size(), - "Not enough unique signers in prepare justifications" - ); - return false; - } + // Validate each prepare message matches highest prepared round/value + for prepare_msg in &msg.qbft_message.prepare_justification { + let deser_prepare = QbftMessage::from_ssz_bytes(prepare_msg.ssv_message().data()) + .map_err(|_| false) + .unwrap(); // todo!() get rid of this unwrap + + // Must be for highest prepared round + if deser_prepare.round != max_prepared_round { + return false; + } + + // Must match highest prepared value + if deser_prepare.root != max_prepared_value.unwrap() { + return false; + } + // Must be from committee member + if !self.check_committee(&OperatorId(*prepare_msg.operator_ids().first().unwrap())) + { + return false; + } + } + + // Verify unique signers for quorum + let unique_signers: HashSet<_> = msg + .qbft_message + .prepare_justification + .iter() + .map(|m| m.operator_ids().first().unwrap()) + .collect(); + + if unique_signers.len() < self.config.quorum_size() { + return false; + } + } true } @@ -630,6 +655,9 @@ where data_hash: D::Hash, ) -> UnsignedSSVMessage { // if we are in a round change, use round + 1 and get the prepare justifications if needed + // Round change. we only include the round change justifications which are the prepare + // messageks + // The justification is a quorum of signed prepare messages that agree on state.LastPreparedValue let (round, prepare_justification) = if matches!(self.state, InstanceState::SentRoundChange) { // If we are sending a round change and have a value that was prepared in the last @@ -647,6 +675,14 @@ where vec![] }; (self.current_round.get() as u64 + 1, prepare_justification) + } else if matches!(self.state, InstanceState::AwaitingProposal) { + let round_changes = self + .round_change_container + .get_messages_for_round(self.current_round) + .iter() + .map(|msg| msg.signed_message.clone()) + .collect(); + (self.current_round.get() as u64, round_changes) } else { (self.current_round.get() as u64, vec![]) }; @@ -654,7 +690,7 @@ where // if round > 1 and we are AwaitingProposal. Include round change justifications. We know // that only the leader can send a message while in the AwaitingProposal state, so this must // be a proposal message. We must include the round change messages that justify the choice - // of value + // of value. We also have to include the round change justifications?? let round_change_justification = if round > 0 && matches!(self.state, InstanceState::AwaitingProposal) { self.round_change_container From d6fda94fe405dc6ca858be9ac66113996c72b676 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Mon, 27 Jan 2025 17:02:21 +0000 Subject: [PATCH 26/45] state bugfix --- anchor/common/qbft/src/lib.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index dc77dd8f..e128de65 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -273,8 +273,10 @@ where self.send_proposal(data_hash, data); self.send_prepare(data_hash); - // Since we are the leader and send the proposal, switch to prepare state + // Since we are the leader and send the proposal, switch to prepare state and accept + // proposal self.state = InstanceState::Prepare; + // how do we accpet this proposal??? } } @@ -391,21 +393,25 @@ where deser_round_change.qbft_message_type, QbftMessageType::RoundChange ) { + warn!("Message is not a RoundChange message"); return false; } // Make sure it is for the correct height if deser_round_change.height != *self.instance_height as u64 { + warn!("Height is incorrect"); return false; } // Make sure this is for the correct round if deser_round_change.round != self.current_round.get() as u64 { + warn!("Round is incorrect"); return false; } // Make sure there is only one signer if signed_round_change.operator_ids().len() != 1 { + warn!("There should only be one signer"); return false; } @@ -433,17 +439,20 @@ where // Must be for highest prepared round if deser_prepare.round != max_prepared_round { + warn!("Prepare round incorrect"); return false; } // Must match highest prepared value if deser_prepare.root != max_prepared_value.unwrap() { + warn!("Prepare root incorrect"); return false; } // Must be from committee member if !self.check_committee(&OperatorId(*prepare_msg.operator_ids().first().unwrap())) { + warn!("not part of the committee"); return false; } } @@ -476,11 +485,13 @@ where return; } + /* // Make sure that we have accepted a proposal for this round if self.proposal_accepted_for_current_round.is_none() { - warn!(from=?operator_id, ?self.state, "Have not accepted Proposal for current round yet"); + warn!(from=?operator_id, ?self.state, self=?self.config.operator_id(), "Have not accepted Proposal for current round yet"); return; } + */ debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "PREPARE received"); @@ -496,7 +507,7 @@ where if let Some(hash) = self.prepare_container.has_quorum(round) { // Make sure we are in the correct state if !matches!(self.state, InstanceState::Prepare) - | !matches!(self.state, InstanceState::AwaitingProposal) + && !matches!(self.state, InstanceState::AwaitingProposal) { warn!(from=?operator_id, ?self.state, "Not in PREPARE state"); return; From 363a672e389de7bac8cb31e29b5980300c10f688 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Mon, 27 Jan 2025 17:05:42 +0000 Subject: [PATCH 27/45] accept proposal in leader --- anchor/common/qbft/src/lib.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index e128de65..6f1ff6d2 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -68,7 +68,7 @@ where round_change_container: MessageContainer, // Current round state - proposal_accepted_for_current_round: Option, + proposal_accepted_for_current_round: bool, last_prepared_round: Option, last_prepared_value: Option, @@ -108,7 +108,7 @@ where commit_container: MessageContainer::new(quorum_size), round_change_container: MessageContainer::new(quorum_size), - proposal_accepted_for_current_round: None, + proposal_accepted_for_current_round: false, last_prepared_round: None, last_prepared_value: None, @@ -276,7 +276,7 @@ where // Since we are the leader and send the proposal, switch to prepare state and accept // proposal self.state = InstanceState::Prepare; - // how do we accpet this proposal??? + self.proposal_accepted_for_current_round = true; } } @@ -370,7 +370,7 @@ where self.data.insert(data_hash, data); // Update state - self.proposal_accepted_for_current_round = Some(wrapped_msg.clone()); + self.proposal_accepted_for_current_round = true; self.state = InstanceState::Prepare; debug!(in = ?self.config.operator_id(), state = ?self.state, "State updated to PREPARE"); @@ -485,13 +485,11 @@ where return; } - /* // Make sure that we have accepted a proposal for this round - if self.proposal_accepted_for_current_round.is_none() { + if !self.proposal_accepted_for_current_round { warn!(from=?operator_id, ?self.state, self=?self.config.operator_id(), "Have not accepted Proposal for current round yet"); return; } - */ debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "PREPARE received"); @@ -788,7 +786,7 @@ where let unsigned_msg = self.new_unsigned_message(QbftMessageType::RoundChange, data_hash); // forget that we accpeted a proposal - self.proposal_accepted_for_current_round = None; + self.proposal_accepted_for_current_round = false; let operator_id = self.config.operator_id(); (self.send_message)(Message::RoundChange(operator_id, unsigned_msg.clone())); From 0d2835d3bca825a957f80f3ba77d653e1617ba14 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Mon, 27 Jan 2025 17:33:11 +0000 Subject: [PATCH 28/45] fix logs --- anchor/common/qbft/src/lib.rs | 68 ++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 6f1ff6d2..f672fed6 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -199,7 +199,6 @@ where return false; } }; - if !data.validate() { warn!(in = ?self.config.operator_id(), "Data failed validation"); return false; @@ -273,7 +272,7 @@ where self.send_proposal(data_hash, data); self.send_prepare(data_hash); - // Since we are the leader and send the proposal, switch to prepare state and accept + // Since we are the leader and sent the proposal, switch to prepare state and accept // proposal self.state = InstanceState::Prepare; self.proposal_accepted_for_current_round = true; @@ -341,11 +340,6 @@ where self.validate_round_change_justification(&wrapped_msg); } - // Validate the prepare justifications if they exist - if !wrapped_msg.qbft_message.prepare_justification.is_empty() { - //self.validate_prepare_justification(wrapped_msg)?; - } - // Verify that the fulldata matches the data root of the qbft message data let data_hash = wrapped_msg.signed_message.hash_fulldata(); if data_hash != wrapped_msg.qbft_message.root { @@ -384,34 +378,49 @@ where let mut max_prepared_round = 0; let mut max_prepared_value = None; + // Go through all of the round changes messages and verify each one for signed_round_change in &msg.qbft_message.round_change_justification { let deser_round_change: QbftMessage = - QbftMessage::from_ssz_bytes(signed_round_change.ssv_message().data()).unwrap(); // todo!() get rid of this unwrap + match QbftMessage::from_ssz_bytes(signed_round_change.ssv_message().data()) { + Ok(data) => data, + Err(_) => return false, + }; // Make sure this is actually a round change message if !matches!( deser_round_change.qbft_message_type, QbftMessageType::RoundChange ) { - warn!("Message is not a RoundChange message"); + warn!(message_type = ?deser_round_change.qbft_message_type, "Message is not a ROUNDCHANGE message"); return false; } // Make sure it is for the correct height if deser_round_change.height != *self.instance_height as u64 { - warn!("Height is incorrect"); + warn!( + got = deser_round_change.height, + expected = *self.instance_height, + "Message for the wrong height" + ); return false; } // Make sure this is for the correct round if deser_round_change.round != self.current_round.get() as u64 { - warn!("Round is incorrect"); + warn!( + got = deser_round_change.round, + expected = self.current_round.get(), + "Message for the wrong round" + ); return false; } // Make sure there is only one signer if signed_round_change.operator_ids().len() != 1 { - warn!("There should only be one signer"); + warn!( + num_signers = signed_round_change.operator_ids().len(), + "More than one message signer found" + ); return false; } @@ -425,34 +434,50 @@ where } } + // If there was a value that was also previously prepared, validate the prepare messages if previously_prepared { // Must have enough prepare messages for quorum if msg.qbft_message.prepare_justification.len() < self.config.quorum_size() { + warn!( + num_justifications = msg.qbft_message.prepare_justification.len(), + "Not enough prepare messages for quorum" + ); return false; } // Validate each prepare message matches highest prepared round/value - for prepare_msg in &msg.qbft_message.prepare_justification { - let deser_prepare = QbftMessage::from_ssz_bytes(prepare_msg.ssv_message().data()) - .map_err(|_| false) - .unwrap(); // todo!() get rid of this unwrap + for signed_prepare in &msg.qbft_message.prepare_justification { + let deser_prepare = + match QbftMessage::from_ssz_bytes(signed_prepare.ssv_message().data()) { + Ok(data) => data, + Err(_) => return false, + }; // Must be for highest prepared round if deser_prepare.round != max_prepared_round { - warn!("Prepare round incorrect"); + warn!( + got = deser_prepare.round, + expected = max_prepared_round, + "Prepare round does not match the highest found prepare" + ); return false; } // Must match highest prepared value - if deser_prepare.root != max_prepared_value.unwrap() { - warn!("Prepare root incorrect"); + if deser_prepare.root != max_prepared_value.expect("Value exist") { + warn!( + got = ?deser_prepare.root, + expected = ?max_prepared_value.expect("Value exist"), + "Prepared root does not match expected max prepared root" + ); return false; } // Must be from committee member - if !self.check_committee(&OperatorId(*prepare_msg.operator_ids().first().unwrap())) + if !self + .check_committee(&OperatorId(*signed_prepare.operator_ids().first().unwrap())) { - warn!("not part of the committee"); + warn!("Operator is not part of the committee"); return false; } } @@ -466,6 +491,7 @@ where .collect(); if unique_signers.len() < self.config.quorum_size() { + warn!("Did not find a quorum of unique signers"); return false; } } From 0cd40edb1b04c37cd1c304cf181a32e4db16f489 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Mon, 27 Jan 2025 23:32:04 +0000 Subject: [PATCH 29/45] justification verification and start on sketch out... --- anchor/common/qbft/src/lib.rs | 300 ++++++++++++++++++++++++++-------- 1 file changed, 236 insertions(+), 64 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index f672fed6..65ff736b 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -1,6 +1,6 @@ use crate::msg_container::MessageContainer; use ssv_types::consensus::{QbftData, QbftMessage, QbftMessageType, UnsignedSSVMessage}; -use ssv_types::message::{MessageID, MsgType, SSVMessage}; +use ssv_types::message::{MessageID, MsgType, SSVMessage, SignedSSVMessage}; use ssv_types::OperatorId; use ssz::{Decode, Encode}; use std::collections::HashMap; @@ -335,9 +335,11 @@ where return; } - // Round change justification validation for rounds after the first - if round > Round::default() { - self.validate_round_change_justification(&wrapped_msg); + // If we are passed the first round, make sure that the justifications actually justify the + // received proposal + if round > Round::default() && !self.validate_justifications(&wrapped_msg) { + warn!(from = ?operator_id, self=?self.config.operator_id(), "Justification verifiction failed"); + return; } // Verify that the fulldata matches the data root of the qbft message data @@ -372,33 +374,45 @@ where self.send_prepare(wrapped_msg.qbft_message.root); } - fn validate_round_change_justification(&self, msg: &WrappedQbftMessage) -> bool { + // Validate the round change and prepare justifications. Returns true if the justifications + // correctly justify the proposal + // + // A QBFT Message contains fields to a list of round change justifications and prepare + // justifications. We must go through each of these individually and verify the validity of each + // one + fn validate_justifications(&self, msg: &WrappedQbftMessage) -> bool { // Record if any of the round change messages have a value that was prepared let mut previously_prepared = false; let mut max_prepared_round = 0; let mut max_prepared_value = None; + let mut max_prepared_msg = None; - // Go through all of the round changes messages and verify each one + // Make sure we have a quorum of round change messages + if msg.qbft_message.round_change_justification.len() < self.config.quorum_size() { + warn!("Did not recieve a quorum of round change messages"); + return false; + } + + // There is a quorum of round change messages, go through and verify each one for signed_round_change in &msg.qbft_message.round_change_justification { - let deser_round_change: QbftMessage = + // The qbft message is represented as a Vec in the signed message, deserialize this + // into a proper QbftMessage + let round_change: QbftMessage = match QbftMessage::from_ssz_bytes(signed_round_change.ssv_message().data()) { Ok(data) => data, Err(_) => return false, }; // Make sure this is actually a round change message - if !matches!( - deser_round_change.qbft_message_type, - QbftMessageType::RoundChange - ) { - warn!(message_type = ?deser_round_change.qbft_message_type, "Message is not a ROUNDCHANGE message"); + if !matches!(round_change.qbft_message_type, QbftMessageType::RoundChange) { + warn!(message_type = ?round_change.qbft_message_type, "Message is not a ROUNDCHANGE message"); return false; } // Make sure it is for the correct height - if deser_round_change.height != *self.instance_height as u64 { + if round_change.height != *self.instance_height as u64 { warn!( - got = deser_round_change.height, + got = round_change.height, expected = *self.instance_height, "Message for the wrong height" ); @@ -406,9 +420,9 @@ where } // Make sure this is for the correct round - if deser_round_change.round != self.current_round.get() as u64 { + if round_change.round != self.current_round.get() as u64 { warn!( - got = deser_round_change.round, + got = round_change.round, expected = self.current_round.get(), "Message for the wrong round" ); @@ -424,12 +438,55 @@ where return false; } - // If the data round != 0, that means we have prepared a value in previous rounds - if deser_round_change.data_round > 0 { + // Make sure the one signer is in our committee + let signer = OperatorId( + *signed_round_change + .operator_ids() + .first() + .expect("Confirmed to exist"), + ); + if !self.check_committee(&signer) { + warn!("Signer is not part of committee"); + return false; + } + + // Verify the signature. TODO!() + + // Addttional veirifction + if round_change.qbft_message_type == QbftMessageType::RoundChange + && round_change.round > 1 + { + // hash the fulldata on the signed message + let signed_hash = signed_round_change.hash_fulldata(); + + // Validate all of the prepare justifications + // todo!(), im going through them once, why am I going though them all again???? and + // why are they called prepare then. 274 round_change.go + // this has to refer to the prepare messages, and why do we do this on every single + // iteration + for signed_prepare_message in &msg.qbft_message.round_change_justification { + // todo!(), figure this part out, they are literally checking the same exact + // thing we are here + } + + // Check the roots match + if signed_hash != round_change.root { + warn!("Data hashes do not match"); + return false; + } + + // check that we have quorum and round again?? + } + + // If the data round > 1, that means we have prepared a value in previous rounds + if round_change.data_round > 1 { previously_prepared = true; - if deser_round_change.data_round > max_prepared_round { - max_prepared_round = deser_round_change.data_round; - max_prepared_value = Some(deser_round_change.root); + + // also track the max prepared value and round + if round_change.data_round > max_prepared_round { + max_prepared_round = round_change.data_round; + max_prepared_value = Some(round_change.root); + max_prepared_msg = Some(round_change); } } } @@ -447,52 +504,78 @@ where // Validate each prepare message matches highest prepared round/value for signed_prepare in &msg.qbft_message.prepare_justification { - let deser_prepare = - match QbftMessage::from_ssz_bytes(signed_prepare.ssv_message().data()) { - Ok(data) => data, - Err(_) => return false, - }; - - // Must be for highest prepared round - if deser_prepare.round != max_prepared_round { + // The qbft message is represented as Vec in the signed message, deserialize + // this into a qbft message + let prepare = match QbftMessage::from_ssz_bytes(signed_prepare.ssv_message().data()) + { + Ok(data) => data, + Err(_) => return false, + }; + + // Make sure the roots match + let msg_fulldata_hashed = msg.signed_message.hash_fulldata(); + if msg_fulldata_hashed != max_prepared_msg.clone().expect("Confirmed to exist").root + { + warn!("Highest prepared does not match proposed data"); + return false; + } + + // validate each prepare message against the highest previously prepared fullData and round + + // Make sure this is a prepare message + if prepare.qbft_message_type != QbftMessageType::Prepare { + warn!("Expected a prepare message"); + return false; + } + + // Make sure it is for the correct height + if prepare.height != *self.instance_height as u64 { warn!( - got = deser_prepare.round, - expected = max_prepared_round, - "Prepare round does not match the highest found prepare" + got = prepare.height, + expected = *self.instance_height, + "Message for the wrong height" ); return false; } - // Must match highest prepared value - if deser_prepare.root != max_prepared_value.expect("Value exist") { + // Make sure this is for the correct round + if prepare.round != self.current_round.get() as u64 { warn!( - got = ?deser_prepare.root, - expected = ?max_prepared_value.expect("Value exist"), - "Prepared root does not match expected max prepared root" + got = prepare.round, + expected = self.current_round.get(), + "Message for the wrong round" ); return false; } - // Must be from committee member - if !self - .check_committee(&OperatorId(*signed_prepare.operator_ids().first().unwrap())) - { - warn!("Operator is not part of the committee"); + if prepare.root != msg_fulldata_hashed { + warn!("Proposed data mismatch"); return false; } - } - // Verify unique signers for quorum - let unique_signers: HashSet<_> = msg - .qbft_message - .prepare_justification - .iter() - .map(|m| m.operator_ids().first().unwrap()) - .collect(); + // Make sure there is only one signer + if signed_prepare.operator_ids().len() != 1 { + warn!( + num_signers = signed_prepare.operator_ids().len(), + "More than one message signer found" + ); + return false; + } - if unique_signers.len() < self.config.quorum_size() { - warn!("Did not find a quorum of unique signers"); - return false; + // Make sure the one signer is in our committee + let signer = OperatorId( + *signed_prepare + .operator_ids() + .first() + .expect("Confirmed to exist"), + ); + if !self.check_committee(&signer) { + warn!("Signer is not part of committee"); + return false; + } + + // verify the signature + // todo!() } } true @@ -595,6 +678,75 @@ where } } + fn has_received_proposal_justification_for_leading( + &self, + ) -> (Option, Vec) { + // get all the round change messages for the current round + let round_change_msg = self + .round_change_container + .get_messages_for_round(self.current_round); + + // if there is not a quorum, just return false + if round_change_msg.len() < self.config.quorum_size() { + return (None, vec![]); + } + + // Important! + // We iterate on all round chance msgs for liveliness in case the last round change msg is malicious. + for msg in round_change_msg { + // Chose proposal value. + // If justifiedRoundChangeMsg has no prepare justification chose state value + // If justifiedRoundChangeMsg has prepare justification chose prepared value + let mut value_to_propose = vec![]; + if msg.qbft_message.qbft_message_type == QbftMessageType::RoundChange + && msg.qbft_message.data_round != 0 + { + // this is a round change message and it says that there was a value prepared + value_to_propose = msg.signed_message.full_data().to_vec(); + } + + // all of the round change justifications in this message + let round_change_justifications = msg.qbft_message.round_change_justification.clone(); + + // todo!() they are then turne dinto wrapped, do we need this?? + if self.is_proposal_justification_for_leading_round() { + return (Some(msg.clone()), value_to_propose); + } + } + + todo!() + } + + fn is_proposal_justification_for_leading_round(&self) -> bool { + if !self.is_received_proposal_justification() { + return false; + } + /* + if proposer(state, config, roundChangeMsg.QBFTMessage.Round) != state.CommitteeMember.OperatorID { + return errors.New("not proposer") + } + + currentRoundProposal := state.ProposalAcceptedForCurrentRound == nil && state.Round == newRound + futureRoundProposal := newRound > state.Round + + if !currentRoundProposal && !futureRoundProposal { + return errors.New("proposal round mismatch") + } + */ + + let current_round_proposal = self.proposal_accepted_for_current_round == false + && self.current_round == Round::default(); + // figure out the round check + + // todo!() some more here + true + } + + // isReceivedProposalJustification - returns nil if we have a quorum of round change msgs and highest justified value + fn is_received_proposal_justification(&self) -> bool { + todo!() + } + /// We have received a round change message. fn received_round_change( &mut self, @@ -618,6 +770,9 @@ where warn!(from = ?operator_id, "ROUNDCHANGE message is a duplicate") } + // has received proposal justiufication for current round + let (justified_msg, value) = self.has_received_proposal_justification_for_leading(); + // There are two cases to check here // 1. If we have received a quorum of round change messages, we need to start a new round @@ -689,10 +844,35 @@ where msg_type: QbftMessageType, data_hash: D::Hash, ) -> UnsignedSSVMessage { + // Round change justifications + let round_change_justifications: Vec = + if self.current_round > Round::default() { + if matches!(self.state, InstanceState::AwaitingProposal) { + // For proposal: contains a list of round changes messages for the current round + // This is to justify that we indeed had a consensus of round change messages + // allowing us to move to the next round + self.round_change_container + .get_messages_for_round(self.current_round) + .iter() + .map(|msg| msg.signed_message.clone()) + .collect() + } else if matches!(self.state, InstanceState::SentRoundChange) { + // For round change: contains a list of prepare messages + // Allows us to prove that this value was prepared + todo!() + } else { + vec![] + } + } else { + vec![] + }; + + // Prepare justifications + // Used for Proposal messages + // For Proposal: contains a list of prepare messages + + // todo!() do the above // if we are in a round change, use round + 1 and get the prepare justifications if needed - // Round change. we only include the round change justifications which are the prepare - // messageks - // The justification is a quorum of signed prepare messages that agree on state.LastPreparedValue let (round, prepare_justification) = if matches!(self.state, InstanceState::SentRoundChange) { // If we are sending a round change and have a value that was prepared in the last @@ -710,14 +890,6 @@ where vec![] }; (self.current_round.get() as u64 + 1, prepare_justification) - } else if matches!(self.state, InstanceState::AwaitingProposal) { - let round_changes = self - .round_change_container - .get_messages_for_round(self.current_round) - .iter() - .map(|msg| msg.signed_message.clone()) - .collect(); - (self.current_round.get() as u64, round_changes) } else { (self.current_round.get() as u64, vec![]) }; From 1b848e02aed44cf684941746b79a55879e4ec02d Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Tue, 28 Jan 2025 16:43:41 +0000 Subject: [PATCH 30/45] simplify prepare justifications --- anchor/common/qbft/src/lib.rs | 309 ++++++++++++++-------------------- 1 file changed, 129 insertions(+), 180 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 65ff736b..388bb670 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -260,6 +260,7 @@ where if self.check_leader(&self.config.operator_id()) { // We are the leader + // todo!() how does this tie into the justification // Check justification of round change quorum. If there is a justification, we will use // that data. Otherwise, use the initial state data let (data_hash, data) = self @@ -384,7 +385,6 @@ where // Record if any of the round change messages have a value that was prepared let mut previously_prepared = false; let mut max_prepared_round = 0; - let mut max_prepared_value = None; let mut max_prepared_msg = None; // Make sure we have a quorum of round change messages @@ -393,7 +393,8 @@ where return false; } - // There is a quorum of round change messages, go through and verify each one + // There was a quorum of round change justifications. We need to go though and verify each + // one. Each will be a SignedSSVMessage for signed_round_change in &msg.qbft_message.round_change_justification { // The qbft message is represented as a Vec in the signed message, deserialize this // into a proper QbftMessage @@ -452,31 +453,8 @@ where // Verify the signature. TODO!() - // Addttional veirifction - if round_change.qbft_message_type == QbftMessageType::RoundChange - && round_change.round > 1 - { - // hash the fulldata on the signed message - let signed_hash = signed_round_change.hash_fulldata(); - - // Validate all of the prepare justifications - // todo!(), im going through them once, why am I going though them all again???? and - // why are they called prepare then. 274 round_change.go - // this has to refer to the prepare messages, and why do we do this on every single - // iteration - for signed_prepare_message in &msg.qbft_message.round_change_justification { - // todo!(), figure this part out, they are literally checking the same exact - // thing we are here - } - - // Check the roots match - if signed_hash != round_change.root { - warn!("Data hashes do not match"); - return false; - } - - // check that we have quorum and round again?? - } + // Addtional verifiction. TODO!(). this is literally the exact same thing just over + // again??? // If the data round > 1, that means we have prepared a value in previous rounds if round_change.data_round > 1 { @@ -485,15 +463,15 @@ where // also track the max prepared value and round if round_change.data_round > max_prepared_round { max_prepared_round = round_change.data_round; - max_prepared_value = Some(round_change.root); max_prepared_msg = Some(round_change); } } } - // If there was a value that was also previously prepared, validate the prepare messages + // If there was a value that was also previously prepared, we must also verify all of the + // prepare justifications if previously_prepared { - // Must have enough prepare messages for quorum + // Make sure we have a quorum of prepare messages if msg.qbft_message.prepare_justification.len() < self.config.quorum_size() { warn!( num_justifications = msg.qbft_message.prepare_justification.len(), @@ -502,6 +480,14 @@ where return false; } + // Make sure the roots match, this doesnt maek sense, it does not even pertain to + // the prepare message + let msg_fulldata_hashed = msg.signed_message.hash_fulldata(); + if msg_fulldata_hashed != max_prepared_msg.clone().expect("Confirmed to exist").root { + warn!("Highest prepared does not match proposed data"); + return false; + } + // Validate each prepare message matches highest prepared round/value for signed_prepare in &msg.qbft_message.prepare_justification { // The qbft message is represented as Vec in the signed message, deserialize @@ -512,14 +498,6 @@ where Err(_) => return false, }; - // Make sure the roots match - let msg_fulldata_hashed = msg.signed_message.hash_fulldata(); - if msg_fulldata_hashed != max_prepared_msg.clone().expect("Confirmed to exist").root - { - warn!("Highest prepared does not match proposed data"); - return false; - } - // validate each prepare message against the highest previously prepared fullData and round // Make sure this is a prepare message @@ -678,75 +656,6 @@ where } } - fn has_received_proposal_justification_for_leading( - &self, - ) -> (Option, Vec) { - // get all the round change messages for the current round - let round_change_msg = self - .round_change_container - .get_messages_for_round(self.current_round); - - // if there is not a quorum, just return false - if round_change_msg.len() < self.config.quorum_size() { - return (None, vec![]); - } - - // Important! - // We iterate on all round chance msgs for liveliness in case the last round change msg is malicious. - for msg in round_change_msg { - // Chose proposal value. - // If justifiedRoundChangeMsg has no prepare justification chose state value - // If justifiedRoundChangeMsg has prepare justification chose prepared value - let mut value_to_propose = vec![]; - if msg.qbft_message.qbft_message_type == QbftMessageType::RoundChange - && msg.qbft_message.data_round != 0 - { - // this is a round change message and it says that there was a value prepared - value_to_propose = msg.signed_message.full_data().to_vec(); - } - - // all of the round change justifications in this message - let round_change_justifications = msg.qbft_message.round_change_justification.clone(); - - // todo!() they are then turne dinto wrapped, do we need this?? - if self.is_proposal_justification_for_leading_round() { - return (Some(msg.clone()), value_to_propose); - } - } - - todo!() - } - - fn is_proposal_justification_for_leading_round(&self) -> bool { - if !self.is_received_proposal_justification() { - return false; - } - /* - if proposer(state, config, roundChangeMsg.QBFTMessage.Round) != state.CommitteeMember.OperatorID { - return errors.New("not proposer") - } - - currentRoundProposal := state.ProposalAcceptedForCurrentRound == nil && state.Round == newRound - futureRoundProposal := newRound > state.Round - - if !currentRoundProposal && !futureRoundProposal { - return errors.New("proposal round mismatch") - } - */ - - let current_round_proposal = self.proposal_accepted_for_current_round == false - && self.current_round == Round::default(); - // figure out the round check - - // todo!() some more here - true - } - - // isReceivedProposalJustification - returns nil if we have a quorum of round change msgs and highest justified value - fn is_received_proposal_justification(&self) -> bool { - todo!() - } - /// We have received a round change message. fn received_round_change( &mut self, @@ -770,9 +679,6 @@ where warn!(from = ?operator_id, "ROUNDCHANGE message is a duplicate") } - // has received proposal justiufication for current round - let (justified_msg, value) = self.has_received_proposal_justification_for_leading(); - // There are two cases to check here // 1. If we have received a quorum of round change messages, we need to start a new round @@ -843,77 +749,14 @@ where &self, msg_type: QbftMessageType, data_hash: D::Hash, + round_change_justification: Vec, + prepare_justification: Vec, ) -> UnsignedSSVMessage { - // Round change justifications - let round_change_justifications: Vec = - if self.current_round > Round::default() { - if matches!(self.state, InstanceState::AwaitingProposal) { - // For proposal: contains a list of round changes messages for the current round - // This is to justify that we indeed had a consensus of round change messages - // allowing us to move to the next round - self.round_change_container - .get_messages_for_round(self.current_round) - .iter() - .map(|msg| msg.signed_message.clone()) - .collect() - } else if matches!(self.state, InstanceState::SentRoundChange) { - // For round change: contains a list of prepare messages - // Allows us to prove that this value was prepared - todo!() - } else { - vec![] - } - } else { - vec![] - }; - - // Prepare justifications - // Used for Proposal messages - // For Proposal: contains a list of prepare messages - - // todo!() do the above - // if we are in a round change, use round + 1 and get the prepare justifications if needed - let (round, prepare_justification) = if matches!(self.state, InstanceState::SentRoundChange) - { - // If we are sending a round change and have a value that was prepared in the last - // round, we must include the prepare messages that prove that this value was actually - // prepared - let prepare_justification = if self.last_prepared_round.is_some() { - // Get all prepare messages for our last prepared value - self.prepare_container - .get_messages_for_round(self.last_prepared_round.expect("Value was prepared")) - .iter() - .filter(|msg| msg.qbft_message.root == data_hash) - .map(|msg| msg.signed_message.clone()) - .collect() - } else { - vec![] - }; - (self.current_round.get() as u64 + 1, prepare_justification) - } else { - (self.current_round.get() as u64, vec![]) - }; - - // if round > 1 and we are AwaitingProposal. Include round change justifications. We know - // that only the leader can send a message while in the AwaitingProposal state, so this must - // be a proposal message. We must include the round change messages that justify the choice - // of value. We also have to include the round change justifications?? - let round_change_justification = - if round > 0 && matches!(self.state, InstanceState::AwaitingProposal) { - self.round_change_container - .get_messages_for_round(self.current_round) - .iter() - .map(|msg| msg.signed_message.clone()) - .collect() - } else { - vec![] - }; - // Create the QBFT message let qbft_message = QbftMessage { qbft_message_type: msg_type, height: *self.instance_height as u64, - round, + round: self.current_round.get() as u64, identifier: self.identifier.clone(), root: data_hash, data_round: self @@ -942,13 +785,112 @@ where } } + // Get all of the round change jusitifcation messages + fn get_round_change_justifications(&self) -> Vec { + // Make sure we are past the first round + if self.current_round > Round::default() { + // If we are past the first round and awaiting proposal, that means that there was a + // round change and we must have a quorum of round change messages. We include these so + // that we can prove that we had a consensus allowing us to change + if matches!(self.state, InstanceState::AwaitingProposal) { + return self + .round_change_container + .get_messages_for_round(self.current_round) + .iter() + .map(|msg| msg.signed_message.clone()) + .collect(); + } + // If we are past the first round and are sending a round change. We have to include + // prepare messages that prove we have prepared a value + else if matches!(self.state, InstanceState::SentRoundChange) { + todo!() + } + } + + // We are either in the first round + vec![] + } + + // Get all of the prepare justifications for proposals + fn get_prepare_justifications(&self) -> Vec { + // Make sure we are past the first round + if self.current_round > Round::default() { + // We only send prepare justifications with for proposal messages. If we are in the + // state AwaitingProposal and sending a message, we know this is a proposal. This will + // happen when we have come to a consensus of round change messages and have started a + // new round + if matches!(self.state, InstanceState::AwaitingProposal) { + // go through all of the prepares for the leading round and see if we have have come + // to a justification? + + // Get all of the round change messages for the current round and make sure we have + // a quorum of them. + let round_change_msg = self + .round_change_container + .get_messages_for_round(self.current_round); + if round_change_msg.len() < self.config.quorum_size() { + return vec![]; + } + + // Go through each message and see if any have a value that was already prepared + // Just want to take the first one that is valid and has a prepared value + for wrapped_round_change in round_change_msg { + // Deserialize into a qbft message for sanity checks + let round_change: QbftMessage = QbftMessage::from_ssz_bytes( + wrapped_round_change.signed_message.ssv_message().data(), + ) + .unwrap(); + + // Round sanity check + let current_round_proposal = self.proposal_accepted_for_current_round + && self.current_round.get() as u64 == round_change.round; + let future_round_proposal = + round_change.round > self.current_round.get() as u64; + if !current_round_proposal && !future_round_proposal { + continue; + } + + // Validate the proposal, if this is a valid proposal then this is our prepare + // justification + if self.validate_justifications(wrapped_round_change) { + return vec![wrapped_round_change.signed_message.clone()]; + } + } + } + } + // We are either in the first round, or not sending a proposal. Just return empty vec + vec![] + } + // Send a new qbft proposal message fn send_proposal(&mut self, hash: D::Hash, data: D) { // Store the data we're proposing self.data.insert(hash, data.clone()); + // For Proposal messages + // round_change_justification: list of round change messages + let round_change_justifications = self.get_round_change_justifications(); + // prepare_justification: list of prepare messages + let prepare_justifications = self.get_prepare_justifications(); + + // If we have a prepare justification, we have to use that value in message, else we just + // use the start value + /* + let value_to_propose = if prepare_justification.len() > 1 { + // extract the root that we need to propose + let signed_msg = prepare_justifications.first().expect("Confirmed to exist"); + + + } + */ + // Construct a unsigned proposal - let unsigned_msg = self.new_unsigned_message(QbftMessageType::Proposal, hash); + let unsigned_msg = self.new_unsigned_message( + QbftMessageType::Proposal, + hash, + round_change_justifications, + prepare_justifications, + ); let operator_id = self.config.operator_id(); (self.send_message)(Message::Propose(operator_id, unsigned_msg.clone())); @@ -963,7 +905,8 @@ where } // Construct unsigned prepare - let unsigned_msg = self.new_unsigned_message(QbftMessageType::Prepare, data_hash); + let unsigned_msg = + self.new_unsigned_message(QbftMessageType::Prepare, data_hash, vec![], vec![]); let operator_id = self.config.operator_id(); (self.send_message)(Message::Prepare(operator_id, unsigned_msg.clone())); @@ -972,7 +915,8 @@ where // Send a new qbft commit message fn send_commit(&mut self, data_hash: D::Hash) { // Construct unsigned commit - let unsigned_msg = self.new_unsigned_message(QbftMessageType::Commit, data_hash); + let unsigned_msg = + self.new_unsigned_message(QbftMessageType::Commit, data_hash, vec![], vec![]); let operator_id = self.config.operator_id(); (self.send_message)(Message::Commit(operator_id, unsigned_msg.clone())); @@ -980,8 +924,13 @@ where // Send a new qbft round change message fn send_round_change(&mut self, data_hash: D::Hash) { + // For Round Change messages + // round_change_justification: list of prepare messages + // prepare_justification: N/A + // Construct unsigned round change - let unsigned_msg = self.new_unsigned_message(QbftMessageType::RoundChange, data_hash); + let unsigned_msg = + self.new_unsigned_message(QbftMessageType::RoundChange, data_hash, vec![], vec![]); // forget that we accpeted a proposal self.proposal_accepted_for_current_round = false; From 5998dedbb52e937a345be11e62901f6a5b74a5f1 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Tue, 28 Jan 2025 16:51:22 +0000 Subject: [PATCH 31/45] proposal justifications --- anchor/common/qbft/src/lib.rs | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 388bb670..b4ce0632 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -812,7 +812,7 @@ where } // Get all of the prepare justifications for proposals - fn get_prepare_justifications(&self) -> Vec { + fn get_prepare_justifications(&self) -> (Vec, Option) { // Make sure we are past the first round if self.current_round > Round::default() { // We only send prepare justifications with for proposal messages. If we are in the @@ -829,7 +829,7 @@ where .round_change_container .get_messages_for_round(self.current_round); if round_change_msg.len() < self.config.quorum_size() { - return vec![]; + return (vec![], None); } // Go through each message and see if any have a value that was already prepared @@ -853,13 +853,16 @@ where // Validate the proposal, if this is a valid proposal then this is our prepare // justification if self.validate_justifications(wrapped_round_change) { - return vec![wrapped_round_change.signed_message.clone()]; + return ( + vec![wrapped_round_change.signed_message.clone()], + Some(round_change.root), + ); } } } } // We are either in the first round, or not sending a proposal. Just return empty vec - vec![] + (vec![], None) } // Send a new qbft proposal message @@ -871,23 +874,19 @@ where // round_change_justification: list of round change messages let round_change_justifications = self.get_round_change_justifications(); // prepare_justification: list of prepare messages - let prepare_justifications = self.get_prepare_justifications(); - - // If we have a prepare justification, we have to use that value in message, else we just - // use the start value - /* - let value_to_propose = if prepare_justification.len() > 1 { - // extract the root that we need to propose - let signed_msg = prepare_justifications.first().expect("Confirmed to exist"); - + let (prepare_justifications, value_to_propose) = self.get_prepare_justifications(); - } - */ + // Determine the value that should be proposed based off of justification. If we have a + // prepare justification, we want to propose that value. Else, just propose the start data + let value_to_propose = match value_to_propose { + Some(value) => value, + None => self.start_data_hash, + }; // Construct a unsigned proposal let unsigned_msg = self.new_unsigned_message( QbftMessageType::Proposal, - hash, + value_to_propose, round_change_justifications, prepare_justifications, ); From 3952a7654248fcbefb17d415dbaef152a17c632a Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Tue, 28 Jan 2025 22:00:25 +0000 Subject: [PATCH 32/45] round change justifications --- anchor/common/qbft/src/lib.rs | 108 +++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 16 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index b4ce0632..b2157862 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -4,7 +4,6 @@ use ssv_types::message::{MessageID, MsgType, SSVMessage, SignedSSVMessage}; use ssv_types::OperatorId; use ssz::{Decode, Encode}; use std::collections::HashMap; -use std::collections::HashSet; use tracing::{debug, error, warn}; use types::Hash256; @@ -743,6 +742,58 @@ where self.start_round(); } + // Get data for the qbft message. Todo!() does this work??? + // ( + // data_round (u64) -> round related to the proposed data + // round (u64) -> the message round + // root (D::Hash) -> the hash of the proposed data, if necessary + // full_data (Vec) -> serialized data that we are coming to conseusn on + // ) + fn get_message_data( + &self, + msg_type: &QbftMessageType, + data_hash: D::Hash, + ) -> (u64, u64, D::Hash, Vec) { + // Get the fulldata for the data_hash + let full_data = if let Some(data) = self.data.get(&data_hash) { + data.as_ssz_bytes() + } else { + vec![] + }; + + match msg_type { + QbftMessageType::Proposal | QbftMessageType::Prepare | QbftMessageType::Commit => { + // todo!() for prepare, does it leave fulldata blank?? + let data_round = 0; // todo!(), why is this zero + let round = self.current_round.get() as u64; + (data_round, round, data_hash, full_data) + } + QbftMessageType::RoundChange => { + if self.last_prepared_round.is_some() && self.last_prepared_value.is_some() { + let last_prepared_value = + self.last_prepared_value.expect("Confirmed to be Some"); + let last_prepared_round = + self.last_prepared_round.expect("Confirmed to be Some"); + + let full_data = if let Some(data) = self.data.get(&last_prepared_value) { + data.as_ssz_bytes() + } else { + vec![] + }; + + ( + last_prepared_round.get() as u64, + self.current_round.get() as u64 + 1, + last_prepared_value, + full_data, + ) + } else { + todo!() + } + } + } + } + // Construct a new unsigned message. This will be passed to the processor to be signed and then // sent on the network fn new_unsigned_message( @@ -752,16 +803,17 @@ where round_change_justification: Vec, prepare_justification: Vec, ) -> UnsignedSSVMessage { + // Get misc data to populate the message with based off of the message type + let (data_round, round, root, full_data) = self.get_message_data(&msg_type, data_hash); + // Create the QBFT message let qbft_message = QbftMessage { qbft_message_type: msg_type, height: *self.instance_height as u64, - round: self.current_round.get() as u64, + round, identifier: self.identifier.clone(), - root: data_hash, - data_round: self - .last_prepared_round - .map_or(0, |round| round.get() as u64), + root, + data_round, round_change_justification, prepare_justification, }; @@ -772,12 +824,6 @@ where qbft_message.as_ssz_bytes(), ); - let full_data = if let Some(data) = self.data.get(&data_hash) { - data.as_ssz_bytes() - } else { - vec![] - }; - // Wrap in unsigned SSV message UnsignedSSVMessage { ssv_message, @@ -803,11 +849,36 @@ where // If we are past the first round and are sending a round change. We have to include // prepare messages that prove we have prepared a value else if matches!(self.state, InstanceState::SentRoundChange) { - todo!() + // if we have a last prepared value and a last prepared round... + if self.last_prepared_round.is_some() && self.last_prepared_value.is_some() { + // todo!() diff syntax for this + let _last_prepared_value = + self.last_prepared_value.expect("Confirmed to be Some"); + let last_prepared_round = + self.last_prepared_round.expect("Confirmed to be Some"); + + // Get all of the prepare messages for the last prepared round + let last_prepared_messages = self + .prepare_container + .get_messages_for_round(last_prepared_round); + + // confirm that root of all messages match the last_prepared_value. TODO!(). is + // this necessary??? + + // Make sure we have a quorum of prepare message + if last_prepared_messages.len() < self.config.quorum_size() { + return vec![]; + } + return last_prepared_messages + .iter() + .map(|msg| msg.signed_message.clone()) + .collect(); + } + return vec![]; } } - // We are either in the first round + // We are either in the first round or sending a prepare/commit message vec![] } @@ -925,11 +996,16 @@ where fn send_round_change(&mut self, data_hash: D::Hash) { // For Round Change messages // round_change_justification: list of prepare messages + let round_change_justifications = self.get_round_change_justifications(); // prepare_justification: N/A // Construct unsigned round change - let unsigned_msg = - self.new_unsigned_message(QbftMessageType::RoundChange, data_hash, vec![], vec![]); + let unsigned_msg = self.new_unsigned_message( + QbftMessageType::RoundChange, + data_hash, + round_change_justifications, + vec![], + ); // forget that we accpeted a proposal self.proposal_accepted_for_current_round = false; From acc06bc2f56f97063d15b28093987864ba642324 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Tue, 28 Jan 2025 22:39:51 +0000 Subject: [PATCH 33/45] simplify justification validation --- anchor/common/qbft/src/lib.rs | 131 ++++++++++++++-------------------- 1 file changed, 53 insertions(+), 78 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index b2157862..2afcdb60 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -409,44 +409,8 @@ where return false; } - // Make sure it is for the correct height - if round_change.height != *self.instance_height as u64 { - warn!( - got = round_change.height, - expected = *self.instance_height, - "Message for the wrong height" - ); - return false; - } - - // Make sure this is for the correct round - if round_change.round != self.current_round.get() as u64 { - warn!( - got = round_change.round, - expected = self.current_round.get(), - "Message for the wrong round" - ); - return false; - } - - // Make sure there is only one signer - if signed_round_change.operator_ids().len() != 1 { - warn!( - num_signers = signed_round_change.operator_ids().len(), - "More than one message signer found" - ); - return false; - } - - // Make sure the one signer is in our committee - let signer = OperatorId( - *signed_round_change - .operator_ids() - .first() - .expect("Confirmed to exist"), - ); - if !self.check_committee(&signer) { - warn!("Signer is not part of committee"); + if !self.validate_qbft_message_against_state(&round_change, signed_round_change) { + warn!("ROUNDCHANGE message validation failed"); return false; } @@ -497,31 +461,14 @@ where Err(_) => return false, }; - // validate each prepare message against the highest previously prepared fullData and round - // Make sure this is a prepare message if prepare.qbft_message_type != QbftMessageType::Prepare { warn!("Expected a prepare message"); return false; } - // Make sure it is for the correct height - if prepare.height != *self.instance_height as u64 { - warn!( - got = prepare.height, - expected = *self.instance_height, - "Message for the wrong height" - ); - return false; - } - - // Make sure this is for the correct round - if prepare.round != self.current_round.get() as u64 { - warn!( - got = prepare.round, - expected = self.current_round.get(), - "Message for the wrong round" - ); + if !self.validate_qbft_message_against_state(&prepare, signed_prepare) { + warn!("PREPARE message validation failed"); return false; } @@ -530,27 +477,6 @@ where return false; } - // Make sure there is only one signer - if signed_prepare.operator_ids().len() != 1 { - warn!( - num_signers = signed_prepare.operator_ids().len(), - "More than one message signer found" - ); - return false; - } - - // Make sure the one signer is in our committee - let signer = OperatorId( - *signed_prepare - .operator_ids() - .first() - .expect("Confirmed to exist"), - ); - if !self.check_committee(&signer) { - warn!("Signer is not part of committee"); - return false; - } - // verify the signature // todo!() } @@ -558,6 +484,55 @@ where true } + // Validate a qbft message against the state + fn validate_qbft_message_against_state( + &self, + msg: &QbftMessage, + signed_msg: &SignedSSVMessage, + ) -> bool { + // Make sure it is for the correct height + if msg.height != *self.instance_height as u64 { + warn!( + got = msg.height, + expected = *self.instance_height, + "Message for the wrong height" + ); + return false; + } + + // Make sure this is for the correct round + if msg.round != self.current_round.get() as u64 { + warn!( + got = msg.round, + expected = self.current_round.get(), + "Message for the wrong round" + ); + return false; + } + + // Make sure there is only one signer + if signed_msg.operator_ids().len() != 1 { + warn!( + num_signers = signed_msg.operator_ids().len(), + "More than one message signer found" + ); + return false; + } + + // Make sure the one signer is in our committee + let signer = OperatorId( + *signed_msg + .operator_ids() + .first() + .expect("Confirmed to exist"), + ); + if !self.check_committee(&signer) { + warn!("Signer is not part of committee"); + return false; + } + true + } + /// We have received a prepare message fn received_prepare( &mut self, From 685f2677d4b05f8e750b4f4855c21c2ee8f38ec8 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Tue, 28 Jan 2025 23:04:46 +0000 Subject: [PATCH 34/45] small fix --- anchor/common/qbft/src/lib.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 2afcdb60..d0ce8a45 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -744,12 +744,9 @@ where (data_round, round, data_hash, full_data) } QbftMessageType::RoundChange => { - if self.last_prepared_round.is_some() && self.last_prepared_value.is_some() { - let last_prepared_value = - self.last_prepared_value.expect("Confirmed to be Some"); - let last_prepared_round = - self.last_prepared_round.expect("Confirmed to be Some"); - + if let (Some(last_prepared_value), Some(last_prepared_round)) = + (self.last_prepared_value, self.last_prepared_round) + { let full_data = if let Some(data) = self.data.get(&last_prepared_value) { data.as_ssz_bytes() } else { From 3b5ef62fe1006153f1a52b8bec448cefe58655c0 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 29 Jan 2025 14:12:56 +0000 Subject: [PATCH 35/45] syntax fix --- anchor/common/qbft/src/lib.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index d0ce8a45..ea50a99f 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -621,6 +621,8 @@ where // Check if we have a commit quorum if let Some(hash) = self.prepare_container.has_quorum(round) { if matches!(self.state, InstanceState::Commit) { + // Commit aggregation??? todo!() + // We have come to consensus, mark ourself as completed and record the agreed upon // value self.state = InstanceState::Complete; @@ -675,8 +677,6 @@ where if num_messages_for_round > self.config.get_f() && !(matches!(self.state, InstanceState::SentRoundChange)) { - // send our own round change message - // Set the state so SendRoundChange so we include Round + 1 in message self.state = InstanceState::SentRoundChange; @@ -822,13 +822,9 @@ where // prepare messages that prove we have prepared a value else if matches!(self.state, InstanceState::SentRoundChange) { // if we have a last prepared value and a last prepared round... - if self.last_prepared_round.is_some() && self.last_prepared_value.is_some() { - // todo!() diff syntax for this - let _last_prepared_value = - self.last_prepared_value.expect("Confirmed to be Some"); - let last_prepared_round = - self.last_prepared_round.expect("Confirmed to be Some"); - + if let (Some(last_prepared_value), Some(last_prepared_round)) = + (self.last_prepared_value, self.last_prepared_round) + { // Get all of the prepare messages for the last prepared round let last_prepared_messages = self .prepare_container From 7c9373835e879274153f599529c9172c2ade0874 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 29 Jan 2025 14:47:30 +0000 Subject: [PATCH 36/45] simplify repeat validation --- anchor/common/qbft/src/lib.rs | 101 +++++++++++++++------------------- 1 file changed, 45 insertions(+), 56 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index ea50a99f..3a9a2370 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -68,6 +68,7 @@ where // Current round state proposal_accepted_for_current_round: bool, + proposal_root: Option, last_prepared_round: Option, last_prepared_value: Option, @@ -108,6 +109,7 @@ where round_change_container: MessageContainer::new(quorum_size), proposal_accepted_for_current_round: false, + proposal_root: None, last_prepared_round: None, last_prepared_value: None, @@ -181,6 +183,18 @@ where return false; } + // Make sure the one signer is in our committee + let signer = OperatorId( + *wrapped_msg.signed_message + .operator_ids() + .first() + .expect("Confirmed to exist"), + ); + if !self.check_committee(&signer) { + warn!("Signer is not part of committee"); + return false; + } + // Make sure we are at the correct instance height if wrapped_msg.qbft_message.height != *self.instance_height as u64 { warn!( @@ -367,6 +381,7 @@ where // Update state self.proposal_accepted_for_current_round = true; + self.proposal_root = Some(data_hash); self.state = InstanceState::Prepare; debug!(in = ?self.config.operator_id(), state = ?self.state, "State updated to PREPARE"); @@ -409,7 +424,9 @@ where return false; } - if !self.validate_qbft_message_against_state(&round_change, signed_round_change) { + // Convert to a wrapped message and perform verification + let wrapped = WrappedQbftMessage { signed_message: signed_round_change.clone(), qbft_message: round_change.clone()}; + if !self.validate_message(&wrapped) { warn!("ROUNDCHANGE message validation failed"); return false; } @@ -467,7 +484,8 @@ where return false; } - if !self.validate_qbft_message_against_state(&prepare, signed_prepare) { + let wrapped = WrappedQbftMessage { signed_message: signed_prepare.clone(), qbft_message: prepare.clone()}; + if !self.validate_message(&wrapped) { warn!("PREPARE message validation failed"); return false; } @@ -484,54 +502,6 @@ where true } - // Validate a qbft message against the state - fn validate_qbft_message_against_state( - &self, - msg: &QbftMessage, - signed_msg: &SignedSSVMessage, - ) -> bool { - // Make sure it is for the correct height - if msg.height != *self.instance_height as u64 { - warn!( - got = msg.height, - expected = *self.instance_height, - "Message for the wrong height" - ); - return false; - } - - // Make sure this is for the correct round - if msg.round != self.current_round.get() as u64 { - warn!( - got = msg.round, - expected = self.current_round.get(), - "Message for the wrong round" - ); - return false; - } - - // Make sure there is only one signer - if signed_msg.operator_ids().len() != 1 { - warn!( - num_signers = signed_msg.operator_ids().len(), - "More than one message signer found" - ); - return false; - } - - // Make sure the one signer is in our committee - let signer = OperatorId( - *signed_msg - .operator_ids() - .first() - .expect("Confirmed to exist"), - ); - if !self.check_committee(&signer) { - warn!("Signer is not part of committee"); - return false; - } - true - } /// We have received a prepare message fn received_prepare( @@ -540,12 +510,22 @@ where round: Round, wrapped_msg: WrappedQbftMessage, ) { - // Check that we are in the correct state + // Check that we are in the correct state. We do not have to be in the PREPARE state right + // now as this message may have been delayed if (self.state as u8) >= (InstanceState::SentRoundChange as u8) { warn!(from=?operator_id, ?self.state, "PREPARE message while in invalid state"); return; } + // Make sure this is actually a prepare message + if !(matches!( + wrapped_msg.qbft_message.qbft_message_type, + QbftMessageType::Prepare, + )) { + warn!(from=?operator_id, self=?self.config.operator_id(), "Expected a PREPARE message"); + return; + } + // Make sure that we have accepted a proposal for this round if !self.proposal_accepted_for_current_round { warn!(from=?operator_id, ?self.state, self=?self.config.operator_id(), "Have not accepted Proposal for current round yet"); @@ -562,7 +542,7 @@ where warn!(from = ?operator_id, "PREPARE message is a duplicate") } - // Check if we have reached quorum, if so send the commit message + // Check if we have reached a prepare quorum for this round, if so send the commit message if let Some(hash) = self.prepare_container.has_quorum(round) { // Make sure we are in the correct state if !matches!(self.state, InstanceState::Prepare) @@ -572,13 +552,20 @@ where return; } + // Make sure that the root of the data that we have come to a prepare consensus on + // matches the root of the proposal that we have accepted + if hash != self.proposal_root.expect("Proposal has been accepted") { + warn!("PREPARE quorum root does not match accepted PROPOSAL root"); + return; + } + + // Success! We have come to a prepare consensus on a value + // Move the state forward since we have a prepare quorum self.state = InstanceState::Commit; debug!(in = ?self.config.operator_id(), state = ?self.state, "Reached a PREPARE consensus. State updated to COMMIT"); - // Record this prepare consensus - // todo!() may need to record all of the prepare messages for the hash and save that - // too, used for justifications + // Record that we have come to a consensus on this value self.past_consensus.insert(round, hash); // Record as last prepared value and round @@ -822,7 +809,7 @@ where // prepare messages that prove we have prepared a value else if matches!(self.state, InstanceState::SentRoundChange) { // if we have a last prepared value and a last prepared round... - if let (Some(last_prepared_value), Some(last_prepared_round)) = + if let (Some(_), Some(last_prepared_round)) = (self.last_prepared_value, self.last_prepared_round) { // Get all of the prepare messages for the last prepared round @@ -837,6 +824,8 @@ where if last_prepared_messages.len() < self.config.quorum_size() { return vec![]; } + + // This will hold the value that we want to propose return last_prepared_messages .iter() .map(|msg| msg.signed_message.clone()) From a99444a1854c78c0c35dddf315e6db1ae1e5580f Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 29 Jan 2025 14:55:36 +0000 Subject: [PATCH 37/45] extra validation --- anchor/common/qbft/src/lib.rs | 37 +++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 3a9a2370..ca19952d 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -185,7 +185,8 @@ where // Make sure the one signer is in our committee let signer = OperatorId( - *wrapped_msg.signed_message + *wrapped_msg + .signed_message .operator_ids() .first() .expect("Confirmed to exist"), @@ -425,7 +426,10 @@ where } // Convert to a wrapped message and perform verification - let wrapped = WrappedQbftMessage { signed_message: signed_round_change.clone(), qbft_message: round_change.clone()}; + let wrapped = WrappedQbftMessage { + signed_message: signed_round_change.clone(), + qbft_message: round_change.clone(), + }; if !self.validate_message(&wrapped) { warn!("ROUNDCHANGE message validation failed"); return false; @@ -484,7 +488,10 @@ where return false; } - let wrapped = WrappedQbftMessage { signed_message: signed_prepare.clone(), qbft_message: prepare.clone()}; + let wrapped = WrappedQbftMessage { + signed_message: signed_prepare.clone(), + qbft_message: prepare.clone(), + }; if !self.validate_message(&wrapped) { warn!("PREPARE message validation failed"); return false; @@ -502,7 +509,6 @@ where true } - /// We have received a prepare message fn received_prepare( &mut self, @@ -595,6 +601,21 @@ where return; } + // Make sure this is actually a commit message + if !(matches!( + wrapped_msg.qbft_message.qbft_message_type, + QbftMessageType::Commit, + )) { + warn!(from=?operator_id, self=?self.config.operator_id(), "Expected a COMMIT message"); + return; + } + + // Make sure that we have accepted a proposal for this round + if !self.proposal_accepted_for_current_round { + warn!(from=?operator_id, ?self.state, self=?self.config.operator_id(), "Have not accepted Proposal for current round yet"); + return; + } + debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "COMMIT received"); // Store the received commit message @@ -607,6 +628,14 @@ where // Check if we have a commit quorum if let Some(hash) = self.prepare_container.has_quorum(round) { + // Make sure that the root of the data that we have come to a commit consensus on + // matches the root of the proposal that we have accepted + if hash != self.proposal_root.expect("Proposal has been accepted") { + warn!("COMMIT quorum root does not match accepted PROPOSAL root"); + return; + } + + // All validation successful, make sure we are in the proper commit state if matches!(self.state, InstanceState::Commit) { // Commit aggregation??? todo!() From 50b546c080d796301db9cbffcbc739c454f7420b Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 29 Jan 2025 15:39:04 +0000 Subject: [PATCH 38/45] small cleanup --- anchor/common/qbft/src/lib.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index ca19952d..1e45bb62 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -291,6 +291,7 @@ where // proposal self.state = InstanceState::Prepare; self.proposal_accepted_for_current_round = true; + self.proposal_root = Some(data_hash); } } @@ -437,9 +438,6 @@ where // Verify the signature. TODO!() - // Addtional verifiction. TODO!(). this is literally the exact same thing just over - // again??? - // If the data round > 1, that means we have prepared a value in previous rounds if round_change.data_round > 1 { previously_prepared = true; @@ -502,8 +500,7 @@ where return false; } - // verify the signature - // todo!() + // verify the signature. TODO!() } } true @@ -639,7 +636,7 @@ where if matches!(self.state, InstanceState::Commit) { // Commit aggregation??? todo!() - // We have come to consensus, mark ourself as completed and record the agreed upon + // We have come to commit consensus, mark ourself as completed and record the agreed upon // value self.state = InstanceState::Complete; self.completed = Some(Completed::Success(hash)); From f66437072126b0a66e1a184dce3b7df39b45ed0f Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Mon, 3 Feb 2025 14:30:41 +0000 Subject: [PATCH 39/45] openssl bump for vul --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2fb71784..eceb0452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2054,7 +2054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1145d32e826a7748b69ee8fc62d3e6355ff7f1051df53141e7048162fc90481b" dependencies = [ "data-encoding", - "syn 2.0.96", + "syn 1.0.109", ] [[package]] @@ -5322,9 +5322,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.69" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -5363,9 +5363,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", From 07797e688eb4b59db2ef722325f3e4df73271b5c Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Mon, 3 Feb 2025 14:40:05 +0000 Subject: [PATCH 40/45] handle error --- anchor/common/qbft/src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 0f88c810..f3673c3a 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -891,10 +891,12 @@ where // Just want to take the first one that is valid and has a prepared value for wrapped_round_change in round_change_msg { // Deserialize into a qbft message for sanity checks - let round_change: QbftMessage = QbftMessage::from_ssz_bytes( + let round_change: QbftMessage = match QbftMessage::from_ssz_bytes( wrapped_round_change.signed_message.ssv_message().data(), - ) - .unwrap(); + ) { + Ok(data) => data, + Err(_) => return (vec![], None), + }; // Round sanity check let current_round_proposal = self.proposal_accepted_for_current_round From 70194ed24c3e0eccbf52a83e6ab753fe72ae56f7 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 5 Feb 2025 15:47:24 +0000 Subject: [PATCH 41/45] clean up todo & address pr comments --- anchor/common/qbft/src/lib.rs | 312 +++++++++++------------ anchor/common/qbft/src/msg_container.rs | 6 +- anchor/common/ssv_types/src/consensus.rs | 2 - anchor/qbft_manager/src/lib.rs | 3 - 4 files changed, 157 insertions(+), 166 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index f3673c3a..b2530651 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -25,6 +25,28 @@ mod qbft_types; #[cfg(test)] mod tests; +// Internal structure to hold the data that is to be included in a new outgoing message +struct MessageData> { + data_round: u64, + round: u64, + root: D::Hash, + full_data: Vec, +} + +impl MessageData +where + D: QbftData, +{ + pub fn new(data_round: u64, round: u64, root: D::Hash, full_data: Vec) -> Self { + Self { + data_round, + round, + root, + full_data, + } + } +} + /// The structure that defines the Quorum Based Fault Tolerance (QBFT) instance. /// /// This builds and runs an entire QBFT process until it completes. It can complete either @@ -117,6 +139,8 @@ where send_message, }; + qbft.data + .insert(qbft.start_data_hash, qbft.start_data.clone()); qbft.start_round(); qbft } @@ -255,7 +279,11 @@ where // Verify we have also seen this consensus if let Some(hash) = self.past_consensus.get(&prepared_round) { // We have seen consensus on the data, get the value - let our_data = self.data.get(hash).expect("Data must exist").clone(); + let our_data = self + .data + .get(hash) + .expect("Data must exist since we have seen consensus on it") + .clone(); return Some((*hash, our_data)); } } @@ -275,7 +303,6 @@ where if self.check_leader(&self.config.operator_id()) { // We are the leader - // todo!() how does this tie into the justification // Check justification of round change quorum. If there is a justification, we will use // that data. Otherwise, use the initial state data let (data_hash, data) = self @@ -296,7 +323,7 @@ where } } - // Receive a new message from the network + /// Receive a new message from the network pub fn receive(&mut self, wrapped_msg: WrappedQbftMessage) { // Perform base qbft releveant verification on the message if !self.validate_message(&wrapped_msg) { @@ -437,9 +464,7 @@ where return false; } - // Verify the signature. TODO!() - - // If the data round > 1, that means we have prepared a value in previous rounds + // If the data_round > 1, that means we have prepared a value in previous rounds if round_change.data_round > 1 { previously_prepared = true; @@ -463,8 +488,7 @@ where return false; } - // Make sure the roots match, this doesnt maek sense, it does not even pertain to - // the prepare message + // Make sure that the roots match let msg_fulldata_hashed = msg.signed_message.hash_fulldata(); if msg_fulldata_hashed != max_prepared_msg.clone().expect("Confirmed to exist").root { warn!("Highest prepared does not match proposed data"); @@ -500,8 +524,6 @@ where warn!("Proposed data mismatch"); return false; } - - // verify the signature. TODO!() } } true @@ -625,7 +647,7 @@ where } // Check if we have a commit quorum - if let Some(hash) = self.prepare_container.has_quorum(round) { + if let Some(hash) = self.commit_container.has_quorum(round) { // Make sure that the root of the data that we have come to a commit consensus on // matches the root of the proposal that we have accepted if hash != self.proposal_root.expect("Proposal has been accepted") { @@ -635,7 +657,7 @@ where // All validation successful, make sure we are in the proper commit state if matches!(self.state, InstanceState::Commit) { - // Commit aggregation??? todo!() + // Todo!(). Commit aggregation // We have come to commit consensus, mark ourself as completed and record the agreed upon // value @@ -672,10 +694,9 @@ where // There are two cases to check here // 1. If we have received a quorum of round change messages, we need to start a new round - // todo!() do we ignore this hash? - if let Some(_hash) = self.round_change_container.has_quorum(round) { + if self.round_change_container.has_quorum(round).is_some() { if matches!(self.state, InstanceState::SentRoundChange) { - // 1. If we have reached a quorum for this round, advance to that round. + // If we have reached a quorum for this round and have already sent a round change, advance to that round. debug!( operator_id = ?self.config.operator_id(), round = *round, @@ -694,14 +715,7 @@ where // Set the state so SendRoundChange so we include Round + 1 in message self.state = InstanceState::SentRoundChange; - // Use the value from our last prepare if we have one - let value_to_propose = if let Some(prepared_value) = self.last_prepared_value { - prepared_value - } else { - self.start_data_hash - }; - - self.send_round_change(value_to_propose); + self.send_round_change(Hash256::default()); } } } @@ -724,60 +738,42 @@ where // Set the state so SendRoundChange so we include Round + 1 in message self.state = InstanceState::SentRoundChange; - // Check if we have a prepared value, if so we want to send a round change proposing the - // value. Else, send a blank hash - let hash = self.last_prepared_value.unwrap_or_default(); - self.send_round_change(hash); + self.send_round_change(Hash256::default()); self.start_round(); } - // Get data for the qbft message. Todo!() does this work??? - // ( - // data_round (u64) -> round related to the proposed data - // round (u64) -> the message round - // root (D::Hash) -> the hash of the proposed data, if necessary - // full_data (Vec) -> serialized data that we are coming to conseusn on - // ) - fn get_message_data( - &self, - msg_type: &QbftMessageType, - data_hash: D::Hash, - ) -> (u64, u64, D::Hash, Vec) { - // Get the fulldata for the data_hash - let full_data = if let Some(data) = self.data.get(&data_hash) { - data.as_ssz_bytes() + // Get data for the qbft message + fn get_message_data(&self, msg_type: &QbftMessageType, data_hash: D::Hash) -> MessageData { + // Only include fulldata if we are sending a proposal or a round change + let full_data = if matches!(msg_type, QbftMessageType::Proposal) { + self.data + .get(&data_hash) + .expect("Value exists") + .as_ssz_bytes() } else { vec![] }; - match msg_type { - QbftMessageType::Proposal | QbftMessageType::Prepare | QbftMessageType::Commit => { - // todo!() for prepare, does it leave fulldata blank?? - let data_round = 0; // todo!(), why is this zero - let round = self.current_round.get() as u64; - (data_round, round, data_hash, full_data) - } - QbftMessageType::RoundChange => { - if let (Some(last_prepared_value), Some(last_prepared_round)) = - (self.last_prepared_value, self.last_prepared_round) - { - let full_data = if let Some(data) = self.data.get(&last_prepared_value) { - data.as_ssz_bytes() - } else { - vec![] - }; - - ( - last_prepared_round.get() as u64, - self.current_round.get() as u64 + 1, - last_prepared_value, - full_data, - ) - } else { - todo!() - } + let mut round = self.current_round.get() as u64; + if matches!(msg_type, QbftMessageType::RoundChange) { + round += 1; + if let (Some(last_prepared_value), Some(last_prepared_round)) = + (self.last_prepared_value, self.last_prepared_round) + { + return MessageData::new( + last_prepared_round.get() as u64, + self.current_round.get() as u64 + 1, + last_prepared_value, + self.data + .get(&last_prepared_value) + .expect("Value exists") + .as_ssz_bytes(), + ); } } + + // Standard message data for Proposal, Prepare, and Commit + MessageData::new(0, round, data_hash, full_data) } // Construct a new unsigned message. This will be passed to the processor to be signed and then @@ -789,17 +785,16 @@ where round_change_justification: Vec, prepare_justification: Vec, ) -> UnsignedSSVMessage { - // Get misc data to populate the message with based off of the message type - let (data_round, round, root, full_data) = self.get_message_data(&msg_type, data_hash); + let data = self.get_message_data(&msg_type, data_hash); // Create the QBFT message let qbft_message = QbftMessage { qbft_message_type: msg_type, height: *self.instance_height as u64, - round, + round: data.round, identifier: self.identifier.clone(), - root, - data_round, + root: data.root, + data_round: data.data_round, round_change_justification, prepare_justification, }; @@ -813,112 +808,113 @@ where // Wrap in unsigned SSV message UnsignedSSVMessage { ssv_message, - full_data, + full_data: data.full_data, } } // Get all of the round change jusitifcation messages fn get_round_change_justifications(&self) -> Vec { - // Make sure we are past the first round - if self.current_round > Round::default() { - // If we are past the first round and awaiting proposal, that means that there was a - // round change and we must have a quorum of round change messages. We include these so - // that we can prove that we had a consensus allowing us to change - if matches!(self.state, InstanceState::AwaitingProposal) { - return self - .round_change_container - .get_messages_for_round(self.current_round) + // Short circuit if we are in first round + if self.current_round <= Round::default() { + return vec![]; + } + + // If we are past the first round and awaiting proposal, that means that there was a + // round change and we must have a quorum of round change messages. We include these so + // that we can prove that we had a consensus allowing us to change + if matches!(self.state, InstanceState::AwaitingProposal) { + return self + .round_change_container + .get_messages_for_round(self.current_round) + .iter() + .map(|msg| msg.signed_message.clone()) + .collect(); + } + // If we are past the first round and are sending a round change. We have to include + // prepare messages that prove we have prepared a value + else if matches!(self.state, InstanceState::SentRoundChange) { + // if we have a last prepared value and a last prepared round... + if let (Some(_), Some(last_prepared_round)) = + (self.last_prepared_value, self.last_prepared_round) + { + // Get all of the prepare messages for the last prepared round + let last_prepared_messages = self + .prepare_container + .get_messages_for_round(last_prepared_round); + + // Make sure we have a quorum of prepare message + if last_prepared_messages.len() < self.config.quorum_size() { + return vec![]; + } + + // This will hold the value that we want to propose + return last_prepared_messages .iter() .map(|msg| msg.signed_message.clone()) .collect(); } - // If we are past the first round and are sending a round change. We have to include - // prepare messages that prove we have prepared a value - else if matches!(self.state, InstanceState::SentRoundChange) { - // if we have a last prepared value and a last prepared round... - if let (Some(_), Some(last_prepared_round)) = - (self.last_prepared_value, self.last_prepared_round) - { - // Get all of the prepare messages for the last prepared round - let last_prepared_messages = self - .prepare_container - .get_messages_for_round(last_prepared_round); - - // confirm that root of all messages match the last_prepared_value. TODO!(). is - // this necessary??? - - // Make sure we have a quorum of prepare message - if last_prepared_messages.len() < self.config.quorum_size() { - return vec![]; - } - - // This will hold the value that we want to propose - return last_prepared_messages - .iter() - .map(|msg| msg.signed_message.clone()) - .collect(); - } - return vec![]; - } + return vec![]; } - // We are either in the first round or sending a prepare/commit message + // Sending prepare/commit message vec![] } // Get all of the prepare justifications for proposals fn get_prepare_justifications(&self) -> (Vec, Option) { - // Make sure we are past the first round - if self.current_round > Round::default() { - // We only send prepare justifications with for proposal messages. If we are in the - // state AwaitingProposal and sending a message, we know this is a proposal. This will - // happen when we have come to a consensus of round change messages and have started a - // new round - if matches!(self.state, InstanceState::AwaitingProposal) { - // go through all of the prepares for the leading round and see if we have have come - // to a justification? - - // Get all of the round change messages for the current round and make sure we have - // a quorum of them. - let round_change_msg = self - .round_change_container - .get_messages_for_round(self.current_round); - if round_change_msg.len() < self.config.quorum_size() { - return (vec![], None); - } + // No justifications if we are in the first round + if self.current_round <= Round::default() { + return (vec![], None); + } + + // We only send prepare justifications with for proposal messages. If we are in the + // state AwaitingProposal and sending a message, we know this is a proposal. This will + // happen when we have come to a consensus of round change messages and have started a + // new round + if matches!(self.state, InstanceState::AwaitingProposal) { + // go through all of the prepares for the leading round and see if we have have come + // to a justification? + + // Get all of the round change messages for the current round and make sure we have + // a quorum of them. + let round_change_msg = self + .round_change_container + .get_messages_for_round(self.current_round); + if round_change_msg.len() < self.config.quorum_size() { + return (vec![], None); + } - // Go through each message and see if any have a value that was already prepared - // Just want to take the first one that is valid and has a prepared value - for wrapped_round_change in round_change_msg { - // Deserialize into a qbft message for sanity checks - let round_change: QbftMessage = match QbftMessage::from_ssz_bytes( - wrapped_round_change.signed_message.ssv_message().data(), - ) { - Ok(data) => data, - Err(_) => return (vec![], None), - }; - - // Round sanity check - let current_round_proposal = self.proposal_accepted_for_current_round - && self.current_round.get() as u64 == round_change.round; - let future_round_proposal = - round_change.round > self.current_round.get() as u64; - if !current_round_proposal && !future_round_proposal { - continue; - } + // Go through each message and see if any have a value that was already prepared + // Just want to take the first one that is valid and has a prepared value + for wrapped_round_change in round_change_msg { + // Deserialize into a qbft message for sanity checks + let round_change: QbftMessage = match QbftMessage::from_ssz_bytes( + wrapped_round_change.signed_message.ssv_message().data(), + ) { + Ok(data) => data, + Err(_) => return (vec![], None), + }; - // Validate the proposal, if this is a valid proposal then this is our prepare - // justification - if self.validate_justifications(wrapped_round_change) { - return ( - vec![wrapped_round_change.signed_message.clone()], - Some(round_change.root), - ); - } + // Round sanity check + let current_round_proposal = self.proposal_accepted_for_current_round + && self.current_round.get() as u64 == round_change.round; + let future_round_proposal = round_change.round > self.current_round.get() as u64; + if !current_round_proposal && !future_round_proposal { + continue; + } + + // Validate the proposal, if this is a valid proposal then this is our prepare + // justification + if self.validate_justifications(wrapped_round_change) { + return ( + vec![wrapped_round_change.signed_message.clone()], + Some(round_change.root), + ); } } } - // We are either in the first round, or not sending a proposal. Just return empty vec + + // Not sending a proposal (vec![], None) } diff --git a/anchor/common/qbft/src/msg_container.rs b/anchor/common/qbft/src/msg_container.rs index 61adc70c..93c82f90 100644 --- a/anchor/common/qbft/src/msg_container.rs +++ b/anchor/common/qbft/src/msg_container.rs @@ -24,7 +24,7 @@ impl MessageContainer { } } - // Add a new message to the container for the round + /// Add a new message to the container for the round pub fn add_message( &mut self, round: Round, @@ -55,8 +55,8 @@ impl MessageContainer { true } - // Check if we have a quorum of messages for the round. If so, return the hash of the value with - // the quorum + /// Check if we have a quorum of messages for the round. If so, return the hash of the value with + /// the quorum pub fn has_quorum(&self, round: Round) -> Option { let round_messages = self.messages.get(&round)?; diff --git a/anchor/common/ssv_types/src/consensus.rs b/anchor/common/ssv_types/src/consensus.rs index 4f82819f..09c0774f 100644 --- a/anchor/common/ssv_types/src/consensus.rs +++ b/anchor/common/ssv_types/src/consensus.rs @@ -64,8 +64,6 @@ impl QbftMessage { if self.qbft_message_type > QbftMessageType::RoundChange { return false; } - - // todo!(). Any other validation? true } } diff --git a/anchor/qbft_manager/src/lib.rs b/anchor/qbft_manager/src/lib.rs index c7442ab2..3fa4d1ef 100644 --- a/anchor/qbft_manager/src/lib.rs +++ b/anchor/qbft_manager/src/lib.rs @@ -322,9 +322,6 @@ async fn qbft_instance>(mut rx: UnboundedReceiver Date: Wed, 5 Feb 2025 16:13:07 +0000 Subject: [PATCH 42/45] bugfix --- anchor/common/qbft/src/lib.rs | 23 +++++++++++++++++------ anchor/common/qbft/src/msg_container.rs | 6 ++---- anchor/common/ssv_types/src/message.rs | 10 ---------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index b2530651..0e98081b 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -229,6 +229,11 @@ where return false; } + // Fulldata may be empty + if wrapped_msg.signed_message.full_data().is_empty() { + return true; + } + // Try to decode the data. If we can decode the data, then also validate it let data = match D::from_ssz_bytes(wrapped_msg.signed_message.full_data()) { Ok(data) => data, @@ -386,13 +391,19 @@ where return; } - // Verify that the fulldata matches the data root of the qbft message data - let data_hash = wrapped_msg.signed_message.hash_fulldata(); - if data_hash != wrapped_msg.qbft_message.root { + // We have previously verified that this data is able to be de-serialized. Store it now + let data = D::from_ssz_bytes(wrapped_msg.signed_message.full_data()) + .expect("Data has already been validated"); + + // Verify that the data root matches what was in the message + let data_hash = data.hash(); + if data.hash() != wrapped_msg.qbft_message.root { warn!(from = ?operator_id, self=?self.config.operator_id(), "Data roots do not match"); return; } + self.data.insert(wrapped_msg.qbft_message.root, data); + debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "PROPOSE received"); // Store the received propse message @@ -489,8 +500,7 @@ where } // Make sure that the roots match - let msg_fulldata_hashed = msg.signed_message.hash_fulldata(); - if msg_fulldata_hashed != max_prepared_msg.clone().expect("Confirmed to exist").root { + if msg.qbft_message.root != max_prepared_msg.clone().expect("Confirmed to exist").root { warn!("Highest prepared does not match proposed data"); return false; } @@ -520,7 +530,7 @@ where return false; } - if prepare.root != msg_fulldata_hashed { + if prepare.root != msg.qbft_message.root { warn!("Proposed data mismatch"); return false; } @@ -580,6 +590,7 @@ where // Make sure that the root of the data that we have come to a prepare consensus on // matches the root of the proposal that we have accepted + println!("{:?} {:?}", hash, self.proposal_root); if hash != self.proposal_root.expect("Proposal has been accepted") { warn!("PREPARE quorum root does not match accepted PROPOSAL root"); return; diff --git a/anchor/common/qbft/src/msg_container.rs b/anchor/common/qbft/src/msg_container.rs index 93c82f90..75b329df 100644 --- a/anchor/common/qbft/src/msg_container.rs +++ b/anchor/common/qbft/src/msg_container.rs @@ -50,7 +50,7 @@ impl MessageContainer { self.values_by_round .entry(round) .or_default() - .insert(msg.signed_message.hash_fulldata()); + .insert(msg.qbft_message.root); true } @@ -63,9 +63,7 @@ impl MessageContainer { // Count occurrences of each value let mut value_counts: HashMap = HashMap::new(); for msg in round_messages.values() { - *value_counts - .entry(msg.signed_message.hash_fulldata()) - .or_default() += 1; + *value_counts.entry(msg.qbft_message.root).or_default() += 1; } // Find any value that has reached quorum diff --git a/anchor/common/ssv_types/src/message.rs b/anchor/common/ssv_types/src/message.rs index a82bc75d..baf69931 100644 --- a/anchor/common/ssv_types/src/message.rs +++ b/anchor/common/ssv_types/src/message.rs @@ -1,11 +1,9 @@ -use sha2::{Digest, Sha256}; use ssz::{Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; use std::collections::HashSet; use std::fmt; use std::fmt::Debug; use std::hash::Hash; -use types::Hash256; const MESSAGE_ID_LEN: usize = 56; @@ -302,14 +300,6 @@ impl SignedSSVMessage { &self.full_data } - /// Returns a hash of the fulldata - pub fn hash_fulldata(&self) -> Hash256 { - let mut hasher = Sha256::new(); - hasher.update(self.full_data.clone()); - let hash: [u8; 32] = hasher.finalize().into(); - Hash256::from(hash) - } - // Validate the signed message to ensure that it is well formed for qbft processing pub fn validate(&self) -> bool { // OperatorID must have at least one element From fae75d327ae41b00a204f871100e780f7f22f5c9 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 5 Feb 2025 16:20:16 +0000 Subject: [PATCH 43/45] remove print --- anchor/common/qbft/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 0e98081b..28f5a289 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -590,7 +590,6 @@ where // Make sure that the root of the data that we have come to a prepare consensus on // matches the root of the proposal that we have accepted - println!("{:?} {:?}", hash, self.proposal_root); if hash != self.proposal_root.expect("Proposal has been accepted") { warn!("PREPARE quorum root does not match accepted PROPOSAL root"); return; From 9885c75a80bfd34aedc5b565fc66c48182daa094 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Wed, 5 Feb 2025 16:21:43 +0000 Subject: [PATCH 44/45] fmt fix --- anchor/common/qbft/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 28f5a289..45642537 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -754,7 +754,6 @@ where // Get data for the qbft message fn get_message_data(&self, msg_type: &QbftMessageType, data_hash: D::Hash) -> MessageData { - // Only include fulldata if we are sending a proposal or a round change let full_data = if matches!(msg_type, QbftMessageType::Proposal) { self.data .get(&data_hash) From 6420e8a16f3ee8ee850c4349d7df62247aabbbc7 Mon Sep 17 00:00:00 2001 From: Zacholme7 Date: Thu, 6 Feb 2025 14:27:48 +0000 Subject: [PATCH 45/45] remove duplicate insert --- anchor/common/qbft/src/lib.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/anchor/common/qbft/src/lib.rs b/anchor/common/qbft/src/lib.rs index 45642537..ce647bc3 100644 --- a/anchor/common/qbft/src/lib.rs +++ b/anchor/common/qbft/src/lib.rs @@ -402,7 +402,7 @@ where return; } - self.data.insert(wrapped_msg.qbft_message.root, data); + self.data.insert(data_hash, data); debug!(from = ?operator_id, in = ?self.config.operator_id(), state = ?self.state, "PROPOSE received"); @@ -415,11 +415,6 @@ where return; } - // We have previously verified that this data is able to be de-serialized. Store it now - let data = D::from_ssz_bytes(wrapped_msg.signed_message.full_data()) - .expect("Data has already been validated"); - self.data.insert(data_hash, data); - // Update state self.proposal_accepted_for_current_round = true; self.proposal_root = Some(data_hash);