-
Notifications
You must be signed in to change notification settings - Fork 1
feat(core): implement consensus qbft transport and sniffer #448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
372bb91
d17b023
1b1b03b
2d7b362
eeacd5e
4c5be6a
5bf0e11
aa05e05
cf52760
d6b7fe6
099bb28
7da6acd
2378143
68ea846
8515fd9
08b0edf
ff3f933
209b9d5
c76522e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,3 +2,6 @@ | |
| /// QBFT protobuf message wrapper. | ||
| pub mod msg; | ||
|
|
||
| pub(crate) mod sniffer; | ||
| pub(crate) mod transport; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| //! QBFT consensus message sniffer. | ||
|
|
||
| // TODO: Remove once the consensus component exports sniffer lifecycle hooks. | ||
| #![allow(dead_code)] | ||
|
|
||
| use std::{ | ||
| sync::{Mutex, PoisonError}, | ||
| time::SystemTime, | ||
| }; | ||
|
|
||
| use prost_types::Timestamp; | ||
|
|
||
| use crate::{ | ||
| consensus::protocols::QBFT_V2_PROTOCOL_ID, | ||
| corepb::v1::consensus::{QbftConsensusMsg, SniffedConsensusInstance, SniffedConsensusMsg}, | ||
| }; | ||
|
|
||
| /// Buffers consensus messages for the debug API. | ||
| #[derive(Debug)] | ||
| pub(crate) struct Sniffer { | ||
| nodes: i64, | ||
| peer_idx: i64, | ||
| started_at: SystemTime, | ||
| msgs: Mutex<Vec<SniffedConsensusMsg>>, | ||
| } | ||
|
|
||
| impl Sniffer { | ||
| /// Returns a new QBFT consensus sniffer. | ||
| pub(crate) fn new(nodes: i64, peer_idx: i64) -> Self { | ||
| Self { | ||
| nodes, | ||
| peer_idx, | ||
| started_at: SystemTime::now(), | ||
| msgs: Mutex::default(), | ||
| } | ||
| } | ||
|
|
||
| /// Adds a message to the sniffer buffer. | ||
| pub(crate) fn add(&self, msg: QbftConsensusMsg) { | ||
| self.msgs | ||
| .lock() | ||
| .unwrap_or_else(PoisonError::into_inner) | ||
| .push(SniffedConsensusMsg { | ||
| timestamp: Some(Timestamp::from(SystemTime::now())), | ||
| msg: Some(msg), | ||
| }); | ||
| } | ||
|
|
||
| /// Returns the buffered messages as a sniffed consensus instance. | ||
| pub(crate) fn instance(&self) -> SniffedConsensusInstance { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If this is only called at instance end for the debug API, the deep clone is fine. The test helper |
||
| SniffedConsensusInstance { | ||
| nodes: self.nodes, | ||
| peer_idx: self.peer_idx, | ||
| started_at: Some(Timestamp::from(self.started_at)), | ||
| msgs: self | ||
| .msgs | ||
| .lock() | ||
| .unwrap_or_else(PoisonError::into_inner) | ||
| .clone(), | ||
| protocol_id: QBFT_V2_PROTOCOL_ID.to_string(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use crate::corepb::v1::consensus::QbftMsg; | ||
|
|
||
| #[test] | ||
| fn sniffer_add_records_messages() { | ||
| let sniffer = Sniffer::new(4, 2); | ||
| let msg = consensus_msg(7); | ||
|
|
||
| sniffer.add(msg.clone()); | ||
|
|
||
| let instance = sniffer.instance(); | ||
| assert_eq!(instance.msgs.len(), 1); | ||
| assert_eq!(instance.msgs[0].msg, Some(msg)); | ||
| assert!(instance.msgs[0].timestamp.is_some()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn sniffer_instance_maps_fields() { | ||
| let sniffer = Sniffer::new(4, 3); | ||
|
|
||
| let instance = sniffer.instance(); | ||
|
|
||
| assert_eq!(instance.nodes, 4); | ||
| assert_eq!(instance.peer_idx, 3); | ||
| assert!(instance.started_at.is_some()); | ||
| assert!(instance.msgs.is_empty()); | ||
| assert_eq!(instance.protocol_id, QBFT_V2_PROTOCOL_ID); | ||
| } | ||
|
|
||
| fn consensus_msg(round: i64) -> QbftConsensusMsg { | ||
| QbftConsensusMsg { | ||
| msg: Some(QbftMsg { | ||
| round, | ||
| ..Default::default() | ||
| }), | ||
| ..Default::default() | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hash_proto(line 266) explicitly rejectsAny(testhash_proto_rejects_any).hash_proto_byteshas no such guard — it hashes whatever bytes it is handed. Doc currently reads onlyReturns the consensus hash for deterministic protobuf bytes.Given the helper is
pub(crate)and is the natural shortcut a future inbound-validation layer would reach for, the docstring should spell out the invariant explicitly: bytes MUST be the deterministic prost encoding of a non-Anyconcrete message, equivalent toprost::Message::encode_to_vecover a typed proto whose map fields areBTreeMap. Anything else risks silent hash mismatch against Go peers, which re-marshal deterministically before hashing.