From 8a17c614f0a737252ed80abce3323fa811422564 Mon Sep 17 00:00:00 2001 From: Robert Habermeier <rphmeier@gmail.com> Date: Thu, 10 Mar 2022 14:49:35 -0600 Subject: [PATCH] Check signatures as "Compact" in statement distribution (#5071) * allow converting payloads _up_ * convert to superpayload in statement-distribution * Update primitives/src/v2/signed.rs Co-authored-by: Andronik <write@reusable.software> Co-authored-by: Andronik <write@reusable.software> --- polkadot/node/network/protocol/src/lib.rs | 8 +++ .../network/statement-distribution/src/lib.rs | 70 ++++++++++++++----- .../statement-distribution/src/tests.rs | 32 ++++++--- polkadot/primitives/src/v2/mod.rs | 2 + polkadot/primitives/src/v2/signed.rs | 24 +++++++ 5 files changed, 109 insertions(+), 27 deletions(-) diff --git a/polkadot/node/network/protocol/src/lib.rs b/polkadot/node/network/protocol/src/lib.rs index e97cb882039..ae92bc0f7f2 100644 --- a/polkadot/node/network/protocol/src/lib.rs +++ b/polkadot/node/network/protocol/src/lib.rs @@ -357,6 +357,14 @@ pub mod v1 { } } + /// Get the signature from the statement. + pub fn get_signature(&self) -> ValidatorSignature { + match self { + Self::Statement(_, statement) => statement.unchecked_signature().clone(), + Self::LargeStatement(metadata) => metadata.signature.clone(), + } + } + /// Get contained relay parent. pub fn get_relay_parent(&self) -> Hash { match self { diff --git a/polkadot/node/network/statement-distribution/src/lib.rs b/polkadot/node/network/statement-distribution/src/lib.rs index 702e2db6a71..f68860c1529 100644 --- a/polkadot/node/network/statement-distribution/src/lib.rs +++ b/polkadot/node/network/statement-distribution/src/lib.rs @@ -36,7 +36,8 @@ use polkadot_node_subsystem_util::{self as util, MIN_GOSSIP_PEERS}; use polkadot_primitives::v2::{ AuthorityDiscoveryId, CandidateHash, CommittedCandidateReceipt, CompactStatement, Hash, - SigningContext, ValidatorId, ValidatorIndex, ValidatorSignature, + SignedStatement, SigningContext, UncheckedSignedStatement, ValidatorId, ValidatorIndex, + ValidatorSignature, }; use polkadot_subsystem::{ jaeger, @@ -780,10 +781,10 @@ impl ActiveHeadData { /// without modifying the internal state. fn check_useful_or_unknown( &self, - statement: &UncheckedSignedFullStatement, + statement: &UncheckedSignedStatement, ) -> std::result::Result<(), DeniedStatement> { let validator_index = statement.unchecked_validator_index(); - let compact = statement.unchecked_payload().to_compact(); + let compact = statement.unchecked_payload(); let comparator = StoredStatementComparator { compact: compact.clone(), validator_index, @@ -857,8 +858,8 @@ impl ActiveHeadData { fn check_statement_signature( head: &ActiveHeadData, relay_parent: Hash, - statement: UncheckedSignedFullStatement, -) -> std::result::Result<SignedFullStatement, UncheckedSignedFullStatement> { + statement: UncheckedSignedStatement, +) -> std::result::Result<SignedStatement, UncheckedSignedStatement> { let signing_context = SigningContext { session_index: head.session_index, parent_hash: relay_parent }; @@ -1387,24 +1388,57 @@ async fn handle_incoming_message<'a>( return None } + let checked_compact = { + let (compact, validator_index) = message.get_fingerprint(); + let signature = message.get_signature(); + + let unchecked_compact = UncheckedSignedStatement::new(compact, validator_index, signature); + + match active_head.check_useful_or_unknown(&unchecked_compact) { + Ok(()) => {}, + Err(DeniedStatement::NotUseful) => return None, + Err(DeniedStatement::UsefulButKnown) => { + report_peer(ctx, peer, BENEFIT_VALID_STATEMENT).await; + return None + }, + } + + // check the signature on the statement. + match check_statement_signature(&active_head, relay_parent, unchecked_compact) { + Err(statement) => { + tracing::debug!( + target: LOG_TARGET, + ?peer, + ?statement, + "Invalid statement signature" + ); + report_peer(ctx, peer, COST_INVALID_SIGNATURE).await; + return None + }, + Ok(statement) => statement, + } + }; + + // Fetch from the network only after signature and usefulness checks are completed. + let is_large_statement = message.is_large_statement(); let statement = retrieve_statement_from_message(peer, message, active_head, ctx, req_sender, metrics) .await?; - match active_head.check_useful_or_unknown(&statement) { - Ok(()) => {}, - Err(DeniedStatement::NotUseful) => return None, - Err(DeniedStatement::UsefulButKnown) => { - report_peer(ctx, peer, BENEFIT_VALID_STATEMENT).await; - return None - }, - } + let payload = statement.unchecked_into_payload(); - // check the signature on the statement. - let statement = match check_statement_signature(&active_head, relay_parent, statement) { - Err(statement) => { - tracing::debug!(target: LOG_TARGET, ?peer, ?statement, "Invalid statement signature"); - report_peer(ctx, peer, COST_INVALID_SIGNATURE).await; + // Upgrade the `Signed` wrapper from the compact payload to the full payload. + // This fails if the payload doesn't encode correctly. + let statement: SignedFullStatement = match checked_compact.convert_to_superpayload(payload) { + Err((compact, _)) => { + tracing::debug!( + target: LOG_TARGET, + ?peer, + ?compact, + is_large_statement, + "Full statement had bad payload." + ); + report_peer(ctx, peer, COST_WRONG_HASH).await; return None }, Ok(statement) => statement, diff --git a/polkadot/node/network/statement-distribution/src/tests.rs b/polkadot/node/network/statement-distribution/src/tests.rs index 5e885e71e0c..eda99cae1d3 100644 --- a/polkadot/node/network/statement-distribution/src/tests.rs +++ b/polkadot/node/network/statement-distribution/src/tests.rs @@ -105,14 +105,16 @@ fn active_head_accepts_only_2_seconded_per_validator() { .ok() .flatten() .expect("should be signed"); - assert!(head_data.check_useful_or_unknown(&a_seconded_val_0.clone().into()).is_ok()); + assert!(head_data + .check_useful_or_unknown(&a_seconded_val_0.clone().convert_payload().into()) + .is_ok()); let noted = head_data.note_statement(a_seconded_val_0.clone()); assert_matches!(noted, NotedStatement::Fresh(_)); // note A (duplicate) assert_eq!( - head_data.check_useful_or_unknown(&a_seconded_val_0.clone().into()), + head_data.check_useful_or_unknown(&a_seconded_val_0.clone().convert_payload().into()), Err(DeniedStatement::UsefulButKnown), ); let noted = head_data.note_statement(a_seconded_val_0); @@ -130,7 +132,9 @@ fn active_head_accepts_only_2_seconded_per_validator() { .ok() .flatten() .expect("should be signed"); - assert!(head_data.check_useful_or_unknown(&statement.clone().into()).is_ok()); + assert!(head_data + .check_useful_or_unknown(&statement.clone().convert_payload().into()) + .is_ok()); let noted = head_data.note_statement(statement); assert_matches!(noted, NotedStatement::Fresh(_)); @@ -146,7 +150,7 @@ fn active_head_accepts_only_2_seconded_per_validator() { .flatten() .expect("should be signed"); assert_eq!( - head_data.check_useful_or_unknown(&statement.clone().into()), + head_data.check_useful_or_unknown(&statement.clone().convert_payload().into()), Err(DeniedStatement::NotUseful), ); let noted = head_data.note_statement(statement); @@ -163,7 +167,9 @@ fn active_head_accepts_only_2_seconded_per_validator() { .ok() .flatten() .expect("should be signed"); - assert!(head_data.check_useful_or_unknown(&statement.clone().into()).is_ok()); + assert!(head_data + .check_useful_or_unknown(&statement.clone().convert_payload().into()) + .is_ok()); let noted = head_data.note_statement(statement); assert_matches!(noted, NotedStatement::Fresh(_)); @@ -178,7 +184,9 @@ fn active_head_accepts_only_2_seconded_per_validator() { .ok() .flatten() .expect("should be signed"); - assert!(head_data.check_useful_or_unknown(&statement.clone().into()).is_ok()); + assert!(head_data + .check_useful_or_unknown(&statement.clone().convert_payload().into()) + .is_ok()); let noted = head_data.note_statement(statement); assert_matches!(noted, NotedStatement::Fresh(_)); } @@ -428,7 +436,9 @@ fn peer_view_update_sends_messages() { .ok() .flatten() .expect("should be signed"); - assert!(data.check_useful_or_unknown(&statement.clone().into()).is_ok()); + assert!(data + .check_useful_or_unknown(&statement.clone().convert_payload().into()) + .is_ok()); let noted = data.note_statement(statement); assert_matches!(noted, NotedStatement::Fresh(_)); @@ -443,7 +453,9 @@ fn peer_view_update_sends_messages() { .ok() .flatten() .expect("should be signed"); - assert!(data.check_useful_or_unknown(&statement.clone().into()).is_ok()); + assert!(data + .check_useful_or_unknown(&statement.clone().convert_payload().into()) + .is_ok()); let noted = data.note_statement(statement); assert_matches!(noted, NotedStatement::Fresh(_)); @@ -458,7 +470,9 @@ fn peer_view_update_sends_messages() { .ok() .flatten() .expect("should be signed"); - assert!(data.check_useful_or_unknown(&statement.clone().into()).is_ok()); + assert!(data + .check_useful_or_unknown(&statement.clone().convert_payload().into()) + .is_ok()); let noted = data.note_statement(statement); assert_matches!(noted, NotedStatement::Fresh(_)); diff --git a/polkadot/primitives/src/v2/mod.rs b/polkadot/primitives/src/v2/mod.rs index 36a9ad35ac1..9d47a90366d 100644 --- a/polkadot/primitives/src/v2/mod.rs +++ b/polkadot/primitives/src/v2/mod.rs @@ -660,6 +660,8 @@ impl From<BitVec<u8, bitvec::order::Lsb0>> for AvailabilityBitfield { /// A signed compact statement, suitable to be sent to the chain. pub type SignedStatement = Signed<CompactStatement>; +/// A signed compact statement, with signature not yet checked. +pub type UncheckedSignedStatement = UncheckedSigned<CompactStatement>; /// A bitfield signed by a particular validator about the availability of pending candidates. pub type SignedAvailabilityBitfield = Signed<AvailabilityBitfield>; diff --git a/polkadot/primitives/src/v2/signed.rs b/polkadot/primitives/src/v2/signed.rs index 9e199033a61..e58a09a0ea7 100644 --- a/polkadot/primitives/src/v2/signed.rs +++ b/polkadot/primitives/src/v2/signed.rs @@ -148,6 +148,30 @@ impl<Payload: EncodeAs<RealPayload>, RealPayload: Encode> Signed<Payload, RealPa { Signed(self.0.unchecked_convert_payload()) } + + /// Convert `Payload` into some claimed `SuperPayload` if the encoding matches. + /// + /// Succeeds if and only if the super-payload provided actually encodes as + /// the expected payload. + pub fn convert_to_superpayload<SuperPayload>( + self, + claimed: SuperPayload, + ) -> Result<Signed<SuperPayload, RealPayload>, (Self, SuperPayload)> + where + SuperPayload: EncodeAs<RealPayload>, + Payload: Encode, + { + if claimed.encode_as() == self.0.payload.encode_as() { + Ok(Signed(UncheckedSigned { + payload: claimed, + validator_index: self.0.validator_index, + signature: self.0.signature, + real_payload: sp_std::marker::PhantomData, + })) + } else { + Err((self, claimed)) + } + } } // We can't bound this on `Payload: Into<RealPayload>` because that conversion consumes -- GitLab