From 77a9690eb52831fb56fe0fdbe905167748908087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Ver=C5=A1i=C4=87?= Date: Mon, 27 May 2024 09:40:24 +0300 Subject: [PATCH] refactor: remove public key from transaction and query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marin Veršić --- client/examples/million_accounts_genesis.rs | 2 +- client/src/client.rs | 18 +- client/src/query_builder.rs | 5 +- client/tests/integration/asset.rs | 2 +- .../integration/domain_owner_permissions.rs | 8 +- client/tests/integration/permissions.rs | 10 +- client/tests/integration/roles.rs | 8 +- client/tests/integration/tx_chain_id.rs | 6 +- client/tests/integration/upgrade.rs | 13 +- configs/swarm/executor.wasm | Bin 509411 -> 509058 bytes core/benches/blocks/apply_blocks.rs | 2 +- core/benches/blocks/common.rs | 8 +- core/benches/blocks/validate_blocks.rs | 8 +- core/benches/kura.rs | 4 +- core/benches/validation.rs | 16 +- core/src/block.rs | 118 ++++--- core/src/gossiper.rs | 2 +- core/src/queue.rs | 18 +- core/src/smartcontracts/isi/mod.rs | 4 +- core/src/smartcontracts/isi/query.rs | 20 +- core/src/snapshot.rs | 5 +- core/src/sumeragi/main_loop.rs | 323 +++++++++++------- core/src/sumeragi/message.rs | 79 ++++- core/src/sumeragi/mod.rs | 17 +- core/src/sumeragi/network_topology.rs | 23 +- core/src/sumeragi/view_change.rs | 60 +++- core/src/tx.rs | 21 +- core/test_network/src/lib.rs | 4 +- crypto/src/lib.rs | 6 - data_model/src/block.rs | 26 +- data_model/src/events/pipeline.rs | 4 +- data_model/src/query/mod.rs | 18 +- data_model/src/transaction.rs | 26 +- docs/source/references/schema.json | 34 +- genesis/src/lib.rs | 2 +- torii/src/lib.rs | 2 - torii/src/routing.rs | 2 +- 37 files changed, 521 insertions(+), 403 deletions(-) diff --git a/client/examples/million_accounts_genesis.rs b/client/examples/million_accounts_genesis.rs index affb34c130d..6d9ca52b1a9 100644 --- a/client/examples/million_accounts_genesis.rs +++ b/client/examples/million_accounts_genesis.rs @@ -3,7 +3,7 @@ use std::{thread, time::Duration}; use iroha::{ crypto::KeyPair, - data_model::{::isi::InstructionBox, prelude::*}, + data_model::{isi::InstructionBox, prelude::*}, }; use iroha_genesis::{GenesisTransaction, GenesisTransactionBuilder}; use iroha_primitives::unique_vec; diff --git a/client/src/client.rs b/client/src/client.rs index 0ad711a0301..efd0b914510 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -450,7 +450,9 @@ impl Client { tx_builder.set_nonce(nonce); }; - tx_builder.with_metadata(metadata).sign(&self.key_pair) + tx_builder + .with_metadata(metadata) + .sign(self.key_pair.private_key()) } /// Signs transaction @@ -458,7 +460,7 @@ impl Client { /// # Errors /// Fails if signature generation fails pub fn sign_transaction(&self, transaction: TransactionBuilder) -> SignedTransaction { - transaction.sign(&self.key_pair) + transaction.sign(self.key_pair.private_key()) } /// Signs query @@ -1634,20 +1636,12 @@ mod tests { use http::Response; use super::*; - use crate::data_model::{asset::Asset, query::error::QueryExecutionFail, ValidationFail}; + use crate::data_model::{asset::Asset, ValidationFail}; #[test] fn certain_errors() -> Result<()> { let mut sut = QueryResponseHandler::>::new(QueryRequest::dummy()); - let responses = vec![ - ( - StatusCode::UNAUTHORIZED, - ValidationFail::QueryFailed(QueryExecutionFail::Signature( - "whatever".to_owned(), - )), - ), - (StatusCode::UNPROCESSABLE_ENTITY, ValidationFail::TooComplex), - ]; + let responses = vec![(StatusCode::UNPROCESSABLE_ENTITY, ValidationFail::TooComplex)]; for (status_code, err) in responses { let resp = Response::builder().status(status_code).body(err.encode())?; diff --git a/client/src/query_builder.rs b/client/src/query_builder.rs index 7a59c893907..d77321f9542 100644 --- a/client/src/query_builder.rs +++ b/client/src/query_builder.rs @@ -1,11 +1,10 @@ use std::fmt::Debug; -use iroha_data_model::query::{IterableQuery, QueryOutputBox}; - use crate::{ client::{Client, QueryOutput, QueryResult}, data_model::query::{ - predicate::PredicateBox, sorting::Sorting, FetchSize, Pagination, Query, QueryOutputBox, + predicate::PredicateBox, sorting::Sorting, FetchSize, IterableQuery, Pagination, Query, + QueryOutputBox, }, }; diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index 919df14ce97..804083d8550 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -307,7 +307,7 @@ fn find_rate_and_make_exchange_isi_should_succeed() { asset_id.account_id().clone(), ) .with_instructions([instruction]) - .sign(&owner_key_pair); + .sign(owner_key_pair.private_key()); test_client .submit_transaction_blocking(&transaction) diff --git a/client/tests/integration/domain_owner_permissions.rs b/client/tests/integration/domain_owner_permissions.rs index b2f3bf88444..e53442bec65 100644 --- a/client/tests/integration/domain_owner_permissions.rs +++ b/client/tests/integration/domain_owner_permissions.rs @@ -26,7 +26,7 @@ fn domain_owner_domain_permissions() -> Result<()> { // Asset definitions can't be registered by "bob@kingdom" by default let transaction = TransactionBuilder::new(chain_id.clone(), bob_id.clone()) .with_instructions([Register::asset_definition(coin.clone())]) - .sign(&bob_keypair); + .sign(bob_keypair.private_key()); let err = test_client .submit_transaction_blocking(&transaction) .expect_err("Tx should fail due to permissions"); @@ -52,7 +52,7 @@ fn domain_owner_domain_permissions() -> Result<()> { test_client.submit_blocking(Grant::permission(token.clone(), bob_id.clone()))?; let transaction = TransactionBuilder::new(chain_id, bob_id.clone()) .with_instructions([Register::asset_definition(coin)]) - .sign(&bob_keypair); + .sign(bob_keypair.private_key()); test_client.submit_transaction_blocking(&transaction)?; test_client.submit_blocking(Revoke::permission(token, bob_id.clone()))?; @@ -148,7 +148,7 @@ fn domain_owner_asset_definition_permissions() -> Result<()> { let coin = AssetDefinition::numeric(coin_id.clone()); let transaction = TransactionBuilder::new(chain_id, bob_id.clone()) .with_instructions([Register::asset_definition(coin)]) - .sign(&bob_keypair); + .sign(bob_keypair.private_key()); test_client.submit_transaction_blocking(&transaction)?; // check that "alice@wonderland" as owner of domain can transfer asset definitions in her domain @@ -217,7 +217,7 @@ fn domain_owner_asset_permissions() -> Result<()> { Register::asset_definition(coin), Register::asset_definition(store), ]) - .sign(&bob_keypair); + .sign(bob_keypair.private_key()); test_client.submit_transaction_blocking(&transaction)?; // check that "alice@wonderland" as owner of domain can register and unregister assets in her domain diff --git a/client/tests/integration/permissions.rs b/client/tests/integration/permissions.rs index 59d5de0c439..51c938d31e4 100644 --- a/client/tests/integration/permissions.rs +++ b/client/tests/integration/permissions.rs @@ -102,7 +102,7 @@ fn permissions_disallow_asset_transfer() { ); let transfer_tx = TransactionBuilder::new(chain_id, mouse_id) .with_instructions([transfer_asset]) - .sign(&mouse_keypair); + .sign(mouse_keypair.private_key()); let err = iroha .submit_transaction_blocking(&transfer_tx) .expect_err("Transaction was not rejected."); @@ -151,7 +151,7 @@ fn permissions_disallow_asset_burn() { ); let burn_tx = TransactionBuilder::new(chain_id, mouse_id) .with_instructions([burn_asset]) - .sign(&mouse_keypair); + .sign(mouse_keypair.private_key()); let err = iroha .submit_transaction_blocking(&burn_tx) @@ -239,7 +239,7 @@ fn permissions_differ_not_only_by_names() { let grant_hats_access_tx = TransactionBuilder::new(chain_id.clone(), mouse_id.clone()) .with_instructions([allow_alice_to_set_key_value_in_hats]) - .sign(&mouse_keypair); + .sign(mouse_keypair.private_key()); client .submit_transaction_blocking(&grant_hats_access_tx) .expect("Failed grant permission to modify Mouse's hats"); @@ -275,7 +275,7 @@ fn permissions_differ_not_only_by_names() { let grant_shoes_access_tx = TransactionBuilder::new(chain_id, mouse_id) .with_instructions([allow_alice_to_set_key_value_in_shoes]) - .sign(&mouse_keypair); + .sign(mouse_keypair.private_key()); client .submit_transaction_blocking(&grant_shoes_access_tx) @@ -328,7 +328,7 @@ fn stored_vs_granted_token_payload() -> Result<()> { let transaction = TransactionBuilder::new(chain_id, mouse_id) .with_instructions([allow_alice_to_set_key_value_in_mouse_asset]) - .sign(&mouse_keypair); + .sign(mouse_keypair.private_key()); iroha .submit_transaction_blocking(&transaction) .expect("Failed to grant permission to alice."); diff --git a/client/tests/integration/roles.rs b/client/tests/integration/roles.rs index 599ccb3bc91..9832bfc7ad9 100644 --- a/client/tests/integration/roles.rs +++ b/client/tests/integration/roles.rs @@ -76,7 +76,7 @@ fn register_and_grant_role_for_metadata_access() -> Result<()> { let grant_role = Grant::role(role_id.clone(), alice_id.clone()); let grant_role_tx = TransactionBuilder::new(chain_id, mouse_id.clone()) .with_instructions([grant_role]) - .sign(&mouse_keypair); + .sign(mouse_keypair.private_key()); test_client.submit_transaction_blocking(&grant_role_tx)?; // Alice modifies Mouse's metadata @@ -236,7 +236,7 @@ fn grant_revoke_role_permissions() -> Result<()> { let grant_role = Grant::role(role_id.clone(), alice_id.clone()); let grant_role_tx = TransactionBuilder::new(chain_id.clone(), mouse_id.clone()) .with_instructions([grant_role]) - .sign(&mouse_keypair); + .sign(mouse_keypair.private_key()); test_client.submit_transaction_blocking(&grant_role_tx)?; let set_key_value = SetKeyValue::account( @@ -263,7 +263,7 @@ fn grant_revoke_role_permissions() -> Result<()> { // Alice can modify Mouse's metadata after permission token is granted to role let grant_role_permission_tx = TransactionBuilder::new(chain_id.clone(), mouse_id.clone()) .with_instructions([grant_role_permission]) - .sign(&mouse_keypair); + .sign(mouse_keypair.private_key()); test_client.submit_transaction_blocking(&grant_role_permission_tx)?; let found_permissions = test_client .request(FindPermissionsByAccountId::new(alice_id.clone()))? @@ -274,7 +274,7 @@ fn grant_revoke_role_permissions() -> Result<()> { // Alice can't modify Mouse's metadata after permission token is removed from role let revoke_role_permission_tx = TransactionBuilder::new(chain_id.clone(), mouse_id.clone()) .with_instructions([revoke_role_permission]) - .sign(&mouse_keypair); + .sign(mouse_keypair.private_key()); test_client.submit_transaction_blocking(&revoke_role_permission_tx)?; let found_permissions = test_client .request(FindPermissionsByAccountId::new(alice_id.clone()))? diff --git a/client/tests/integration/tx_chain_id.rs b/client/tests/integration/tx_chain_id.rs index 53dd1a5617a..f0ac7d63f88 100644 --- a/client/tests/integration/tx_chain_id.rs +++ b/client/tests/integration/tx_chain_id.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use iroha_client::data_model::prelude::*; +use iroha::data_model::prelude::*; use iroha_primitives::numeric::numeric; use test_network::*; use test_samples::gen_account_in; @@ -44,10 +44,10 @@ fn send_tx_with_different_chain_id() { ); let asset_transfer_tx_0 = TransactionBuilder::new(chain_id_0, sender_id.clone()) .with_instructions([transfer_instruction.clone()]) - .sign(&sender_keypair); + .sign(sender_keypair.private_key()); let asset_transfer_tx_1 = TransactionBuilder::new(chain_id_1, sender_id.clone()) .with_instructions([transfer_instruction]) - .sign(&sender_keypair); + .sign(sender_keypair.private_key()); test_client .submit_transaction_blocking(&asset_transfer_tx_0) .unwrap(); diff --git a/client/tests/integration/upgrade.rs b/client/tests/integration/upgrade.rs index 1a4f998d87f..97455d66c11 100644 --- a/client/tests/integration/upgrade.rs +++ b/client/tests/integration/upgrade.rs @@ -4,7 +4,6 @@ use eyre::Result; use futures_util::TryStreamExt as _; use iroha::{ client::{self, Client, QueryResult}, - crypto::KeyPair, data_model::prelude::*, }; use iroha_logger::info; @@ -23,11 +22,9 @@ fn executor_upgrade_should_work() -> Result<()> { let admin_id: AccountId = format!("{ADMIN_PUBLIC_KEY_MULTIHASH}@admin") .parse() .unwrap(); - let admin_keypair = KeyPair::new( - admin_id.signatory().clone(), - ADMIN_PRIVATE_KEY_MULTIHASH.parse().unwrap(), - ) - .unwrap(); + let admin_private_key = ADMIN_PRIVATE_KEY_MULTIHASH + .parse::() + .unwrap(); let (_rt, _peer, client) = ::new().with_port(10_795).start_with_runtime(); wait_for_genesis_committed(&vec![client.clone()], 0); @@ -48,7 +45,7 @@ fn executor_upgrade_should_work() -> Result<()> { let transfer_alice_rose = Transfer::asset_numeric(alice_rose, 1u32, admin_id.clone()); let transfer_rose_tx = TransactionBuilder::new(chain_id.clone(), admin_id.clone()) .with_instructions([transfer_alice_rose.clone()]) - .sign(&admin_keypair); + .sign(&admin_private_key); let _ = client .submit_transaction_blocking(&transfer_rose_tx) .expect_err("Should fail"); @@ -62,7 +59,7 @@ fn executor_upgrade_should_work() -> Result<()> { // Creating new transaction instead of cloning, because we need to update it's creation time let transfer_rose_tx = TransactionBuilder::new(chain_id, admin_id.clone()) .with_instructions([transfer_alice_rose]) - .sign(&admin_keypair); + .sign(&admin_private_key); client .submit_transaction_blocking(&transfer_rose_tx) .expect("Should succeed"); diff --git a/configs/swarm/executor.wasm b/configs/swarm/executor.wasm index e7188d6cbaa40e4622b21841d5acb7a8e4bba6c7..2e81bade893a2ef8b7e9457eff53133b700f7909 100644 GIT binary patch delta 89281 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 delta 89574 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 b4>Sb&05lG|3i<-{Z_qa&xLKC&Dw diff --git a/core/benches/blocks/apply_blocks.rs b/core/benches/blocks/apply_blocks.rs index 28daef9e720..eef0b25d7dd 100644 --- a/core/benches/blocks/apply_blocks.rs +++ b/core/benches/blocks/apply_blocks.rs @@ -44,7 +44,7 @@ impl StateApplyBlocks { &mut state_block, instructions, alice_id.clone(), - &alice_keypair, + alice_keypair.private_key(), ); let _events = state_block.apply_without_execution(&block); state_block.commit(); diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index 13eedfb89ed..879ccbcc931 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -25,25 +25,25 @@ pub fn create_block( state: &mut StateBlock<'_>, instructions: Vec, account_id: AccountId, - key_pair: &KeyPair, + private_key: &PrivateKey, ) -> CommittedBlock { let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); let transaction = TransactionBuilder::new(chain_id.clone(), account_id) .with_instructions(instructions) - .sign(key_pair); + .sign(private_key); let limits = state.transaction_executor().transaction_limits; 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()], + vec![AcceptedTransaction::accept(transaction, &chain_id, limits).unwrap()], topology.clone(), Vec::new(), ) .chain(0, state) - .sign(key_pair.private_key()) + .sign(private_key) .unpack(|_| {}) .commit(&topology) .unpack(|_| {}) diff --git a/core/benches/blocks/validate_blocks.rs b/core/benches/blocks/validate_blocks.rs index 0d26e9ed407..c9e7e082ab4 100644 --- a/core/benches/blocks/validate_blocks.rs +++ b/core/benches/blocks/validate_blocks.rs @@ -10,7 +10,7 @@ use common::*; pub struct StateValidateBlocks { state: State, instructions: Vec>, - key_pair: KeyPair, + private_key: PrivateKey, account_id: AccountId, } @@ -41,7 +41,7 @@ impl StateValidateBlocks { Self { state, instructions, - key_pair: alice_keypair, + private_key: alice_keypair.private_key().clone(), account_id: alice_id, } } @@ -58,7 +58,7 @@ impl StateValidateBlocks { Self { state, instructions, - key_pair, + private_key, account_id, }: Self, ) { @@ -68,7 +68,7 @@ impl StateValidateBlocks { &mut state_block, instructions, account_id.clone(), - &key_pair, + &private_key, ); let _events = state_block.apply_without_execution(&block); assert_eq!(state_block.height(), i); diff --git a/core/benches/kura.rs b/core/benches/kura.rs index 7f15ed8d8be..bc3b55a49e6 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -28,12 +28,12 @@ async fn measure_block_size_for_n_executors(n_executors: u32) { let transfer = Transfer::asset_numeric(alice_xor_id, 10u32, bob_id); let tx = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([transfer]) - .sign(&alice_keypair); + .sign(alice_keypair.private_key()); let transaction_limits = TransactionLimits { max_instruction_number: 4096, max_wasm_size_bytes: 0, }; - let tx = AcceptedTransaction::accept(tx, &chain_id, &transaction_limits) + let tx = AcceptedTransaction::accept(tx, &chain_id, transaction_limits) .expect("Failed to accept Transaction."); let dir = tempfile::tempdir().expect("Could not create tempfile."); let cfg = Config { diff --git a/core/benches/validation.rs b/core/benches/validation.rs index a4c43460399..009e39fe00a 100644 --- a/core/benches/validation.rs +++ b/core/benches/validation.rs @@ -80,12 +80,12 @@ fn build_test_and_transient_state() -> State { fn accept_transaction(criterion: &mut Criterion) { let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); - let transaction = build_test_transaction(chain_id.clone()).sign(&STARTER_KEYPAIR); + let transaction = build_test_transaction(chain_id.clone()).sign(STARTER_KEYPAIR.private_key()); let mut success_count = 0; let mut failures_count = 0; let _ = criterion.bench_function("accept", |b| { b.iter(|| { - match AcceptedTransaction::accept(transaction.clone(), &chain_id, &TRANSACTION_LIMITS) { + match AcceptedTransaction::accept(transaction.clone(), &chain_id, TRANSACTION_LIMITS) { Ok(_) => success_count += 1, Err(_) => failures_count += 1, } @@ -98,13 +98,13 @@ fn sign_transaction(criterion: &mut Criterion) { let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); let transaction = build_test_transaction(chain_id); - let key_pair = KeyPair::random(); + let (_, private_key) = KeyPair::random().into_parts(); let mut count = 0; let _ = criterion.bench_function("sign", |b| { b.iter_batched( || transaction.clone(), |transaction| { - let _: SignedTransaction = transaction.sign(&key_pair); + let _: SignedTransaction = transaction.sign(&private_key); count += 1; }, BatchSize::SmallInput, @@ -117,9 +117,9 @@ fn validate_transaction(criterion: &mut Criterion) { let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); let transaction = AcceptedTransaction::accept( - build_test_transaction(chain_id.clone()).sign(&STARTER_KEYPAIR), + build_test_transaction(chain_id.clone()).sign(STARTER_KEYPAIR.private_key()), &chain_id, - &TRANSACTION_LIMITS, + TRANSACTION_LIMITS, ) .expect("Failed to accept transaction."); let mut success_count = 0; @@ -142,9 +142,9 @@ fn sign_blocks(criterion: &mut Criterion) { let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); let transaction = AcceptedTransaction::accept( - build_test_transaction(chain_id.clone()).sign(&STARTER_KEYPAIR), + build_test_transaction(chain_id.clone()).sign(STARTER_KEYPAIR.private_key()), &chain_id, - &TRANSACTION_LIMITS, + TRANSACTION_LIMITS, ) .expect("Failed to accept transaction."); let kura = iroha_core::kura::Kura::blank_kura_for_testing(); diff --git a/core/src/block.rs b/core/src/block.rs index 064bde62ec4..6e5cacdcaad 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -132,8 +132,6 @@ mod pending { commit_topology: Topology, event_recommendations: Vec, ) -> Self { - assert!(!transactions.is_empty(), "Empty block created"); - Self(Pending { commit_topology, transactions, @@ -154,14 +152,17 @@ mod pending { .iter() .map(|value| value.as_ref().hash()) .collect::>() - .hash(), + .hash() + .expect("INTERNAL BUG: Empty block created"), timestamp_ms: SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("Failed to get the current system time") .as_millis() .try_into() .expect("Time should fit into u64"), - view_change_index: view_change_index as u32, + view_change_index: view_change_index + .try_into() + .expect("View change index should fit into u32"), consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION .as_millis() .try_into() @@ -239,7 +240,7 @@ mod chained { mod valid { use indexmap::IndexMap; - use iroha_data_model::ChainId; + use iroha_data_model::{account::AccountId, ChainId}; use super::*; use crate::{state::StateBlock, sumeragi::network_topology::Role}; @@ -289,24 +290,14 @@ mod valid { block: &SignedBlock, topology: &Topology, ) -> Result<(), SignatureVerificationError> { - // TODO: ? - //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]; + let valid_roles: &[Role] = if topology.view_change_index() >= 1 { + &[Role::ValidatingPeer, Role::ObservingPeer] + } else { + &[Role::ValidatingPeer] + }; topology - .filter_signatures_by_roles(roles, block.signatures()) + .filter_signatures_by_roles(valid_roles, block.signatures()) .try_fold(IndexMap::::default(), |mut acc, signature| { let signatory_idx = usize::try_from(signature.0) .map_err(|_err| SignatureVerificationError::UnknownSignatory)?; @@ -333,6 +324,13 @@ mod valid { Ok(()) })?; + Ok(()) + } + + fn verify_no_undefined_signatures( + block: &SignedBlock, + topology: &Topology, + ) -> Result<(), SignatureVerificationError> { if topology .filter_signatures_by_roles(&[Role::Undefined], block.signatures()) .next() @@ -398,7 +396,7 @@ mod valid { block: SignedBlock, topology: &Topology, expected_chain_id: &ChainId, - genesis_public_key: &PublicKey, + genesis_account: &AccountId, state_block: &mut StateBlock<'_>, ) -> WithEvents> { let expected_block_height = state_block.height() + 1; @@ -430,6 +428,7 @@ mod valid { if !block.header().is_genesis() { if let Err(err) = Self::verify_leader_signature(&block, topology) .and_then(|()| Self::verify_validator_signatures(&block, topology)) + .and_then(|()| Self::verify_no_undefined_signatures(&block, topology)) { return WithEvents::new(Err((block, err.into()))); } @@ -460,12 +459,9 @@ mod valid { ))); } - if let Err(error) = Self::validate_transactions( - &block, - expected_chain_id, - genesis_public_key, - state_block, - ) { + if let Err(error) = + Self::validate_transactions(&block, expected_chain_id, genesis_account, state_block) + { return WithEvents::new(Err((block, error.into()))); } @@ -475,7 +471,7 @@ mod valid { fn validate_transactions( block: &SignedBlock, expected_chain_id: &ChainId, - genesis_public_key: &PublicKey, + genesis_account: &AccountId, state_block: &mut StateBlock<'_>, ) -> Result<(), TransactionValidationError> { let is_genesis = block.header().is_genesis(); @@ -486,16 +482,19 @@ mod valid { .cloned() .try_for_each(|CommittedTransaction { value, error }| { let transaction_executor = state_block.transaction_executor(); - let limits = &transaction_executor.transaction_limits; let tx = if is_genesis { AcceptedTransaction::accept_genesis( GenesisTransaction(value), expected_chain_id, - genesis_public_key, + genesis_account, ) } else { - AcceptedTransaction::accept(value, expected_chain_id, limits) + AcceptedTransaction::accept( + value, + expected_chain_id, + transaction_executor.transaction_limits, + ) }?; if error.is_some() { @@ -522,9 +521,21 @@ mod valid { &mut self, signature: BlockSignature, topology: &Topology, - ) -> Result<(), iroha_crypto::Error> { - let signatory = &topology.as_ref()[signature.0 as usize]; - self.0.add_signature(signature, signatory.public_key()) + ) -> Result<(), SignatureVerificationError> { + let signatory_idx = usize::try_from(signature.0) + .expect("INTERNAL BUG: Number of peers exceeds usize::MAX"); + let signatory = &topology.as_ref()[signatory_idx]; + + assert_ne!(Role::Leader, topology.role(signatory)); + if topology.view_change_index() == 0 { + assert_ne!(Role::ObservingPeer, topology.role(signatory),); + } + assert_ne!(Role::Undefined, topology.role(signatory)); + assert_ne!(Role::ProxyTail, topology.role(signatory)); + + self.0 + .add_signature(signature, signatory.public_key()) + .map_err(|_err| SignatureVerificationError::UnknownSignature) } /// Replace block's signatures. Returns previous block signatures @@ -544,6 +555,7 @@ mod valid { if let Err(err) = Self::verify_leader_signature(self.as_ref(), topology) .and_then(|()| Self::verify_validator_signatures(self.as_ref(), topology)) + .and_then(|()| Self::verify_no_undefined_signatures(self.as_ref(), topology)) { self.0.replace_signatures_unchecked(prev_signatures); WithEvents::new(Err(err)) @@ -604,7 +616,9 @@ mod valid { header: BlockHeader { height: 2, previous_block_hash: None, - transactions_hash: None, + transactions_hash: HashOf::from_untyped_unchecked(Hash::prehashed( + [1; Hash::LENGTH], + )), timestamp_ms: 0, view_change_index: 0, consensus_estimation_ms: 0, @@ -635,13 +649,9 @@ mod valid { #[cfg(test)] mod tests { use iroha_crypto::SignatureOf; - use iroha_primitives::unique_vec::UniqueVec; use super::*; - use crate::{ - kura::Kura, query::store::LiveQueryStore, state::State, - sumeragi::network_topology::test_peers, - }; + use crate::sumeragi::network_topology::test_peers; #[test] fn signature_verification_ok() { @@ -892,7 +902,7 @@ mod tests { use iroha_data_model::prelude::*; use iroha_genesis::GENESIS_DOMAIN_ID; use iroha_primitives::unique_vec::UniqueVec; - use test_samples::gen_account_in; + use test_samples::{gen_account_in, SAMPLE_GENESIS_ACCOUNT_ID}; use super::*; use crate::{ @@ -937,10 +947,10 @@ mod tests { Register::asset_definition(AssetDefinition::numeric(asset_definition_id)); // Making two transactions that have the same instruction - let transaction_limits = &state_block.transaction_executor().transaction_limits; + let transaction_limits = state_block.transaction_executor().transaction_limits; let tx = TransactionBuilder::new(chain_id.clone(), alice_id) .with_instructions([create_asset_definition]) - .sign(&alice_keypair); + .sign(alice_keypair.private_key()); let tx = AcceptedTransaction::accept(tx, &chain_id, transaction_limits).expect("Valid"); // Creating a block of two identical transactions and validating it @@ -994,10 +1004,10 @@ mod tests { Register::asset_definition(AssetDefinition::numeric(asset_definition_id.clone())); // Making two transactions that have the same instruction - let transaction_limits = &state_block.transaction_executor().transaction_limits; + let transaction_limits = state_block.transaction_executor().transaction_limits; let tx = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([create_asset_definition]) - .sign(&alice_keypair); + .sign(alice_keypair.private_key()); let tx = AcceptedTransaction::accept(tx, &chain_id, transaction_limits).expect("Valid"); let fail_mint = Mint::asset_numeric( @@ -1010,12 +1020,12 @@ mod tests { let tx0 = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([fail_mint]) - .sign(&alice_keypair); + .sign(alice_keypair.private_key()); let tx0 = AcceptedTransaction::accept(tx0, &chain_id, transaction_limits).expect("Valid"); let tx2 = TransactionBuilder::new(chain_id.clone(), alice_id) .with_instructions([succeed_mint]) - .sign(&alice_keypair); + .sign(alice_keypair.private_key()); let tx2 = AcceptedTransaction::accept(tx2, &chain_id, transaction_limits).expect("Valid"); // Creating a block of two identical transactions and validating it @@ -1065,7 +1075,7 @@ mod tests { let query_handle = LiveQueryStore::test().start(); let state = State::new(world, kura, query_handle); let mut state_block = state.block(); - let transaction_limits = &state_block.transaction_executor().transaction_limits; + let transaction_limits = state_block.transaction_executor().transaction_limits; let domain_id = DomainId::from_str("domain").expect("Valid"); let create_domain = Register::domain(Domain::new(domain_id)); @@ -1079,12 +1089,12 @@ mod tests { let instructions_accept: [InstructionBox; 2] = [create_domain.into(), create_asset.into()]; let tx_fail = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions(instructions_fail) - .sign(&alice_keypair); + .sign(alice_keypair.private_key()); let tx_fail = AcceptedTransaction::accept(tx_fail, &chain_id, transaction_limits).expect("Valid"); let tx_accept = TransactionBuilder::new(chain_id.clone(), alice_id) .with_instructions(instructions_accept) - .sign(&alice_keypair); + .sign(alice_keypair.private_key()); let tx_accept = AcceptedTransaction::accept(tx_accept, &chain_id, transaction_limits).expect("Valid"); @@ -1159,11 +1169,11 @@ mod tests { // Sign with `genesis_wrong_key` as peer which has incorrect genesis key pair let tx = TransactionBuilder::new(chain_id.clone(), genesis_wrong_account_id.clone()) .with_instructions([isi]) - .sign(&genesis_wrong_key); + .sign(genesis_wrong_key.private_key()); let tx = AcceptedTransaction::accept_genesis( iroha_genesis::GenesisTransaction(tx), &chain_id, - genesis_wrong_key.public_key(), + &SAMPLE_GENESIS_ACCOUNT_ID, ) .expect("Valid"); @@ -1184,7 +1194,7 @@ mod tests { block, &topology, &chain_id, - genesis_correct_key.public_key(), + &SAMPLE_GENESIS_ACCOUNT_ID, &mut state_block, ) .unpack(|_| {}) diff --git a/core/src/gossiper.rs b/core/src/gossiper.rs index c964bbb151f..4f5018aa9f6 100644 --- a/core/src/gossiper.rs +++ b/core/src/gossiper.rs @@ -110,7 +110,7 @@ impl TransactionGossiper { let state_view = self.state.view(); for tx in txs { - let transaction_limits = &state_view.config.transaction_limits; + let transaction_limits = state_view.config.transaction_limits; match AcceptedTransaction::accept(tx, &self.chain_id, transaction_limits) { Ok(tx) => match self.queue.push(tx, &state_view) { diff --git a/core/src/queue.rs b/core/src/queue.rs index 1fb321da655..1e343eb4033 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -22,12 +22,6 @@ use crate::{prelude::*, EventsSender}; impl AcceptedTransaction { // TODO: We should have another type of transaction like `CheckedTransaction` in the type system? - fn is_signatory_consistent(&self) -> bool { - let authority = self.as_ref().authority(); - let signatory = &self.as_ref().signature().0; - authority.signatory_matches(signatory) - } - /// Check if [`self`] is committed or rejected. fn is_in_blockchain(&self, state_view: &StateView<'_>) -> bool { state_view.has_transaction(self.as_ref().hash()) @@ -77,8 +71,6 @@ pub enum Error { MaximumTransactionsPerUser, /// The transaction is already in the queue IsInQueue, - /// Signatories in signature and payload mismatch - SignatoryInconsistent, } /// Failure that can pop up when pushing transaction into the queue @@ -175,8 +167,6 @@ impl Queue { Err(Error::Expired) } else if tx.is_in_blockchain(state_view) { Err(Error::InBlockchain) - } else if !tx.is_signatory_consistent() { - Err(Error::SignatoryInconsistent) } else { Ok(()) } @@ -436,12 +426,12 @@ pub mod tests { let tx = TransactionBuilder::new_with_time_source(chain_id.clone(), account_id, time_source) .with_instructions(instructions) - .sign(key_pair); + .sign(key_pair.private_key()); let limits = TransactionLimits { max_instruction_number: 4096, max_wasm_size_bytes: 0, }; - AcceptedTransaction::accept(tx, &chain_id, &limits).expect("Failed to accept Transaction.") + AcceptedTransaction::accept(tx, &chain_id, limits).expect("Failed to accept Transaction.") } pub fn world_with_test_domains() -> World { @@ -687,13 +677,13 @@ pub mod tests { TransactionBuilder::new_with_time_source(chain_id.clone(), alice_id, &time_source) .with_instructions(instructions); tx.set_ttl(Duration::from_millis(TTL_MS)); - let tx = tx.sign(&alice_keypair); + let tx = tx.sign(alice_keypair.private_key()); let limits = TransactionLimits { max_instruction_number: 4096, max_wasm_size_bytes: 0, }; let tx_hash = tx.hash(); - let tx = AcceptedTransaction::accept(tx, &chain_id, &limits) + let tx = AcceptedTransaction::accept(tx, &chain_id, limits) .expect("Failed to accept Transaction."); queue .push(tx.clone(), &state_view) diff --git a/core/src/smartcontracts/isi/mod.rs b/core/src/smartcontracts/isi/mod.rs index 659f778ecea..052a3ac5ebd 100644 --- a/core/src/smartcontracts/isi/mod.rs +++ b/core/src/smartcontracts/isi/mod.rs @@ -493,8 +493,8 @@ mod tests { let instructions: [InstructionBox; 0] = []; let tx = TransactionBuilder::new(chain_id.clone(), SAMPLE_GENESIS_ACCOUNT_ID.clone()) .with_instructions(instructions) - .sign(&SAMPLE_GENESIS_ACCOUNT_KEYPAIR); - let tx_limits = &state_block.transaction_executor().transaction_limits; + .sign(SAMPLE_GENESIS_ACCOUNT_KEYPAIR.private_key()); + let tx_limits = state_block.transaction_executor().transaction_limits; assert!(matches!( AcceptedTransaction::accept(tx, &chain_id, tx_limits), Err(AcceptTransactionFail::UnexpectedGenesisAccountSignature) diff --git a/core/src/smartcontracts/isi/query.rs b/core/src/smartcontracts/isi/query.rs index 39d7038fac6..e027929ab83 100644 --- a/core/src/smartcontracts/isi/query.rs +++ b/core/src/smartcontracts/isi/query.rs @@ -182,12 +182,6 @@ impl ValidQueryRequest { query: SignedQuery, state_ro: &impl StateReadOnly, ) -> Result { - if !query.authority().signatory_matches(&query.signature().0) { - return Err(Error::Signature(String::from( - "Signature public key doesn't correspond to the account.", - )) - .into()); - } state_ro.world().executor().validate_query( state_ro, query.authority(), @@ -395,15 +389,15 @@ mod tests { let instructions: [InstructionBox; 0] = []; let tx = TransactionBuilder::new(chain_id.clone(), ALICE_ID.clone()) .with_instructions(instructions) - .sign(&ALICE_KEYPAIR); - AcceptedTransaction::accept(tx, &chain_id, &limits)? + .sign(ALICE_KEYPAIR.private_key()); + AcceptedTransaction::accept(tx, &chain_id, limits)? }; let invalid_tx = { let isi = Fail::new("fail".to_owned()); let tx = TransactionBuilder::new(chain_id.clone(), ALICE_ID.clone()) .with_instructions([isi.clone(), isi]) - .sign(&ALICE_KEYPAIR); - AcceptedTransaction::accept(tx, &chain_id, &huge_limits)? + .sign(ALICE_KEYPAIR.private_key()); + AcceptedTransaction::accept(tx, &chain_id, huge_limits)? }; let mut transactions = vec![valid_tx; valid_tx_per_block]; @@ -560,9 +554,9 @@ mod tests { let instructions: [InstructionBox; 0] = []; let tx = TransactionBuilder::new(chain_id.clone(), ALICE_ID.clone()) .with_instructions(instructions) - .sign(&ALICE_KEYPAIR); + .sign(ALICE_KEYPAIR.private_key()); - let tx_limits = &state_block.transaction_executor().transaction_limits; + let tx_limits = state_block.transaction_executor().transaction_limits; let va_tx = AcceptedTransaction::accept(tx, &chain_id, tx_limits)?; let (peer_public_key, _) = KeyPair::random().into_parts(); @@ -584,7 +578,7 @@ mod tests { let unapplied_tx = TransactionBuilder::new(chain_id, ALICE_ID.clone()) .with_instructions([Unregister::account(gen_account_in("domain").0)]) - .sign(&ALICE_KEYPAIR); + .sign(ALICE_KEYPAIR.private_key()); let wrong_hash = unapplied_tx.hash(); let not_found = FindTransactionByHash::new(wrong_hash).execute(&state_view); assert!(matches!( diff --git a/core/src/snapshot.rs b/core/src/snapshot.rs index 08c908e0b73..a2d100e86bb 100644 --- a/core/src/snapshot.rs +++ b/core/src/snapshot.rs @@ -161,7 +161,10 @@ pub fn try_read_snapshot( }; let state = seed.deserialize(&mut deserializer)?; let state_view = state.view(); - let snapshot_height = state_view.height() as usize; + let snapshot_height = state_view + .height() + .try_into() + .expect("Blockchain height should fit into usize"); if snapshot_height > block_count { return Err(TryReadError::MismatchedHeight { snapshot_height, diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index 7bb5e735e74..53fc50d764b 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -1,5 +1,5 @@ //! The main event loop that powers sumeragi. -use std::sync::mpsc; +use std::{collections::BTreeSet, sync::mpsc}; use iroha_crypto::{HashOf, KeyPair}; use iroha_data_model::{block::*, events::pipeline::PipelineEventBox, peer::PeerId}; @@ -209,7 +209,7 @@ impl Sumeragi { fn init_listen_for_genesis( &mut self, - genesis_public_key: &PublicKey, + genesis_account: &AccountId, state: &State, shutdown_receiver: &mut tokio::sync::oneshot::Receiver<()>, ) -> Result<(), EarlyReturn> { @@ -242,7 +242,7 @@ impl Sumeragi { block, &self.topology, &self.chain_id, - genesis_public_key, + genesis_account, &mut state_block, ) .unpack(|e| self.send_event(e)) @@ -286,10 +286,10 @@ impl Sumeragi { } } - fn sumeragi_init_commit_genesis( + fn init_commit_genesis( &mut self, genesis_network: GenesisNetwork, - genesis_public_key: &PublicKey, + genesis_account: &AccountId, state: &State, ) { std::thread::sleep(Duration::from_millis(250)); // TODO: Why this sleep? @@ -303,7 +303,7 @@ impl Sumeragi { let transactions: Vec<_> = genesis_network .into_transactions() .into_iter() - .map(|tx| AcceptedTransaction::accept_genesis(tx, &self.chain_id, genesis_public_key)) + .map(|tx| AcceptedTransaction::accept_genesis(tx, &self.chain_id, genesis_account)) .collect::>() .expect("Genesis invalid"); @@ -353,14 +353,18 @@ impl Sumeragi { .block_committed(block.as_ref(), state_block.world.peers().cloned()); self.connect_peers(&self.topology); + let block_hash = block.as_ref().hash(); + let block_height = block.as_ref().header().height(); + Strategy::kura_store_block(&self.kura, block); + // Commit new block making it's effect visible for the rest of application state_block.commit(); info!( peer_id=%self.peer_id, %prev_role, next_role=%self.role(), - block_hash=%block.as_ref().hash(), - new_height=%block.as_ref().header().height, + block_hash=%block_hash, + new_height=%block_height, "{}", Strategy::LOG_MESSAGE, ); #[cfg(debug_assertions)] @@ -371,8 +375,6 @@ impl Sumeragi { "Topology after commit" ); - Strategy::kura_store_block(&self.kura, block); - // NOTE: This sends `BlockStatus::Applied` event, // so it should be done AFTER public facing state update state_events.into_iter().for_each(|e| self.send_event(e)); @@ -408,7 +410,7 @@ impl Sumeragi { &self, state: &'state State, topology: &Topology, - genesis_public_key: &PublicKey, + genesis_account: &AccountId, BlockCreated { block }: BlockCreated, ) -> Option> { let mut state_block = state.block(); @@ -417,7 +419,7 @@ impl Sumeragi { block, topology, &self.chain_id, - genesis_public_key, + genesis_account, &mut state_block, ) .unpack(|e| self.send_event(e)) @@ -444,14 +446,15 @@ impl Sumeragi { } #[allow(clippy::too_many_lines)] + #[allow(clippy::too_many_arguments)] fn handle_message<'state>( &mut self, message: BlockMessage, state: &'state State, voting_block: &mut Option>, view_change_index: usize, - genesis_public_key: &PublicKey, - voting_signatures: &mut Vec, + genesis_account: &AccountId, + voting_signatures: &mut BTreeSet, #[cfg_attr(not(debug_assertions), allow(unused_variables))] is_genesis_peer: bool, ) { #[allow(clippy::suspicious_operation_groupings)] @@ -467,9 +470,10 @@ impl Sumeragi { // Release block writer before creating new one // FIX: Restore `voting_block` if `handle_block_sync` returns Err // Currently it's not possible because block writer needs to be released + // Look at https://github.com/hyperledger/iroha/issues/4643 let _ = voting_block.take(); - match handle_block_sync(&self.chain_id, block, state, genesis_public_key, &|e| { + match handle_block_sync(&self.chain_id, block, state, genesis_account, &|e| { self.send_event(e) }) { Ok(BlockSyncOk::CommitBlock(block, state_block, topology)) => { @@ -556,7 +560,7 @@ impl Sumeragi { let _ = voting_block.take(); if let Some(mut v_block) = - self.validate_block(state, topology, genesis_public_key, block_created) + self.validate_block(state, topology, genesis_account, block_created) { v_block.block.sign(&self.key_pair, topology); @@ -567,6 +571,7 @@ impl Sumeragi { } else { // FIX: Restore `voting_block` // Currently it's not possible because block writer needs to be released + // Look at https://github.com/hyperledger/iroha/issues/4643 } } (BlockMessage::BlockCreated(block_created), Role::ObservingPeer) => { @@ -579,7 +584,7 @@ impl Sumeragi { let _ = voting_block.take(); if let Some(mut v_block) = - self.validate_block(state, topology, genesis_public_key, block_created) + self.validate_block(state, topology, genesis_account, block_created) { if view_change_index >= 1 { v_block.block.sign(&self.key_pair, topology); @@ -599,6 +604,7 @@ impl Sumeragi { } else { // FIX: Restore `voting_block` // Currently it's not possible because block writer needs to be released + // Look at https://github.com/hyperledger/iroha/issues/4643 } } (BlockMessage::BlockCreated(block_created), Role::ProxyTail) => { @@ -613,98 +619,165 @@ impl Sumeragi { let _ = voting_block.take(); if let Some(mut valid_block) = - self.validate_block(state, &self.topology, genesis_public_key, block_created) + self.validate_block(state, &self.topology, genesis_account, 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 valid_block, - voting_signatures.drain(..), - &self.topology, - ); + + for signature in core::mem::take(voting_signatures) { + if let Err(error) = + valid_block.block.add_signature(signature, &self.topology) + { + debug!(?error, "Signature not valid"); + } + } *voting_block = self.try_commit_block(valid_block, is_genesis_peer); } else { // FIX: Restore `voting_block` // Currently it's not possible because block writer needs to be released + // Look at https://github.com/hyperledger/iroha/issues/4643 } } - (BlockMessage::BlockSigned(BlockSigned { signatures }), Role::ProxyTail) => { + (BlockMessage::BlockSigned(BlockSigned { hash, signature }), Role::ProxyTail) => { info!( peer_id=%self.peer_id, role=%self.role(), "Received block signatures" ); - let roles: &[Role] = if view_change_index >= 1 { - &[Role::ValidatingPeer, Role::ObservingPeer] - } else { - &[Role::ValidatingPeer] - }; - let valid_signatures = self - .topology - .filter_signatures_by_roles(roles, &signatures) - .cloned(); + if let Ok(signatory_idx) = usize::try_from(signature.0) { + let signatory = &self.topology.as_ref()[signatory_idx]; - if let Some(mut voted_block) = voting_block.take() { - add_signatures::(&mut voted_block, valid_signatures, &self.topology); - *voting_block = self.try_commit_block(voted_block, is_genesis_peer); + match self.topology.role(signatory) { + Role::Leader => error!( + peer_id=%self.peer_id, + role=%self.role(), + "Signatory is leader" + ), + Role::Undefined => error!( + peer_id=%self.peer_id, + role=%self.role(), + "Unknown signatory" + ), + Role::ObservingPeer if view_change_index == 0 => error!( + peer_id=%self.peer_id, + role=%self.role(), + "Signatory is observing peer" + ), + Role::ProxyTail => error!( + peer_id=%self.peer_id, + role=%self.role(), + "Signatory is proxy tail" + ), + _ => { + if let Some(mut voted_block) = voting_block.take() { + let actual_hash = voted_block.block.as_ref().hash_of_payload(); + + if hash != actual_hash { + error!( + peer_id=%self.peer_id, + role=%self.role(), + expected_hash=?hash, + ?actual_hash, + "Block hash mismatch" + ); + } else if let Err(err) = + voted_block.block.add_signature(signature, &self.topology) + { + error!( + peer_id=%self.peer_id, + role=%self.role(), + ?err, + "Signature not valid" + ); + } else { + *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 + if !voting_signatures.insert(signature) { + error!( + peer_id=%self.peer_id, + role=%self.role(), + "Duplicate signature" + ); + } + } + } + } } 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 - voting_signatures.extend(valid_signatures); + error!( + peer_id=%self.peer_id, + role=%self.role(), + "Signatory index exceeds usize::MAX" + ); } } (BlockMessage::BlockCommitted(BlockCommitted { .. }), Role::Leader) if self.topology.is_consensus_required().is_none() => {} ( - BlockMessage::BlockCommitted(BlockCommitted { signatures }), + BlockMessage::BlockCommitted(BlockCommitted { hash, signatures }), Role::Leader | Role::ValidatingPeer | Role::ObservingPeer, ) => { if let Some(mut voted_block) = voting_block.take() { - match voted_block - .block - // 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, &self.topology) - .unpack(|e| self.send_event(e)) - { - Ok(prev_signatures) => { - match voted_block - .block - .commit(&self.topology) - .unpack(|e| self.send_event(e)) - { - Ok(committed_block) => { - self.commit_block(committed_block, voted_block.block_state) - } - Err((mut block, error)) => { - error!( - peer_id=%self.peer_id, - role=%self.role(), - ?error, - "Block failed to be committed" - ); - - block - .replace_signatures(prev_signatures, &self.topology) - .unpack(|e| self.send_event(e)) - .expect("INTERNAL BUG: Failed to replace signatures"); - voted_block.block = block; - *voting_block = Some(voted_block); + let actual_hash = voted_block.block.as_ref().hash(); + + if actual_hash == hash { + match voted_block + .block + // 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 has giveth. + .replace_signatures(signatures, &self.topology) + .unpack(|e| self.send_event(e)) + { + Ok(prev_signatures) => { + match voted_block + .block + .commit(&self.topology) + .unpack(|e| self.send_event(e)) + { + Ok(committed_block) => { + self.commit_block(committed_block, voted_block.state_block) + } + Err((mut block, error)) => { + error!( + peer_id=%self.peer_id, + role=%self.role(), + ?error, + "Block failed to be committed" + ); + + block + .replace_signatures(prev_signatures, &self.topology) + .unpack(|e| self.send_event(e)) + .expect("INTERNAL BUG: Failed to replace signatures"); + voted_block.block = block; + *voting_block = Some(voted_block); + } } } + Err(error) => { + error!( + peer_id=%self.peer_id, + role=%self.role(), + ?error, + "Received incorrect signatures" + ); + + *voting_block = Some(voted_block); + } } - Err(error) => { - error!( - peer_id=%self.peer_id, - role=%self.role(), - ?error, - "Received incorrect signatures" - ); - - *voting_block = Some(voted_block); - } + } else { + error!( + peer_id=%self.peer_id, + role=%self.role(), + expected_hash=?hash, + ?actual_hash, + "Block hash mismatch" + ); } } else { error!( @@ -757,7 +830,7 @@ impl Sumeragi { self.broadcast_packet(msg); } - self.commit_block(committed_block, voting_block.block_state); + self.commit_block(committed_block, voting_block.state_block); return None; } @@ -836,7 +909,7 @@ fn reset_state( was_commit: &mut bool, topology: &mut Topology, voting_block: &mut Option, - voting_signatures: &mut Vec, + voting_signatures: &mut BTreeSet, last_view_change_time: &mut Instant, view_change_time: &mut Duration, ) { @@ -899,26 +972,30 @@ pub(crate) fn run( // Connect peers with initial topology sumeragi.connect_peers(&sumeragi.topology); + let genesis_account = AccountId::new( + iroha_genesis::GENESIS_DOMAIN_ID.clone(), + genesis_network.public_key.clone(), + ); + let span = span!(tracing::Level::TRACE, "genesis").entered(); - let is_genesis_peer = - if state.view().height() == 0 || state.view().latest_block_hash().is_none() { - if let Some(genesis) = genesis_network.genesis { - sumeragi.sumeragi_init_commit_genesis(genesis, &genesis_network.public_key, &state); - true - } else { - if let Err(err) = sumeragi.init_listen_for_genesis( - &genesis_network.public_key, - &state, - &mut shutdown_receiver, - ) { - info!(?err, "Sumeragi Thread is being shut down."); - return; - } - false - } + let is_genesis_peer = if state.view().height() == 0 + || state.view().latest_block_hash().is_none() + { + if let Some(genesis) = genesis_network.genesis { + sumeragi.init_commit_genesis(genesis, &genesis_account, &state); + true } else { + if let Err(err) = + sumeragi.init_listen_for_genesis(&genesis_account, &state, &mut shutdown_receiver) + { + info!(?err, "Sumeragi Thread is being shut down."); + return; + } false - }; + } + } else { + false + }; span.exit(); info!( @@ -929,7 +1006,7 @@ pub(crate) fn run( let mut voting_block = None; // Proxy tail collection of voting block signatures - let mut voting_signatures = Vec::new(); + let mut voting_signatures = BTreeSet::new(); let mut should_sleep = false; let mut view_change_proof_chain = ProofChain::default(); // Duration after which a view change is suggested @@ -1004,7 +1081,7 @@ pub(crate) fn run( &state, &mut voting_block, view_change_index, - &genesis_network.public_key, + &genesis_account, &mut voting_signatures, is_genesis_peer, ); @@ -1085,24 +1162,6 @@ pub(crate) fn run( } } -fn add_signatures( - block: &mut VotingBlock, - signatures: impl IntoIterator, - topology: &Topology, -) { - for signature in signatures { - if let Err(error) = block.block.add_signature(signature, topology) { - let err_msg = "Signature not valid"; - - if EXPECT_VALID { - error!(?error, err_msg); - } else { - debug!(?error, err_msg); - } - } - } -} - /// Type enumerating early return types to reduce cyclomatic /// complexity of the main loop items and allow direct short /// circuiting with the `?` operator. Candidate for `impl @@ -1184,7 +1243,7 @@ fn handle_block_sync<'state, F: Fn(PipelineEventBox)>( chain_id: &ChainId, block: SignedBlock, state: &'state State, - genesis_public_key: &PublicKey, + genesis_account: &AccountId, handle_events: &F, ) -> Result, (SignedBlock, BlockSyncError)> { let block_height = block.header().height; @@ -1234,7 +1293,7 @@ fn handle_block_sync<'state, F: Fn(PipelineEventBox)>( block, &topology, chain_id, - genesis_public_key, + genesis_account, &mut state_block, ) .unpack(handle_events) @@ -1265,6 +1324,7 @@ fn handle_block_sync<'state, F: Fn(PipelineEventBox)>( #[cfg(test)] mod tests { + use iroha_genesis::GENESIS_DOMAIN_ID; use test_samples::gen_account_in; use tokio::test; @@ -1287,10 +1347,13 @@ mod tests { chain_id: &ChainId, topology: &Topology, leader_private_key: &PrivateKey, - ) -> (State, Arc, SignedBlock, PublicKey) { + ) -> (State, Arc, SignedBlock, AccountId) { // Predefined world state let (alice_id, alice_keypair) = gen_account_in("wonderland"); - let genesis_public_key = alice_keypair.public_key().clone(); + let genesis_account = AccountId::new( + GENESIS_DOMAIN_ID.clone(), + alice_keypair.public_key().clone(), + ); let account = Account::new(alice_id.clone()).build(&alice_id); let domain_id = "wonderland".parse().expect("Valid"); let mut domain = Domain::new(domain_id).build(&alice_id); @@ -1308,11 +1371,11 @@ mod tests { // Making two transactions that have the same instruction let tx = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([fail_box]) - .sign(&alice_keypair); + .sign(alice_keypair.private_key()); let tx = AcceptedTransaction::accept( tx, chain_id, - &state_block.transaction_executor().transaction_limits, + state_block.transaction_executor().transaction_limits, ) .expect("Valid"); @@ -1342,21 +1405,21 @@ mod tests { let tx1 = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([create_asset_definition1]) - .sign(&alice_keypair); + .sign(alice_keypair.private_key()); let tx1 = AcceptedTransaction::accept( tx1, chain_id, - &state_block.transaction_executor().transaction_limits, + state_block.transaction_executor().transaction_limits, ) .map(Into::into) .expect("Valid"); let tx2 = TransactionBuilder::new(chain_id.clone(), alice_id) .with_instructions([create_asset_definition2]) - .sign(&alice_keypair); + .sign(alice_keypair.private_key()); let tx2 = AcceptedTransaction::accept( tx2, chain_id, - &state_block.transaction_executor().transaction_limits, + state_block.transaction_executor().transaction_limits, ) .map(Into::into) .expect("Valid"); @@ -1368,7 +1431,7 @@ mod tests { .unpack(|_| {}) }; - (state, kura, block.into(), genesis_public_key) + (state, kura, block.into(), genesis_account) } #[test] diff --git a/core/src/sumeragi/message.rs b/core/src/sumeragi/message.rs index 390763094f7..30bb1ae3ea3 100644 --- a/core/src/sumeragi/message.rs +++ b/core/src/sumeragi/message.rs @@ -1,5 +1,6 @@ //! Contains message structures for p2p communication during consensus. -use iroha_data_model::block::{BlockSignature, SignedBlock}; +use iroha_crypto::HashOf; +use iroha_data_model::block::{BlockPayload, BlockSignature, SignedBlock}; use iroha_macro::*; use parity_scale_codec::{Decode, Encode}; @@ -37,7 +38,6 @@ impl ControlFlowMessage { /// `BlockCreated` message structure. #[derive(Debug, Clone, Decode, Encode)] -#[non_exhaustive] pub struct BlockCreated { /// The corresponding block. pub block: SignedBlock, @@ -54,24 +54,32 @@ impl From<&ValidBlock> for BlockCreated { /// `BlockSigned` message structure. #[derive(Debug, Clone, Decode, Encode)] -#[non_exhaustive] pub struct BlockSigned { - /// Set of signatures. - pub signatures: Vec, + /// Hash of the block being signed. + pub hash: HashOf, + /// Signature of the block + pub signature: BlockSignature, } impl From<&ValidBlock> for BlockSigned { fn from(block: &ValidBlock) -> Self { Self { - signatures: block.as_ref().signatures().cloned().collect(), + hash: block.as_ref().hash_of_payload(), + signature: block + .as_ref() + .signatures() + .last() + .cloned() + .expect("INTERNAL BUG: Block must have at least one signature"), } } } /// `BlockCommitted` message structure. -#[derive(Debug, Clone, Decode, Encode)] -#[non_exhaustive] +#[derive(Debug, Clone, Encode)] pub struct BlockCommitted { + /// Hash of the block being signed. + pub hash: HashOf, /// Set of signatures. pub signatures: Vec, } @@ -79,6 +87,7 @@ pub struct BlockCommitted { impl From<&CommittedBlock> for BlockCommitted { fn from(block: &CommittedBlock) -> Self { Self { + hash: block.as_ref().hash(), signatures: block.as_ref().signatures().cloned().collect(), } } @@ -86,7 +95,6 @@ impl From<&CommittedBlock> for BlockCommitted { /// `BlockSyncUpdate` message structure #[derive(Debug, Clone, Decode, Encode)] -#[non_exhaustive] pub struct BlockSyncUpdate { /// The corresponding block. pub block: SignedBlock, @@ -100,3 +108,56 @@ impl From<&SignedBlock> for BlockSyncUpdate { } } } + +mod candidate { + use indexmap::IndexSet; + use parity_scale_codec::Input; + + use super::*; + + #[derive(Decode)] + struct BlockCommittedCandidate { + /// Hash of the block being signed. + pub hash: HashOf, + /// Set of signatures. + pub signatures: Vec, + } + + impl BlockCommittedCandidate { + fn validate(self) -> Result { + self.validate_signatures()?; + + Ok(BlockCommitted { + hash: self.hash, + signatures: self.signatures, + }) + } + + fn validate_signatures(&self) -> Result<(), &'static str> { + if self.signatures.is_empty() { + return Err("No signatures in block"); + } + + self.signatures + .iter() + .map(|signature| &signature.0) + .try_fold(IndexSet::new(), |mut acc, elem| { + if !acc.insert(elem) { + return Err("Duplicate signature"); + } + + Ok(acc) + })?; + + Ok(()) + } + } + + impl Decode for BlockCommitted { + fn decode(input: &mut I) -> Result { + BlockCommittedCandidate::decode(input)? + .validate() + .map_err(Into::into) + } + } +} diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 6c0fe4d93ae..a0df438d211 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -9,7 +9,7 @@ use std::{ use eyre::Result; use iroha_config::parameters::actual::{Common as CommonConfig, Sumeragi as SumeragiConfig}; -use iroha_data_model::{block::SignedBlock, prelude::*}; +use iroha_data_model::{account::AccountId, block::SignedBlock, prelude::*}; use iroha_genesis::GenesisNetwork; use iroha_logger::prelude::*; use network_topology::{Role, Topology}; @@ -72,7 +72,7 @@ impl SumeragiHandle { fn replay_block( chain_id: &ChainId, - genesis_public_key: &PublicKey, + genesis_account: &AccountId, block: &SignedBlock, state_block: &mut StateBlock<'_>, events_sender: &EventsSender, @@ -85,7 +85,7 @@ impl SumeragiHandle { block.clone(), topology, chain_id, - genesis_public_key, + genesis_account, state_block, ) .unpack(|e| { @@ -178,11 +178,16 @@ impl SumeragiHandle { }; } + let genesis_account = AccountId::new( + iroha_genesis::GENESIS_DOMAIN_ID.clone(), + genesis_network.public_key.clone(), + ); + for block in blocks_iter { let mut state_block = state.block(); Self::replay_block( &common_config.chain_id, - &genesis_network.public_key, + &genesis_account, &block, &mut state_block, &events_sender, @@ -268,7 +273,7 @@ pub struct VotingBlock<'state> { /// At what time has this peer voted for this block pub voted_at: Instant, /// [`WorldState`] after applying transactions to it but before it was committed - pub block_state: StateBlock<'state>, + pub state_block: StateBlock<'state>, } impl AsRef for VotingBlock<'_> { @@ -283,7 +288,7 @@ impl VotingBlock<'_> { VotingBlock { block, voted_at: Instant::now(), - block_state: state_block, + state_block, } } } diff --git a/core/src/sumeragi/network_topology.rs b/core/src/sumeragi/network_topology.rs index 0b7ec997c4b..4b58d52a5b3 100644 --- a/core/src/sumeragi/network_topology.rs +++ b/core/src/sumeragi/network_topology.rs @@ -20,7 +20,12 @@ use iroha_data_model::{ /// /// Above is an illustration of how the various operations work for a f = 2 topology. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Topology(Vec, usize); +pub struct Topology( + /// Ordered set of peers + Vec, + /// Current view change index. Reset to 0 after every block commit + usize, +); /// Topology with at least one peer #[derive(Debug, Clone, PartialEq, Eq, derive_more::Deref)] @@ -147,9 +152,11 @@ impl Topology { }; } - signatures - .into_iter() - .filter(move |signature| filtered.contains(&(signature.0 as usize))) + signatures.into_iter().filter(move |signature| { + filtered.contains( + &(usize::try_from(signature.0).expect("Peer index should fit into usize")), + ) + }) } /// What role does this peer have in the topology. @@ -209,13 +216,17 @@ impl Topology { self.0 = topology.into_iter().map(|(_, peer)| peer).collect(); } - /// Recreate topology for given block + /// Rotate topology after a block has been committed 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.lift_up_peers( + block + .signatures() + .map(|s| s.0.try_into().expect("Peer index should fit into usize")), + ); self.rotate_set_a(); self.update_peer_list(new_peers); self.1 = 0; diff --git a/core/src/sumeragi/view_change.rs b/core/src/sumeragi/view_change.rs index c2a7b80bb46..88a68337780 100644 --- a/core/src/sumeragi/view_change.rs +++ b/core/src/sumeragi/view_change.rs @@ -2,6 +2,7 @@ //! Where view change is a process of changing topology due to some faulty network behavior. use eyre::Result; +use indexmap::IndexSet; use iroha_crypto::{HashOf, PublicKey, SignatureOf}; use iroha_data_model::block::SignedBlock; use parity_scale_codec::{Decode, Encode}; @@ -26,7 +27,7 @@ struct ViewChangeProofPayload { /// Hash of the latest committed block. latest_block: HashOf, /// Within a round, what is the index of the view change this proof is trying to prove. - view_change_index: u64, + view_change_index: u32, } /// 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. @@ -43,7 +44,9 @@ pub struct ProofBuilder(SignedViewChangeProof); impl ProofBuilder { /// Constructor from index. pub fn new(latest_block: HashOf, view_change_index: usize) -> Self { - let view_change_index = view_change_index as u64; + let view_change_index = view_change_index + .try_into() + .expect("INTERNAL BUG: view change index is too large"); let proof = SignedViewChangeProof { payload: ViewChangeProofPayload { @@ -67,13 +70,21 @@ impl ProofBuilder { impl SignedViewChangeProof { /// Verify the signatures of `other` and add them to this proof. fn merge_signatures(&mut self, other: Vec, topology: &Topology) { - for (public_key, signature) in other { - if topology.position(&public_key).is_none() { - continue; - } - - self.signatures.push((public_key, signature)); - } + let signatures = core::mem::take(&mut self.signatures) + .into_iter() + .collect::>(); + + self.signatures = other + .into_iter() + .fold(signatures, |mut acc, (public_key, signature)| { + if topology.position(&public_key).is_some() { + acc.insert((public_key, signature)); + } + + acc + }) + .into_iter() + .collect(); } /// Verify if the proof is valid, given the peers in `topology`. @@ -104,8 +115,10 @@ impl ProofChain { .iter() .enumerate() .take_while(|(i, proof)| { + let view_change_index = proof.payload.view_change_index as usize; + proof.payload.latest_block == latest_block - && proof.payload.view_change_index == (*i as u64) + && view_change_index == *i && proof.verify(topology) }) .count() @@ -118,8 +131,8 @@ impl ProofChain { .iter() .enumerate() .take_while(|(i, proof)| { - proof.payload.latest_block == latest_block - && proof.payload.view_change_index == (*i as u64) + let view_change_index = proof.payload.view_change_index as usize; + proof.payload.latest_block == latest_block && view_change_index == *i }) .count(); self.0.truncate(valid_count); @@ -140,7 +153,7 @@ impl ProofChain { return Err(Error::BlockHashMismatch); } let next_unfinished_view_change = self.verify_with_state(topology, latest_block); - if new_proof.payload.view_change_index != (next_unfinished_view_change as u64) { + if new_proof.payload.view_change_index as usize != next_unfinished_view_change { return Err(Error::ViewChangeNotFound); // We only care about the current view change that may or may not happen. } @@ -216,17 +229,28 @@ mod candidate { fn validate(self) -> Result { self.validate_signatures()?; - // TODO: If it is possible, we should instead reject decoding proofs that - // have duplicated signatures. This would require change in the code as well - let unique_signatures = self.signatures.into_iter().collect::>(); - Ok(SignedViewChangeProof { + signatures: self.signatures, payload: self.payload, - signatures: unique_signatures.into_iter().collect(), }) } fn validate_signatures(&self) -> Result<(), &'static str> { + if self.signatures.is_empty() { + return Err("Proof missing signatures"); + } + + self.signatures + .iter() + .map(|signature| &signature.0) + .try_fold(IndexSet::new(), |mut acc, elem| { + if !acc.insert(elem) { + return Err("Duplicate signature"); + } + + Ok(acc) + })?; + self.signatures .iter() .try_for_each(|(public_key, payload)| { diff --git a/core/src/tx.rs b/core/src/tx.rs index efbd0170f82..624a77f9918 100644 --- a/core/src/tx.rs +++ b/core/src/tx.rs @@ -14,9 +14,7 @@ pub use iroha_data_model::prelude::*; use iroha_data_model::{ isi::error::Mismatch, query::error::FindError, - transaction::{ - error::TransactionLimitError, TransactionLimits, TransactionPayload, TransactionSignature, - }, + transaction::{error::TransactionLimitError, TransactionLimits, TransactionPayload}, }; use iroha_genesis::GenesisTransaction; use iroha_logger::{debug, error}; @@ -71,7 +69,7 @@ impl AcceptedTransaction { pub fn accept_genesis( tx: GenesisTransaction, expected_chain_id: &ChainId, - genesis_public_key: &PublicKey, + genesis_account: &AccountId, ) -> Result { let actual_chain_id = tx.0.chain_id(); @@ -82,13 +80,8 @@ impl AcceptedTransaction { })); } - let TransactionSignature(public_key, signature) = tx.0.signature(); - if public_key != genesis_public_key { - return Err(SignatureVerificationFail { - signature: signature.clone(), - reason: "Signature doesn't correspond to genesis public key".to_string(), - } - .into()); + if genesis_account != tx.0.authority() { + return Err(AcceptTransactionFail::UnexpectedGenesisAccountSignature); } Ok(Self(tx.0)) @@ -102,7 +95,7 @@ impl AcceptedTransaction { pub fn accept( tx: SignedTransaction, expected_chain_id: &ChainId, - limits: &TransactionLimits, + limits: TransactionLimits, ) -> Result { let actual_chain_id = tx.chain_id(); @@ -113,10 +106,6 @@ impl AcceptedTransaction { })); } - if *iroha_genesis::GENESIS_DOMAIN_ID == *tx.authority().domain_id() { - return Err(AcceptTransactionFail::UnexpectedGenesisAccountSignature); - } - match &tx.instructions() { Executable::Instructions(instructions) => { let instruction_count = instructions.len(); diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index b63ddd92a50..16e3a850e22 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -1,7 +1,5 @@ //! Module for starting peers and networks. Used only for tests use core::{fmt::Debug, str::FromStr as _, time::Duration}; -#[cfg(debug_assertions)] -use std::sync::atomic::AtomicBool; use std::{collections::BTreeMap, ops::Deref, path::Path, sync::Arc, thread}; use eyre::Result; @@ -138,7 +136,7 @@ impl TestGenesis for GenesisNetwork { impl Network { /// Collect the freeze handles from all the peers in the network. #[cfg(debug_assertions)] - pub fn get_freeze_status_handles(&self) -> Vec { + pub fn get_freeze_status_handles(&self) -> Vec { self.peers() .filter_map(|peer| peer.irohad.as_ref()) .map(|iroha| iroha.freeze_status()) diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index aa7270b6e00..dfe5fa32471 100755 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -839,12 +839,6 @@ pub mod error { /// Returned when an error occurs during key generation #[display(fmt = "Key generation failed. {_0}")] KeyGen(String), - /// Returned when an error occurs during digest generation - #[display(fmt = "Digest generation failed. {_0}")] - DigestGen(String), - /// Returned when an error occurs during creation of [`SignaturesOf`] - #[display(fmt = "`SignaturesOf` must contain at least one signature")] - EmptySignatureIter, /// A General purpose error message that doesn't fit in any category #[display(fmt = "General error. {_0}")] // This is going to cause a headache Other(String), diff --git a/data_model/src/block.rs b/data_model/src/block.rs index 6c24e10ba5e..ba0c09c8ab7 100644 --- a/data_model/src/block.rs +++ b/data_model/src/block.rs @@ -54,12 +54,11 @@ mod model { #[getset(get_copy = "pub")] pub height: u64, /// Hash of the previous block in the chain. - #[getset(get = "pub")] + #[getset(get_copy = "pub")] pub previous_block_hash: Option>, /// Hash of merkle tree root of transactions' hashes. - #[getset(get = "pub")] - // TODO: How can it be `None`??? - pub transactions_hash: Option>>, + #[getset(get_copy = "pub")] + pub transactions_hash: HashOf>, /// Creation timestamp (unix time in milliseconds). #[getset(skip)] pub timestamp_ms: u64, @@ -231,6 +230,12 @@ impl SignedBlock { signature: BlockSignature, public_key: &iroha_crypto::PublicKey, ) -> Result<(), iroha_crypto::Error> { + if self.signatures().any(|s| signature.0 == s.0) { + return Err(iroha_crypto::Error::Signing( + "Duplicate signature".to_owned(), + )); + } + signature.1.verify(public_key, self.payload())?; let SignedBlock::V1(block) = self; @@ -292,10 +297,6 @@ mod candidate { self.validate_signatures()?; self.validate_header()?; - if self.payload.transactions.is_empty() { - return Err("Block is empty"); - } - Ok(SignedBlockV1 { signatures: self.signatures, payload: self.payload, @@ -303,8 +304,13 @@ mod candidate { } fn validate_signatures(&self) -> Result<(), &'static str> { + if self.signatures.is_empty() { + return Err("Block missing signatures"); + } + self.signatures .iter() + .map(|signature| signature.0) .try_fold(BTreeSet::new(), |mut acc, elem| { if !acc.insert(elem) { return Err("Duplicate signature in block"); @@ -315,6 +321,7 @@ mod candidate { Ok(()) } + fn validate_header(&self) -> Result<(), &'static str> { let actual_txs_hash = self.payload.header.transactions_hash; @@ -324,7 +331,8 @@ mod candidate { .iter() .map(|value| value.as_ref().hash()) .collect::>() - .hash(); + .hash() + .ok_or("Block is empty")?; if expected_txs_hash != actual_txs_hash { return Err("Transactions' hash incorrect. Expected: {expected_txs_hash:?}, actual: {actual_txs_hash:?}"); diff --git a/data_model/src/events/pipeline.rs b/data_model/src/events/pipeline.rs index 2b7bba7f745..f4708ed9c93 100644 --- a/data_model/src/events/pipeline.rs +++ b/data_model/src/events/pipeline.rs @@ -354,7 +354,9 @@ mod tests { Self { height, previous_block_hash: None, - transactions_hash: None, + transactions_hash: HashOf::from_untyped_unchecked(Hash::prehashed( + [1_u8; Hash::LENGTH], + )), timestamp_ms: 0, view_change_index: 0, consensus_estimation_ms: 0, diff --git a/data_model/src/query/mod.rs b/data_model/src/query/mod.rs index 4bcc2381c76..ea32111d21a 100644 --- a/data_model/src/query/mod.rs +++ b/data_model/src/query/mod.rs @@ -1268,7 +1268,7 @@ pub mod http { Serialize, IntoSchema, )] - pub struct QuerySignature(pub PublicKey, pub SignatureOf); + pub struct QuerySignature(pub SignatureOf); /// I/O ready structure to send queries. #[derive(Debug, Clone, Encode, Serialize, IntoSchema)] @@ -1311,11 +1311,11 @@ pub mod http { impl SignedQueryCandidate { fn validate(self) -> Result { - let QuerySignature(public_key, signature) = &self.signature; + let QuerySignature(signature) = &self.signature; - if signature.verify(public_key, &self.payload).is_err() { - return Err("Query signature not valid"); - } + signature + .verify(&self.payload.authority.signatory, &self.payload) + .map_err(|_| "Query signature is not valid")?; Ok(SignedQueryV1 { payload: self.payload, @@ -1438,7 +1438,7 @@ pub mod http { let signature = SignatureOf::new(key_pair.private_key(), &self.payload); SignedQueryV1 { - signature: QuerySignature(key_pair.public_key().clone(), signature), + signature: QuerySignature(signature), payload: self.payload, } .into() @@ -1488,12 +1488,6 @@ pub mod error { )] #[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum QueryExecutionFail { - /// Query has the wrong signature: {0} - Signature( - #[skip_from] - #[skip_try_from] - String, - ), /// {0} #[cfg_attr(feature = "std", error(transparent))] Find(FindError), diff --git a/data_model/src/transaction.rs b/data_model/src/transaction.rs index 178e06fe0ea..a86e069ced1 100644 --- a/data_model/src/transaction.rs +++ b/data_model/src/transaction.rs @@ -158,10 +158,7 @@ mod model { Serialize, IntoSchema, )] - pub struct TransactionSignature( - pub iroha_crypto::PublicKey, - pub SignatureOf, - ); + pub struct TransactionSignature(pub SignatureOf); /// Transaction that contains a signature /// @@ -263,6 +260,13 @@ declare_versioned!(SignedTransaction 1..2, Debug, Display, Clone, PartialEq, Eq, declare_versioned!(SignedTransaction 1..2, Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord, FromVariant, IntoSchema); impl SignedTransaction { + /// Transaction payload. Used for tests + #[cfg(feature = "transparent_api")] + pub fn payload(&self) -> &TransactionPayload { + let SignedTransaction::V1(tx) = self; + &tx.payload + } + /// Return transaction instructions #[inline] pub fn instructions(&self) -> &Executable { @@ -383,10 +387,11 @@ mod candidate { } fn validate_signature(&self) -> Result<(), &'static str> { - let TransactionSignature(public_key, signature) = &self.signature; + let TransactionSignature(signature) = &self.signature; + signature - .verify(public_key, &self.payload) - .map_err(|_| "Transaction contains invalid signatures")?; + .verify(&self.payload.authority.signatory, &self.payload) + .map_err(|_| "Transaction signature is invalid")?; Ok(()) } @@ -756,11 +761,8 @@ mod http { /// Sign transaction with provided key pair. #[must_use] - pub fn sign(self, key_pair: &iroha_crypto::KeyPair) -> SignedTransaction { - let signature = TransactionSignature( - key_pair.public_key().clone(), - SignatureOf::new(key_pair.private_key(), &self.payload), - ); + pub fn sign(self, private_key: &iroha_crypto::PrivateKey) -> SignedTransaction { + let signature = TransactionSignature(SignatureOf::new(private_key, &self.payload)); SignedTransactionV1 { signature, diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index 490b88ef5e2..44ced7ddc36 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -612,7 +612,7 @@ }, { "name": "transactions_hash", - "type": "Option>>" + "type": "HashOf>" }, { "name": "timestamp_ms", @@ -2469,9 +2469,6 @@ "Option": { "Option": "Duration" }, - "Option>>": { - "Option": "HashOf>" - }, "Option>": { "Option": "HashOf" }, @@ -2904,32 +2901,27 @@ }, "QueryExecutionFail": { "Enum": [ - { - "tag": "Signature", - "discriminant": 0, - "type": "String" - }, { "tag": "Find", - "discriminant": 1, + "discriminant": 0, "type": "FindError" }, { "tag": "Conversion", - "discriminant": 2, + "discriminant": 1, "type": "String" }, { "tag": "UnknownCursor", - "discriminant": 3 + "discriminant": 2 }, { "tag": "FetchSizeTooBig", - "discriminant": 4 + "discriminant": 3 }, { "tag": "InvalidSingularParameters", - "discriminant": 5 + "discriminant": 4 } ] }, @@ -3020,12 +3012,7 @@ } ] }, - "QuerySignature": { - "Tuple": [ - "PublicKey", - "SignatureOf" - ] - }, + "QuerySignature": "SignatureOf", "Register": { "Struct": [ { @@ -3903,12 +3890,7 @@ } ] }, - "TransactionSignature": { - "Tuple": [ - "PublicKey", - "SignatureOf" - ] - }, + "TransactionSignature": "SignatureOf", "TransactionStatus": { "Enum": [ { diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index 8784f7dc074..79c5d046c6d 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -121,7 +121,7 @@ fn build_and_sign_genesis( ); let transaction = TransactionBuilder::new(chain_id, genesis_account_id) .with_instructions(instructions) - .sign(genesis_key_pair); + .sign(genesis_key_pair.private_key()); GenesisTransaction(transaction) } diff --git a/torii/src/lib.rs b/torii/src/lib.rs index a987608ee3f..4db82d4c7f1 100644 --- a/torii/src/lib.rs +++ b/torii/src/lib.rs @@ -338,7 +338,6 @@ impl Error { Config(_) | StatusSegmentNotFound(_) => StatusCode::NOT_FOUND, PushIntoQueue(err) => match **err { queue::Error::Full => StatusCode::INTERNAL_SERVER_ERROR, - queue::Error::SignatoryInconsistent => StatusCode::UNAUTHORIZED, _ => StatusCode::BAD_REQUEST, }, #[cfg(feature = "telemetry")] @@ -363,7 +362,6 @@ impl Error { Conversion(_) | UnknownCursor | FetchSizeTooBig | InvalidSingularParameters => { StatusCode::BAD_REQUEST } - Signature(_) => StatusCode::UNAUTHORIZED, Find(_) => StatusCode::NOT_FOUND, }, TooComplex => StatusCode::UNPROCESSABLE_ENTITY, diff --git a/torii/src/routing.rs b/torii/src/routing.rs index b75eacf6b05..1d52394630e 100644 --- a/torii/src/routing.rs +++ b/torii/src/routing.rs @@ -53,7 +53,7 @@ pub async fn handle_transaction( ) -> Result { let state_view = state.view(); let transaction_limits = state_view.config.transaction_limits; - let transaction = AcceptedTransaction::accept(transaction, &chain_id, &transaction_limits) + let transaction = AcceptedTransaction::accept(transaction, &chain_id, transaction_limits) .map_err(Error::AcceptTransaction)?; queue .push(transaction, &state_view)