use anyhow::{bail, ensure};
use exonum_derive::{BinaryValue, ObjectHash};
use exonum_proto::ProtobufConvert;
use log::warn;
use std::collections::{HashMap, HashSet};
use crate::{
crypto::PublicKey,
helpers::{Milliseconds, ValidateInput, ValidatorId},
keys::Keys,
merkledb::BinaryValue,
messages::SIGNED_MESSAGE_MIN_SIZE,
proto::schema,
runtime::{ArtifactId, ArtifactSpec, InstanceId, InstanceSpec},
};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[derive(Serialize, Deserialize)]
#[derive(ProtobufConvert)]
#[protobuf_convert(source = "schema::blockchain::ValidatorKeys")]
#[non_exhaustive]
pub struct ValidatorKeys {
pub consensus_key: PublicKey,
pub service_key: PublicKey,
}
impl ValidatorKeys {
pub fn new(consensus_key: PublicKey, service_key: PublicKey) -> Self {
Self {
consensus_key,
service_key,
}
}
}
impl ValidateInput for ValidatorKeys {
type Error = anyhow::Error;
fn validate(&self) -> Result<(), Self::Error> {
if self.consensus_key == self.service_key {
bail!("Consensus and service keys must be different.");
}
Ok(())
}
}
#[protobuf_convert(source = "schema::blockchain::Config")]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Serialize, Deserialize)]
#[derive(ProtobufConvert, BinaryValue, ObjectHash)]
#[non_exhaustive]
pub struct ConsensusConfig {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub validator_keys: Vec<ValidatorKeys>,
pub first_round_timeout: Milliseconds,
pub status_timeout: Milliseconds,
pub peers_timeout: Milliseconds,
pub txs_block_limit: u32,
pub max_message_len: u32,
pub min_propose_timeout: Milliseconds,
pub max_propose_timeout: Milliseconds,
pub propose_timeout_threshold: u32,
}
impl Default for ConsensusConfig {
fn default() -> Self {
Self {
validator_keys: Vec::default(),
first_round_timeout: 3_000,
status_timeout: 5_000,
peers_timeout: 10_000,
txs_block_limit: 1_000,
max_message_len: Self::DEFAULT_MAX_MESSAGE_LEN,
min_propose_timeout: 10,
max_propose_timeout: 200,
propose_timeout_threshold: 500,
}
}
}
impl ConsensusConfig {
pub const DEFAULT_MAX_MESSAGE_LEN: u32 = 1024 * 1024;
pub const TIMEOUT_LINEAR_INCREASE_PERCENT: u64 = 10;
pub fn with_validator_keys(mut self, validator_keys: Vec<ValidatorKeys>) -> Self {
self.validator_keys = validator_keys;
self
}
pub fn for_tests(validator_count: u16) -> (Self, Keys) {
assert!(
validator_count > 0,
"Cannot create network without validators"
);
let mut node_keys = None;
let validator_keys = (0..validator_count)
.map(|i| {
let keys = Keys::random();
let consensus_pk = keys.consensus_pk();
let service_pk = keys.service_pk();
if i == 0 {
node_keys = Some(keys);
}
ValidatorKeys::new(consensus_pk, service_pk)
})
.collect();
let config = Self {
validator_keys,
..Self::default()
};
(config, node_keys.unwrap())
}
fn validate_keys(&self) -> anyhow::Result<()> {
ensure!(
!self.validator_keys.is_empty(),
"Consensus configuration must have at least one validator."
);
let mut exist_keys = HashSet::with_capacity(self.validator_keys.len() * 2);
for validator_keys in &self.validator_keys {
validator_keys.validate()?;
if exist_keys.contains(&validator_keys.consensus_key)
|| exist_keys.contains(&validator_keys.service_key)
{
bail!("Duplicated keys are found: each consensus and service key must be unique");
}
exist_keys.insert(validator_keys.consensus_key);
exist_keys.insert(validator_keys.service_key);
}
Ok(())
}
pub fn find_validator(
&self,
predicate: impl Fn(&ValidatorKeys) -> bool,
) -> Option<ValidatorId> {
self.validator_keys
.iter()
.position(predicate)
.map(|id| ValidatorId(id as u16))
}
fn warn_if_nonoptimal(&self) {
const MIN_TXS_BLOCK_LIMIT: u32 = 100;
const MAX_TXS_BLOCK_LIMIT: u32 = 10_000;
if self.first_round_timeout <= 2 * self.max_propose_timeout {
warn!(
"It is recommended that first_round_timeout ({}) be at least twice as large \
as max_propose_timeout ({})",
self.first_round_timeout, self.max_propose_timeout
);
}
if self.txs_block_limit < MIN_TXS_BLOCK_LIMIT || self.txs_block_limit > MAX_TXS_BLOCK_LIMIT
{
warn!(
"It is recommended that txs_block_limit ({}) is in [{}..{}] range",
self.txs_block_limit, MIN_TXS_BLOCK_LIMIT, MAX_TXS_BLOCK_LIMIT
);
}
if self.max_message_len < Self::DEFAULT_MAX_MESSAGE_LEN {
warn!(
"It is recommended that max_message_len ({}) is at least {}.",
self.max_message_len,
Self::DEFAULT_MAX_MESSAGE_LEN
);
}
}
}
#[derive(Debug, Default)]
pub struct ConsensusConfigBuilder {
config: ConsensusConfig,
}
impl ConsensusConfigBuilder {
pub fn new() -> Self {
Self {
config: ConsensusConfig::default(),
}
}
pub fn build(self) -> ConsensusConfig {
self.config
}
pub fn validator_keys(self, validator_keys: Vec<ValidatorKeys>) -> Self {
let config = ConsensusConfig {
validator_keys,
..self.config
};
Self { config }
}
pub fn first_round_timeout(self, first_round_timeout: Milliseconds) -> Self {
let config = ConsensusConfig {
first_round_timeout,
..self.config
};
Self { config }
}
pub fn status_timeout(self, status_timeout: Milliseconds) -> Self {
let config = ConsensusConfig {
status_timeout,
..self.config
};
Self { config }
}
pub fn peers_timeout(self, peers_timeout: Milliseconds) -> Self {
let config = ConsensusConfig {
peers_timeout,
..self.config
};
Self { config }
}
pub fn txs_block_limit(self, txs_block_limit: u32) -> Self {
let config = ConsensusConfig {
txs_block_limit,
..self.config
};
Self { config }
}
pub fn min_propose_timeout(self, min_propose_timeout: Milliseconds) -> Self {
let config = ConsensusConfig {
min_propose_timeout,
..self.config
};
Self { config }
}
pub fn max_propose_timeout(self, max_propose_timeout: Milliseconds) -> Self {
let config = ConsensusConfig {
max_propose_timeout,
..self.config
};
Self { config }
}
pub fn max_message_len(self, max_message_len: u32) -> Self {
let config = ConsensusConfig {
max_message_len,
..self.config
};
Self { config }
}
pub fn propose_timeout_threshold(self, propose_timeout_threshold: u32) -> Self {
let config = ConsensusConfig {
propose_timeout_threshold,
..self.config
};
Self { config }
}
}
impl ValidateInput for ConsensusConfig {
type Error = anyhow::Error;
fn validate(&self) -> Result<(), Self::Error> {
const MINIMAL_BODY_SIZE: usize = 256;
const MINIMAL_MESSAGE_LENGTH: u32 = (MINIMAL_BODY_SIZE + SIGNED_MESSAGE_MIN_SIZE) as u32;
self.validate_keys()?;
if self.min_propose_timeout > self.max_propose_timeout {
bail!(
"Invalid propose timeouts: min_propose_timeout should be less or equal then \
max_propose_timeout: min = {}, max = {}",
self.min_propose_timeout,
self.max_propose_timeout
);
}
if self.first_round_timeout <= self.max_propose_timeout {
bail!(
"first_round_timeout({}) must be strictly larger than max_propose_timeout({})",
self.first_round_timeout,
self.max_propose_timeout
);
}
if self.txs_block_limit == 0 {
bail!("txs_block_limit should not be equal to zero",);
}
if self.max_message_len < MINIMAL_MESSAGE_LENGTH {
bail!(
"max_message_len ({}) must be at least {}",
self.max_message_len,
MINIMAL_MESSAGE_LENGTH
);
}
self.warn_if_nonoptimal();
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Serialize, Deserialize)]
#[derive(ProtobufConvert, BinaryValue, ObjectHash)]
#[protobuf_convert(source = "schema::lifecycle::GenesisConfig")]
#[non_exhaustive]
pub struct GenesisConfig {
pub consensus_config: ConsensusConfig,
pub artifacts: Vec<ArtifactSpec>,
pub builtin_instances: Vec<InstanceInitParams>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Serialize, Deserialize)]
#[derive(ProtobufConvert, BinaryValue, ObjectHash)]
#[protobuf_convert(source = "schema::lifecycle::InstanceInitParams")]
#[non_exhaustive]
pub struct InstanceInitParams {
pub instance_spec: InstanceSpec,
pub constructor: Vec<u8>,
}
impl InstanceInitParams {
pub fn new(
id: InstanceId,
name: impl Into<String>,
artifact: ArtifactId,
constructor: impl BinaryValue,
) -> Self {
Self {
instance_spec: InstanceSpec::from_raw_parts(id, name.into(), artifact),
constructor: constructor.into_bytes(),
}
}
pub fn with_constructor(self, constructor: impl BinaryValue) -> Self {
Self {
instance_spec: self.instance_spec,
constructor: constructor.into_bytes(),
}
}
}
impl From<InstanceSpec> for InstanceInitParams {
fn from(instance_spec: InstanceSpec) -> Self {
Self {
instance_spec,
constructor: Vec::new(),
}
}
}
#[derive(Debug, Default)]
pub struct GenesisConfigBuilder {
consensus_config: ConsensusConfig,
artifacts: HashMap<ArtifactId, Vec<u8>>,
builtin_instances: Vec<InstanceInitParams>,
}
impl GenesisConfigBuilder {
pub fn with_consensus_config(consensus_config: ConsensusConfig) -> Self {
Self {
consensus_config,
artifacts: HashMap::new(),
builtin_instances: vec![],
}
}
pub fn with_artifact(self, artifact: impl Into<ArtifactId>) -> Self {
self.with_parametric_artifact(artifact, ())
}
pub fn with_parametric_artifact(
mut self,
artifact: impl Into<ArtifactId>,
payload: impl BinaryValue,
) -> Self {
let artifact = artifact.into();
self.artifacts
.entry(artifact)
.or_insert_with(|| payload.into_bytes());
self
}
pub fn with_instance(mut self, instance_params: InstanceInitParams) -> Self {
self.builtin_instances.push(instance_params);
self
}
pub fn build(self) -> GenesisConfig {
let artifacts = self
.artifacts
.into_iter()
.map(|(artifact, payload)| ArtifactSpec::new(artifact, payload))
.collect::<Vec<_>>();
GenesisConfig {
consensus_config: self.consensus_config,
artifacts,
builtin_instances: self.builtin_instances,
}
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use std::fmt::Display;
use super::*;
use crate::crypto::{self, gen_keypair_from_seed, Seed, SEED_LENGTH};
fn assert_err_contains(actual: impl Display, expected: impl AsRef<str>) {
let actual = actual.to_string();
let expected = expected.as_ref();
assert!(
actual.contains(expected),
"Actual is {}, expected: {}",
actual,
expected
);
}
fn gen_validator_keys(i: u8) -> ValidatorKeys {
ValidatorKeys::new(
gen_keypair_from_seed(&Seed::new([i; SEED_LENGTH])).0,
gen_keypair_from_seed(&Seed::new([u8::max_value() - i; SEED_LENGTH])).0,
)
}
fn gen_keys_pool(count: usize) -> Vec<PublicKey> {
(0..count)
.map(|_| crypto::gen_keypair().0)
.collect::<Vec<_>>()
}
fn gen_consensus_config() -> ConsensusConfig {
ConsensusConfig {
validator_keys: (0..4).map(gen_validator_keys).collect(),
..ConsensusConfig::default()
}
}
#[test]
fn validate_validator_keys_err_same() {
let pk = crypto::gen_keypair().0;
let keys = ValidatorKeys::new(pk, pk);
let e = keys.validate().unwrap_err();
assert_err_contains(e, "Consensus and service keys must be different");
}
#[test]
fn consensus_config_validate_ok() {
let cfg = ConsensusConfig {
validator_keys: (0..4).map(gen_validator_keys).collect(),
..ConsensusConfig::default()
};
cfg.validate().expect("Expected valid consensus config");
}
#[test]
fn consensus_config_validate_err_round_trip() {
let keys = gen_keys_pool(4);
let cases = [
(
ConsensusConfig::default(),
"Consensus configuration must have at least one validator",
),
(
ConsensusConfig {
validator_keys: vec![ValidatorKeys::new(keys[0], keys[0])],
..ConsensusConfig::default()
},
"Consensus and service keys must be different",
),
(
ConsensusConfig {
validator_keys: vec![
ValidatorKeys::new(keys[0], keys[1]),
ValidatorKeys::new(keys[0], keys[2]),
],
..ConsensusConfig::default()
},
"Duplicated keys are found",
),
(
ConsensusConfig {
validator_keys: vec![
ValidatorKeys::new(keys[0], keys[1]),
ValidatorKeys::new(keys[2], keys[1]),
],
..ConsensusConfig::default()
},
"Duplicated keys are found",
),
(
ConsensusConfig {
min_propose_timeout: 10,
max_propose_timeout: 5,
..gen_consensus_config()
},
"min_propose_timeout should be less or",
),
(
ConsensusConfig {
first_round_timeout: 10,
max_propose_timeout: 15,
..gen_consensus_config()
},
"first_round_timeout(10) must be strictly larger than max_propose_timeout(15)",
),
(
ConsensusConfig {
txs_block_limit: 0,
..gen_consensus_config()
},
"txs_block_limit should not be equal to zero",
),
(
ConsensusConfig {
max_message_len: 0,
..gen_consensus_config()
},
"max_message_len (0) must be at least",
),
];
for (cfg, expected_msg) in &cases {
assert_err_contains(cfg.validate().unwrap_err(), expected_msg);
}
}
#[test]
fn genesis_config_creation() {
let consensus = gen_consensus_config();
let version = "1.0.0".parse().unwrap();
let artifact1 = ArtifactId::from_raw_parts(42, "test_artifact1".into(), version);
let version = "0.2.8".parse().unwrap();
let artifact2 = ArtifactId::from_raw_parts(42, "test_artifact2".into(), version);
let genesis_config = GenesisConfigBuilder::with_consensus_config(consensus.clone())
.with_artifact(artifact1.clone())
.with_parametric_artifact(artifact2.clone(), vec![1_u8, 2, 3])
.with_instance(artifact1.clone().into_default_instance(1, "art1_inst1"))
.with_instance(
artifact1
.into_default_instance(2, "art1_inst2")
.with_constructor(vec![4_u8, 5, 6]),
)
.with_instance(artifact2.into_default_instance(1, "art2_inst1"))
.build();
assert_eq!(genesis_config.consensus_config, consensus);
assert_eq!(genesis_config.artifacts.len(), 2);
assert_eq!(genesis_config.builtin_instances.len(), 3);
}
#[test]
fn genesis_config_check_artifacts_duplication() {
let consensus = gen_consensus_config();
let version = "1.1.5-rc.3".parse().unwrap();
let artifact = ArtifactId::new(42_u32, "test_artifact", version).unwrap();
let correct_payload = vec![1_u8, 2, 3];
let genesis_config = GenesisConfigBuilder::with_consensus_config(consensus)
.with_parametric_artifact(artifact.clone(), correct_payload.clone())
.with_parametric_artifact(artifact, vec![4_u8, 5, 6])
.build();
assert_eq!(genesis_config.artifacts.len(), 1);
assert_eq!(genesis_config.artifacts[0].payload, correct_payload);
}
}