From fb852e95cb88a7bdcbf6570c49a4b87ca3effde7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Ver=C5=A1i=C4=87?= Date: Tue, 23 Apr 2024 15:31:49 +0300 Subject: [PATCH] refactor!: don't send public key with signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marin Veršić --- Cargo.lock | 1 - cli/src/lib.rs | 2 +- client/benches/tps/utils.rs | 2 +- client/examples/million_accounts_genesis.rs | 7 +- client/examples/register_1000_triggers.rs | 6 +- client/src/client.rs | 19 +- client/src/config.rs | 8 +- client/src/config/user.rs | 8 +- client/src/config/user/boilerplate.rs | 16 +- client/src/query_builder.rs | 6 +- client/tests/integration/asset.rs | 12 +- client/tests/integration/burn_public_keys.rs | 3 +- .../integration/domain_owner_permissions.rs | 6 +- client/tests/integration/events/pipeline.rs | 14 +- .../extra_functional/offline_peers.rs | 2 +- client/tests/integration/mod.rs | 6 +- client/tests/integration/non_mintable.rs | 3 +- client/tests/integration/permissions.rs | 8 +- client/tests/integration/roles.rs | 3 +- client/tests/integration/sorting.rs | 2 +- client/tests/integration/transfer_asset.rs | 15 +- .../integration/triggers/data_trigger.rs | 6 +- .../integration/triggers/time_trigger.rs | 7 +- client/tests/integration/tx_chain_id.rs | 3 +- config/src/parameters/actual.rs | 7 - config/src/parameters/user.rs | 2 +- config/tests/fixtures.rs | 2 +- configs/swarm/executor.wasm | Bin 526781 -> 514232 bytes core/benches/blocks/common.rs | 6 +- core/benches/kura.rs | 15 +- core/benches/validation.rs | 7 +- core/src/block.rs | 481 +++++++----- core/src/kura.rs | 2 - core/src/queue.rs | 3 +- core/src/smartcontracts/isi/query.rs | 17 +- core/src/state.rs | 11 +- core/src/sumeragi/main_loop.rs | 705 +++++++++--------- core/src/sumeragi/message.rs | 23 +- core/src/sumeragi/mod.rs | 53 +- core/src/sumeragi/network_topology.rs | 530 +++++-------- core/src/sumeragi/view_change.rs | 102 +-- core/src/tx.rs | 44 +- crypto/src/signature/mod.rs | 446 +---------- data_model/src/block.rs | 102 +-- data_model/src/query/mod.rs | 30 +- data_model/src/transaction.rs | 70 +- docs/source/references/schema.json | 58 +- genesis/Cargo.toml | 1 - p2p/src/peer.rs | 15 +- schema/gen/src/lib.rs | 6 - 50 files changed, 1274 insertions(+), 1629 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dff2ee23c08..727fff239be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3172,7 +3172,6 @@ dependencies = [ "eyre", "iroha_crypto", "iroha_data_model", - "iroha_schema", "once_cell", "serde", "serde_json", diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 947f0a96321..78d70135689 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -292,7 +292,7 @@ impl Iroha { &config.block_sync, sumeragi.clone(), Arc::clone(&kura), - config.common.peer_id(), + config.common.peer_id.clone(), network.clone(), Arc::clone(&state), ) diff --git a/client/benches/tps/utils.rs b/client/benches/tps/utils.rs index ca63d75d89b..6a91fde39ba 100644 --- a/client/benches/tps/utils.rs +++ b/client/benches/tps/utils.rs @@ -4,11 +4,11 @@ use eyre::{Result, WrapErr}; use iroha_client::{ client::Client, data_model::{ + events::pipeline::{BlockEventFilter, BlockStatus}, parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, prelude::*, }, }; -use iroha_data_model::events::pipeline::{BlockEventFilter, BlockStatus}; use nonzero_ext::nonzero; use serde::Deserialize; use test_network::*; diff --git a/client/examples/million_accounts_genesis.rs b/client/examples/million_accounts_genesis.rs index 5a6b1dc6692..126354bbabb 100644 --- a/client/examples/million_accounts_genesis.rs +++ b/client/examples/million_accounts_genesis.rs @@ -2,9 +2,10 @@ use std::{thread, time::Duration}; use iroha::samples::{construct_executor, get_config}; -use iroha_client::data_model::prelude::*; -use iroha_crypto::KeyPair; -use iroha_data_model::isi::InstructionBox; +use iroha_client::{ + crypto::KeyPair, + data_model::{isi::InstructionBox, prelude::*}, +}; use iroha_genesis::{GenesisNetwork, RawGenesisBlock, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; use test_network::{ diff --git a/client/examples/register_1000_triggers.rs b/client/examples/register_1000_triggers.rs index 6de1efd77bc..3dde24a5cb9 100644 --- a/client/examples/register_1000_triggers.rs +++ b/client/examples/register_1000_triggers.rs @@ -3,8 +3,10 @@ use std::str::FromStr; use iroha::samples::{construct_executor, get_config}; -use iroha_client::{client::Client, data_model::prelude::*}; -use iroha_data_model::trigger::TriggerId; +use iroha_client::{ + client::Client, + data_model::{prelude::*, trigger::TriggerId}, +}; use iroha_genesis::{GenesisNetwork, RawGenesisBlock, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; use test_network::{ diff --git a/client/src/client.rs b/client/src/client.rs index 2a4eed202d7..15be1ce3b5a 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -14,13 +14,6 @@ use eyre::{eyre, Result, WrapErr}; use futures_util::StreamExt; use http_default::{AsyncWebSocketStream, WebSocketStream}; pub use iroha_config::client_api::ConfigDTO; -use iroha_data_model::{ - events::pipeline::{ - BlockEventFilter, BlockStatus, PipelineEventBox, PipelineEventFilterBox, - TransactionEventFilter, TransactionStatus, - }, - query::QueryOutputBox, -}; use iroha_logger::prelude::*; use iroha_telemetry::metrics::Status; use iroha_torii_const::uri as torii_uri; @@ -35,9 +28,13 @@ use crate::{ crypto::{HashOf, KeyPair}, data_model::{ block::SignedBlock, + events::pipeline::{ + BlockEventFilter, BlockStatus, PipelineEventBox, PipelineEventFilterBox, + TransactionEventFilter, TransactionStatus, + }, isi::Instruction, prelude::*, - query::{predicate::PredicateBox, Pagination, Query, Sorting}, + query::{predicate::PredicateBox, Pagination, Query, QueryOutputBox, Sorting}, BatchedResponse, ChainId, ValidationFail, }, http::{Method as HttpMethod, RequestBuilder, Response, StatusCode}, @@ -70,17 +67,17 @@ pub type QueryResult = core::result::Result; /// Trait for signing transactions pub trait Sign { /// Sign transaction with provided key pair. - fn sign(self, key_pair: &crate::crypto::KeyPair) -> SignedTransaction; + fn sign(self, key_pair: &KeyPair) -> SignedTransaction; } impl Sign for TransactionBuilder { - fn sign(self, key_pair: &crate::crypto::KeyPair) -> SignedTransaction { + fn sign(self, key_pair: &KeyPair) -> SignedTransaction { self.sign(key_pair) } } impl Sign for SignedTransaction { - fn sign(self, key_pair: &crate::crypto::KeyPair) -> SignedTransaction { + fn sign(self, key_pair: &KeyPair) -> SignedTransaction { self.sign(key_pair) } } diff --git a/client/src/config.rs b/client/src/config.rs index 72bb909d8c7..d2c44603b25 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -9,14 +9,16 @@ use iroha_config::{ base, base::{FromEnv, StdEnv, UnwrapPartial}, }; -use iroha_crypto::KeyPair; -use iroha_data_model::{prelude::*, ChainId}; use iroha_primitives::small::SmallStr; use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use url::Url; -use crate::config::user::RootPartial; +use crate::{ + config::user::RootPartial, + crypto::KeyPair, + data_model::{prelude::*, ChainId}, +}; mod user; diff --git a/client/src/config/user.rs b/client/src/config/user.rs index 30a684e5bac..62869f563bf 100644 --- a/client/src/config/user.rs +++ b/client/src/config/user.rs @@ -7,13 +7,15 @@ use std::{fs::File, io::Read, path::Path, str::FromStr, time::Duration}; pub use boilerplate::*; use eyre::{eyre, Context, Report}; use iroha_config::base::{Emitter, ErrorsCollection}; -use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; -use iroha_data_model::{account::AccountId, ChainId}; use merge::Merge; use serde_with::DeserializeFromStr; use url::Url; -use crate::config::BasicAuth; +use crate::{ + config::BasicAuth, + crypto::{KeyPair, PrivateKey, PublicKey}, + data_model::{account::AccountId, ChainId}, +}; impl RootPartial { /// Reads the partial layer from TOML diff --git a/client/src/config/user/boilerplate.rs b/client/src/config/user/boilerplate.rs index 500b13afecb..a1a71dd01a8 100644 --- a/client/src/config/user/boilerplate.rs +++ b/client/src/config/user/boilerplate.rs @@ -8,15 +8,17 @@ use iroha_config::base::{ Emitter, FromEnv, HumanDuration, Merge, ParseEnvResult, UnwrapPartial, UnwrapPartialResult, UserField, }; -use iroha_crypto::{PrivateKey, PublicKey}; -use iroha_data_model::{account::AccountId, ChainId}; use serde::Deserialize; -use crate::config::{ - base::{FromEnvResult, ReadEnv}, - user::{Account, OnlyHttpUrl, Root, Transaction}, - BasicAuth, DEFAULT_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, - DEFAULT_TRANSACTION_TIME_TO_LIVE, +use crate::{ + config::{ + base::{FromEnvResult, ReadEnv}, + user::{Account, OnlyHttpUrl, Root, Transaction}, + BasicAuth, DEFAULT_TRANSACTION_NONCE, DEFAULT_TRANSACTION_STATUS_TIMEOUT, + DEFAULT_TRANSACTION_TIME_TO_LIVE, + }, + crypto::{PrivateKey, PublicKey}, + data_model::{account::AccountId, ChainId}, }; #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Default, Merge)] diff --git a/client/src/query_builder.rs b/client/src/query_builder.rs index 4ccfe7c99db..5e4cefa6a97 100644 --- a/client/src/query_builder.rs +++ b/client/src/query_builder.rs @@ -1,10 +1,10 @@ use std::fmt::Debug; -use iroha_data_model::query::QueryOutputBox; - use crate::{ client::{Client, QueryOutput, QueryResult}, - data_model::query::{predicate::PredicateBox, sorting::Sorting, FetchSize, Pagination, Query}, + data_model::query::{ + predicate::PredicateBox, sorting::Sorting, FetchSize, Pagination, Query, QueryOutputBox, + }, }; pub struct QueryRequestBuilder<'a, R> { diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index 34a102afd93..fb9ab077fb2 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -4,14 +4,14 @@ use eyre::Result; use iroha_client::{ client::{self, QueryResult}, crypto::{KeyPair, PublicKey}, - data_model::prelude::*, + data_model::{ + asset::{AssetId, AssetValue, AssetValueType}, + isi::error::{InstructionEvaluationError, InstructionExecutionError, Mismatch, TypeError}, + prelude::*, + transaction::error::TransactionRejectionReason, + }, }; use iroha_config::parameters::actual::Root as Config; -use iroha_data_model::{ - asset::{AssetId, AssetValue, AssetValueType}, - isi::error::{InstructionEvaluationError, InstructionExecutionError, Mismatch, TypeError}, - transaction::error::TransactionRejectionReason, -}; use serde_json::json; use test_network::*; diff --git a/client/tests/integration/burn_public_keys.rs b/client/tests/integration/burn_public_keys.rs index 8d245051aea..b606855131d 100644 --- a/client/tests/integration/burn_public_keys.rs +++ b/client/tests/integration/burn_public_keys.rs @@ -1,9 +1,8 @@ use iroha_client::{ client::{account, transaction, Client}, crypto::{HashOf, KeyPair, PublicKey}, - data_model::{isi::Instruction, prelude::*}, + data_model::{isi::Instruction, prelude::*, query::TransactionQueryOutput}, }; -use iroha_data_model::query::TransactionQueryOutput; use test_network::*; fn submit( diff --git a/client/tests/integration/domain_owner_permissions.rs b/client/tests/integration/domain_owner_permissions.rs index af78eff12ac..beda25b3f47 100644 --- a/client/tests/integration/domain_owner_permissions.rs +++ b/client/tests/integration/domain_owner_permissions.rs @@ -1,9 +1,11 @@ use eyre::Result; use iroha_client::{ crypto::KeyPair, - data_model::{account::SignatureCheckCondition, prelude::*}, + data_model::{ + account::SignatureCheckCondition, prelude::*, + transaction::error::TransactionRejectionReason, + }, }; -use iroha_data_model::transaction::error::TransactionRejectionReason; use serde_json::json; use test_network::*; diff --git a/client/tests/integration/events/pipeline.rs b/client/tests/integration/events/pipeline.rs index cd8288e0f05..ba7046d32fd 100644 --- a/client/tests/integration/events/pipeline.rs +++ b/client/tests/integration/events/pipeline.rs @@ -4,19 +4,17 @@ use eyre::Result; use iroha_client::{ crypto::HashOf, data_model::{ + events::pipeline::{ + BlockEvent, BlockEventFilter, BlockStatus, TransactionEventFilter, TransactionStatus, + }, + isi::error::InstructionExecutionError, parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, prelude::*, + transaction::error::TransactionRejectionReason, + ValidationFail, }, }; use iroha_config::parameters::actual::Root as Config; -use iroha_data_model::{ - events::pipeline::{ - BlockEvent, BlockEventFilter, BlockStatus, TransactionEventFilter, TransactionStatus, - }, - isi::error::InstructionExecutionError, - transaction::error::TransactionRejectionReason, - ValidationFail, -}; use test_network::*; // Needed to re-enable ignored tests. diff --git a/client/tests/integration/extra_functional/offline_peers.rs b/client/tests/integration/extra_functional/offline_peers.rs index 988b0271acb..14ad9479574 100644 --- a/client/tests/integration/extra_functional/offline_peers.rs +++ b/client/tests/integration/extra_functional/offline_peers.rs @@ -1,13 +1,13 @@ use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, + crypto::KeyPair, data_model::{ peer::{Peer as DataModelPeer, PeerId}, prelude::*, }, }; use iroha_config::parameters::actual::Root as Config; -use iroha_crypto::KeyPair; use iroha_primitives::addr::socket_addr; use test_network::*; use tokio::runtime::Runtime; diff --git a/client/tests/integration/mod.rs b/client/tests/integration/mod.rs index 98b17659895..eb462f66174 100644 --- a/client/tests/integration/mod.rs +++ b/client/tests/integration/mod.rs @@ -1,5 +1,7 @@ -use iroha_crypto::KeyPair; -use iroha_data_model::account::{Account, AccountId, NewAccount}; +use iroha_client::{ + crypto::KeyPair, + data_model::account::{Account, AccountId, NewAccount}, +}; mod add_account; mod add_domain; diff --git a/client/tests/integration/non_mintable.rs b/client/tests/integration/non_mintable.rs index e9652f29390..0c595c612ac 100644 --- a/client/tests/integration/non_mintable.rs +++ b/client/tests/integration/non_mintable.rs @@ -3,9 +3,8 @@ use std::str::FromStr as _; use eyre::Result; use iroha_client::{ client::{self, QueryResult}, - data_model::{metadata::UnlimitedMetadata, prelude::*}, + data_model::{isi::InstructionBox, metadata::UnlimitedMetadata, prelude::*}, }; -use iroha_data_model::isi::InstructionBox; use test_network::*; #[test] diff --git a/client/tests/integration/permissions.rs b/client/tests/integration/permissions.rs index 16a4c85140a..3896773bc67 100644 --- a/client/tests/integration/permissions.rs +++ b/client/tests/integration/permissions.rs @@ -4,10 +4,10 @@ use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, crypto::KeyPair, - data_model::prelude::*, -}; -use iroha_data_model::{ - permission::PermissionToken, role::RoleId, transaction::error::TransactionRejectionReason, + data_model::{ + permission::PermissionToken, prelude::*, role::RoleId, + transaction::error::TransactionRejectionReason, + }, }; use iroha_genesis::GenesisNetwork; use serde_json::json; diff --git a/client/tests/integration/roles.rs b/client/tests/integration/roles.rs index 3a3bcd7aff6..514685d28a2 100644 --- a/client/tests/integration/roles.rs +++ b/client/tests/integration/roles.rs @@ -4,9 +4,8 @@ use eyre::Result; use iroha_client::{ client::{self, QueryResult}, crypto::KeyPair, - data_model::prelude::*, + data_model::{prelude::*, transaction::error::TransactionRejectionReason}, }; -use iroha_data_model::transaction::error::TransactionRejectionReason; use serde_json::json; use test_network::*; diff --git a/client/tests/integration/sorting.rs b/client/tests/integration/sorting.rs index bddfcd7ee39..0d1907c8268 100644 --- a/client/tests/integration/sorting.rs +++ b/client/tests/integration/sorting.rs @@ -5,6 +5,7 @@ use iroha_client::{ client::{self, QueryResult}, data_model::{ account::Account, + isi::InstructionBox, prelude::*, query::{ predicate::{string, value, PredicateBox}, @@ -12,7 +13,6 @@ use iroha_client::{ }, }, }; -use iroha_data_model::isi::InstructionBox; use nonzero_ext::nonzero; use test_network::*; diff --git a/client/tests/integration/transfer_asset.rs b/client/tests/integration/transfer_asset.rs index 31c2750068f..e822644c059 100644 --- a/client/tests/integration/transfer_asset.rs +++ b/client/tests/integration/transfer_asset.rs @@ -3,13 +3,14 @@ use std::str::FromStr; use iroha_client::{ client::{self, QueryResult}, crypto::KeyPair, - data_model::{isi::Instruction, prelude::*, Registered}, -}; -use iroha_data_model::{ - account::{Account, AccountId}, - asset::{Asset, AssetDefinition}, - isi::InstructionBox, - name::Name, + data_model::{ + account::{Account, AccountId}, + asset::{Asset, AssetDefinition}, + isi::{Instruction, InstructionBox}, + name::Name, + prelude::*, + Registered, + }, }; use test_network::*; diff --git a/client/tests/integration/triggers/data_trigger.rs b/client/tests/integration/triggers/data_trigger.rs index 46f505a9f9f..70f47902402 100644 --- a/client/tests/integration/triggers/data_trigger.rs +++ b/client/tests/integration/triggers/data_trigger.rs @@ -1,6 +1,8 @@ use eyre::Result; -use iroha_client::{client, data_model::prelude::*}; -use iroha_data_model::asset::AssetValue; +use iroha_client::{ + client, + data_model::{asset::AssetValue, prelude::*}, +}; use test_network::*; use crate::integration::new_account_with_random_public_key; diff --git a/client/tests/integration/triggers/time_trigger.rs b/client/tests/integration/triggers/time_trigger.rs index 8a9bb9fb034..dbed2f51b00 100644 --- a/client/tests/integration/triggers/time_trigger.rs +++ b/client/tests/integration/triggers/time_trigger.rs @@ -3,10 +3,13 @@ use std::{str::FromStr as _, time::Duration}; use eyre::Result; use iroha_client::{ client::{self, Client, QueryResult}, - data_model::{prelude::*, transaction::WasmSmartContract}, + data_model::{ + events::pipeline::{BlockEventFilter, BlockStatus}, + prelude::*, + transaction::WasmSmartContract, + }, }; use iroha_config::parameters::defaults::chain_wide::DEFAULT_CONSENSUS_ESTIMATION; -use iroha_data_model::events::pipeline::{BlockEventFilter, BlockStatus}; use iroha_logger::info; use test_network::*; diff --git a/client/tests/integration/tx_chain_id.rs b/client/tests/integration/tx_chain_id.rs index 9e16a90a898..5ad88365582 100644 --- a/client/tests/integration/tx_chain_id.rs +++ b/client/tests/integration/tx_chain_id.rs @@ -1,7 +1,6 @@ use std::str::FromStr; -use iroha_crypto::KeyPair; -use iroha_data_model::prelude::*; +use iroha_client::{crypto::KeyPair, data_model::prelude::*}; use iroha_primitives::numeric::numeric; use test_network::*; diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index c9dc0973ab6..51b7a141cd4 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -75,13 +75,6 @@ pub struct Common { pub peer_id: PeerId, } -impl Common { - /// Construct an id of this peer - pub fn peer_id(&self) -> PeerId { - self.peer_id.clone() - } -} - /// Network options #[allow(missing_docs)] #[derive(Debug, Clone)] diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index 35774e882a6..73491c1bd34 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -236,7 +236,7 @@ impl Root { let genesis = genesis.unwrap(); let sumeragi = { let mut x = sumeragi.unwrap(); - x.trusted_peers.push(peer.peer_id()); + x.trusted_peers.push(peer.peer_id.clone()); x }; diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index e88b2fdf155..88ce5be4a99 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -241,7 +241,7 @@ fn self_is_presented_in_trusted_peers() -> Result<()> { assert!(config .sumeragi .trusted_peers - .contains(&config.common.peer_id())); + .contains(&config.common.peer_id)); Ok(()) } diff --git a/configs/swarm/executor.wasm b/configs/swarm/executor.wasm index 55348c7b4cb483198f8426b9cce3bddb0050e6f1..bdce8736f47ea38569a70c8635edf1e07a4d2e28 100644 GIT binary patch delta 150073 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 diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index d88514f7c9f..5b061477532 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -34,14 +34,16 @@ pub fn create_block( .sign(key_pair); let limits = state.transaction_executor().transaction_limits; - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, _) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let block = BlockBuilder::new( vec![AcceptedTransaction::accept(transaction, &chain_id, &limits).unwrap()], topology.clone(), Vec::new(), ) .chain(0, state) - .sign(key_pair) + .sign(key_pair.private_key()) .unpack(|_| {}) .commit(&topology) .unpack(|_| {}) diff --git a/core/benches/kura.rs b/core/benches/kura.rs index f9b53e25190..7bf60f757c1 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -15,7 +15,6 @@ use iroha_core::{ }; use iroha_crypto::KeyPair; use iroha_data_model::{prelude::*, transaction::TransactionLimits}; -use iroha_primitives::unique_vec::UniqueVec; use tokio::{fs, runtime::Runtime}; async fn measure_block_size_for_n_executors(n_executors: u32) { @@ -26,13 +25,13 @@ async fn measure_block_size_for_n_executors(n_executors: u32) { let xor_id = AssetDefinitionId::from_str("xor#test").expect("tested"); let alice_xor_id = AssetId::new(xor_id, alice_id); let transfer = Transfer::asset_numeric(alice_xor_id, 10u32, bob_id); - let keypair = KeyPair::random(); + let key_pair = KeyPair::random(); let tx = TransactionBuilder::new( chain_id.clone(), AccountId::from_str("alice@wonderland").expect("checked"), ) .with_instructions([transfer]) - .sign(&keypair); + .sign(&key_pair); let transaction_limits = TransactionLimits { max_instruction_number: 4096, max_wasm_size_bytes: 0, @@ -50,17 +49,19 @@ async fn measure_block_size_for_n_executors(n_executors: u32) { let query_handle = LiveQueryStore::test().start(); let state = State::new(World::new(), kura, query_handle); - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, peer_private_key) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let mut block = { let mut state_block = state.block(); - BlockBuilder::new(vec![tx], topology, Vec::new()) + BlockBuilder::new(vec![tx], topology.clone(), Vec::new()) .chain(0, &mut state_block) - .sign(&KeyPair::random()) + .sign(&peer_private_key) .unpack(|_| {}) }; for _ in 1..n_executors { - block = block.sign(&KeyPair::random()); + block.sign(&key_pair, &topology); } let mut block_store = BlockStore::new(dir.path(), LockStatus::Unlocked); block_store.create_files_if_they_do_not_exist().unwrap(); diff --git a/core/benches/validation.rs b/core/benches/validation.rs index d7e5459f090..128340a3ad5 100644 --- a/core/benches/validation.rs +++ b/core/benches/validation.rs @@ -170,11 +170,12 @@ fn sign_blocks(criterion: &mut Criterion) { &TRANSACTION_LIMITS, ) .expect("Failed to accept transaction."); - let key_pair = KeyPair::random(); let kura = iroha_core::kura::Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); let state = State::new(World::new(), kura, query_handle); - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, peer_private_key) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let mut count = 0; @@ -186,7 +187,7 @@ fn sign_blocks(criterion: &mut Criterion) { b.iter_batched( || block.clone(), |block| { - let _: ValidBlock = block.sign(&key_pair).unpack(|_| {}); + let _: ValidBlock = block.sign(&peer_private_key).unpack(|_| {}); count += 1; }, BatchSize::SmallInput, diff --git a/core/src/block.rs b/core/src/block.rs index 68df8771241..bbee8f0240a 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -7,7 +7,7 @@ use std::error::Error as _; use iroha_config::parameters::defaults::chain_wide::DEFAULT_CONSENSUS_ESTIMATION; -use iroha_crypto::{HashOf, KeyPair, MerkleTree, SignatureOf, SignaturesOf}; +use iroha_crypto::{HashOf, MerkleTree, SignatureOf}; use iroha_data_model::{ block::*, events::prelude::*, @@ -15,7 +15,6 @@ use iroha_data_model::{ transaction::{error::TransactionRejectionReason, prelude::*}, }; use iroha_genesis::GenesisTransaction; -use iroha_primitives::unique_vec::UniqueVec; use thiserror::Error; pub(crate) use self::event::WithEvents; @@ -23,7 +22,7 @@ pub use self::{chained::Chained, commit::CommittedBlock, valid::ValidBlock}; use crate::{prelude::*, sumeragi::network_topology::Topology, tx::AcceptTransactionFail}; /// Error during transaction validation -#[derive(Debug, displaydoc::Display, Error)] +#[derive(Debug, displaydoc::Display, PartialEq, Eq, Error)] pub enum TransactionValidationError { /// Failed to accept transaction Accept(#[from] AcceptTransactionFail), @@ -34,31 +33,24 @@ pub enum TransactionValidationError { } /// Errors occurred on block validation -#[derive(Debug, displaydoc::Display, Error)] +#[derive(Debug, displaydoc::Display, PartialEq, Eq, Error)] pub enum BlockValidationError { /// Block has committed transactions HasCommittedTransactions, - /// Mismatch between the actual and expected hashes of the latest block. Expected: {expected:?}, actual: {actual:?} - LatestBlockHashMismatch { + /// Mismatch between the actual and expected hashes of the previous block. Expected: {expected:?}, actual: {actual:?} + PrevBlockHashMismatch { /// Expected value expected: Option>, /// Actual value actual: Option>, }, - /// Mismatch between the actual and expected height of the latest block. Expected: {expected}, actual: {actual} - LatestBlockHeightMismatch { + /// Mismatch between the actual and expected height of the previous block. Expected: {expected}, actual: {actual} + PrevBlockHeightMismatch { /// Expected value expected: u64, /// Actual value actual: u64, }, - /// Mismatch between the actual and expected hashes of the current block. Expected: {expected:?}, actual: {actual:?} - IncorrectHash { - /// Expected value - expected: HashOf, - /// Actual value - actual: HashOf, - }, /// The transaction hash stored in the block header does not match the actual transaction hash TransactionHashMismatch, /// Error during transaction validation @@ -66,9 +58,9 @@ pub enum BlockValidationError { /// Mismatch between the actual and expected topology. Expected: {expected:?}, actual: {actual:?} TopologyMismatch { /// Expected value - expected: UniqueVec, + expected: Vec, /// Actual value - actual: UniqueVec, + actual: Vec, }, /// Error during block signatures check SignatureVerification(#[from] SignatureVerificationError), @@ -77,7 +69,7 @@ pub enum BlockValidationError { } /// Error during signature verification -#[derive(thiserror::Error, displaydoc::Display, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, displaydoc::Display, Clone, Copy, PartialEq, Eq, Error)] pub enum SignatureVerificationError { /// The block doesn't have enough valid signatures to be committed (`votes_count` out of `min_votes_for_commit`) NotEnoughSignatures { @@ -86,9 +78,14 @@ pub enum SignatureVerificationError { /// Minimal required number of signatures min_votes_for_commit: usize, }, - /// The block doesn't contain an expected signature. Expected signature can be leader or the current peer - SignatureMissing, - /// Found signature that does not correspond to block payload + /// Block was signed by the same node multiple times + DuplicateSignatures { + /// Index of the faulty node in the topology + signatory: usize, + }, + /// Block signatory doesn't correspond to any in topology + UnknownSignatory, + /// Block signature doesn't correspond to block payload UnknownSignature, /// The block doesn't have proxy tail signature ProxyTailMissing, @@ -147,7 +144,7 @@ mod pending { fn make_header( previous_height: u64, prev_block_hash: Option>, - view_change_index: u64, + view_change_index: usize, transactions: &[TransactionValue], ) -> BlockHeader { BlockHeader { @@ -164,7 +161,7 @@ mod pending { .as_millis() .try_into() .expect("Time should fit into u64"), - view_change_index, + view_change_index: view_change_index as u32, consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION .as_millis() .try_into() @@ -205,7 +202,7 @@ mod pending { /// Upon executing this method current timestamp is stored in the block header. pub fn chain( self, - view_change_index: u64, + view_change_index: usize, state: &mut StateBlock<'_>, ) -> BlockBuilder { let transactions = Self::categorize_transactions(self.0.transactions, state); @@ -218,7 +215,7 @@ mod pending { &transactions, ), transactions, - commit_topology: self.0.commit_topology.ordered_peers, + commit_topology: self.0.commit_topology.into_iter().collect(), event_recommendations: self.0.event_recommendations, })) } @@ -234,13 +231,13 @@ mod chained { impl BlockBuilder { /// Sign this block and get [`SignedBlock`]. - pub fn sign(self, key_pair: &KeyPair) -> WithEvents { - let signature = SignatureOf::new(key_pair, &self.0 .0); + pub fn sign(self, private_key: &PrivateKey) -> WithEvents { + let signature = BlockSignature(0, SignatureOf::new(private_key, &self.0 .0)); WithEvents::new(ValidBlock( SignedBlockV1 { + signatures: vec![signature], payload: self.0 .0, - signatures: SignaturesOf::from(signature), } .into(), )) @@ -249,6 +246,7 @@ mod chained { } mod valid { + use indexmap::IndexMap; use iroha_data_model::ChainId; use super::*; @@ -260,17 +258,152 @@ mod valid { pub struct ValidBlock(pub(super) SignedBlock); impl ValidBlock { + fn verify_leader_signature( + block: &SignedBlock, + topology: &Topology, + ) -> Result<(), SignatureVerificationError> { + let leader_index = topology.leader_index(); + let mut signatures = block.signatures(); + + match signatures.next() { + Some(BlockSignature(signatory, signature)) + if usize::try_from(*signatory) + .map_err(|_err| SignatureVerificationError::LeaderMissing)? + == leader_index => + { + let leader = &topology.as_ref()[leader_index]; + + let mut additional_leader_signatures = + topology.filter_signatures_by_roles(&[Role::Leader], signatures); + + if additional_leader_signatures.next().is_some() { + return Err(SignatureVerificationError::DuplicateSignatures { + signatory: leader_index, + }); + } + + let SignedBlock::V1(block) = block; + signature + .verify(leader.public_key(), &block.payload) + .map_err(|_err| SignatureVerificationError::LeaderMissing)?; + } + _ => { + return Err(SignatureVerificationError::LeaderMissing); + } + } + + Ok(()) + } + + fn verify_validator_signatures( + block: &SignedBlock, + topology: &Topology, + ) -> Result<(), SignatureVerificationError> { + //let roles: &[Role] = if topology.view_change_index() >= 1 { + // &[Role::ValidatingPeer, Role::ObservingPeer] + //} else { + // if topology + // .filter_signatures_by_roles(&[Role::ObservingPeer], block.signatures()) + // .next() + // .is_some() + // { + // return Err(SignatureVerificationError::UnknownSignatory); + // } + + // &[Role::ValidatingPeer] + //}; + let roles = &[Role::ValidatingPeer, Role::ObservingPeer]; + + topology + .filter_signatures_by_roles(roles, block.signatures()) + .try_fold(IndexMap::::default(), |mut acc, signature| { + let signatory_idx = usize::try_from(signature.0) + .map_err(|_err| SignatureVerificationError::UnknownSignatory)?; + + if acc.insert(signatory_idx, signature.1).is_some() { + return Err(SignatureVerificationError::DuplicateSignatures { + signatory: signatory_idx, + }); + } + + Ok(acc) + })? + .into_iter() + .try_for_each(|(signatory_idx, signature)| { + let signatory: &PeerId = topology + .as_ref() + .get(signatory_idx) + .ok_or(SignatureVerificationError::UnknownSignatory)?; + + let SignedBlock::V1(block) = block; + signature + .verify(signatory.public_key(), &block.payload) + .map_err(|_err| SignatureVerificationError::UnknownSignature)?; + + Ok(()) + })?; + + if topology + .filter_signatures_by_roles(&[Role::Undefined], block.signatures()) + .next() + .is_some() + { + return Err(SignatureVerificationError::UnknownSignatory); + } + + Ok(()) + } + + fn verify_proxy_tail_signature( + block: &SignedBlock, + topology: &Topology, + ) -> Result<(), SignatureVerificationError> { + let proxy_tail_index = topology.proxy_tail_index(); + let mut signatures = block.signatures().rev(); + + match signatures.next() { + Some(BlockSignature(signatory, signature)) + if usize::try_from(*signatory) + .map_err(|_err| SignatureVerificationError::ProxyTailMissing)? + == proxy_tail_index => + { + let proxy_tail = &topology.as_ref()[proxy_tail_index]; + + let mut additional_proxy_tail_signatures = + topology.filter_signatures_by_roles(&[Role::ProxyTail], signatures); + + if additional_proxy_tail_signatures.next().is_some() { + return Err(SignatureVerificationError::DuplicateSignatures { + signatory: proxy_tail_index, + }); + } + + let SignedBlock::V1(block) = block; + signature + .verify(proxy_tail.public_key(), &block.payload) + .map_err(|_err| SignatureVerificationError::ProxyTailMissing)?; + } + _ => { + return Err(SignatureVerificationError::ProxyTailMissing); + } + } + + Ok(()) + } + /// Validate a block against the current state of the world. /// /// # Errors /// - /// - Block is empty /// - There is a mismatch between candidate block height and actual blockchain height - /// - There is a mismatch between candidate block previous block hash and actual latest block hash + /// - There is a mismatch between candidate block previous block hash and actual previous block hash + /// - Block is not signed by the leader + /// - Block has duplicate signatures + /// - Block has unknown signatories + /// - Block has incorrect signatures + /// - Topology field is incorrect /// - Block has committed transactions - /// - Block header transaction hashes don't match with computed transaction hashes /// - Error during validation of individual transactions - /// - Topology field is incorrect /// - Transaction in the genesis block is not signed by the genesis public key pub fn validate( block: SignedBlock, @@ -285,7 +418,7 @@ mod valid { if expected_block_height != actual_height { return WithEvents::new(Err(( block, - BlockValidationError::LatestBlockHeightMismatch { + BlockValidationError::PrevBlockHeightMismatch { expected: expected_block_height, actual: actual_height, }, @@ -298,39 +431,34 @@ mod valid { if expected_prev_block_hash != actual_prev_block_hash { return WithEvents::new(Err(( block, - BlockValidationError::LatestBlockHashMismatch { + BlockValidationError::PrevBlockHashMismatch { expected: expected_prev_block_hash, actual: actual_prev_block_hash, }, ))); } - // NOTE: should be checked AFTER height and hash, both this issues lead to topology mismatch if !block.header().is_genesis() { - let actual_commit_topology = block.commit_topology(); - let expected_commit_topology = &topology.ordered_peers; + if let Err(err) = Self::verify_leader_signature(&block, topology) + .map(|()| Self::verify_validator_signatures(&block, topology)) + { + return WithEvents::new(Err((block, err.into()))); + } - if actual_commit_topology != expected_commit_topology { - let actual_commit_topology = actual_commit_topology.clone(); + let actual_commit_topology = block.commit_topology().cloned().collect(); + let expected_commit_topology = topology.as_ref(); + // NOTE: checked AFTER height and hash because + // both of them can lead to a topology mismatch + if actual_commit_topology != expected_commit_topology { return WithEvents::new(Err(( block, BlockValidationError::TopologyMismatch { - expected: expected_commit_topology.clone(), + expected: expected_commit_topology.to_owned(), actual: actual_commit_topology, }, ))); } - - if topology - .filter_signatures_by_roles(&[Role::Leader], block.signatures()) - .is_empty() - { - return WithEvents::new(Err(( - block, - SignatureVerificationError::LeaderMissing.into(), - ))); - } } if block @@ -403,91 +531,93 @@ mod valid { }) } - /// The manipulation of the topology relies upon all peers seeing the same signature set. - /// Therefore we must clear the signatures and accept what the proxy tail giveth. + /// Add additional signature for [`Self`] /// /// # Errors /// - /// - Not enough signatures - /// - Not signed by proxy tail - pub fn commit_with_signatures( - mut self, + /// If given signature doesn't match block hash + pub fn add_signature( + &mut self, + signature: BlockSignature, topology: &Topology, - signatures: SignaturesOf, - expected_hash: HashOf, - ) -> WithEvents> { - if topology - .filter_signatures_by_roles(&[Role::Leader], &signatures) - .is_empty() - { - return WithEvents::new(Err(( - self, - SignatureVerificationError::LeaderMissing.into(), - ))); - } + ) -> Result<(), iroha_crypto::Error> { + let signatory = &topology.as_ref()[signature.0 as usize]; - if !self.as_ref().signatures().is_subset(&signatures) { - return WithEvents::new(Err(( - self, - SignatureVerificationError::SignatureMissing.into(), - ))); - } + let SignedBlock::V1(block) = &mut self.0; + signature.1.verify(signatory.public_key(), &block.payload)?; + block.signatures.push(signature); - if !self.0.replace_signatures(signatures) { - return WithEvents::new(Err(( - self, - SignatureVerificationError::UnknownSignature.into(), - ))); - } + Ok(()) + } - let actual_block_hash = self.as_ref().hash(); - if actual_block_hash != expected_hash { - let err = BlockValidationError::IncorrectHash { - expected: expected_hash, - actual: actual_block_hash, - }; + /// Replace block's signatures. Returns previous block signatures + /// + /// # Errors + /// + /// - Replacement signatures don't contain the leader signature + /// - Replacement signatures contain duplicate signatures + /// - Replacement signatures contain unknown signatories + /// - Replacement signatures contain incorrect signatures + pub fn replace_signatures( + &mut self, + mut signatures: Vec, + topology: &Topology, + ) -> WithEvents, SignatureVerificationError>> { + if !self.as_ref().header().is_genesis() { + let SignedBlock::V1(block) = &mut self.0; - return WithEvents::new(Err((self, err))); + core::mem::swap(&mut block.signatures, &mut signatures); + if let Err(err) = Self::verify_leader_signature(self.as_ref(), topology) + .map(|()| Self::verify_validator_signatures(self.as_ref(), topology)) + { + let SignedBlock::V1(block) = &mut self.0; + core::mem::swap(&mut block.signatures, &mut signatures); + return WithEvents::new(Err(err)); + } } - self.commit(topology) + WithEvents::new(Ok(signatures)) } - /// Verify signatures and commit block to the store. + /// commit block to the store. /// /// # Errors /// - /// - Not enough signatures - /// - Not signed by proxy tail + /// - Block has duplicate proxy tail signatures + /// - Block is not signed by the proxy tail + /// - Block doesn't have enough signatures pub fn commit( self, topology: &Topology, ) -> WithEvents> { - if !self.0.header().is_genesis() { - if let Err(err) = self.verify_signatures(topology) { + if !self.as_ref().header().is_genesis() { + if let Err(err) = Self::verify_proxy_tail_signature(self.as_ref(), topology) { return WithEvents::new(Err((self, err.into()))); } + + let votes_count = self.as_ref().signatures().len(); + if votes_count < topology.min_votes_for_commit() { + return WithEvents::new(Err(( + self, + SignatureVerificationError::NotEnoughSignatures { + votes_count, + min_votes_for_commit: topology.min_votes_for_commit(), + } + .into(), + ))); + } } WithEvents::new(Ok(CommittedBlock(self))) } /// Add additional signatures for [`Self`]. - #[must_use] - pub fn sign(self, key_pair: &KeyPair) -> ValidBlock { - ValidBlock(self.0.sign(key_pair)) - } + pub fn sign(&mut self, key_pair: &KeyPair, topology: &Topology) { + let signatory_idx = topology + .position(key_pair.public_key()) + .expect("BUG: Node is not in topology"); - /// Add additional signature for [`Self`] - /// - /// # Errors - /// - /// If given signature doesn't match block hash - pub fn add_signature( - &mut self, - signature: SignatureOf, - ) -> Result<(), iroha_crypto::error::Error> { - self.0.add_signature(signature) + self.0.sign(key_pair.private_key(), signatory_idx); } #[cfg(test)] @@ -505,56 +635,12 @@ mod valid { .expect("Time should fit into u64"), }, transactions: Vec::new(), - commit_topology: UniqueVec::new(), + commit_topology: Vec::new(), event_recommendations: Vec::new(), })) - .sign(&KeyPair::random()) + .sign(iroha_crypto::KeyPair::random().private_key()) .unpack(|_| {}) } - - /// Check if block's signatures meet requirements for given topology. - /// - /// In order for block to be considered valid there should be at least $2f + 1$ signatures (including proxy tail and leader signature) where f is maximum number of faulty nodes. - /// For further information please refer to the [whitepaper](docs/source/iroha_2_whitepaper.md) section 2.8 consensus. - /// - /// # Errors - /// - Not enough signatures - /// - Missing proxy tail signature - fn verify_signatures(&self, topology: &Topology) -> Result<(), SignatureVerificationError> { - // TODO: Should the peer that serves genesis have a fixed role of ProxyTail in topology? - if !self.as_ref().header().is_genesis() - && topology.is_consensus_required().is_some() - && topology - .filter_signatures_by_roles(&[Role::ProxyTail], self.as_ref().signatures()) - .is_empty() - { - return Err(SignatureVerificationError::ProxyTailMissing); - } - - #[allow(clippy::collapsible_else_if)] - if self.as_ref().header().is_genesis() { - // At genesis round we blindly take on the network topology from the genesis block. - } else { - let roles = [ - Role::ValidatingPeer, - Role::Leader, - Role::ProxyTail, - Role::ObservingPeer, - ]; - - let votes_count = topology - .filter_signatures_by_roles(&roles, self.as_ref().signatures()) - .len(); - if votes_count < topology.min_votes_for_commit() { - return Err(SignatureVerificationError::NotEnoughSignatures { - votes_count, - min_votes_for_commit: topology.min_votes_for_commit(), - }); - } - } - - Ok(()) - } } impl From for SignedBlock { @@ -592,11 +678,13 @@ mod valid { let payload = payload(&block).clone(); key_pairs .iter() - .map(|key_pair| SignatureOf::new(key_pair, &payload)) - .try_for_each(|signature| block.add_signature(signature)) + .map(|key_pair| { + BlockSignature(0, SignatureOf::new(key_pair.private_key(), &payload)) + }) + .try_for_each(|signature| block.add_signature(signature, &topology)) .expect("Failed to add signatures"); - assert_eq!(block.verify_signatures(&topology), Ok(())); + assert!(block.commit(&topology).unpack(|_| {}).is_ok()); } #[test] @@ -612,11 +700,13 @@ mod valid { let payload = payload(&block).clone(); key_pairs .iter() - .map(|key_pair| SignatureOf::new(key_pair, &payload)) - .try_for_each(|signature| block.add_signature(signature)) + .map(|key_pair| { + BlockSignature(0, SignatureOf::new(key_pair.private_key(), &payload)) + }) + .try_for_each(|signature| block.add_signature(signature, &topology)) .expect("Failed to add signatures"); - assert_eq!(block.verify_signatures(&topology), Ok(())); + assert!(block.commit(&topology).unpack(|_| {}).is_ok()); } /// Check requirement of having at least $2f + 1$ signatures in $3f + 1$ network @@ -631,17 +721,19 @@ mod valid { let mut block = ValidBlock::new_dummy(); let payload = payload(&block).clone(); - let proxy_tail_signature = SignatureOf::new(&key_pairs[4], &payload); + let proxy_tail_signature = + BlockSignature(0, SignatureOf::new(key_pairs[4].private_key(), &payload)); block - .add_signature(proxy_tail_signature) + .add_signature(proxy_tail_signature, &topology) .expect("Failed to add signature"); assert_eq!( - block.verify_signatures(&topology), - Err(SignatureVerificationError::NotEnoughSignatures { + block.commit(&topology).unpack(|_| {}).unwrap_err().1, + SignatureVerificationError::NotEnoughSignatures { votes_count: 1, min_votes_for_commit: topology.min_votes_for_commit(), - }) + } + .into() ) } @@ -661,13 +753,15 @@ mod valid { .iter() .enumerate() .filter(|(i, _)| *i != 4) // Skip proxy tail - .map(|(_, key_pair)| SignatureOf::new(key_pair, &payload)) - .try_for_each(|signature| block.add_signature(signature)) + .map(|(_, key_pair)| { + BlockSignature(0, SignatureOf::new(key_pair.private_key(), &payload)) + }) + .try_for_each(|signature| block.add_signature(signature, &topology)) .expect("Failed to add signatures"); assert_eq!( - block.verify_signatures(&topology), - Err(SignatureVerificationError::ProxyTailMissing) + block.commit(&topology).unpack(|_| {}).unwrap_err().1, + SignatureVerificationError::ProxyTailMissing.into() ) } } @@ -732,6 +826,17 @@ mod event { } } } + impl WithEvents, SignatureVerificationError>> { + pub fn unpack( + self, + f: F, + ) -> Result, SignatureVerificationError> { + match self.0 { + Ok(ok) => Ok(ok), + Err(err) => Err(WithEvents(err).unpack(f)), + } + } + } impl WithEvents { pub fn unpack(self, f: F) -> B { self.0.produce_events().for_each(f); @@ -789,6 +894,14 @@ mod event { impl EventProducer for BlockValidationError { fn produce_events(&self) -> impl Iterator { + // TODO: + core::iter::empty() + } + } + + impl EventProducer for SignatureVerificationError { + fn produce_events(&self) -> impl Iterator { + // TODO: core::iter::empty() } } @@ -798,20 +911,22 @@ mod event { mod tests { use std::str::FromStr as _; - use iroha_crypto::SignatureVerificationFail; use iroha_data_model::prelude::*; use iroha_genesis::{GENESIS_ACCOUNT_ID, GENESIS_DOMAIN_ID}; + use iroha_primitives::unique_vec::UniqueVec; use super::*; use crate::{ kura::Kura, query::store::LiveQueryStore, smartcontracts::isi::Registrable as _, - state::State, + state::State, tx::SignatureVerificationFail, }; #[test] pub fn committed_and_valid_block_hashes_are_equal() { let valid_block = ValidBlock::new_dummy(); - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, _) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let committed_block = valid_block .clone() .commit(&topology) @@ -853,10 +968,12 @@ mod tests { // Creating a block of two identical transactions and validating it let transactions = vec![tx.clone(), tx]; - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, _) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let valid_block = BlockBuilder::new(transactions, topology, Vec::new()) .chain(0, &mut state_block) - .sign(&alice_keys) + .sign(alice_keys.private_key()) .unpack(|_| {}); // The first transaction should be confirmed @@ -928,10 +1045,12 @@ mod tests { // Creating a block of two identical transactions and validating it let transactions = vec![tx0, tx, tx2]; - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, _) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let valid_block = BlockBuilder::new(transactions, topology, Vec::new()) .chain(0, &mut state_block) - .sign(&alice_keys) + .sign(alice_keys.private_key()) .unpack(|_| {}); // The first transaction should fail @@ -998,10 +1117,12 @@ mod tests { // Creating a block of where first transaction must fail and second one fully executed let transactions = vec![tx_fail, tx_accept]; - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, _) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let valid_block = BlockBuilder::new(transactions, topology, Vec::new()) .chain(0, &mut state_block) - .sign(&alice_keys) + .sign(alice_keys.private_key()) .unpack(|_| {}); // The first transaction should be rejected @@ -1069,10 +1190,12 @@ mod tests { // Create genesis block let transactions = vec![tx]; - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, _) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let valid_block = BlockBuilder::new(transactions, topology.clone(), Vec::new()) .chain(0, &mut state_block) - .sign(&KeyPair::random()) + .sign(KeyPair::random().private_key()) .unpack(|_| {}); // Validate genesis block diff --git a/core/src/kura.rs b/core/src/kura.rs index ef36bbdf19e..94cf045b428 100644 --- a/core/src/kura.rs +++ b/core/src/kura.rs @@ -291,8 +291,6 @@ impl Kura { } /// Get a reference to block by height, loading it from disk if needed. - // The below lint suggests changing the code into something that does not compile due - // to the borrow checker. pub fn get_block_by_height(&self, block_height: u64) -> Option> { let mut data_array_guard = self.block_data.lock(); if block_height == 0 || block_height > data_array_guard.len() as u64 { diff --git a/core/src/queue.rs b/core/src/queue.rs index 51e3ad38a5b..09b62b9e180 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -29,8 +29,7 @@ impl AcceptedTransaction { let transaction_signatories = self .as_ref() .signatures() - .iter() - .map(|signature| signature.public_key()) + .map(|signature| &signature.0) .cloned() .collect(); diff --git a/core/src/smartcontracts/isi/query.rs b/core/src/smartcontracts/isi/query.rs index daf7faae917..9eb38215fae 100644 --- a/core/src/smartcontracts/isi/query.rs +++ b/core/src/smartcontracts/isi/query.rs @@ -75,7 +75,7 @@ impl ValidQueryRequest { let account_has_public_key = state_ro .world() .map_account(query.authority(), |account| { - account.contains_signatory(query.signature().public_key()) + account.contains_signatory(&query.signature().0) }) .map_err(Error::from)?; if !account_has_public_key { @@ -190,7 +190,6 @@ mod tests { use iroha_data_model::{ metadata::MetadataValueBox, query::error::FindError, transaction::TransactionLimits, }; - use iroha_primitives::unique_vec::UniqueVec; use once_cell::sync::Lazy; use tokio::test; @@ -312,10 +311,12 @@ mod tests { let mut transactions = vec![valid_tx; valid_tx_per_block]; transactions.append(&mut vec![invalid_tx; invalid_tx_per_block]); - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, _) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let first_block = BlockBuilder::new(transactions.clone(), topology.clone(), Vec::new()) .chain(0, &mut state_block) - .sign(&ALICE_KEYS) + .sign(ALICE_KEYS.private_key()) .unpack(|_| {}) .commit(&topology) .unpack(|_| {}) @@ -327,7 +328,7 @@ mod tests { for _ in 1u64..blocks { let block = BlockBuilder::new(transactions.clone(), topology.clone(), Vec::new()) .chain(0, &mut state_block) - .sign(&ALICE_KEYS) + .sign(ALICE_KEYS.private_key()) .unpack(|_| {}) .commit(&topology) .unpack(|_| {}) @@ -466,10 +467,12 @@ mod tests { let tx_limits = &state_block.transaction_executor().transaction_limits; let va_tx = AcceptedTransaction::accept(tx, &chain_id, tx_limits)?; - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, _) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let vcb = BlockBuilder::new(vec![va_tx.clone()], topology.clone(), Vec::new()) .chain(0, &mut state_block) - .sign(&ALICE_KEYS) + .sign(ALICE_KEYS.private_key()) .unpack(|_| {}) .commit(&topology) .unpack(|_| {}) diff --git a/core/src/state.rs b/core/src/state.rs index 81ec0c79dec..f51484ea7ef 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -1022,7 +1022,7 @@ pub trait StateReadOnly { } /// Return the view change index of the latest block - fn latest_block_view_change_index(&self) -> u64 { + fn latest_block_view_change_index(&self) -> u32 { self.kura() .get_block_by_height(self.height()) .map_or(0, |block| block.header().view_change_index) @@ -1765,7 +1765,6 @@ pub(crate) mod deserialize { #[cfg(test)] mod tests { use iroha_data_model::block::BlockPayload; - use iroha_primitives::unique_vec::UniqueVec; use super::*; use crate::{ @@ -1783,7 +1782,9 @@ mod tests { async fn get_block_hashes_after_hash() { const BLOCK_CNT: usize = 10; - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, _) = iroha_crypto::KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let block = ValidBlock::new_dummy() .commit(&topology) .unpack(|_| {}) @@ -1814,7 +1815,9 @@ mod tests { async fn get_blocks_from_height() { const BLOCK_CNT: usize = 10; - let topology = Topology::new(UniqueVec::new()); + let (peer_public_key, _) = iroha_crypto::KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), peer_public_key); + let topology = Topology::new(vec![peer_id]); let block = ValidBlock::new_dummy() .commit(&topology) .unpack(|_| {}) diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index 49e1c1db8b6..8d4c8ceecea 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -1,7 +1,7 @@ //! The main event loop that powers sumeragi. use std::sync::mpsc; -use iroha_crypto::HashOf; +use iroha_crypto::{HashOf, KeyPair}; use iroha_data_model::{block::*, events::pipeline::PipelineEventBox, peer::PeerId}; use iroha_p2p::UpdateTopology; use tracing::{span, Level}; @@ -41,7 +41,7 @@ pub struct Sumeragi { /// is the proxy tail. pub debug_force_soft_fork: bool, /// The current network topology. - pub current_topology: Topology, + pub topology: Topology, /// In order to *be fast*, we must minimize communication with /// other subsystems where we can. This way the performance of /// sumeragi is more dependent on the code that is internal to the @@ -78,11 +78,11 @@ impl Sumeragi { self.network.post(post); } - #[allow(clippy::needless_pass_by_value, single_use_lifetimes)] // TODO: uncomment when anonymous lifetimes are stable - fn broadcast_packet_to<'peer_id>( + #[allow(clippy::needless_pass_by_value)] + fn broadcast_packet_to<'peer_id, I: IntoIterator + Send>( &self, msg: impl Into, - ids: impl IntoIterator + Send, + ids: I, ) { let msg = msg.into(); @@ -107,7 +107,7 @@ impl Sumeragi { /// Connect or disconnect peers according to the current network topology. fn connect_peers(&self, topology: &Topology) { - let peers = topology.ordered_peers.clone().into_iter().collect(); + let peers = topology.iter().cloned().collect(); self.network.update_topology(UpdateTopology(peers)); } @@ -155,30 +155,28 @@ impl Sumeragi { should_sleep = false; if let Err(error) = view_change_proof_chain.merge( msg.view_change_proofs, - &self.current_topology.ordered_peers, - self.current_topology.max_faults(), + &self.topology, state_view.latest_block_hash(), ) { trace!(%error, "Failed to add proofs into view change proof chain") } - let current_view_change_index = view_change_proof_chain.verify_with_state( - &self.current_topology.ordered_peers, - self.current_topology.max_faults(), + let curr_view_change_index = view_change_proof_chain.verify_with_state( + &self.topology, state_view.latest_block_hash(), - ) as u64; + ); let mut should_prune = false; if let Some(msg) = block_msg.as_ref() { - let vc_index : Option = match msg { + let vc_index = match msg { BlockMessage::BlockCreated(bc) => Some(bc.block.header().view_change_index), // Signed and Committed contain no block. // Block sync updates are exempt from early pruning. BlockMessage::BlockSigned(_) | BlockMessage::BlockCommitted(_) | BlockMessage::BlockSyncUpdate(_) => None, }; if let Some(vc_index) = vc_index { - if vc_index < current_view_change_index { + if (vc_index as usize) < curr_view_change_index { should_prune = true; } } @@ -228,7 +226,7 @@ impl Sumeragi { let mut state_block = state.block(); let block = match ValidBlock::validate( block, - &self.current_topology, + &self.topology, &self.chain_id, genesis_public_key, &mut state_block, @@ -236,7 +234,7 @@ impl Sumeragi { .unpack(|e| self.send_event(e)) .and_then(|block| { block - .commit(&self.current_topology) + .commit(&self.topology) .unpack(|e| self.send_event(e)) .map_err(|(block, error)| (block.into(), error)) }) { @@ -247,7 +245,8 @@ impl Sumeragi { } }; - *state_block.world.trusted_peers_ids = block.as_ref().commit_topology().clone(); + *state_block.world.trusted_peers_ids = + block.as_ref().commit_topology().cloned().collect(); self.commit_block(block, state_block); return Err(EarlyReturn::GenesisBlockReceivedAndCommitted); } @@ -279,15 +278,15 @@ impl Sumeragi { .expect("Genesis invalid"); let mut state_block = state.block(); - let genesis = BlockBuilder::new(transactions, self.current_topology.clone(), vec![]) + let genesis = BlockBuilder::new(transactions, self.topology.clone(), vec![]) .chain(0, &mut state_block) - .sign(&self.key_pair) + .sign(self.key_pair.private_key()) .unpack(|e| self.send_event(e)); let genesis_msg = BlockCreated::from(genesis.clone()); let genesis = genesis - .commit(&self.current_topology) + .commit(&self.topology) .unpack(|e| self.send_event(e)) .expect("Genesis invalid"); @@ -297,7 +296,7 @@ impl Sumeragi { ); info!( - role = ?self.current_topology.role(&self.peer_id), + role = ?self.topology.role(&self.peer_id), block_hash = %genesis.as_ref().hash(), "Genesis block created", ); @@ -321,7 +320,7 @@ impl Sumeragi { ) { info!( addr=%self.peer_id.address, - role=%self.current_topology.role(&self.peer_id), + role=%self.topology.role(&self.peer_id), block_height=%block.as_ref().header().height, block_hash=%block.as_ref().hash(), "{}", Strategy::LOG_MESSAGE, @@ -329,26 +328,18 @@ impl Sumeragi { let state_events = state_block.apply_without_execution(&block); - let new_topology = Topology::recreate_topology( - block.as_ref(), - 0, - state_block.world.peers().cloned().collect(), - ); - - // https://github.com/hyperledger/iroha/issues/3396 - // Kura should store the block only upon successful application to the internal state to avoid storing a corrupted block. - // Public-facing state update should happen after that and be followed by `BlockCommited` event to prevent client access to uncommitted data. - Strategy::kura_store_block(&self.kura, block); - // Parameters are updated before updating public copy of sumeragi self.update_params(&state_block); self.cache_transaction(&state_block); - self.current_topology = new_topology; - self.connect_peers(&self.current_topology); + self.topology + .block_committed(block.as_ref(), state_block.world.peers().cloned()); + self.connect_peers(&self.topology); // Commit new block making it's effect visible for the rest of application state_block.commit(); + Strategy::kura_store_block(&self.kura, block); + // NOTE: This sends "Block committed" event, // so it should be done AFTER public facing state update state_events.into_iter().for_each(|e| self.send_event(e)); @@ -386,11 +377,11 @@ impl Sumeragi { ) -> Option> { let block_hash = block.hash(); let addr = &self.peer_id.address; - let role = self.current_topology.role(&self.peer_id); + let role = self.topology.role(&self.peer_id); trace!(%addr, %role, block=%block_hash, "Block received, voting..."); let mut state_block = state.block(); - let block = match ValidBlock::validate( + let mut block = match ValidBlock::validate( block, topology, &self.chain_id, @@ -406,21 +397,21 @@ impl Sumeragi { } }; - let signed_block = block.sign(&self.key_pair); - Some(VotingBlock::new(signed_block, state_block)) + // NOTE: Proxy tail has to be the last to sign the block + if self.topology.role(&self.peer_id) == Role::ProxyTail { + block.sign(&self.key_pair, topology); + }; + + Some(VotingBlock::new(block, state_block)) } fn prune_view_change_proofs_and_calculate_current_index( &self, state_view: &StateView<'_>, view_change_proof_chain: &mut ProofChain, - ) -> u64 { + ) -> usize { view_change_proof_chain.prune(state_view.latest_block_hash()); - view_change_proof_chain.verify_with_state( - &self.current_topology.ordered_peers, - self.current_topology.max_faults(), - state_view.latest_block_hash(), - ) as u64 + view_change_proof_chain.verify_with_state(&self.topology, state_view.latest_block_hash()) } #[allow(clippy::too_many_lines)] @@ -429,13 +420,14 @@ impl Sumeragi { message: BlockMessage, state: &'state State, voting_block: &mut Option>, - current_view_change_index: u64, + curr_view_change_index: usize, genesis_public_key: &PublicKey, - voting_signatures: &mut Vec>, + voting_signatures: &mut Vec, + #[cfg_attr(not(debug_assertions), allow(unused_variables))] is_genesis_peer: bool, ) { - let current_topology = &self.current_topology; - let role = current_topology.role(&self.peer_id); let addr = &self.peer_id.address; + let topology = &self.topology; + let role = topology.role(&self.peer_id); #[allow(clippy::suspicious_operation_groupings)] match (message, role) { @@ -444,8 +436,9 @@ impl Sumeragi { info!(%addr, %role, block=%block_hash, "Block sync update received"); // Release writer before handling block sync - let _ = voting_block.take(); - match handle_block_sync(&self.chain_id, genesis_public_key, block, state, &|e| { + let curr_block = voting_block.take(); + + match handle_block_sync(&self.chain_id, block, state, genesis_public_key, &|e| { self.send_event(e) }) { Ok(BlockSyncOk::CommitBlock(block, state_block)) => { @@ -494,147 +487,151 @@ impl Sumeragi { warn!(%addr, %role, %block_hash, %block_height, %peer_height, "Other peer send irrelevant or outdated block to the peer (it's neither `peer_height` nor `peer_height + 1`).") } } + + *voting_block = curr_block; } ( - BlockMessage::BlockCommitted(BlockCommitted { hash, signatures }), - Role::Leader | Role::ValidatingPeer | Role::ProxyTail | Role::ObservingPeer, + BlockMessage::BlockCommitted(BlockCommitted { signatures }), + Role::Leader | Role::ValidatingPeer | Role::ObservingPeer | Role::ProxyTail, ) => { - let is_consensus_required = current_topology.is_consensus_required().is_some(); - if role == Role::ProxyTail && is_consensus_required - || role == Role::Leader && !is_consensus_required - { + if role == Role::Leader && topology.is_consensus_required().is_none() { error!(%addr, %role, "Received BlockCommitted message, but shouldn't"); - } else if let Some(voted_block) = voting_block.take() { + } else if let Some(mut voted_block) = voting_block.take() { match voted_block .block - .commit_with_signatures(current_topology, signatures, hash) + // NOTE: The manipulation of the topology relies upon all peers seeing the same signature set. + // Therefore we must clear the signatures and accept what the proxy tail giveth. + .replace_signatures(signatures, topology) .unpack(|e| self.send_event(e)) { - Ok(committed_block) => { - self.commit_block(committed_block, voted_block.state_block) - } - Err(( - valid_block, - BlockValidationError::IncorrectHash { expected, actual }, - )) => { - error!(%addr, %role, %expected, %actual, "The hash of the committed block does not match the hash of the block stored by the peer."); - - *voting_block = Some(VotingBlock { - voted_at: voted_block.voted_at, - block: valid_block, - state_block: voted_block.state_block, - }); + Ok(prev_signatures) => { + match voted_block + .block + .commit(topology) + .unpack(|e| self.send_event(e)) + { + Ok(committed_block) => { + self.commit_block(committed_block, voted_block.state_block) + } + Err((mut block, error)) => { + error!(%addr, %role, ?error, "Block failed to be committed"); + + block + .replace_signatures(prev_signatures, topology) + .unpack(|e| self.send_event(e)) + .expect("Can't fail"); + voted_block.block = block; + } + } } - Err((_, error)) => { - error!(%addr, %role, %hash, ?error, "Block failed to be committed") + Err(error) => { + error!(%addr, %role, ?error, "Received incorrect signatures"); } } } else { - error!(%addr, %role, %hash, "Peer missing voting block") + error!(%addr, %role, "Peer missing voting block") } } (BlockMessage::BlockCreated(block_created), Role::ValidatingPeer) => { - let current_topology = current_topology + let topology = topology .is_consensus_required() .expect("Peer has `ValidatingPeer` role, which mean that current topology require consensus"); // Release block writer before creating a new one - let _ = voting_block.take(); - if let Some(v_block) = - self.vote_for_block(state, ¤t_topology, genesis_public_key, block_created) + let curr_block = voting_block.take(); + + *voting_block = if let Some(v_block) = + self.vote_for_block(state, &topology, genesis_public_key, block_created) { let block_hash = v_block.block.as_ref().hash(); let msg = BlockSigned::from(&v_block.block); - self.broadcast_packet_to(msg, [current_topology.proxy_tail()]); + self.broadcast_packet_to(msg, [topology.proxy_tail()]); info!(%addr, block=%block_hash, "Block validated, signed and forwarded"); - *voting_block = Some(v_block); - } + Some(v_block) + } else { + curr_block + }; } (BlockMessage::BlockCreated(BlockCreated { block }), Role::ObservingPeer) => { - let current_topology = current_topology.is_consensus_required().expect( + let current_topology = topology.is_consensus_required().expect( "Peer has `ObservingPeer` role, which mean that current topology require consensus" ); - // Release block writer before creating new one - let _ = voting_block.take(); + let block_hash = block.hash_of_payload(); + let role = self.topology.role(&self.peer_id); + trace!(%addr, %role, %block_hash, "Block received, voting..."); - let v_block = { - let block_hash = block.hash_of_payload(); - let role = self.current_topology.role(&self.peer_id); - trace!(%addr, %role, %block_hash, "Block received, voting..."); + let mut state_block = state.block(); - let mut state_block = state.block(); - match ValidBlock::validate( - block, - ¤t_topology, - &self.chain_id, - genesis_public_key, - &mut state_block, - ) - .unpack(|e| self.send_event(e)) - { - Ok(block) => { - let block = if current_view_change_index >= 1 { - block.sign(&self.key_pair) - } else { - block - }; - - Some(VotingBlock::new(block, state_block)) - } - Err((_, error)) => { - warn!(%addr, %role, ?error, "Block validation failed"); - None + // Release block writer before creating new one + let curr_block = voting_block.take(); + *voting_block = match ValidBlock::validate( + block, + topology, + &self.chain_id, + genesis_public_key, + &mut state_block, + ) + .unpack(|e| self.send_event(e)) + { + Ok(mut block) => { + if curr_view_change_index >= 1 { + block.sign(&self.key_pair, topology); + }; + + let new_block = VotingBlock::new(block, state_block); + let block_hash = new_block.block.as_ref().hash(); + info!(%addr, %block_hash, "Block validated"); + if curr_view_change_index >= 1 { + self.broadcast_packet_to( + BlockSigned::from(&new_block.block), + [current_topology.proxy_tail()], + ); + info!(%addr, block=%block_hash, "Block signed and forwarded"); } + Some(new_block) } - }; - - if let Some(v_block) = v_block { - let block_hash = v_block.block.as_ref().hash(); - info!(%addr, %block_hash, "Block validated"); - if current_view_change_index >= 1 { - self.broadcast_packet_to( - BlockSigned::from(&v_block.block), - [current_topology.proxy_tail()], - ); - info!(%addr, block=%block_hash, "Block signed and forwarded"); + Err((_, error)) => { + warn!(%addr, %role, ?error, "Block validation failed"); + curr_block } - *voting_block = Some(v_block); - } + }; } (BlockMessage::BlockCreated(block_created), Role::ProxyTail) => { + info!(%addr, %role, block=%block_created.block.hash(), "Block received"); + // Release block writer before creating new one - let _ = voting_block.take(); - if let Some(mut new_block) = - self.vote_for_block(state, current_topology, genesis_public_key, block_created) - { - // NOTE: Up until this point it was unknown which block is expected to be received, - // therefore all the signatures (of any hash) were collected and will now be pruned - add_signatures::(&mut new_block, voting_signatures.drain(..)); - *voting_block = Some(new_block); - } + let prev_voting_block = voting_block.take(); + + *voting_block = self + .vote_for_block(state, topology, genesis_public_key, block_created) + .map_or(prev_voting_block, |mut new_block| { + // NOTE: Up until this point it was unknown which block is expected to be received, + // therefore all the signatures (of any hash) were collected and will now be pruned + add_signatures::( + &mut new_block, + voting_signatures.drain(..), + &self.topology, + ); + + self.try_commit_block(new_block, is_genesis_peer) + }); } - (BlockMessage::BlockSigned(BlockSigned { hash, signatures }), Role::ProxyTail) => { - trace!(block_hash=%hash, "Received block signatures"); + (BlockMessage::BlockSigned(BlockSigned { signatures }), Role::ProxyTail) => { + info!(%addr, %role, "Received block signatures"); - let roles: &[Role] = if current_view_change_index >= 1 { + let roles: &[Role] = if curr_view_change_index >= 1 { &[Role::ValidatingPeer, Role::ObservingPeer] } else { &[Role::ValidatingPeer] }; - let valid_signatures = - current_topology.filter_signatures_by_roles(roles, &signatures); + let valid_signatures = topology.filter_signatures_by_roles(roles, &signatures); - if let Some(voted_block) = voting_block.as_mut() { - let voting_block_hash = voted_block.block.as_ref().hash_of_payload(); - - if hash == voting_block_hash { - add_signatures::(voted_block, valid_signatures); - } else { - debug!(%voting_block_hash, "Received signatures are not for the current block"); - } + if let Some(mut voted_block) = voting_block.take() { + add_signatures::(&mut voted_block, valid_signatures, topology); + *voting_block = self.try_commit_block(voted_block, is_genesis_peer); } else { // NOTE: Due to the nature of distributed systems, signatures can sometimes be received before // the block (sent by the leader). Collect the signatures and wait for the block to be received @@ -647,107 +644,100 @@ impl Sumeragi { } } + /// Commits block if there are enough votes + fn try_commit_block<'state>( + &mut self, + mut voted_block: VotingBlock<'state>, + #[cfg_attr(not(debug_assertions), allow(unused_variables))] is_genesis_peer: bool, + ) -> Option> { + let topology = &self.topology; + assert_eq!(topology.role(&self.peer_id), Role::ProxyTail); + let votes_count = voted_block.block.as_ref().signatures().len(); + if votes_count + 1 >= topology.min_votes_for_commit() { + info!(block=%voted_block.block.as_ref().hash(), "Block reached required number of votes"); + voted_block.block.sign(&self.key_pair, topology); + + let committed_block = voted_block + .block + .commit(topology) + .unpack(|e| self.send_event(e)) + .unwrap(); + + let msg = BlockCommitted::from(&committed_block); + + #[cfg(debug_assertions)] + if is_genesis_peer && self.debug_force_soft_fork { + std::thread::sleep(self.pipeline_time() * 2); + } else { + self.broadcast_packet(msg); + } + + #[cfg(not(debug_assertions))] + { + self.broadcast_packet(msg); + } + + let state_block = voted_block.state_block; + self.commit_block(committed_block, state_block); + + return None; + } + + Some(voted_block) + } + #[allow(clippy::too_many_lines)] fn process_message_independent<'state>( &mut self, state: &'state State, voting_block: &mut Option>, - current_view_change_index: u64, + view_change_index: usize, round_start_time: &Instant, - #[cfg_attr(not(debug_assertions), allow(unused_variables))] is_genesis_peer: bool, ) { - let current_topology = &self.current_topology; - let role = current_topology.role(&self.peer_id); let addr = &self.peer_id.address; - - match role { - Role::Leader => { - if voting_block.is_none() { - let cache_full = self.transaction_cache.len() >= self.max_txs_in_block; - let deadline_reached = round_start_time.elapsed() > self.block_time; - let cache_non_empty = !self.transaction_cache.is_empty(); - - if cache_full || (deadline_reached && cache_non_empty) { - let transactions = self.transaction_cache.clone(); - info!(%addr, txns=%transactions.len(), "Creating block..."); - let create_block_start_time = Instant::now(); - - // TODO: properly process triggers! - let mut state_block = state.block(); - let event_recommendations = Vec::new(); - let new_block = BlockBuilder::new( - transactions, - self.current_topology.clone(), - event_recommendations, - ) - .chain(current_view_change_index, &mut state_block) - .sign(&self.key_pair) + let topology = &self.topology; + let role = topology.role(&self.peer_id); + + if role == Role::Leader && voting_block.is_none() { + let cache_full = self.transaction_cache.len() >= self.max_txs_in_block; + let deadline_reached = round_start_time.elapsed() > self.block_time; + let cache_non_empty = !self.transaction_cache.is_empty(); + + if cache_full || (deadline_reached && cache_non_empty) { + let transactions = self.transaction_cache.clone(); + info!(%addr, txns=%transactions.len(), "Creating block..."); + let create_block_start_time = Instant::now(); + + // TODO: properly process triggers! + let mut state_block = state.block(); + let event_recommendations = Vec::new(); + let new_block = + BlockBuilder::new(transactions, self.topology.clone(), event_recommendations) + .chain(view_change_index, &mut state_block) + .sign(self.key_pair.private_key()) .unpack(|e| self.send_event(e)); - let created_in = create_block_start_time.elapsed(); - if current_topology.is_consensus_required().is_some() { - info!(%addr, created_in_ms=%created_in.as_millis(), block=%new_block.as_ref().hash(), "Block created"); - - if created_in > self.pipeline_time() / 2 { - warn!("Creating block takes too much time. This might prevent consensus from operating. Consider increasing `commit_time` or decreasing `max_transactions_in_block`"); - } - *voting_block = Some(VotingBlock::new(new_block.clone(), state_block)); + let created_in = create_block_start_time.elapsed(); + if topology.is_consensus_required().is_some() { + info!(%addr, created_in_ms=%created_in.as_millis(), block=%new_block.as_ref().hash(), "Block created"); - let msg = BlockCreated::from(new_block); - self.broadcast_packet(msg); - } else { - match new_block - .commit(current_topology) - .unpack(|e| self.send_event(e)) - { - Ok(committed_block) => { - self.broadcast_packet(BlockCommitted::from(&committed_block)); - self.commit_block(committed_block, state_block); - } - Err(error) => error!(%addr, role=%Role::Leader, ?error), - }; - } + if created_in > self.pipeline_time() / 2 { + warn!("Creating block takes too much time. This might prevent consensus from operating. Consider increasing `commit_time` or decreasing `max_transactions_in_block`"); } - } - } - Role::ProxyTail => { - if let Some(voted_block) = voting_block.take() { - let voted_at = voted_block.voted_at; - let state_block = voted_block.state_block; + *voting_block = Some(VotingBlock::new(new_block.clone(), state_block)); - match voted_block - .block - .commit(current_topology) - .unpack(|e| self.send_event(e)) - { + let msg = BlockCreated::from(new_block); + self.broadcast_packet(msg); + } else { + match new_block.commit(topology).unpack(|e| self.send_event(e)) { Ok(committed_block) => { - info!(block=%committed_block.as_ref().hash(), "Block reached required number of votes"); - - let msg = BlockCommitted::from(&committed_block); - - #[cfg(debug_assertions)] - if is_genesis_peer && self.debug_force_soft_fork { - std::thread::sleep(self.pipeline_time() * 2); - } else { - self.broadcast_packet(msg); - } - - #[cfg(not(debug_assertions))] - { - self.broadcast_packet(msg); - } + self.broadcast_packet(BlockCommitted::from(&committed_block)); self.commit_block(committed_block, state_block); } - Err((block, error)) => { - // Restore the current voting block and continue the round - *voting_block = - Some(VotingBlock::voted_at(block, state_block, voted_at)); - trace!(?error, "Not enough signatures, waiting for more..."); - } - } + Err(error) => error!(%addr, role=%Role::Leader, ?error), + }; } } - _ => {} } } } @@ -756,49 +746,43 @@ impl Sumeragi { fn reset_state( peer_id: &PeerId, pipeline_time: Duration, - current_view_change_index: u64, - old_view_change_index: &mut u64, + curr_view_change_index: usize, + prev_view_change_index: &mut usize, old_latest_block_hash: &mut HashOf, latest_block: &SignedBlock, - // below is the state that gets reset. - current_topology: &mut Topology, + topology: &mut Topology, voting_block: &mut Option, - voting_signatures: &mut Vec>, + voting_signatures: &mut Vec, round_start_time: &mut Instant, last_view_change_time: &mut Instant, view_change_time: &mut Duration, ) { let mut was_commit_or_view_change = false; - let current_latest_block_hash = latest_block.hash(); - if current_latest_block_hash != *old_latest_block_hash { + let curr_latest_block_hash = latest_block.hash(); + if curr_latest_block_hash != *old_latest_block_hash { // Round is only restarted on a block commit, so that in the case of // a view change a new block is immediately created by the leader *round_start_time = Instant::now(); was_commit_or_view_change = true; - *old_view_change_index = 0; + *prev_view_change_index = 0; } - if *old_view_change_index < current_view_change_index { + if *prev_view_change_index < curr_view_change_index { error!(addr=%peer_id.address, "Rotating the entire topology."); - *old_view_change_index = current_view_change_index; + topology.rotate_all_n(curr_view_change_index - *prev_view_change_index); + *prev_view_change_index = curr_view_change_index; was_commit_or_view_change = true; } // Reset state for the next round. if was_commit_or_view_change { - *old_latest_block_hash = current_latest_block_hash; - - *current_topology = Topology::recreate_topology( - latest_block, - current_view_change_index, - current_topology.ordered_peers.iter().cloned().collect(), - ); + *old_latest_block_hash = curr_latest_block_hash; *voting_block = None; voting_signatures.clear(); *last_view_change_time = Instant::now(); *view_change_time = pipeline_time; - info!(addr=%peer_id.address, role=%current_topology.role(peer_id), %current_view_change_index, "View change updated"); + info!(addr=%peer_id.address, role=%topology.role(peer_id), %curr_view_change_index, "View change updated"); } } @@ -823,7 +807,7 @@ pub(crate) fn run( state: Arc, ) { // Connect peers with initial topology - sumeragi.connect_peers(&sumeragi.current_topology); + sumeragi.connect_peers(&sumeragi.topology); let span = span!(tracing::Level::TRACE, "genesis").entered(); let is_genesis_peer = if state.view().height() == 0 @@ -849,7 +833,7 @@ pub(crate) fn run( info!( addr=%sumeragi.peer_id.address, - role_in_next_round=%sumeragi.current_topology.role(&sumeragi.peer_id), + role_in_next_round=%sumeragi.topology.role(&sumeragi.peer_id), "Sumeragi initialized", ); @@ -858,7 +842,7 @@ pub(crate) fn run( let mut voting_signatures = Vec::new(); let mut should_sleep = false; let mut view_change_proof_chain = ProofChain::default(); - let mut old_view_change_index = 0; + let mut prev_view_change_index = 0; let mut old_latest_block_hash = state .view() .latest_block_ref() @@ -899,29 +883,30 @@ pub(crate) fn run( &mut sumeragi.transaction_cache, ); - let current_view_change_index = sumeragi - .prune_view_change_proofs_and_calculate_current_index( - &state_view, - &mut view_change_proof_chain, - ); + let curr_view_change_index = sumeragi.prune_view_change_proofs_and_calculate_current_index( + &state_view, + &mut view_change_proof_chain, + ); reset_state( &sumeragi.peer_id, sumeragi.pipeline_time(), - current_view_change_index, - &mut old_view_change_index, + curr_view_change_index, + &mut prev_view_change_index, &mut old_latest_block_hash, &state_view .latest_block_ref() .expect("state must have blocks"), - &mut sumeragi.current_topology, + &mut sumeragi.topology, &mut voting_block, &mut voting_signatures, &mut round_start_time, &mut last_view_change_time, &mut view_change_time, ); - sumeragi.view_changes_metric.set(old_view_change_index); + sumeragi + .view_changes_metric + .set(prev_view_change_index as u64); if let Some(message) = { let (msg, sleep) = @@ -933,26 +918,26 @@ pub(crate) fn run( message, &state, &mut voting_block, - current_view_change_index, + curr_view_change_index, &genesis_network.public_key, &mut voting_signatures, + is_genesis_peer, ); } // State could be changed after handling message so it is necessary to reset state before handling message independent step let state_view = state.view(); - let current_view_change_index = sumeragi - .prune_view_change_proofs_and_calculate_current_index( - &state_view, - &mut view_change_proof_chain, - ); + let curr_view_change_index = sumeragi.prune_view_change_proofs_and_calculate_current_index( + &state_view, + &mut view_change_proof_chain, + ); // We broadcast our view change suggestion after having processed the latest from others inside `receive_network_packet` let node_expects_block = !sumeragi.transaction_cache.is_empty(); - if (node_expects_block || current_view_change_index > 0) + if (node_expects_block || curr_view_change_index > 0) && last_view_change_time.elapsed() > view_change_time { - let role = sumeragi.current_topology.role(&sumeragi.peer_id); + let role = sumeragi.topology.role(&sumeragi.peer_id); if node_expects_block { if let Some(VotingBlock { block, .. }) = voting_block.as_ref() { @@ -964,16 +949,21 @@ pub(crate) fn run( warn!(peer_public_key=%sumeragi.peer_id.public_key, %role, "No block produced in due time, requesting view change..."); } - let suspect_proof = - ProofBuilder::new(state_view.latest_block_hash(), current_view_change_index) - .sign(&sumeragi.key_pair); + let suspect_proof = { + let signatory_idx = sumeragi + .topology + .position(sumeragi.peer_id.public_key()) + .expect("BUG: Node is not part of consensus"); + + ProofBuilder::new(state_view.latest_block_hash(), curr_view_change_index) + .sign(signatory_idx, sumeragi.key_pair.private_key()) + }; view_change_proof_chain .insert_proof( - &sumeragi.current_topology.ordered_peers, - sumeragi.current_topology.max_faults(), - state_view.latest_block_hash(), suspect_proof, + &sumeragi.topology, + state_view.latest_block_hash(), ) .unwrap_or_else(|err| error!("{err}")); } @@ -989,37 +979,39 @@ pub(crate) fn run( reset_state( &sumeragi.peer_id, sumeragi.pipeline_time(), - current_view_change_index, - &mut old_view_change_index, + curr_view_change_index, + &mut prev_view_change_index, &mut old_latest_block_hash, &state_view .latest_block_ref() .expect("state must have blocks"), - &mut sumeragi.current_topology, + &mut sumeragi.topology, &mut voting_block, &mut voting_signatures, &mut round_start_time, &mut last_view_change_time, &mut view_change_time, ); - sumeragi.view_changes_metric.set(old_view_change_index); + sumeragi + .view_changes_metric + .set(prev_view_change_index as u64); sumeragi.process_message_independent( &state, &mut voting_block, - current_view_change_index, + curr_view_change_index, &round_start_time, - is_genesis_peer, ); } } fn add_signatures( block: &mut VotingBlock, - signatures: impl IntoIterator>, + signatures: impl IntoIterator, + topology: &Topology, ) { for signature in signatures { - if let Err(error) = block.block.add_signature(signature) { + if let Err(error) = block.block.add_signature(signature, topology) { let err_msg = "Signature not valid"; if EXPECT_VALID { @@ -1101,8 +1093,8 @@ enum BlockSyncError { BlockNotValid(BlockValidationError), SoftForkBlockNotValid(BlockValidationError), SoftForkBlockSmallViewChangeIndex { - peer_view_change_index: u64, - block_view_change_index: u64, + peer_view_change_index: u32, + block_view_change_index: u32, }, BlockNotProperHeight { peer_height: u64, @@ -1112,9 +1104,9 @@ enum BlockSyncError { fn handle_block_sync<'state, F: Fn(PipelineEventBox)>( chain_id: &ChainId, - genesis_public_key: &PublicKey, block: SignedBlock, state: &'state State, + genesis_public_key: &PublicKey, handle_events: &F, ) -> Result, (SignedBlock, BlockSyncError)> { let block_height = block.header().height; @@ -1126,9 +1118,10 @@ fn handle_block_sync<'state, F: Fn(PipelineEventBox)>( let last_committed_block = state_block .latest_block_ref() .expect("Not in genesis round so must have at least genesis block"); - let new_peers = state_block.world.peers().cloned().collect(); - let view_change_index = block.header().view_change_index; - Topology::recreate_topology(&last_committed_block, view_change_index, new_peers) + let mut topology = Topology::new(last_committed_block.commit_topology().cloned()); + topology.block_committed(&last_committed_block, state_block.world.peers().cloned()); + topology.rotate_all_n(block.header().view_change_index as usize); + topology }; ValidBlock::validate( block, @@ -1167,9 +1160,10 @@ fn handle_block_sync<'state, F: Fn(PipelineEventBox)>( let last_committed_block = state_block .latest_block_ref() .expect("Not in genesis round so must have at least genesis block"); - let new_peers = state_block.world.peers().cloned().collect(); - let view_change_index = block.header().view_change_index; - Topology::recreate_topology(&last_committed_block, view_change_index, new_peers) + let mut topology = Topology::new(last_committed_block.commit_topology().cloned()); + topology.block_committed(&last_committed_block, state_block.world.peers().cloned()); + topology.rotate_all_n(block.header().view_change_index as usize); + topology }; ValidBlock::validate( block, @@ -1201,7 +1195,6 @@ fn handle_block_sync<'state, F: Fn(PipelineEventBox)>( #[cfg(test)] mod tests { - use iroha_primitives::{unique_vec, unique_vec::UniqueVec}; use tokio::test; use super::*; @@ -1216,7 +1209,7 @@ mod tests { fn create_data_for_test( chain_id: &ChainId, topology: &Topology, - leader_key_pair: &KeyPair, + leader_private_key: &PrivateKey, tx_signer_key_pair: &KeyPair, ) -> (State, Arc, SignedBlock) { // Predefined world state @@ -1226,7 +1219,7 @@ mod tests { let domain_id = "wonderland".parse().expect("Valid"); let mut domain = Domain::new(domain_id).build(&alice_id); assert!(domain.add_account(account).is_none()); - let world = World::with([domain], topology.ordered_peers.clone()); + let world = World::with([domain], topology.iter().cloned().collect()); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); let state = State::new(world, Arc::clone(&kura), query_handle); @@ -1250,7 +1243,7 @@ mod tests { // Creating a block of two identical transactions and validating it let block = BlockBuilder::new(vec![tx.clone(), tx], topology.clone(), Vec::new()) .chain(0, &mut state_block) - .sign(leader_key_pair) + .sign(leader_private_key) .unpack(|_| {}); let genesis = block @@ -1295,7 +1288,7 @@ mod tests { // Creating a block of two identical transactions and validating it BlockBuilder::new(vec![tx1, tx2], topology.clone(), Vec::new()) .chain(0, &mut state_block) - .sign(leader_key_pair) + .sign(leader_private_key) .unpack(|_| {}) }; @@ -1308,22 +1301,24 @@ mod tests { let chain_id = ChainId::from("0"); let tx_signer_key_pair = KeyPair::random(); - let leader_key_pair = KeyPair::random(); - let topology = Topology::new(unique_vec![PeerId::new( - "127.0.0.1:8080".parse().unwrap(), - leader_key_pair.public_key().clone(), - )]); - let (state, _, mut block) = - create_data_for_test(&chain_id, &topology, &leader_key_pair, &tx_signer_key_pair); + let (leader_public_key, leader_private_key) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), leader_public_key); + let topology = Topology::new(vec![peer_id]); + let (state, _, mut block) = create_data_for_test( + &chain_id, + &topology, + &leader_private_key, + &tx_signer_key_pair, + ); // Malform block to make it invalid payload_mut(&mut block).commit_topology.clear(); let result = handle_block_sync( &chain_id, - tx_signer_key_pair.public_key(), block, &state, + tx_signer_key_pair.public_key(), &|_| {}, ); assert!(matches!(result, Err((_, BlockSyncError::BlockNotValid(_))))) @@ -1334,13 +1329,15 @@ mod tests { let chain_id = ChainId::from("0"); let tx_signer_key_pair = KeyPair::random(); - let leader_key_pair = KeyPair::random(); - let topology = Topology::new(unique_vec![PeerId::new( - "127.0.0.1:8080".parse().unwrap(), - leader_key_pair.public_key().clone(), - )]); - let (state, kura, mut block) = - create_data_for_test(&chain_id, &topology, &leader_key_pair, &tx_signer_key_pair); + let (leader_public_key, leader_private_key) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), leader_public_key); + let topology = Topology::new(vec![peer_id]); + let (state, kura, mut block) = create_data_for_test( + &chain_id, + &topology, + &leader_private_key, + &tx_signer_key_pair, + ); let mut state_block = state.block(); let committed_block = ValidBlock::validate( @@ -1365,9 +1362,9 @@ mod tests { let result = handle_block_sync( &chain_id, - tx_signer_key_pair.public_key(), block, &state, + tx_signer_key_pair.public_key(), &|_| {}, ); assert!(matches!( @@ -1382,19 +1379,24 @@ mod tests { let chain_id = ChainId::from("0"); let tx_signer_key_pair = KeyPair::random(); - let topology = Topology::new(UniqueVec::new()); - let leader_key_pair = KeyPair::random(); - let (state, _, mut block) = - create_data_for_test(&chain_id, &topology, &leader_key_pair, &tx_signer_key_pair); + let (leader_public_key, leader_private_key) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), leader_public_key); + let topology = Topology::new(vec![peer_id]); + let (state, _, mut block) = create_data_for_test( + &chain_id, + &topology, + &leader_private_key, + &tx_signer_key_pair, + ); // Change block height payload_mut(&mut block).header.height = 42; let result = handle_block_sync( &chain_id, - tx_signer_key_pair.public_key(), block, &state, + tx_signer_key_pair.public_key(), &|_| {}, ); assert!(matches!( @@ -1415,18 +1417,20 @@ mod tests { let chain_id = ChainId::from("0"); let tx_signer_key_pair = KeyPair::random(); - let leader_key_pair = KeyPair::random(); - let topology = Topology::new(unique_vec![PeerId::new( - "127.0.0.1:8080".parse().unwrap(), - leader_key_pair.public_key().clone(), - )]); - let (state, _, block) = - create_data_for_test(&chain_id, &topology, &leader_key_pair, &tx_signer_key_pair); + let (leader_public_key, leader_private_key) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), leader_public_key); + let topology = Topology::new(vec![peer_id]); + let (state, _, block) = create_data_for_test( + &chain_id, + &topology, + &leader_private_key, + &tx_signer_key_pair, + ); let result = handle_block_sync( &chain_id, - tx_signer_key_pair.public_key(), block, &state, + tx_signer_key_pair.public_key(), &|_| {}, ); assert!(matches!(result, Ok(BlockSyncOk::CommitBlock(_, _)))) @@ -1437,13 +1441,15 @@ mod tests { let chain_id = ChainId::from("0"); let tx_signer_key_pair = KeyPair::random(); - let leader_key_pair = KeyPair::random(); - let topology = Topology::new(unique_vec![PeerId::new( - "127.0.0.1:8080".parse().unwrap(), - leader_key_pair.public_key().clone(), - )]); - let (state, kura, mut block) = - create_data_for_test(&chain_id, &topology, &leader_key_pair, &tx_signer_key_pair); + let (leader_public_key, leader_private_key) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), leader_public_key); + let topology = Topology::new(vec![peer_id]); + let (state, kura, mut block) = create_data_for_test( + &chain_id, + &topology, + &leader_private_key, + &tx_signer_key_pair, + ); let mut state_block = state.block(); let committed_block = ValidBlock::validate( @@ -1469,9 +1475,9 @@ mod tests { let result = handle_block_sync( &chain_id, - tx_signer_key_pair.public_key(), block, &state, + tx_signer_key_pair.public_key(), &|_| {}, ); assert!(matches!(result, Ok(BlockSyncOk::ReplaceTopBlock(_, _)))) @@ -1482,13 +1488,15 @@ mod tests { let chain_id = ChainId::from("0"); let tx_signer_key_pair = KeyPair::random(); - let leader_key_pair = KeyPair::random(); - let topology = Topology::new(unique_vec![PeerId::new( - "127.0.0.1:8080".parse().unwrap(), - leader_key_pair.public_key().clone(), - )]); - let (state, kura, mut block) = - create_data_for_test(&chain_id, &topology, &leader_key_pair, &tx_signer_key_pair); + let (leader_public_key, leader_private_key) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), leader_public_key); + let topology = Topology::new(vec![peer_id]); + let (state, kura, mut block) = create_data_for_test( + &chain_id, + &topology, + &leader_private_key, + &tx_signer_key_pair, + ); // Increase block view change index payload_mut(&mut block).header.view_change_index = 42; @@ -1516,9 +1524,9 @@ mod tests { let result = handle_block_sync( &chain_id, - tx_signer_key_pair.public_key(), block, &state, + tx_signer_key_pair.public_key(), &|_| {}, ); assert!(matches!( @@ -1539,10 +1547,15 @@ mod tests { let chain_id = ChainId::from("0"); let tx_signer_key_pair = KeyPair::random(); - let topology = Topology::new(UniqueVec::new()); - let leader_key_pair = KeyPair::random(); - let (state, _, mut block) = - create_data_for_test(&chain_id, &topology, &leader_key_pair, &tx_signer_key_pair); + let (leader_public_key, leader_private_key) = KeyPair::random().into_parts(); + let peer_id = PeerId::new("127.0.0.1:8080".parse().unwrap(), leader_public_key); + let topology = Topology::new(vec![peer_id]); + let (state, _, mut block) = create_data_for_test( + &chain_id, + &topology, + &leader_private_key, + &tx_signer_key_pair, + ); // Change block height and view change index // Soft-fork on genesis block is not possible @@ -1551,9 +1564,9 @@ mod tests { let result = handle_block_sync( &chain_id, - tx_signer_key_pair.public_key(), block, &state, + tx_signer_key_pair.public_key(), &|_| {}, ); assert!(matches!( diff --git a/core/src/sumeragi/message.rs b/core/src/sumeragi/message.rs index c5d4fa27fa7..bcbe44fbd85 100644 --- a/core/src/sumeragi/message.rs +++ b/core/src/sumeragi/message.rs @@ -1,6 +1,5 @@ //! Contains message structures for p2p communication during consensus. -use iroha_crypto::{HashOf, SignaturesOf}; -use iroha_data_model::block::{BlockPayload, SignedBlock}; +use iroha_data_model::block::{BlockSignature, SignedBlock}; use iroha_macro::*; use parity_scale_codec::{Decode, Encode}; @@ -56,20 +55,14 @@ impl From for BlockCreated { #[derive(Debug, Clone, Decode, Encode)] #[non_exhaustive] pub struct BlockSigned { - /// Hash of the block being signed. - pub hash: HashOf, /// Set of signatures. - pub signatures: SignaturesOf, + pub signatures: Vec, } impl From<&ValidBlock> for BlockSigned { fn from(block: &ValidBlock) -> Self { - let block_hash = block.as_ref().hash_of_payload(); - let block_signatures = block.as_ref().signatures().clone(); - Self { - hash: block_hash, - signatures: block_signatures, + signatures: block.as_ref().signatures().cloned().collect(), } } } @@ -78,20 +71,14 @@ impl From<&ValidBlock> for BlockSigned { #[derive(Debug, Clone, Decode, Encode)] #[non_exhaustive] pub struct BlockCommitted { - /// Hash of the block being signed. - pub hash: HashOf, /// Set of signatures. - pub signatures: SignaturesOf, + pub signatures: Vec, } impl From<&CommittedBlock> for BlockCommitted { fn from(block: &CommittedBlock) -> Self { - let block_hash = block.as_ref().hash(); - let block_signatures = block.as_ref().signatures().clone(); - Self { - hash: block_hash, - signatures: block_signatures, + signatures: block.as_ref().signatures().cloned().collect(), } } } diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 92c441f17dd..644fc3b3272 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -9,7 +9,6 @@ use std::{ use eyre::Result; use iroha_config::parameters::actual::{Common as CommonConfig, Sumeragi as SumeragiConfig}; -use iroha_crypto::{KeyPair, SignatureOf}; use iroha_data_model::{block::SignedBlock, prelude::*}; use iroha_genesis::GenesisNetwork; use iroha_logger::prelude::*; @@ -72,14 +71,14 @@ impl SumeragiHandle { block: &SignedBlock, state_block: &mut StateBlock<'_>, events_sender: &EventsSender, - mut current_topology: Topology, - ) -> Topology { + topology: &mut Topology, + ) { // NOTE: topology need to be updated up to block's view_change_index - current_topology.rotate_all_n(block.header().view_change_index); + topology.rotate_all_n(block.header().view_change_index as usize); let block = ValidBlock::validate( block.clone(), - ¤t_topology, + topology, chain_id, genesis_public_key, state_block, @@ -88,14 +87,15 @@ impl SumeragiHandle { let _ = events_sender.send(e.into()); }) .expect("Kura: Invalid block") - .commit(¤t_topology) + .commit(topology) .unpack(|e| { let _ = events_sender.send(e.into()); }) .expect("Kura: Invalid block"); if block.as_ref().header().is_genesis() { - *state_block.world.trusted_peers_ids = block.as_ref().commit_topology().clone(); + *state_block.world.trusted_peers_ids = + block.as_ref().commit_topology().cloned().collect(); } state_block @@ -105,11 +105,7 @@ impl SumeragiHandle { let _ = events_sender.send(e); }); - Topology::recreate_topology( - block.as_ref(), - 0, - state_block.world.peers().cloned().collect(), - ) + topology.block_committed(block.as_ref(), state_block.world.peers().cloned()); } /// Start [`Sumeragi`] actor and return handle to it. @@ -139,7 +135,7 @@ impl SumeragiHandle { let (message_sender, message_receiver) = mpsc::sync_channel(100); let blocks_iter; - let mut current_topology; + let mut topology; { let state_view = state.view(); @@ -151,7 +147,7 @@ impl SumeragiHandle { ) }); - current_topology = match state_view.height() { + topology = match state_view.height() { 0 => { assert!(!sumeragi_config.trusted_peers.is_empty()); Topology::new(sumeragi_config.trusted_peers.clone()) @@ -161,24 +157,23 @@ impl SumeragiHandle { "Sumeragi could not load block that was reported as present. \ Please check that the block storage was not disconnected.", ); - Topology::recreate_topology( - &block_ref, - 0, - state_view.world.peers_ids().iter().cloned().collect(), - ) + let mut topology = Topology::new(block_ref.commit_topology().cloned()); + topology + .block_committed(&block_ref, state_view.world.peers_ids().iter().cloned()); + topology } }; } for block in blocks_iter { let mut state_block = state.block(); - current_topology = Self::replay_block( + Self::replay_block( &common_config.chain_id, &genesis_network.public_key, &block, &mut state_block, &events_sender, - current_topology, + &mut topology, ); state_block.commit(); } @@ -190,7 +185,7 @@ impl SumeragiHandle { #[cfg(not(debug_assertions))] let debug_force_soft_fork = false; - let peer_id = common_config.peer_id(); + let peer_id = common_config.peer_id.clone(); let sumeragi = main_loop::Sumeragi { chain_id: common_config.chain_id, @@ -206,7 +201,7 @@ impl SumeragiHandle { control_message_receiver, message_receiver, debug_force_soft_fork, - current_topology, + topology, transaction_cache: Vec::new(), view_changes_metric: view_changes, }; @@ -274,18 +269,6 @@ impl VotingBlock<'_> { state_block, } } - /// Construct new `VotingBlock` with the given time. - pub(crate) fn voted_at( - block: ValidBlock, - state_block: StateBlock<'_>, - voted_at: Instant, - ) -> VotingBlock { - VotingBlock { - block, - voted_at, - state_block, - } - } } /// Arguments for [`SumeragiHandle::start`] function diff --git a/core/src/sumeragi/network_topology.rs b/core/src/sumeragi/network_topology.rs index dfa22fd9cc5..5ea4e1646ef 100644 --- a/core/src/sumeragi/network_topology.rs +++ b/core/src/sumeragi/network_topology.rs @@ -2,10 +2,11 @@ use derive_more::Display; use indexmap::IndexSet; -use iroha_crypto::{PublicKey, SignatureOf}; -use iroha_data_model::{block::SignedBlock, prelude::PeerId}; -use iroha_logger::trace; -use iroha_primitives::unique_vec::UniqueVec; +use iroha_crypto::PublicKey; +use iroha_data_model::{ + block::{BlockSignature, SignedBlock}, + prelude::PeerId, +}; /// The ordering of the peers which defines their roles in the current round of consensus. /// @@ -19,10 +20,7 @@ use iroha_primitives::unique_vec::UniqueVec; /// /// Above is an illustration of how the various operations work for a f = 2 topology. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Topology { - /// Current order of peers. The roles of peers are defined based on this order. - pub(crate) ordered_peers: UniqueVec, -} +pub struct Topology(Vec); /// Topology with at least one peer #[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref)] @@ -36,32 +34,61 @@ pub struct ConsensusTopology<'topology> { topology: &'topology Topology, } +impl AsRef<[PeerId]> for Topology { + fn as_ref(&self) -> &[PeerId] { + &self.0 + } +} + +impl IntoIterator for Topology { + type Item = PeerId; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + impl Topology { /// Create a new topology. - pub fn new(peers: UniqueVec) -> Self { - Topology { - ordered_peers: peers, - } + pub fn new(peers: impl IntoIterator) -> Self { + let topology = peers.into_iter().collect::>(); + + assert!( + !topology.is_empty(), + "Topology must contain at least one peer" + ); + + Topology(topology.into_iter().collect()) + } + + pub(crate) fn position(&self, peer: &PublicKey) -> Option { + self.0.iter().position(|p| p.public_key() == peer) + } + + pub(crate) fn iter(&self) -> impl ExactSizeIterator { + self.0.iter() } /// True, if the topology contains at least one peer and thus requires consensus pub fn is_non_empty(&self) -> Option { - (!self.ordered_peers.is_empty()).then_some(NonEmptyTopology { topology: self }) + (!self.0.is_empty()).then_some(NonEmptyTopology { topology: self }) } /// Is consensus required, aka are there more than 1 peer. pub fn is_consensus_required(&self) -> Option { - (self.ordered_peers.len() > 1).then_some(ConsensusTopology { topology: self }) + (self.0.len() > 1).then_some(ConsensusTopology { topology: self }) } /// How many faulty peers can this topology tolerate. pub fn max_faults(&self) -> usize { - (self.ordered_peers.len().saturating_sub(1)) / 3 + (self.0.len().saturating_sub(1)) / 3 } /// The required amount of votes to commit a block with this topology. pub fn min_votes_for_commit(&self) -> usize { - let len = self.ordered_peers.len(); + let len = self.0.len(); + if len > 3 { self.max_faults() * 2 + 1 } else { @@ -69,196 +96,144 @@ impl Topology { } } - /// Index of leader among `ordered_peers` + /// Index of leader #[allow(clippy::unused_self)] // In order to be consistent with `proxy_tail_index` method - fn leader_index(&self) -> usize { + pub const fn leader_index(&self) -> usize { 0 } - /// Index of leader among `ordered_peers` - fn proxy_tail_index(&self) -> usize { - // NOTE: proxy tail is the last element from the set A so that's why it's `min_votes_for_commit - 1` + /// Index of proxy tail + pub fn proxy_tail_index(&self) -> usize { + // NOTE: last element of set A self.min_votes_for_commit() - 1 } /// Filter signatures by roles in the topology. - #[allow(clippy::comparison_chain)] - pub fn filter_signatures_by_roles<'a, T: 'a, I: IntoIterator>>( + pub fn filter_signatures_by_roles<'a, I: IntoIterator>( &self, roles: &[Role], signatures: I, - ) -> Vec> { - let mut public_keys = IndexSet::with_capacity(self.ordered_peers.len()); + ) -> impl Iterator + 'a + where + ::IntoIter: 'a, + { + let mut filtered = IndexSet::new(); + for role in roles { match (role, self.is_non_empty(), self.is_consensus_required()) { (Role::Leader, Some(topology), _) => { - public_keys.insert(topology.leader().public_key()); + filtered.insert(topology.leader_index()); } (Role::ProxyTail, _, Some(topology)) => { - public_keys.insert(&topology.proxy_tail().public_key); + filtered.insert(topology.proxy_tail_index()); } (Role::ValidatingPeer, _, Some(topology)) => { - for peer in topology.validating_peers() { - public_keys.insert(peer.public_key()); - } + filtered.extend(topology.leader_index() + 1..topology.proxy_tail_index()); } (Role::ObservingPeer, _, Some(topology)) => { - for peer in topology.observing_peers() { - public_keys.insert(peer.public_key()); - } + filtered.extend(topology.proxy_tail_index() + 1..topology.0.len()); } _ => {} }; } + signatures .into_iter() - .filter(|signature| public_keys.contains(signature.public_key())) + .filter(move |signature| filtered.contains(&(signature.0 as usize))) .cloned() - .collect() } /// What role does this peer have in the topology. - pub fn role(&self, peer_id: &PeerId) -> Role { - match self.ordered_peers.iter().position(|p| p == peer_id) { - Some(index) if index == self.leader_index() => Role::Leader, - Some(index) if index < self.proxy_tail_index() => Role::ValidatingPeer, - Some(index) if index == self.proxy_tail_index() => Role::ProxyTail, + pub fn role(&self, peer: &PeerId) -> Role { + match self.position(peer.public_key()) { + Some(x) if x == self.leader_index() => Role::Leader, + Some(x) if x < self.proxy_tail_index() => Role::ValidatingPeer, + Some(x) if x == self.proxy_tail_index() => Role::ProxyTail, Some(_) => Role::ObservingPeer, - None => { - trace!(%peer_id, "Peer is not in topology."); - Role::Undefined - } + None => Role::Undefined, } } /// Add or remove peers from the topology. - pub fn update_peer_list(&mut self, new_peers: UniqueVec) { - self.modify_peers_directly(|peers| peers.retain(|peer| new_peers.contains(peer))); - self.ordered_peers.extend(new_peers); + fn update_peer_list(&mut self, new_peers: impl IntoIterator) { + let (old_peers, new_peers): (IndexSet<_>, IndexSet<_>) = new_peers + .into_iter() + .partition(|peer| self.0.contains(peer)); + self.0.retain(|peer| old_peers.contains(peer)); + self.0.extend(new_peers); } /// Rotate peers n times where n is a number of failed attempt to create a block. - pub fn rotate_all_n(&mut self, n: u64) { - let len = self - .ordered_peers - .len() - .try_into() - .expect("`usize` should fit into `u64`"); - if let Some(rem) = n.checked_rem(len) { - let rem = rem.try_into().expect( - "`rem` is smaller than `usize::MAX`, because remainder is always smaller than divisor", - ); + pub fn rotate_all_n(&mut self, n: usize) { + let len = self.0.len(); - self.modify_peers_directly(|peers| peers.rotate_left(rem)); + if let Some(rem) = n.checked_rem(len) { + self.0.rotate_left(rem); + unimplemented!() } } /// Re-arrange the set of peers after each successful block commit. pub fn rotate_set_a(&mut self) { let rotate_at = self.min_votes_for_commit(); - if rotate_at > 0 { - self.modify_peers_directly(|peers| peers[..rotate_at].rotate_left(1)); - } + self.0[..rotate_at].rotate_left(1); } /// Pull peers up in the topology to the top of the a set while preserving local order. - pub fn lift_up_peers(&mut self, to_lift_up: &[PublicKey]) { - self.modify_peers_directly(|peers| { - peers.sort_by_cached_key(|peer| !to_lift_up.contains(&peer.public_key)); + pub fn lift_up_peers(&mut self, to_lift_up: impl IntoIterator) { + let to_lift_up: IndexSet<_> = to_lift_up.into_iter().collect(); + + let mut signatory_idx = 0; + self.0.sort_by_cached_key(|_| { + let res = !to_lift_up.contains(&signatory_idx); + signatory_idx += 1; + res }); } - /// Perform sequence of actions after block committed. - pub fn update_topology(&mut self, block_signees: &[PublicKey], new_peers: UniqueVec) { - self.lift_up_peers(block_signees); + /// Recreate topology for given block + pub fn block_committed( + &mut self, + block: &SignedBlock, + new_peers: impl IntoIterator, + ) { + self.lift_up_peers(block.signatures().map(|s| s.0 as usize)); self.rotate_set_a(); self.update_peer_list(new_peers); } - - /// Recreate topology for given block and view change index - pub fn recreate_topology( - block: &SignedBlock, - view_change_index: u64, - new_peers: UniqueVec, - ) -> Self { - let mut topology = Topology::new(block.commit_topology().clone()); - let block_signees = block - .signatures() - .into_iter() - .map(|s| s.public_key()) - .cloned() - .collect::>(); - - topology.update_topology(&block_signees, new_peers); - - // Rotate all once for every view_change - topology.rotate_all_n(view_change_index); - - { - // FIXME: This is a hack to prevent consensus from running amock due to - // a bug in the implementation by reverting to predictable ordering - - let view_change_limit: usize = view_change_index - .saturating_sub(10) - .try_into() - .expect("u64 must fit into usize"); - - if view_change_limit > 1 { - iroha_logger::error!("Restarting consensus(internal bug). Report to developers"); - let mut peers: Vec<_> = topology.ordered_peers.iter().cloned().collect(); - - peers.sort(); - let peers_count = peers.len(); - peers.rotate_right(view_change_limit % peers_count); - topology = Topology::new(peers.into_iter().collect()); - } - } - - topology - } - - /// Modify [`ordered_peers`](Self::ordered_peers) directly as [`Vec`]. - fn modify_peers_directly(&mut self, f: impl FnOnce(&mut Vec)) { - let unique_peers = std::mem::take(&mut self.ordered_peers); - - let mut peers_vec = Vec::from(unique_peers); - f(&mut peers_vec); - - self.ordered_peers = UniqueVec::from_iter(peers_vec); - } } impl<'topology> NonEmptyTopology<'topology> { /// Get leader's [`PeerId`]. pub fn leader(&self) -> &'topology PeerId { - &self.topology.ordered_peers[self.topology.leader_index()] + &self.topology.0[self.topology.leader_index()] } } impl<'topology> ConsensusTopology<'topology> { /// Get proxy tail's peer id. pub fn proxy_tail(&self) -> &'topology PeerId { - &self.topology.ordered_peers[self.topology.proxy_tail_index()] + &self.topology.0[self.topology.proxy_tail_index()] } /// Get leader's [`PeerId`] pub fn leader(&self) -> &'topology PeerId { - &self.topology.ordered_peers[self.topology.leader_index()] + &self.topology.0[self.topology.leader_index()] } /// Get validating [`PeerId`]s. pub fn validating_peers(&self) -> &'topology [PeerId] { - &self.ordered_peers[self.leader_index() + 1..self.proxy_tail_index()] + &self.0[self.leader_index() + 1..self.proxy_tail_index()] } /// Get observing [`PeerId`]s. pub fn observing_peers(&self) -> &'topology [PeerId] { - &self.ordered_peers[self.proxy_tail_index() + 1..] + &self.0[self.proxy_tail_index() + 1..] } /// Get voting [`PeerId`]s. pub fn voting_peers(&self) -> &'topology [PeerId] { - &self.ordered_peers[self.leader_index()..=self.proxy_tail_index()] + &self.0[self.leader_index()..=self.proxy_tail_index()] } } @@ -280,7 +255,10 @@ pub enum Role { #[cfg(test)] macro_rules! test_peers { ($($id:literal),+$(,)?) => {{ - let mut iter = ::core::iter::repeat_with(|| KeyPair::random()); + let mut iter = ::core::iter::repeat_with( + || iroha_crypto::KeyPair::random() + ); + test_peers![$($id),*: iter] }}; ($($id:literal),+$(,)?: $key_pair_iter:expr) => { @@ -299,6 +277,7 @@ mod tests { use iroha_primitives::unique_vec; use super::*; + use crate::block::ValidBlock; fn topology() -> Topology { let peers = test_peers![0, 1, 2, 3, 4, 5, 6]; @@ -306,11 +285,7 @@ mod tests { } fn extract_ports(topology: &Topology) -> Vec { - topology - .ordered_peers - .iter() - .map(|peer| peer.address.port()) - .collect() + topology.0.iter().map(|peer| peer.address.port()).collect() } #[test] @@ -324,12 +299,7 @@ mod tests { fn lift_up_peers() { let mut topology = topology(); // Will lift up 1, 2, 4, 6 - let to_lift_up = &[ - topology.ordered_peers[1].public_key().clone(), - topology.ordered_peers[2].public_key().clone(), - topology.ordered_peers[4].public_key().clone(), - topology.ordered_peers[6].public_key().clone(), - ]; + let to_lift_up = [1, 2, 4, 6]; topology.lift_up_peers(to_lift_up); assert_eq!(extract_ports(&topology), vec![1, 2, 4, 6, 0, 3, 5]) } @@ -340,9 +310,9 @@ mod tests { // New peers will be 0, 2, 5, 7 let new_peers = { let mut peers = unique_vec![ - topology.ordered_peers[5].clone(), - topology.ordered_peers[0].clone(), - topology.ordered_peers[2].clone(), + topology.0[5].clone(), + topology.0[0].clone(), + topology.0[2].clone(), ]; peers.extend(test_peers![7]); peers @@ -358,70 +328,37 @@ mod tests { .collect::>(); let mut key_pairs_iter = key_pairs.iter(); let peers = test_peers![0, 1, 2, 3, 4, 5, 6: key_pairs_iter]; - let topology = Topology::new(peers.clone()); + let topology = Topology::new(peers); - let dummy = "value to sign"; - let signatures = key_pairs - .iter() - .map(|key_pair| SignatureOf::new(key_pair, &dummy)) - .collect::>>(); + let dummy_block = ValidBlock::new_dummy(); + let dummy_signature = &dummy_block.as_ref().signatures().next().unwrap().1; + let dummy_signatures = (0..key_pairs.len()) + .map(|i| BlockSignature(i as u64, dummy_signature.clone())) + .collect::>(); - let leader_signatures = - topology.filter_signatures_by_roles(&[Role::Leader], signatures.iter()); + let leader_signatures = topology + .filter_signatures_by_roles(&[Role::Leader], dummy_signatures.iter()) + .collect::>(); assert_eq!(leader_signatures.len(), 1); - assert_eq!(leader_signatures[0].public_key(), peers[0].public_key()); + assert_eq!(leader_signatures[0].0, 0); - let proxy_tail_signatures = - topology.filter_signatures_by_roles(&[Role::ProxyTail], signatures.iter()); + let proxy_tail_signatures = topology + .filter_signatures_by_roles(&[Role::ProxyTail], dummy_signatures.iter()) + .collect::>(); assert_eq!(proxy_tail_signatures.len(), 1); - assert_eq!(proxy_tail_signatures[0].public_key(), peers[4].public_key()); + assert_eq!(proxy_tail_signatures[0].0, 4); - let validating_peers_signatures = - topology.filter_signatures_by_roles(&[Role::ValidatingPeer], signatures.iter()); + let validating_peers_signatures = topology + .filter_signatures_by_roles(&[Role::ValidatingPeer], dummy_signatures.iter()) + .collect::>(); assert_eq!(validating_peers_signatures.len(), 3); - assert!(validating_peers_signatures - .iter() - .map(|s| s.public_key()) - .eq(peers[1..4].iter().map(PeerId::public_key))); - - let observing_peers_signatures = - topology.filter_signatures_by_roles(&[Role::ObservingPeer], signatures.iter()); - assert_eq!(observing_peers_signatures.len(), 2); - assert!(observing_peers_signatures - .iter() - .map(|s| s.public_key()) - .eq(peers[5..].iter().map(PeerId::public_key))); - } + assert!(validating_peers_signatures.iter().map(|s| s.0).eq(1..4)); - #[test] - fn filter_by_role_empty() { - let key_pairs = core::iter::repeat_with(KeyPair::random) - .take(7) + let observing_peers_signatures = topology + .filter_signatures_by_roles(&[Role::ObservingPeer], dummy_signatures.iter()) .collect::>(); - let peers = UniqueVec::new(); - let topology = Topology::new(peers); - - let dummy = "value to sign"; - let signatures = key_pairs - .iter() - .map(|key_pair| SignatureOf::new(key_pair, &dummy)) - .collect::>>(); - - let leader_signatures = - topology.filter_signatures_by_roles(&[Role::Leader], signatures.iter()); - assert!(leader_signatures.is_empty()); - - let proxy_tail_signatures = - topology.filter_signatures_by_roles(&[Role::ProxyTail], signatures.iter()); - assert!(proxy_tail_signatures.is_empty()); - - let validating_peers_signatures = - topology.filter_signatures_by_roles(&[Role::ValidatingPeer], signatures.iter()); - assert!(validating_peers_signatures.is_empty()); - - let observing_peers_signatures = - topology.filter_signatures_by_roles(&[Role::ObservingPeer], signatures.iter()); - assert!(observing_peers_signatures.is_empty()); + assert_eq!(observing_peers_signatures.len(), 2); + assert!(observing_peers_signatures.iter().map(|s| s.0).eq(5..7)); } #[test] @@ -431,30 +368,31 @@ mod tests { .collect::>(); let mut key_pairs_iter = key_pairs.iter(); let peers = test_peers![0: key_pairs_iter]; - let topology = Topology::new(peers.clone()); + let topology = Topology::new(peers); - let dummy = "value to sign"; - let signatures = key_pairs - .iter() - .map(|key_pair| SignatureOf::new(key_pair, &dummy)) - .collect::>>(); + let dummy_block = ValidBlock::new_dummy(); + let dummy_signature = &dummy_block.as_ref().signatures().next().unwrap().1; + let dummy_signatures = (0..key_pairs.len()) + .map(|i| BlockSignature(i as u64, dummy_signature.clone())) + .collect::>(); - let leader_signatures = - topology.filter_signatures_by_roles(&[Role::Leader], signatures.iter()); + let leader_signatures = topology + .filter_signatures_by_roles(&[Role::Leader], dummy_signatures.iter()) + .collect::>(); assert_eq!(leader_signatures.len(), 1); - assert_eq!(leader_signatures[0].public_key(), peers[0].public_key()); + assert_eq!(leader_signatures[0].0, 0); - let proxy_tail_signatures = - topology.filter_signatures_by_roles(&[Role::ProxyTail], signatures.iter()); - assert!(proxy_tail_signatures.is_empty()); + let mut proxy_tail_signatures = + topology.filter_signatures_by_roles(&[Role::ProxyTail], dummy_signatures.iter()); + assert!(proxy_tail_signatures.next().is_none()); - let validating_peers_signatures = - topology.filter_signatures_by_roles(&[Role::ValidatingPeer], signatures.iter()); - assert!(validating_peers_signatures.is_empty()); + let mut validating_peers_signatures = + topology.filter_signatures_by_roles(&[Role::ValidatingPeer], dummy_signatures.iter()); + assert!(validating_peers_signatures.next().is_none()); - let observing_peers_signatures = - topology.filter_signatures_by_roles(&[Role::ObservingPeer], signatures.iter()); - assert!(observing_peers_signatures.is_empty()); + let mut observing_peers_signatures = + topology.filter_signatures_by_roles(&[Role::ObservingPeer], dummy_signatures.iter()); + assert!(observing_peers_signatures.next().is_none()); } #[test] @@ -464,31 +402,33 @@ mod tests { .collect::>(); let mut key_pairs_iter = key_pairs.iter(); let peers = test_peers![0, 1: key_pairs_iter]; - let topology = Topology::new(peers.clone()); + let topology = Topology::new(peers); - let dummy = "value to sign"; - let signatures = key_pairs - .iter() - .map(|key_pair| SignatureOf::new(key_pair, &dummy)) - .collect::>>(); + let dummy_block = ValidBlock::new_dummy(); + let dummy_signature = &dummy_block.as_ref().signatures().next().unwrap().1; + let dummy_signatures = (0..key_pairs.len()) + .map(|i| BlockSignature(i as u64, dummy_signature.clone())) + .collect::>(); - let leader_signatures = - topology.filter_signatures_by_roles(&[Role::Leader], signatures.iter()); + let leader_signatures = topology + .filter_signatures_by_roles(&[Role::Leader], dummy_signatures.iter()) + .collect::>(); assert_eq!(leader_signatures.len(), 1); - assert_eq!(leader_signatures[0].public_key(), peers[0].public_key()); + assert_eq!(leader_signatures[0].0, 0); - let proxy_tail_signatures = - topology.filter_signatures_by_roles(&[Role::ProxyTail], signatures.iter()); + let proxy_tail_signatures = topology + .filter_signatures_by_roles(&[Role::ProxyTail], dummy_signatures.iter()) + .collect::>(); assert_eq!(proxy_tail_signatures.len(), 1); - assert_eq!(proxy_tail_signatures[0].public_key(), peers[1].public_key()); + assert_eq!(proxy_tail_signatures[0].0, 1); - let validating_peers_signatures = - topology.filter_signatures_by_roles(&[Role::ValidatingPeer], signatures.iter()); - assert!(validating_peers_signatures.is_empty()); + let mut validating_peers_signatures = + topology.filter_signatures_by_roles(&[Role::ValidatingPeer], dummy_signatures.iter()); + assert!(validating_peers_signatures.next().is_none()); - let observing_peers_signatures = - topology.filter_signatures_by_roles(&[Role::ObservingPeer], signatures.iter()); - assert!(observing_peers_signatures.is_empty()); + let mut observing_peers_signatures = + topology.filter_signatures_by_roles(&[Role::ObservingPeer], dummy_signatures.iter()); + assert!(observing_peers_signatures.next().is_none()); } #[test] @@ -498,65 +438,35 @@ mod tests { .collect::>(); let mut key_pairs_iter = key_pairs.iter(); let peers = test_peers![0, 1, 2: key_pairs_iter]; - let topology = Topology::new(peers.clone()); + let topology = Topology::new(peers); - let dummy = "value to sign"; - let signatures = key_pairs - .iter() - .map(|key_pair| SignatureOf::new(key_pair, &dummy)) - .collect::>>(); + let dummy_block = ValidBlock::new_dummy(); + let dummy_signature = &dummy_block.as_ref().signatures().next().unwrap().1; + let dummy_signatures = (0..key_pairs.len()) + .map(|i| BlockSignature(i as u64, dummy_signature.clone())) + .collect::>(); - let leader_signatures = - topology.filter_signatures_by_roles(&[Role::Leader], signatures.iter()); + let leader_signatures = topology + .filter_signatures_by_roles(&[Role::Leader], dummy_signatures.iter()) + .collect::>(); assert_eq!(leader_signatures.len(), 1); - assert_eq!(leader_signatures[0].public_key(), peers[0].public_key()); + assert_eq!(leader_signatures[0].0, 0); - let proxy_tail_signatures = - topology.filter_signatures_by_roles(&[Role::ProxyTail], signatures.iter()); + let proxy_tail_signatures = topology + .filter_signatures_by_roles(&[Role::ProxyTail], dummy_signatures.iter()) + .collect::>(); assert_eq!(proxy_tail_signatures.len(), 1); - assert_eq!(proxy_tail_signatures[0].public_key(), peers[2].public_key()); + assert_eq!(proxy_tail_signatures[0].0, 2); - let validating_peers_signatures = - topology.filter_signatures_by_roles(&[Role::ValidatingPeer], signatures.iter()); + let validating_peers_signatures = topology + .filter_signatures_by_roles(&[Role::ValidatingPeer], dummy_signatures.iter()) + .collect::>(); assert_eq!(validating_peers_signatures.len(), 1); - assert_eq!( - validating_peers_signatures[0].public_key(), - peers[1].public_key() - ); + assert_eq!(validating_peers_signatures[0].0, 1); - let observing_peers_signatures = - topology.filter_signatures_by_roles(&[Role::ObservingPeer], signatures.iter()); - assert!(observing_peers_signatures.is_empty()); - } - - #[test] - fn roles() { - let peers = test_peers![0, 1, 2, 3, 4, 5, 6]; - let not_in_topology_peers = test_peers![7, 8, 9]; - let topology = Topology::new(peers.clone()); - let expected_roles = [ - Role::Leader, - Role::ValidatingPeer, - Role::ValidatingPeer, - Role::ValidatingPeer, - Role::ProxyTail, - Role::ObservingPeer, - Role::ObservingPeer, - Role::Undefined, - Role::Undefined, - Role::Undefined, - ]; - - for ((i, peer), expected_role) in (0..) - .zip(peers.into_iter().chain(not_in_topology_peers)) - .zip(expected_roles) - { - let actual_role = topology.role(&peer); - assert_eq!( - actual_role, expected_role, - "Role detection failed for peer with index: {i}" - ); - } + let mut observing_peers_signatures = + topology.filter_signatures_by_roles(&[Role::ObservingPeer], dummy_signatures.iter()); + assert!(observing_peers_signatures.next().is_none()); } #[test] @@ -574,17 +484,9 @@ mod tests { } #[test] - fn proxy_tail_empty() { - let peers = UniqueVec::new(); - let topology = Topology::new(peers); - - assert_eq!( - topology - .is_consensus_required() - .as_ref() - .map(ConsensusTopology::proxy_tail), - None, - ); + #[should_panic(expected = "Topology must contain at least one peer")] + fn topology_empty() { + let _topology = Topology::new(Vec::new()); } #[test] @@ -643,20 +545,6 @@ mod tests { ); } - #[test] - fn leader_empty() { - let peers = UniqueVec::new(); - let topology = Topology::new(peers); - - assert_eq!( - topology - .is_non_empty() - .as_ref() - .map(NonEmptyTopology::leader), - None, - ); - } - #[test] fn leader_1() { let peers = test_peers![0]; @@ -713,20 +601,6 @@ mod tests { ); } - #[test] - fn validating_peers_empty() { - let peers = UniqueVec::new(); - let topology = Topology::new(peers); - - assert_eq!( - topology - .is_consensus_required() - .as_ref() - .map(ConsensusTopology::validating_peers), - None, - ); - } - #[test] fn validating_peers_1() { let peers = test_peers![0]; @@ -784,20 +658,6 @@ mod tests { ); } - #[test] - fn observing_peers_empty() { - let peers = UniqueVec::new(); - let topology = Topology::new(peers); - - assert_eq!( - topology - .is_consensus_required() - .as_ref() - .map(ConsensusTopology::observing_peers), - None, - ); - } - #[test] fn observing_peers_1() { let peers = test_peers![0]; diff --git a/core/src/sumeragi/view_change.rs b/core/src/sumeragi/view_change.rs index 9a24f0ece33..23aa04c5381 100644 --- a/core/src/sumeragi/view_change.rs +++ b/core/src/sumeragi/view_change.rs @@ -1,14 +1,16 @@ //! Structures related to proofs and reasons of view changes. //! Where view change is a process of changing topology due to some faulty network behavior. -use derive_more::{Deref, DerefMut}; use eyre::Result; -use indexmap::IndexSet; -use iroha_crypto::{HashOf, KeyPair, SignatureOf, SignaturesOf}; -use iroha_data_model::{block::SignedBlock, prelude::PeerId}; +use iroha_crypto::{HashOf, PrivateKey, SignatureOf}; +use iroha_data_model::block::SignedBlock; use parity_scale_codec::{Decode, Encode}; use thiserror::Error; +use super::network_topology::Topology; + +type ViewChangeProofSignature = (u64, SignatureOf); + /// Error emerge during insertion of `Proof` into `ProofChain` #[derive(Error, displaydoc::Display, Debug, Clone, Copy)] #[allow(missing_docs)] @@ -30,8 +32,7 @@ struct ProofPayload { /// The proof of a view change. It needs to be signed by f+1 peers for proof to be valid and view change to happen. #[derive(Debug, Clone, Decode, Encode)] pub struct SignedProof { - signatures: SignaturesOf, - /// Collection of signatures from the different peers. + signatures: Vec, payload: ProofPayload, } @@ -41,90 +42,92 @@ pub struct ProofBuilder(SignedProof); impl ProofBuilder { /// Constructor from index. - pub fn new(latest_block_hash: Option>, view_change_index: u64) -> Self { + pub fn new(latest_block_hash: Option>, view_change_index: usize) -> Self { + let view_change_index = view_change_index as u64; + let proof = SignedProof { payload: ProofPayload { latest_block_hash, view_change_index, }, - signatures: [].into_iter().collect(), + signatures: Vec::new(), }; Self(proof) } /// Sign this message with the peer's public and private key. - pub fn sign(mut self, key_pair: &KeyPair) -> SignedProof { - let signature = SignatureOf::new(key_pair, &self.0.payload); - self.0.signatures.insert(signature); + pub fn sign(mut self, signatory_idx: usize, private_key: &PrivateKey) -> SignedProof { + let signature = SignatureOf::new(private_key, &self.0.payload); + self.0.signatures.push((signatory_idx as u64, signature)); self.0 } } impl SignedProof { /// Verify the signatures of `other` and add them to this proof. - fn merge_signatures(&mut self, other: SignaturesOf) { - for signature in other { - if signature.verify(&self.payload).is_ok() { - self.signatures.insert(signature); + fn merge_signatures(&mut self, other: Vec, topology: &Topology) { + for (signatory_idx, signature) in other { + let public_key = topology.as_ref()[signatory_idx as usize].public_key(); + + if signature.verify(public_key, &self.payload).is_ok() { + self.signatures.push((signatory_idx, signature)); } } } /// Verify if the proof is valid, given the peers in `topology`. - fn verify(&self, peers: &[PeerId], max_faults: usize) -> bool { - let peer_public_keys: IndexSet<_> = peers.iter().map(PeerId::public_key).collect(); - + fn verify(&self, topology: &Topology) -> bool { let valid_count = self .signatures .iter() - .filter(|signature| { - signature.verify(&self.payload).is_ok() - && peer_public_keys.contains(signature.public_key()) + .filter(|&(signatory_idx, signature)| { + let public_key = topology.as_ref()[*signatory_idx as usize].public_key(); + signature.verify(public_key, &self.payload).is_ok() }) .count(); // See Whitepaper for the information on this limit. #[allow(clippy::int_plus_one)] { - valid_count >= max_faults + 1 + valid_count >= topology.max_faults() + 1 } } } /// Structure representing sequence of view change proofs. -#[derive(Debug, Clone, Encode, Decode, Deref, DerefMut, Default)] +#[derive(Debug, Clone, Encode, Decode, Default)] pub struct ProofChain(Vec); impl ProofChain { /// Verify the view change proof chain. pub fn verify_with_state( &self, - peers: &[PeerId], - max_faults: usize, + topology: &Topology, latest_block_hash: Option>, ) -> usize { - self.iter() + self.0 + .iter() .enumerate() .take_while(|(i, proof)| { proof.payload.latest_block_hash == latest_block_hash && proof.payload.view_change_index == (*i as u64) - && proof.verify(peers, max_faults) + && proof.verify(topology) }) .count() } /// Remove invalid proofs from the chain. pub fn prune(&mut self, latest_block_hash: Option>) { - let valid_count = self - .iter() + self.0 = core::mem::take(&mut self.0) + .into_iter() .enumerate() .take_while(|(i, proof)| { proof.payload.latest_block_hash == latest_block_hash && proof.payload.view_change_index == (*i as u64) }) - .count(); - self.truncate(valid_count); + .map(|(_, proof)| proof) + .collect(); } /// Attempt to insert a view chain proof into this `ProofChain`. @@ -134,25 +137,23 @@ impl ProofChain { /// - If proof view change number differs from view change number pub fn insert_proof( &mut self, - peers: &[PeerId], - max_faults: usize, - latest_block_hash: Option>, new_proof: SignedProof, + topology: &Topology, + latest_block_hash: Option>, ) -> Result<(), Error> { if new_proof.payload.latest_block_hash != latest_block_hash { return Err(Error::BlockHashMismatch); } - let next_unfinished_view_change = - self.verify_with_state(peers, max_faults, latest_block_hash); + let next_unfinished_view_change = self.verify_with_state(topology, latest_block_hash); if new_proof.payload.view_change_index != (next_unfinished_view_change as u64) { return Err(Error::ViewChangeNotFound); // We only care about the current view change that may or may not happen. } - let is_proof_chain_incomplete = next_unfinished_view_change < self.len(); + let is_proof_chain_incomplete = next_unfinished_view_change < self.0.len(); if is_proof_chain_incomplete { - self[next_unfinished_view_change].merge_signatures(new_proof.signatures); + self.0[next_unfinished_view_change].merge_signatures(new_proof.signatures, topology); } else { - self.push(new_proof); + self.0.push(new_proof); } Ok(()) } @@ -165,31 +166,30 @@ impl ProofChain { pub fn merge( &mut self, mut other: Self, - peers: &[PeerId], - max_faults: usize, + topology: &Topology, latest_block_hash: Option>, ) -> Result<(), Error> { - // Prune to exclude invalid proofs other.prune(latest_block_hash); - if other.is_empty() { + + if other.0.is_empty() { return Err(Error::BlockHashMismatch); } - let next_unfinished_view_change = - self.verify_with_state(peers, max_faults, latest_block_hash); - let is_proof_chain_incomplete = next_unfinished_view_change < self.len(); - let other_contain_additional_proofs = next_unfinished_view_change < other.len(); + let next_unfinished_view_change = self.verify_with_state(topology, latest_block_hash); + let is_proof_chain_incomplete = next_unfinished_view_change < self.0.len(); + let other_contain_additional_proofs = next_unfinished_view_change < other.0.len(); match (is_proof_chain_incomplete, other_contain_additional_proofs) { // Case 1: proof chain is incomplete and other have corresponding proof. (true, true) => { - let new_proof = other.swap_remove(next_unfinished_view_change); - self[next_unfinished_view_change].merge_signatures(new_proof.signatures); + let new_proof = other.0.swap_remove(next_unfinished_view_change); + self.0[next_unfinished_view_change] + .merge_signatures(new_proof.signatures, topology); } // Case 2: proof chain is complete, but other have additional proof. (false, true) => { - let new_proof = other.swap_remove(next_unfinished_view_change); - self.push(new_proof); + let new_proof = other.0.swap_remove(next_unfinished_view_change); + self.0.push(new_proof); } // Case 3: proof chain is incomplete, but other doesn't contain corresponding proof. // Usually this mean that sender peer is behind receiver peer. diff --git a/core/src/tx.rs b/core/src/tx.rs index 070ff3e7d03..935b3f96259 100644 --- a/core/src/tx.rs +++ b/core/src/tx.rs @@ -9,12 +9,14 @@ //! as various forms of validation are performed. use eyre::Result; -use iroha_crypto::SignatureVerificationFail; +use iroha_crypto::SignatureOf; pub use iroha_data_model::prelude::*; use iroha_data_model::{ isi::error::Mismatch, query::error::FindError, - transaction::{error::TransactionLimitError, TransactionLimits, TransactionPayload}, + transaction::{ + error::TransactionLimitError, TransactionLimits, TransactionPayload, TransactionSignature, + }, }; use iroha_genesis::GenesisTransaction; use iroha_logger::{debug, error}; @@ -27,16 +29,42 @@ use crate::{ /// `AcceptedTransaction` — a transaction accepted by iroha peer. #[derive(Debug, Clone, PartialEq, Eq)] -// TODO: Inner field should be private to maintain invariants +// FIX: Inner field should be private to maintain invariants pub struct AcceptedTransaction(pub(crate) SignedTransaction); +/// Verification failed of some signature due to following reason +#[derive(Clone, PartialEq, Eq)] +pub struct SignatureVerificationFail { + /// Signature which verification has failed + pub signature: SignatureOf, + /// Error which happened during verification + pub reason: String, +} + +impl core::fmt::Debug for SignatureVerificationFail { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("SignatureVerificationFail") + .field("signature", &self.signature) + .field("reason", &self.reason) + .finish() + } +} + +impl core::fmt::Display for SignatureVerificationFail { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Failed to verify signatures: {}", self.reason,) + } +} + +impl std::error::Error for SignatureVerificationFail {} + /// Error type for transaction from [`SignedTransaction`] to [`AcceptedTransaction`] -#[derive(Debug, FromVariant, thiserror::Error, displaydoc::Display)] +#[derive(Debug, displaydoc::Display, PartialEq, Eq, FromVariant, thiserror::Error)] pub enum AcceptTransactionFail { /// Failure during limits check TransactionLimit(#[source] TransactionLimitError), /// Failure during signature verification - SignatureVerification(#[source] SignatureVerificationFail), + SignatureVerification(#[source] SignatureVerificationFail), /// The genesis account can only sign transactions in the genesis block UnexpectedGenesisAccountSignature, /// Transaction's `chain_id` doesn't correspond to the id of current blockchain @@ -63,10 +91,10 @@ impl AcceptedTransaction { })); } - for signature in tx.0.signatures() { - if signature.public_key() != genesis_public_key { + for TransactionSignature(public_key, signature) in tx.0.signatures() { + if public_key != genesis_public_key { return Err(SignatureVerificationFail { - signature: signature.clone().into(), + signature: signature.clone(), reason: "Signature doesn't correspond to genesis public key".to_string(), } .into()); diff --git a/crypto/src/signature/mod.rs b/crypto/src/signature/mod.rs index 29c22cc6844..90a5c75eb51 100644 --- a/crypto/src/signature/mod.rs +++ b/crypto/src/signature/mod.rs @@ -11,13 +11,8 @@ pub(crate) mod ed25519; pub(crate) mod secp256k1; #[cfg(not(feature = "std"))] -use alloc::{ - boxed::Box, collections::btree_set, format, string::String, string::ToString as _, vec, - vec::Vec, -}; +use alloc::{format, string::String, vec, vec::Vec}; use core::{borrow::Borrow as _, marker::PhantomData}; -#[cfg(feature = "std")] -use std::collections::btree_set; use arrayref::array_ref; use derive_more::{Deref, DerefMut}; @@ -30,7 +25,7 @@ use serde::{Deserialize, Serialize}; use sha2::Digest as _; use zeroize::Zeroize as _; -use crate::{error::ParseError, ffi, hex_decode, Error, HashOf, KeyPair, PublicKey}; +use crate::{error::ParseError, ffi, hex_decode, Error, HashOf, PrivateKey, PublicKey}; /// Construct cryptographic RNG from seed. fn rng_from_seed(mut seed: Vec) -> impl CryptoRngCore { @@ -44,31 +39,19 @@ ffi::ffi_item! { #[serde_with::serde_as] #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, getset::Getters)] #[cfg_attr(not(feature="ffi_import"), derive(derive_more::DebugCustom, Hash, Decode, Encode, Deserialize, Serialize, IntoSchema))] - #[cfg_attr(not(feature="ffi_import"), debug( - fmt = "{{ pub_key: {public_key}, payload: {} }}", - "hex::encode_upper(payload)" - ))] + #[cfg_attr(not(feature="ffi_import"), debug(fmt = "{{ {} }}", "hex::encode_upper(payload)"))] pub struct Signature { - /// Public key that is used for verification. Payload is verified by algorithm - /// that corresponds with the public key's digest function. - #[getset(get = "pub")] - public_key: PublicKey, - /// Signature payload #[serde_as(as = "serde_with::hex::Hex")] - payload: ConstVec, + payload: ConstVec } } impl Signature { - /// Access the signature's payload - pub fn payload(&self) -> &[u8] { - self.payload.as_ref() - } - /// Creates new signature by signing payload via [`KeyPair::private_key`]. - pub fn new(key_pair: &KeyPair, payload: &[u8]) -> Self { + pub fn new(private_key: &PrivateKey, payload: &[u8]) -> Self { use crate::secrecy::ExposeSecret; - let signature = match key_pair.private_key.0.expose_secret() { + + let signature = match private_key.0.expose_secret() { crate::PrivateKeyInner::Ed25519(sk) => ed25519::Ed25519Sha512::sign(payload, sk), crate::PrivateKeyInner::Secp256k1(sk) => { secp256k1::EcdsaSecp256k1Sha256::sign(payload, sk) @@ -76,8 +59,8 @@ impl Signature { crate::PrivateKeyInner::BlsSmall(sk) => bls::BlsSmall::sign(payload, sk), crate::PrivateKeyInner::BlsNormal(sk) => bls::BlsNormal::sign(payload, sk), }; + Self { - public_key: key_pair.public_key.clone(), payload: ConstVec::new(signature), } } @@ -88,9 +71,8 @@ impl Signature { /// /// This method exists to allow reproducing the signature in a more efficient way than through /// deserialization. - pub fn from_bytes(public_key: PublicKey, payload: &[u8]) -> Self { + pub fn from_bytes(payload: &[u8]) -> Self { Self { - public_key, payload: ConstVec::new(payload), } } @@ -99,28 +81,28 @@ impl Signature { /// /// # Errors /// If passed string is not a valid hex. - pub fn from_hex(public_key: PublicKey, payload: impl AsRef) -> Result { + pub fn from_hex(payload: impl AsRef) -> Result { let payload: Vec = hex_decode(payload.as_ref())?; - Ok(Self::from_bytes(public_key, &payload)) + Ok(Self::from_bytes(&payload)) } /// Verify `payload` using signed data and [`KeyPair::public_key`]. /// /// # Errors /// Fails if the message doesn't pass verification - pub fn verify(&self, payload: &[u8]) -> Result<(), Error> { - match self.public_key.0.borrow() { + pub fn verify(&self, public_key: &PublicKey, payload: &[u8]) -> Result<(), Error> { + match public_key.0.borrow() { crate::PublicKeyInner::Ed25519(pk) => { - ed25519::Ed25519Sha512::verify(payload, self.payload(), pk) + ed25519::Ed25519Sha512::verify(payload, &self.payload, pk) } crate::PublicKeyInner::Secp256k1(pk) => { - secp256k1::EcdsaSecp256k1Sha256::verify(payload, self.payload(), pk) + secp256k1::EcdsaSecp256k1Sha256::verify(payload, &self.payload, pk) } crate::PublicKeyInner::BlsSmall(pk) => { - bls::BlsSmall::verify(payload, self.payload(), pk) + bls::BlsSmall::verify(payload, &self.payload, pk) } crate::PublicKeyInner::BlsNormal(pk) => { - bls::BlsNormal::verify(payload, self.payload(), pk) + bls::BlsNormal::verify(payload, &self.payload, pk) } }?; @@ -128,19 +110,6 @@ impl Signature { } } -// TODO: Enable in ffi_import -#[cfg(not(feature = "ffi_import"))] -impl From for (PublicKey, Vec) { - fn from( - Signature { - public_key, - payload: signature, - }: Signature, - ) -> Self { - (public_key, signature.into_vec()) - } -} - // TODO: Enable in ffi_import #[cfg(not(feature = "ffi_import"))] impl From> for Signature { @@ -234,8 +203,8 @@ impl SignatureOf { /// # Errors /// Fails if signing fails #[inline] - fn from_hash(key_pair: &KeyPair, hash: HashOf) -> Self { - Self(Signature::new(key_pair, hash.as_ref()), PhantomData) + fn from_hash(private_key: &PrivateKey, hash: HashOf) -> Self { + Self(Signature::new(private_key, hash.as_ref()), PhantomData) } /// Verify signature for this hash @@ -243,8 +212,8 @@ impl SignatureOf { /// # Errors /// /// Fails if the given hash didn't pass verification - fn verify_hash(&self, hash: HashOf) -> Result<(), Error> { - self.0.verify(hash.as_ref()) + pub fn verify_hash(&self, public_key: &PublicKey, hash: HashOf) -> Result<(), Error> { + self.0.verify(public_key, hash.as_ref()) } } @@ -256,324 +225,33 @@ impl SignatureOf { /// # Errors /// Fails if signing fails #[inline] - pub fn new(key_pair: &KeyPair, value: &T) -> Self { - Self::from_hash(key_pair, HashOf::new(value)) + pub fn new(private_key: &PrivateKey, value: &T) -> Self { + Self::from_hash(private_key, HashOf::new(value)) } /// Verifies signature for this item /// /// # Errors /// Fails if verification fails - pub fn verify(&self, value: &T) -> Result<(), Error> { - self.verify_hash(HashOf::new(value)) - } -} - -/// Wrapper around [`SignatureOf`] used to reimplement [`Eq`], [`Ord`], [`Hash`] -/// to compare signatures only by their [`PublicKey`]. -#[derive(Deref, DerefMut, Decode, Encode, Deserialize, Serialize, IntoSchema)] -#[serde(transparent, bound(deserialize = ""))] -#[schema(transparent)] -#[repr(transparent)] -#[cfg(not(feature = "ffi_import"))] -pub struct SignatureWrapperOf( - #[deref] - #[deref_mut] - SignatureOf, -); - -#[cfg(not(feature = "ffi_import"))] -impl SignatureWrapperOf { - #[inline] - fn inner(self) -> SignatureOf { - self.0 - } -} - -#[cfg(not(feature = "ffi_import"))] -impl core::fmt::Debug for SignatureWrapperOf { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - self.0.fmt(f) - } -} - -#[cfg(not(feature = "ffi_import"))] -impl Clone for SignatureWrapperOf { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -#[allow(clippy::unconditional_recursion)] // False-positive -#[cfg(not(feature = "ffi_import"))] -impl PartialEq for SignatureWrapperOf { - fn eq(&self, other: &Self) -> bool { - self.0.public_key().eq(other.0.public_key()) - } -} -#[cfg(not(feature = "ffi_import"))] -impl Eq for SignatureWrapperOf {} - -#[cfg(not(feature = "ffi_import"))] -impl PartialOrd for SignatureWrapperOf { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -#[cfg(not(feature = "ffi_import"))] -impl Ord for SignatureWrapperOf { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.0.public_key().cmp(other.0.public_key()) - } -} - -#[cfg(not(feature = "ffi_import"))] -impl core::hash::Hash for SignatureWrapperOf { - // Implement `Hash` manually to be consistent with `Ord` - fn hash(&self, state: &mut H) { - self.0.public_key().hash(state); - } -} - -/// Container for multiple signatures, each corresponding to a different public key. -/// -/// If the public key of the added signature is already in the set, -/// the associated signature will be replaced with the new one. -/// -/// GUARANTEE 1: Each signature corresponds to a different public key -#[allow(clippy::derived_hash_with_manual_eq)] -#[derive(Hash, Decode, Encode, Deserialize, Serialize, IntoSchema)] -#[serde(transparent)] -// Transmute guard -#[repr(transparent)] -#[cfg(not(feature = "ffi_import"))] -pub struct SignaturesOf { - signatures: btree_set::BTreeSet>, -} - -#[cfg(not(feature = "ffi_import"))] -impl core::fmt::Debug for SignaturesOf { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct(core::any::type_name::()) - .field("signatures", &self.signatures) - .finish() - } -} - -#[cfg(not(feature = "ffi_import"))] -impl Clone for SignaturesOf { - fn clone(&self) -> Self { - let signatures = self.signatures.clone(); - Self { signatures } - } -} - -#[allow(clippy::unconditional_recursion)] // False-positive -#[cfg(not(feature = "ffi_import"))] -impl PartialEq for SignaturesOf { - fn eq(&self, other: &Self) -> bool { - self.signatures.eq(&other.signatures) - } -} - -#[cfg(not(feature = "ffi_import"))] -impl Eq for SignaturesOf {} - -#[cfg(not(feature = "ffi_import"))] -impl PartialOrd for SignaturesOf { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -#[cfg(not(feature = "ffi_import"))] -impl Ord for SignaturesOf { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.signatures.cmp(&other.signatures) - } -} - -#[cfg(not(feature = "ffi_import"))] -impl IntoIterator for SignaturesOf { - type Item = SignatureOf; - type IntoIter = core::iter::Map< - btree_set::IntoIter>, - fn(SignatureWrapperOf) -> SignatureOf, - >; - fn into_iter(self) -> Self::IntoIter { - self.signatures.into_iter().map(SignatureWrapperOf::inner) - } -} - -#[cfg(not(feature = "ffi_import"))] -impl<'itm, T> IntoIterator for &'itm SignaturesOf { - type Item = &'itm SignatureOf; - type IntoIter = core::iter::Map< - btree_set::Iter<'itm, SignatureWrapperOf>, - fn(&'itm SignatureWrapperOf) -> &'itm SignatureOf, - >; - fn into_iter(self) -> Self::IntoIter { - self.signatures.iter().map(core::ops::Deref::deref) - } -} - -#[cfg(not(feature = "ffi_import"))] -impl Extend> for SignaturesOf { - fn extend(&mut self, iter: T) - where - T: IntoIterator>, - { - for signature in iter { - self.insert(signature); - } - } -} - -#[cfg(not(feature = "ffi_import"))] -impl From> for btree_set::BTreeSet> { - fn from(source: SignaturesOf) -> Self { - source.into_iter().collect() - } -} - -#[cfg(not(feature = "ffi_import"))] -impl From>> for SignaturesOf { - fn from(source: btree_set::BTreeSet>) -> Self { - source.into_iter().collect() - } -} - -#[cfg(not(feature = "ffi_import"))] -impl From> for SignaturesOf { - fn from(signature: SignatureOf) -> Self { - Self { - signatures: [SignatureWrapperOf(signature)].into(), - } + pub fn verify(&self, public_key: &PublicKey, value: &T) -> Result<(), Error> { + self.verify_hash(public_key, HashOf::new(value)) } } -#[cfg(not(feature = "ffi_import"))] -impl FromIterator> for SignaturesOf { - fn from_iter>>(signatures: T) -> Self { - Self { - signatures: signatures.into_iter().map(SignatureWrapperOf).collect(), - } - } -} - -#[cfg(not(feature = "ffi_import"))] -impl SignaturesOf { - /// Adds a signature. If the signature with this key was present, replaces it. - pub fn insert(&mut self, signature: SignatureOf) { - self.signatures.insert(SignatureWrapperOf(signature)); - } - - /// Return all signatures. - #[inline] - pub fn iter(&self) -> impl ExactSizeIterator> { - self.into_iter() - } - - /// Number of signatures. - #[inline] - #[allow(clippy::len_without_is_empty)] - pub fn len(&self) -> usize { - self.signatures.len() - } - - /// Verify signatures for this hash - /// - /// # Errors - /// Fails if verificatoin of any signature fails - pub fn verify_hash(&self, hash: HashOf) -> Result<(), SignatureVerificationFail> { - self.iter().try_for_each(|signature| { - signature - .verify_hash(hash) - .map_err(|error| SignatureVerificationFail { - signature: Box::new(signature.clone()), - reason: error.to_string(), - }) - }) - } - - /// Returns true if the set is a subset of another, i.e., other contains at least all the elements in self. - pub fn is_subset(&self, other: &Self) -> bool { - self.signatures.is_subset(&other.signatures) - } -} - -#[cfg(not(feature = "ffi_import"))] -impl SignaturesOf { - /// Create new signatures container - /// - /// # Errors - /// Forwards [`SignatureOf::new`] errors - #[inline] - pub fn new(key_pair: &KeyPair, value: &T) -> Self { - SignatureOf::new(key_pair, value).into() - } - - /// Verifies all signatures - /// - /// # Errors - /// Fails if validation of any signature fails - pub fn verify(&self, item: &T) -> Result<(), SignatureVerificationFail> { - self.verify_hash(HashOf::new(item)) - } -} - -/// Verification failed of some signature due to following reason -#[derive(Clone, PartialEq, Eq)] -pub struct SignatureVerificationFail { - /// Signature which verification has failed - pub signature: Box>, - /// Error which happened during verification - pub reason: String, -} - -#[cfg(not(feature = "ffi_import"))] -impl core::fmt::Debug for SignatureVerificationFail { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("SignatureVerificationFail") - .field("signature", &self.signature.0) - .field("reason", &self.reason) - .finish() - } -} - -#[cfg(not(feature = "ffi_import"))] -impl core::fmt::Display for SignatureVerificationFail { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!( - f, - "Failed to verify signatures because of signature {}: {}", - self.signature.public_key(), - self.reason, - ) - } -} - -#[cfg(feature = "std")] -#[cfg(not(feature = "ffi_import"))] -impl std::error::Error for SignatureVerificationFail {} - #[cfg(test)] mod tests { - use core::str::FromStr; - use serde_json::json; use super::*; - use crate::Algorithm; + use crate::{Algorithm, KeyPair}; #[test] #[cfg(feature = "rand")] fn create_signature_ed25519() { let key_pair = KeyPair::random_with_algorithm(crate::Algorithm::Ed25519); let message = b"Test message to sign."; - let signature = Signature::new(&key_pair, message); - assert_eq!(*signature.public_key(), *key_pair.public_key()); - signature.verify(message).unwrap(); + let signature = Signature::new(key_pair.private_key(), message); + signature.verify(key_pair.public_key(), message).unwrap(); } #[test] @@ -581,9 +259,8 @@ mod tests { fn create_signature_secp256k1() { let key_pair = KeyPair::random_with_algorithm(Algorithm::Secp256k1); let message = b"Test message to sign."; - let signature = Signature::new(&key_pair, message); - assert_eq!(*signature.public_key(), *key_pair.public_key()); - signature.verify(message).unwrap(); + let signature = Signature::new(key_pair.private_key(), message); + signature.verify(key_pair.public_key(), message).unwrap(); } #[test] @@ -591,9 +268,8 @@ mod tests { fn create_signature_bls_normal() { let key_pair = KeyPair::random_with_algorithm(Algorithm::BlsNormal); let message = b"Test message to sign."; - let signature = Signature::new(&key_pair, message); - assert_eq!(*signature.public_key(), *key_pair.public_key()); - signature.verify(message).unwrap(); + let signature = Signature::new(key_pair.private_key(), message); + signature.verify(key_pair.public_key(), message).unwrap(); } #[test] @@ -601,55 +277,8 @@ mod tests { fn create_signature_bls_small() { let key_pair = KeyPair::random_with_algorithm(Algorithm::BlsSmall); let message = b"Test message to sign."; - let signature = Signature::new(&key_pair, message); - assert_eq!(*signature.public_key(), *key_pair.public_key()); - signature.verify(message).unwrap(); - } - - #[test] - #[cfg(all(feature = "rand", not(feature = "ffi_import")))] - fn signatures_of_deduplication_by_public_key() { - let key_pair = KeyPair::random(); - let signatures = [ - SignatureOf::new(&key_pair, &1), - SignatureOf::new(&key_pair, &2), - SignatureOf::new(&key_pair, &3), - ] - .into_iter() - .collect::>(); - // Signatures with the same public key was deduplicated - assert_eq!(signatures.len(), 1); - } - - #[test] - #[cfg(not(feature = "ffi_import"))] - fn signature_wrapper_btree_and_hash_sets_consistent_results() { - use std::collections::{BTreeSet, HashSet}; - - let keys = 5; - let signatures_per_key = 10; - let signatures = core::iter::repeat_with(KeyPair::random) - .take(keys) - .flat_map(|key| { - core::iter::repeat_with(move || key.clone()) - .zip(0..) - .map(|(key, i)| SignatureOf::new(&key, &i)) - .take(signatures_per_key) - }) - .map(SignatureWrapperOf) - .collect::>(); - let hash_set: HashSet<_> = signatures.clone().into_iter().collect(); - let btree_set: BTreeSet<_> = signatures.into_iter().collect(); - - // Check that `hash_set` is subset of `btree_set` - for signature in &hash_set { - assert!(btree_set.contains(signature)); - } - // Check that `btree_set` is subset `hash_set` - for signature in &btree_set { - assert!(hash_set.contains(signature)); - } - // From the above we can conclude that `SignatureWrapperOf` have consistent behavior for `HashSet` and `BTreeSet` + let signature = Signature::new(key_pair.private_key(), message); + signature.verify(key_pair.public_key(), message).unwrap(); } #[test] @@ -666,12 +295,9 @@ mod tests { #[test] fn signature_from_hex_simply_reproduces_the_data() { - let public_key = "e701210312273E8810581E58948D3FB8F9E8AD53AAA21492EBB8703915BBB565A21B7FCC"; let payload = "3a7991af1abb77f3fd27cc148404a6ae4439d095a63591b77c788d53f708a02a1509a611ad6d97b01d871e58ed00c8fd7c3917b6ca61a8c2833a19e000aac2e4"; - let value = Signature::from_hex(PublicKey::from_str(public_key).unwrap(), payload).unwrap(); - - assert_eq!(value.public_key().to_string(), public_key); - assert_eq!(value.payload(), hex::decode(payload).unwrap()); + let value = Signature::from_hex(payload).unwrap(); + assert_eq!(value.payload.as_ref(), &hex::decode(payload).unwrap()); } } diff --git a/data_model/src/block.rs b/data_model/src/block.rs index b0f861757b9..b6dd6be1616 100644 --- a/data_model/src/block.rs +++ b/data_model/src/block.rs @@ -10,11 +10,10 @@ use core::{fmt::Display, time::Duration}; use derive_more::Display; #[cfg(all(feature = "std", feature = "transparent_api"))] -use iroha_crypto::KeyPair; -use iroha_crypto::{HashOf, MerkleTree, SignaturesOf}; +use iroha_crypto::PrivateKey; +use iroha_crypto::{HashOf, MerkleTree, SignatureOf}; use iroha_data_model_derive::model; use iroha_macro::FromVariant; -use iroha_primitives::unique_vec::UniqueVec; use iroha_schema::IntoSchema; use iroha_version::{declare_versioned, version_with_scale}; use parity_scale_codec::{Decode, Encode}; @@ -67,7 +66,7 @@ mod model { pub timestamp_ms: u64, /// Value of view change index. Used to resolve soft forks. #[getset(skip)] - pub view_change_index: u64, + pub view_change_index: u32, /// Estimation of consensus duration (in milliseconds). pub consensus_estimation_ms: u64, } @@ -92,13 +91,34 @@ mod model { /// Block header pub header: BlockHeader, /// Topology of the network at the time of block commit. - pub commit_topology: UniqueVec, + pub commit_topology: Vec, /// array of transactions, which successfully passed validation and consensus step. pub transactions: Vec, /// Event recommendations. pub event_recommendations: Vec, } + /// Signature of a block + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, + )] + pub struct BlockSignature( + /// Index of the block in the topology + pub u64, + /// Payload + pub SignatureOf, + ); + /// Signed block #[version_with_scale(version = 1, versioned_alias = "SignedBlock")] #[derive( @@ -109,8 +129,9 @@ mod model { #[ffi_type] pub struct SignedBlockV1 { /// Signatures of peers which approved this block. - pub signatures: SignaturesOf, + pub signatures: Vec, /// Block payload + #[serde(flatten)] pub payload: BlockPayload, } } @@ -159,16 +180,16 @@ impl SignedBlock { /// Topology of the network at the time of block commit. #[inline] #[cfg(feature = "transparent_api")] - pub fn commit_topology(&self) -> &UniqueVec { + pub fn commit_topology(&self) -> impl ExactSizeIterator { let SignedBlock::V1(block) = self; - &block.payload.commit_topology + block.payload.commit_topology.iter() } /// Signatures of peers which approved this block. #[inline] - pub fn signatures(&self) -> &SignaturesOf { + pub fn signatures(&self) -> impl ExactSizeIterator + DoubleEndedIterator { let SignedBlock::V1(block) = self; - &block.signatures + block.signatures.iter() } /// Calculate block hash @@ -186,56 +207,15 @@ impl SignedBlock { iroha_crypto::HashOf::new(&block.payload) } - /// Add additional signatures to this block - #[must_use] - #[cfg(feature = "transparent_api")] - pub fn sign(mut self, key_pair: &KeyPair) -> Self { - let SignedBlock::V1(block) = &mut self; - let signature = iroha_crypto::SignatureOf::new(key_pair, &block.payload); - block.signatures.insert(signature); - self - } - - /// Add additional signatures to this block - /// - /// # Errors - /// - /// If given signature doesn't match block hash - #[cfg(feature = "transparent_api")] - pub fn add_signature( - &mut self, - signature: iroha_crypto::SignatureOf, - ) -> Result<(), iroha_crypto::error::Error> { - let SignedBlock::V1(block) = self; - signature.verify(&block.payload)?; - - let SignedBlock::V1(block) = self; - block.signatures.insert(signature); - - Ok(()) - } - /// Add additional signatures to this block #[cfg(feature = "transparent_api")] - pub fn replace_signatures( - &mut self, - signatures: iroha_crypto::SignaturesOf, - ) -> bool { - #[cfg(not(feature = "std"))] - use alloc::collections::BTreeSet; - #[cfg(feature = "std")] - use std::collections::BTreeSet; - + pub fn sign(&mut self, private_key: &PrivateKey, node_pos: usize) { let SignedBlock::V1(block) = self; - block.signatures = BTreeSet::new().into(); - - for signature in signatures { - if self.add_signature(signature).is_err() { - return false; - } - } - true + block.signatures.push(BlockSignature( + node_pos as u64, + SignatureOf::new(private_key, &block.payload), + )); } } @@ -246,13 +226,13 @@ mod candidate { #[derive(Decode, Deserialize)] struct SignedBlockCandidate { - signatures: SignaturesOf, + signatures: Vec, + #[serde(flatten)] payload: BlockPayload, } impl SignedBlockCandidate { fn validate(self) -> Result { - self.validate_signatures()?; self.validate_header()?; if self.payload.transactions.is_empty() { @@ -283,12 +263,6 @@ mod candidate { Ok(()) } - - fn validate_signatures(&self) -> Result<(), &'static str> { - self.signatures - .verify(&self.payload) - .map_err(|_| "Transaction contains invalid signatures") - } } impl Decode for SignedBlockV1 { diff --git a/data_model/src/query/mod.rs b/data_model/src/query/mod.rs index 9837f00905d..d5b99bead19 100644 --- a/data_model/src/query/mod.rs +++ b/data_model/src/query/mod.rs @@ -1184,12 +1184,28 @@ pub mod http { pub filter: PredicateBox, } + /// Signature of query + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, + )] + pub struct QuerySignature(pub PublicKey, pub SignatureOf); + /// I/O ready structure to send queries. #[derive(Debug, Clone, Encode, Serialize, IntoSchema)] #[version_with_scale(version = 1, versioned_alias = "SignedQuery")] pub struct SignedQueryV1 { /// Signature of the client who sends this query. - pub signature: SignatureOf, + pub signature: QuerySignature, /// Payload pub payload: QueryPayload, } @@ -1225,13 +1241,15 @@ pub mod http { #[derive(Decode, Deserialize)] struct SignedQueryCandidate { - signature: SignatureOf, + signature: QuerySignature, payload: QueryPayload, } impl SignedQueryCandidate { fn validate(self) -> Result { - if self.signature.verify(&self.payload).is_err() { + let QuerySignature(public_key, signature) = &self.signature; + + if signature.verify(public_key, &self.payload).is_err() { return Err("Query signature not valid"); } @@ -1267,7 +1285,7 @@ pub mod http { #[cfg(feature = "transparent_api")] impl SignedQuery { /// Return query signature - pub fn signature(&self) -> &SignatureOf { + pub fn signature(&self) -> &QuerySignature { let SignedQuery::V1(query) = self; &query.signature } @@ -1314,8 +1332,10 @@ pub mod http { #[inline] #[must_use] pub fn sign(self, key_pair: &iroha_crypto::KeyPair) -> SignedQuery { + let signature = SignatureOf::new(key_pair.private_key(), &self.payload); + SignedQueryV1 { - signature: SignatureOf::new(key_pair, &self.payload), + signature: QuerySignature(key_pair.public_key().clone(), signature), payload: self.payload, } .into() diff --git a/data_model/src/transaction.rs b/data_model/src/transaction.rs index 0a541f70d04..41439df5833 100644 --- a/data_model/src/transaction.rs +++ b/data_model/src/transaction.rs @@ -9,7 +9,7 @@ use core::{ }; use derive_more::{DebugCustom, Display}; -use iroha_crypto::SignaturesOf; +use iroha_crypto::{KeyPair, PublicKey, SignatureOf}; use iroha_data_model_derive::model; use iroha_macro::FromVariant; use iroha_schema::IntoSchema; @@ -30,6 +30,7 @@ mod model { use getset::{CopyGetters, Getters}; use super::*; + use crate::account::AccountId; /// Either ISI or Wasm binary #[derive( @@ -140,6 +141,22 @@ mod model { pub max_wasm_size_bytes: u64, } + /// Signature of transaction + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, + )] + pub struct TransactionSignature(pub PublicKey, pub SignatureOf); + /// Transaction that contains at least one signature /// /// `Iroha` and its clients use [`Self`] to send transactions over the network. @@ -155,8 +172,9 @@ mod model { #[ffi_type] pub struct SignedTransactionV1 { /// [`iroha_crypto::SignatureOf`]<[`TransactionPayload`]>. - pub(super) signatures: SignaturesOf, + pub(super) signatures: Vec, /// [`Transaction`] payload. + #[serde(flatten)] pub(super) payload: TransactionPayload, } @@ -292,9 +310,9 @@ impl SignedTransaction { /// Return transaction signatures #[inline] - pub fn signatures(&self) -> &SignaturesOf { + pub fn signatures(&self) -> impl ExactSizeIterator { let SignedTransaction::V1(tx) = self; - &tx.signatures + tx.signatures.iter() } /// Calculate transaction [`Hash`](`iroha_crypto::HashOf`). @@ -305,10 +323,13 @@ impl SignedTransaction { /// Sign transaction with provided key pair. #[must_use] - pub fn sign(self, key_pair: &iroha_crypto::KeyPair) -> SignedTransaction { + pub fn sign(self, key_pair: &KeyPair) -> SignedTransaction { let SignedTransaction::V1(mut tx) = self; - let signature = iroha_crypto::SignatureOf::new(key_pair, &tx.payload); - tx.signatures.insert(signature); + let signature = SignatureOf::new(key_pair.private_key(), &tx.payload); + tx.signatures.push(TransactionSignature( + key_pair.public_key().clone(), + signature, + )); SignedTransactionV1 { payload: tx.payload, @@ -346,33 +367,40 @@ mod candidate { #[derive(Decode, Deserialize)] struct SignedTransactionCandidate { - signatures: SignaturesOf, + signatures: Vec, + #[serde(flatten)] payload: TransactionPayload, } impl SignedTransactionCandidate { fn validate(self) -> Result { + self.validate_instructions()?; self.validate_signatures()?; - self.validate_instructions() + + Ok(SignedTransactionV1 { + signatures: self.signatures, + payload: self.payload, + }) } - fn validate_instructions(self) -> Result { + fn validate_instructions(&self) -> Result<(), &'static str> { if let Executable::Instructions(instructions) = &self.payload.instructions { if instructions.is_empty() { return Err("Transaction is empty"); } } - Ok(SignedTransactionV1 { - payload: self.payload, - signatures: self.signatures, - }) + Ok(()) } fn validate_signatures(&self) -> Result<(), &'static str> { - self.signatures - .verify(&self.payload) - .map_err(|_| "Transaction contains invalid signatures") + for TransactionSignature(public_key, signature) in &self.signatures { + signature + .verify(public_key, &self.payload) + .map_err(|_| "Transaction contains invalid signatures")?; + } + + Ok(()) } } @@ -740,8 +768,12 @@ mod http { /// Sign transaction with provided key pair. #[must_use] - pub fn sign(self, key_pair: &iroha_crypto::KeyPair) -> SignedTransaction { - let signatures = SignaturesOf::new(key_pair, &self.payload); + pub fn sign(self, key_pair: &KeyPair) -> SignedTransaction { + let signature = SignatureOf::new(key_pair.private_key(), &self.payload); + let signatures = vec![TransactionSignature( + key_pair.public_key().clone(), + signature, + )]; SignedTransactionV1 { payload: self.payload, diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index e9196b814df..7b73f45f0b8 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -642,7 +642,7 @@ }, { "name": "view_change_index", - "type": "u64" + "type": "u32" }, { "name": "consensus_estimation_ms", @@ -679,6 +679,12 @@ } ] }, + "BlockSignature": { + "Tuple": [ + "u64", + "SignatureOf" + ] + }, "BlockStatus": { "Enum": [ { @@ -3048,6 +3054,12 @@ } ] }, + "QuerySignature": { + "Tuple": [ + "PublicKey", + "SignatureOf" + ] + }, "Register": { "Struct": [ { @@ -3567,10 +3579,6 @@ }, "Signature": { "Struct": [ - { - "name": "public_key", - "type": "PublicKey" - }, { "name": "payload", "type": "Vec" @@ -3594,22 +3602,6 @@ "SignatureOf": "Signature", "SignatureOf": "Signature", "SignatureOf": "Signature", - "SignaturesOf": { - "Struct": [ - { - "name": "signatures", - "type": "SortedVec>" - } - ] - }, - "SignaturesOf": { - "Struct": [ - { - "name": "signatures", - "type": "SortedVec>" - } - ] - }, "SignedBlock": { "Enum": [ { @@ -3623,7 +3615,7 @@ "Struct": [ { "name": "signatures", - "type": "SignaturesOf" + "type": "Vec" }, { "name": "payload", @@ -3644,7 +3636,7 @@ "Struct": [ { "name": "signature", - "type": "SignatureOf" + "type": "QuerySignature" }, { "name": "payload", @@ -3665,7 +3657,7 @@ "Struct": [ { "name": "signatures", - "type": "SignaturesOf" + "type": "Vec" }, { "name": "payload", @@ -3776,12 +3768,6 @@ "SortedVec": { "Vec": "PublicKey" }, - "SortedVec>": { - "Vec": "SignatureOf" - }, - "SortedVec>": { - "Vec": "SignatureOf" - }, "String": "String", "StringPredicate": { "Enum": [ @@ -3957,6 +3943,12 @@ } ] }, + "TransactionSignature": { + "Tuple": [ + "PublicKey", + "SignatureOf" + ] + }, "TransactionStatus": { "Enum": [ { @@ -4408,6 +4400,9 @@ } ] }, + "Vec": { + "Vec": "BlockSignature" + }, "Vec": { "Vec": "EventBox" }, @@ -4435,6 +4430,9 @@ "Vec": { "Vec": "QueryOutputBox" }, + "Vec": { + "Vec": "TransactionSignature" + }, "Vec": { "Vec": "TransactionValue" }, diff --git a/genesis/Cargo.toml b/genesis/Cargo.toml index 75b8186cc7e..e30bd125b2b 100644 --- a/genesis/Cargo.toml +++ b/genesis/Cargo.toml @@ -13,7 +13,6 @@ workspace = true [dependencies] iroha_crypto = { workspace = true } iroha_data_model = { workspace = true, features = ["http"] } -iroha_schema = { workspace = true } derive_more = { workspace = true, features = ["deref"] } serde = { workspace = true, features = ["derive"] } diff --git a/p2p/src/peer.rs b/p2p/src/peer.rs index bfa8af93b80..801a688dbb6 100644 --- a/p2p/src/peer.rs +++ b/p2p/src/peer.rs @@ -258,7 +258,7 @@ mod run { } } }; - // Reset idle and ping timeout as peer received message from another peer + // Reset idle and ping timeout as peer received message from another peer idle_interval.reset(); ping_interval.reset(); } @@ -428,7 +428,7 @@ mod run { mod state { //! Module for peer stages. - use iroha_crypto::{KeyGenOption, KeyPair, Signature}; + use iroha_crypto::{KeyGenOption, KeyPair, PublicKey, Signature}; use iroha_primitives::addr::SocketAddr; use super::{cryptographer::Cryptographer, *}; @@ -572,8 +572,8 @@ mod state { let write_half = &mut connection.write; let payload = create_payload::(&kx_local_pk, &kx_remote_pk); - let signature = Signature::new(&key_pair, &payload); - let data = signature.encode(); + let signature = Signature::new(key_pair.private_key(), &payload); + let data = (key_pair.public_key(), signature).encode(); let data = &cryptographer.encrypt(data.as_slice())?; @@ -621,13 +621,12 @@ mod state { let data = cryptographer.decrypt(data.as_slice())?; - let signature: Signature = DecodeAll::decode_all(&mut data.as_slice())?; + let (remote_pub_key, signature): (PublicKey, Signature) = + DecodeAll::decode_all(&mut data.as_slice())?; // Swap order of keys since we are verifying for other peer order remote/local keys is reversed let payload = create_payload::(&kx_remote_pk, &kx_local_pk); - signature.verify(&payload)?; - - let (remote_pub_key, _) = signature.into(); + signature.verify(&remote_pub_key, &payload)?; let peer_id = PeerId::new(peer_addr, remote_pub_key); diff --git a/schema/gen/src/lib.rs b/schema/gen/src/lib.rs index e3c56afd90c..81274c989db 100644 --- a/schema/gen/src/lib.rs +++ b/schema/gen/src/lib.rs @@ -92,8 +92,6 @@ types!( BTreeMap, BTreeSet, BTreeSet, - BTreeSet>, - BTreeSet>, BatchedResponse, BatchedResponseV1, BlockEvent, @@ -321,10 +319,6 @@ types!( SignatureOf, SignatureOf, SignatureOf, - SignatureWrapperOf, - SignatureWrapperOf, - SignaturesOf, - SignaturesOf, SignedBlock, SignedBlockV1, SignedQuery,