diff --git a/Cargo.lock b/Cargo.lock index 8ec364773b2..4011d92fa92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5332,6 +5332,7 @@ dependencies = [ "nonempty 0.12.0", "parity-scale-codec", "prometheus-client 0.23.1", + "proptest", "rand 0.8.5", "thiserror 2.0.17", "tokio", diff --git a/ethexe/network/Cargo.toml b/ethexe/network/Cargo.toml index 6e17aa1f4dd..1e38e768d0f 100644 --- a/ethexe/network/Cargo.toml +++ b/ethexe/network/Cargo.toml @@ -53,5 +53,6 @@ gear-workspace-hack.workspace = true libp2p-swarm-test = { version = "0.6.0", default-features = false, features = ["tokio"] } tokio = { workspace = true, features = ["full", "test-util"] } tracing-subscriber.workspace = true +proptest = { workspace = true } ethexe-common = { workspace = true, features = ["mock"] } ethexe-db = { workspace = true, features = ["mock"] } diff --git a/ethexe/network/src/injected.rs b/ethexe/network/src/injected.rs index 34fd8bd9720..0acd18c7c66 100644 --- a/ethexe/network/src/injected.rs +++ b/ethexe/network/src/injected.rs @@ -403,10 +403,10 @@ impl NetworkBehaviour for Behaviour { mod tests { use super::*; use crate::{ - utils::tests::init_logger, + utils::tests::{arb_value, init_logger}, validator::discovery::{SignedValidatorIdentity, ValidatorAddresses, ValidatorIdentity}, }; - use ethexe_common::{injected::InjectedTransaction, mock::Mock}; + use ethexe_common::injected::InjectedTransaction; use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; use libp2p::{ Swarm, Transport, @@ -420,7 +420,7 @@ mod tests { let signer = Signer::memory(); let pub_key = signer.generate().unwrap(); - let tx = InjectedTransaction::mock(()); + let tx = arb_value::(()); let tx = signer.signed_message(pub_key, tx, None).unwrap(); AddressedInjectedTransaction { recipient, tx } diff --git a/ethexe/network/src/lib.rs b/ethexe/network/src/lib.rs index 8a4feb2ae42..027ff14b04b 100644 --- a/ethexe/network/src/lib.rs +++ b/ethexe/network/src/lib.rs @@ -845,11 +845,11 @@ mod tests { use super::*; use crate::{ db_sync::{ExternalDataProvider, tests::fill_data_provider}, - utils::tests::init_logger, + utils::tests::{arb_value, init_logger}, }; use assert_matches::assert_matches; use async_trait::async_trait; - use ethexe_common::{BlockHeader, ProtocolTimelines, db::*, gear::CodeState, mock::*}; + use ethexe_common::{BlockHeader, ProtocolTimelines, db::*, gear::CodeState}; use ethexe_db::Database; use gprimitives::{ActorId, CodeId, H256}; use gsigner::secp256k1::Signer; @@ -971,7 +971,7 @@ mod tests { db.set_config(DBConfig { timelines: TIMELINES, - ..DBConfig::mock(()) + ..arb_value::(()) }); let key = signer.generate().unwrap(); diff --git a/ethexe/network/src/utils.rs b/ethexe/network/src/utils.rs index ab9571a6476..5b35824f12f 100644 --- a/ethexe/network/src/utils.rs +++ b/ethexe/network/src/utils.rs @@ -409,6 +409,11 @@ pub(crate) mod tests { utils::{ConnectionMap, ExponentialBackoffInterval}, }; use libp2p::swarm::ConnectionId; + use proptest::{ + arbitrary::Arbitrary, + strategy::{Strategy, ValueTree}, + test_runner::TestRunner, + }; use std::{collections::HashSet, future, time::Duration}; use tokio::time; use tracing_subscriber::EnvFilter; @@ -423,6 +428,16 @@ pub(crate) mod tests { .try_init(); } + pub fn arb_value(args: impl Into) -> T + where + T: Arbitrary + 'static, + { + T::arbitrary_with(args.into()) + .new_tree(&mut TestRunner::default()) + .expect("strategy must produce a value") + .current() + } + #[test] fn connection_map_key_cleared() { let mut map = ConnectionMap::without_limits(); diff --git a/ethexe/network/src/validator/discovery.rs b/ethexe/network/src/validator/discovery.rs index a994dde7ebf..ac8e4143312 100644 --- a/ethexe/network/src/validator/discovery.rs +++ b/ethexe/network/src/validator/discovery.rs @@ -769,6 +769,7 @@ mod tests { swarm::{SwarmEvent, behaviour::ExternalAddrConfirmed}, }; use libp2p_swarm_test::SwarmExt; + use proptest::{prelude::*, test_runner::Config as ProptestConfig}; use std::sync::Arc; use tokio::time; @@ -800,20 +801,44 @@ mod tests { .expect("failed to sign validator identity") } - #[test] - fn encode_decode_identity() { - let signer = Signer::memory(); - let validator_key = signer.generate().unwrap(); - let keypair = Keypair::generate_secp256k1(); - let identity = ValidatorIdentity { - addresses: ValidatorAddresses::new(keypair.public().to_peer_id(), test_addr()), - creation_time: 999_999, - }; - let identity = identity.sign(&signer, validator_key, &keypair).unwrap(); + fn signed_identity_strategy() -> impl Strategy { + (any::<[u8; 32]>(), any::<[u8; 32]>(), any::()).prop_filter_map( + "valid secp256k1 validator and network keys", + |(validator_seed, mut network_seed, creation_time)| { + let validator_private_key = PrivateKey::from_seed(validator_seed).ok()?; + let signer = Signer::memory(); + let validator_key = signer.import(validator_private_key).ok()?; + + let network_secret = + libp2p::identity::secp256k1::SecretKey::try_from_bytes(&mut network_seed) + .ok()?; + let network_keypair = + Keypair::from(libp2p::identity::secp256k1::Keypair::from(network_secret)); + let identity = ValidatorIdentity { + addresses: ValidatorAddresses::new( + network_keypair.public().to_peer_id(), + test_addr(), + ), + creation_time, + }; + + identity.sign(&signer, validator_key, &network_keypair).ok() + }, + ) + } - let decoded_identity = - SignedValidatorIdentity::decode(&mut &identity.encode()[..]).unwrap(); - assert_eq!(identity, decoded_identity); + proptest! { + #![proptest_config(ProptestConfig::with_cases(64))] + + #[test] + fn proptest_signed_validator_identity_encode_decode( + identity in signed_identity_strategy(), + ) { + let decoded_identity = + SignedValidatorIdentity::decode(&mut &identity.encode()[..]).unwrap(); + + prop_assert_eq!(identity, decoded_identity); + } } #[test] diff --git a/ethexe/network/src/validator/topic.rs b/ethexe/network/src/validator/topic.rs index efd94f3d29f..38b349c50c8 100644 --- a/ethexe/network/src/validator/topic.rs +++ b/ethexe/network/src/validator/topic.rs @@ -326,16 +326,18 @@ impl ValidatorTopic { #[cfg(test)] mod tests { use super::*; + use crate::utils::tests::arb_value; use assert_matches::assert_matches; use ethexe_common::{ - Announce, + Announce, HashOf, + ecdsa::SignedData, gear_core::{message::ReplyCode, rpc::ReplyInfo}, injected::Promise, - mock::Mock, network::{SignedValidatorMessage, ValidatorMessage}, }; - use gsigner::secp256k1::{Secp256k1SignerExt, Signer}; + use gsigner::secp256k1::{PrivateKey, Secp256k1SignerExt, Signer}; use nonempty::{NonEmpty, nonempty}; + use proptest::{prelude::*, test_runner::Config as ProptestConfig}; const CHAIN_HEAD_ERA: u64 = 10; @@ -357,24 +359,25 @@ mod tests { ) } - fn new_validator_message(era_index: u64) -> VerifiedValidatorMessage { - let signer = Signer::memory(); - let pub_key = signer.generate().unwrap(); - - signer - .signed_data( - pub_key, - ValidatorMessage { - era_index, - payload: Announce::mock(()), - }, - None, - ) + fn validator_message_from_private_key( + private_key: PrivateKey, + era_index: u64, + payload: Announce, + ) -> VerifiedValidatorMessage { + SignedData::create(&private_key, ValidatorMessage { era_index, payload }) .map(SignedValidatorMessage::from) .unwrap() .into_verified() } + fn new_validator_message(era_index: u64) -> VerifiedValidatorMessage { + validator_message_from_private_key( + PrivateKey::random(), + era_index, + arb_value::(()), + ) + } + fn signed_promise() -> SignedPromise { let signer = Signer::memory(); let pub_key = signer.generate().unwrap(); @@ -390,6 +393,103 @@ mod tests { signer.signed_message(pub_key, promise, None).unwrap() } + fn test_announce() -> Announce { + Announce { + block_hash: Default::default(), + parent: HashOf::zero(), + gas_allowance: Some(100), + injected_transactions: Vec::new(), + } + } + + #[derive(Debug, Clone, Copy)] + enum EraRelation { + TooOld(u64), + Old, + Current, + Next, + TooNew(u64), + } + + impl EraRelation { + fn message_era(self, snapshot_era: u64) -> u64 { + match self { + Self::TooOld(delta) => snapshot_era - delta, + Self::Old => snapshot_era - 1, + Self::Current => snapshot_era, + Self::Next => snapshot_era + 1, + Self::TooNew(delta) => snapshot_era + delta, + } + } + + fn expected_verification(self, snapshot_era: u64) -> Result<(), VerifyMessageError> { + let message_era = self.message_era(snapshot_era); + + match self { + Self::TooOld(_) => Err(VerifyMessageRejectReason::TooOldEra { + expected_era: snapshot_era, + received_era: message_era, + } + .into()), + Self::Old => Err(VerifyMessageIgnoreReason::OldEra { + expected_era: snapshot_era, + received_era: message_era, + } + .into()), + Self::Current => Ok(()), + Self::Next => Err(VerifyMessageCacheReason::NewEra { + expected_era: snapshot_era, + received_era: message_era, + } + .into()), + Self::TooNew(_) => Err(VerifyMessageRejectReason::TooNewEra { + expected_era: snapshot_era, + received_era: message_era, + } + .into()), + } + } + } + + fn era_relation_strategy() -> impl Strategy { + ( + 128u64..(u64::MAX - 128), + prop_oneof![ + (2u64..128).prop_map(EraRelation::TooOld).boxed(), + Just(EraRelation::Old).boxed(), + Just(EraRelation::Current).boxed(), + Just(EraRelation::Next).boxed(), + (2u64..128).prop_map(EraRelation::TooNew).boxed(), + ], + ) + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(64))] + + #[test] + fn proptest_message_era_is_checked_against_snapshot_era( + (snapshot_era, relation) in era_relation_strategy(), + ) { + let private_key = PrivateKey::from_seed([1; 32]).expect("seed is valid"); + let message_era = relation.message_era(snapshot_era); + let message = + validator_message_from_private_key(private_key, message_era, test_announce()); + let validator = message.address(); + let snapshot = ValidatorListSnapshot { + current_era_index: snapshot_era, + current_validators: nonempty![validator].into(), + next_validators: Some(nonempty![validator].into()), + }; + let alice = ValidatorTopic::new(peer_score::Handle::new_test(), Arc::new(snapshot)); + + prop_assert_eq!( + alice.inner_verify_validator_message(&message), + relation.expected_verification(snapshot_era) + ); + } + } + #[test] fn too_old_era() { let bob_message = new_validator_message(CHAIN_HEAD_ERA - 2);