From f5f4af5eb94bb2fc5b1287ef70dfb6106bf81e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Ver=C5=A1i=C4=87?= Date: Mon, 15 Jan 2024 11:56:19 +0100 Subject: [PATCH] [feature] #4126: Add chain_id to prevent replay attacks (#4185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [feature] #4126: Add chain_id to prevent replay attacks Signed-off-by: Marin Veršić * [feature]: add implementation for Box FFI conversion Signed-off-by: Marin Veršić --- cli/src/lib.rs | 22 +- cli/src/samples.rs | 19 +- client/benches/torii.rs | 26 +- client/benches/tps/utils.rs | 7 +- client/examples/million_accounts_genesis.rs | 13 +- client/src/client.rs | 9 +- client/src/lib.rs | 4 +- client/tests/integration/asset.rs | 10 +- client/tests/integration/burn_public_keys.rs | 4 +- client/tests/integration/domain_owner.rs | 7 +- client/tests/integration/permissions.rs | 18 +- client/tests/integration/roles.rs | 4 +- client/tests/integration/upgrade.rs | 6 +- client_cli/pytests/README.md | 16 +- config/iroha_test_config.json | 1 + config/src/client.rs | 23 +- config/src/iroha.rs | 14 +- configs/client/config.json | 5 +- configs/peer/config.json | 1 + configs/peer/executor.wasm | Bin 479380 -> 479813 bytes core/benches/blocks/common.rs | 7 +- core/benches/kura.rs | 15 +- core/benches/validation.rs | 51 +- core/src/block.rs | 44 +- core/src/gossiper.rs | 14 +- core/src/queue.rs | 38 +- core/src/smartcontracts/isi/query.rs | 18 +- core/src/sumeragi/main_loop.rs | 791 ++++++++++--------- core/src/sumeragi/mod.rs | 10 +- core/src/tx.rs | 41 +- core/test_network/src/lib.rs | 29 +- data_model/src/lib.rs | 27 +- data_model/src/transaction.rs | 8 +- docker-compose.dev.local.yml | 4 + docker-compose.dev.single.yml | 1 + docker-compose.dev.yml | 4 + docs/source/references/schema.json | 5 + ffi/derive/src/lib.rs | 2 +- ffi/derive/src/wrapper.rs | 6 +- ffi/src/ir.rs | 52 +- ffi/src/lib.rs | 2 +- ffi/src/primitives.rs | 4 +- ffi/src/repr_c.rs | 286 ++++--- ffi/src/slice.rs | 33 +- ffi/src/std_impls.rs | 21 +- ffi/tests/export_shared_fns.rs | 2 +- ffi/tests/ffi_export.rs | 8 +- ffi/tests/ffi_import.rs | 16 +- ffi/tests/ffi_import_opaque.rs | 6 +- ffi/tests/import_getset.rs | 6 +- ffi/tests/import_shared_fns.rs | 4 +- ffi/tests/transparent.rs | 6 +- genesis/src/lib.rs | 18 +- schema/src/lib.rs | 22 + scripts/test_env.py | 4 +- tools/kagami/src/config.rs | 1 + tools/swarm/src/compose.rs | 39 +- torii/src/lib.rs | 8 +- torii/src/routing.rs | 3 +- 59 files changed, 1130 insertions(+), 735 deletions(-) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index d4f9265f598..47e6faf5a7b 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -252,6 +252,7 @@ impl Iroha { let kura_thread_handler = Kura::start(Arc::clone(&kura)); let sumeragi = SumeragiHandle::start(SumeragiStartArgs { + chain_id: config.chain_id.clone(), configuration: &config.sumeragi, events_sender: events_sender.clone(), wsv, @@ -272,6 +273,7 @@ impl Iroha { .start(); let gossiper = TransactionGossiper::from_configuration( + config.chain_id.clone(), &config.sumeragi, network.clone(), Arc::clone(&queue), @@ -301,6 +303,7 @@ impl Iroha { let kiso = KisoHandle::new(config.clone()); let torii = Torii::new( + config.chain_id, kiso.clone(), &config.torii, Arc::clone(&queue), @@ -583,7 +586,7 @@ pub fn read_config( .wrap_err("Invalid genesis configuration")? { Some( - GenesisNetwork::new(raw_block, &key_pair) + GenesisNetwork::new(raw_block, &config.chain_id, &key_pair) .wrap_err("Failed to construct the genesis")?, ) } else { @@ -634,21 +637,24 @@ mod tests { use super::*; fn config_factory() -> Result { - let mut base = ConfigurationProxy::default(); - let key_pair = KeyPair::generate()?; - base.public_key = Some(key_pair.public_key().clone()); - base.private_key = Some(key_pair.private_key().clone()); + let mut base = ConfigurationProxy { + chain_id: Some(ChainId::new("0")), - let torii = base.torii.as_mut().unwrap(); - torii.p2p_addr = Some(socket_addr!(127.0.0.1:1337)); - torii.api_url = Some(socket_addr!(127.0.0.1:1337)); + public_key: Some(key_pair.public_key().clone()), + private_key: Some(key_pair.private_key().clone()), + ..ConfigurationProxy::default() + }; let genesis = base.genesis.as_mut().unwrap(); genesis.private_key = Some(Some(key_pair.private_key().clone())); genesis.public_key = Some(key_pair.public_key().clone()); + let torii = base.torii.as_mut().unwrap(); + torii.p2p_addr = Some(socket_addr!(127.0.0.1:1337)); + torii.api_url = Some(socket_addr!(127.0.0.1:1337)); + Ok(base) } diff --git a/cli/src/samples.rs b/cli/src/samples.rs index 1a59f2b0a25..05858803f19 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -7,7 +7,7 @@ use iroha_config::{ torii::{uri::DEFAULT_API_ADDR, DEFAULT_TORII_P2P_ADDR}, }; use iroha_crypto::{KeyPair, PublicKey}; -use iroha_data_model::{peer::PeerId, prelude::*}; +use iroha_data_model::{peer::PeerId, prelude::*, ChainId}; use iroha_primitives::unique_vec::UniqueVec; /// Get sample trusted peers. The public key must be the same as `configuration.public_key` @@ -52,12 +52,19 @@ pub fn get_trusted_peers(public_key: Option<&PublicKey>) -> HashSet { /// /// # Panics /// - when [`KeyPair`] generation fails (rare case). -pub fn get_config_proxy(peers: UniqueVec, key_pair: Option) -> ConfigurationProxy { +pub fn get_config_proxy( + peers: UniqueVec, + chain_id: Option, + key_pair: Option, +) -> ConfigurationProxy { + let chain_id = chain_id.unwrap_or_else(|| ChainId::new("0")); + let (public_key, private_key) = key_pair .unwrap_or_else(|| KeyPair::generate().expect("Key pair generation failed")) .into(); iroha_logger::info!(%public_key); ConfigurationProxy { + chain_id: Some(chain_id), public_key: Some(public_key.clone()), private_key: Some(private_key.clone()), sumeragi: Some(Box::new(iroha_config::sumeragi::ConfigurationProxy { @@ -94,8 +101,12 @@ pub fn get_config_proxy(peers: UniqueVec, key_pair: Option) -> /// /// # Panics /// - when [`KeyPair`] generation fails (rare case). -pub fn get_config(trusted_peers: UniqueVec, key_pair: Option) -> Configuration { - get_config_proxy(trusted_peers, key_pair) +pub fn get_config( + trusted_peers: UniqueVec, + chain_id: Option, + key_pair: Option, +) -> Configuration { + get_config_proxy(trusted_peers, chain_id, key_pair) .build() .expect("Iroha config should build as all required fields were provided") } diff --git a/client/benches/torii.rs b/client/benches/torii.rs index 088ec7eb406..f73433108a0 100644 --- a/client/benches/torii.rs +++ b/client/benches/torii.rs @@ -12,7 +12,7 @@ use iroha_client::{ use iroha_genesis::{GenesisNetwork, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; use iroha_version::Encode; -use test_network::{get_key_pair, Peer as TestPeer, PeerBuilder, TestRuntime}; +use test_network::{get_chain_id, get_key_pair, Peer as TestPeer, PeerBuilder, TestRuntime}; use tokio::runtime::Runtime; const MINIMUM_SUCCESS_REQUEST_RATIO: f32 = 0.9; @@ -30,7 +30,13 @@ fn get_genesis_key_pair(config: &iroha_config::iroha::Configuration) -> KeyPair fn query_requests(criterion: &mut Criterion) { let mut peer = ::new().expect("Failed to create peer"); - let configuration = get_config(unique_vec![peer.id.clone()], Some(get_key_pair())); + + let chain_id = get_chain_id(); + let configuration = get_config( + unique_vec![peer.id.clone()], + Some(chain_id.clone()), + Some(get_key_pair()), + ); let rt = Runtime::test(); let genesis = GenesisNetwork::new( @@ -45,6 +51,7 @@ fn query_requests(criterion: &mut Criterion) { construct_executor("../default_executor").expect("Failed to construct executor"), ) .build(), + &chain_id, &get_genesis_key_pair(&configuration), ) .expect("genesis creation failed"); @@ -76,7 +83,8 @@ fn query_requests(criterion: &mut Criterion) { quantity, AssetId::new(asset_definition_id, account_id.clone()), ); - let mut client_config = iroha_client::samples::get_client_config(&get_key_pair()); + let mut client_config = + iroha_client::samples::get_client_config(get_chain_id(), &get_key_pair()); client_config.torii_api_url = format!("http://{}", peer.api_address).parse().unwrap(); @@ -130,7 +138,13 @@ fn instruction_submits(criterion: &mut Criterion) { println!("instruction submits"); let rt = Runtime::test(); let mut peer = ::new().expect("Failed to create peer"); - let configuration = get_config(unique_vec![peer.id.clone()], Some(get_key_pair())); + + let chain_id = get_chain_id(); + let configuration = get_config( + unique_vec![peer.id.clone()], + Some(chain_id.clone()), + Some(get_key_pair()), + ); let genesis = GenesisNetwork::new( RawGenesisBlockBuilder::default() .domain("wonderland".parse().expect("Valid")) @@ -143,6 +157,7 @@ fn instruction_submits(criterion: &mut Criterion) { construct_executor("../default_executor").expect("Failed to construct executor"), ) .build(), + &chain_id, &get_genesis_key_pair(&configuration), ) .expect("failed to create genesis"); @@ -159,7 +174,8 @@ fn instruction_submits(criterion: &mut Criterion) { .into(); let create_account = Register::account(Account::new(account_id.clone(), [public_key])).into(); let asset_definition_id = AssetDefinitionId::new("xor".parse().expect("Valid"), domain_id); - let mut client_config = iroha_client::samples::get_client_config(&get_key_pair()); + let mut client_config = + iroha_client::samples::get_client_config(get_chain_id(), &get_key_pair()); client_config.torii_api_url = format!("http://{}", peer.api_address).parse().unwrap(); let iroha_client = Client::new(&client_config).expect("Invalid client configuration"); thread::sleep(std::time::Duration::from_millis(5000)); diff --git a/client/benches/tps/utils.rs b/client/benches/tps/utils.rs index c1a3494260f..e11e2febe90 100644 --- a/client/benches/tps/utils.rs +++ b/client/benches/tps/utils.rs @@ -207,6 +207,8 @@ impl MeasurerUnit { /// Spawn who periodically submits transactions fn spawn_transaction_submitter(&self, shutdown_signal: mpsc::Receiver<()>) -> JoinHandle<()> { + let chain_id = ChainId::new("0"); + let submitter = self.client.clone(); let interval_us_per_tx = self.config.interval_us_per_tx; let instructions = self.instructions(); @@ -218,8 +220,9 @@ impl MeasurerUnit { for instruction in instructions { match shutdown_signal.try_recv() { Err(mpsc::TryRecvError::Empty) => { - let mut transaction = TransactionBuilder::new(alice_id.clone()) - .with_instructions([instruction]); + let mut transaction = + TransactionBuilder::new(chain_id.clone(), alice_id.clone()) + .with_instructions([instruction]); transaction.set_nonce(nonce); // Use nonce to avoid transaction duplication within the same thread let transaction = submitter diff --git a/client/examples/million_accounts_genesis.rs b/client/examples/million_accounts_genesis.rs index a6de431c796..ec000d4a0f5 100644 --- a/client/examples/million_accounts_genesis.rs +++ b/client/examples/million_accounts_genesis.rs @@ -7,7 +7,8 @@ use iroha_data_model::isi::InstructionBox; use iroha_genesis::{GenesisNetwork, RawGenesisBlock, RawGenesisBlockBuilder}; use iroha_primitives::unique_vec; use test_network::{ - get_key_pair, wait_for_genesis_committed, Peer as TestPeer, PeerBuilder, TestRuntime, + get_chain_id, get_key_pair, wait_for_genesis_committed, Peer as TestPeer, PeerBuilder, + TestRuntime, }; use tokio::runtime::Runtime; @@ -36,9 +37,15 @@ fn generate_genesis(num_domains: u32) -> RawGenesisBlock { fn main_genesis() { let mut peer = ::new().expect("Failed to create peer"); - let configuration = get_config(unique_vec![peer.id.clone()], Some(get_key_pair())); + + let chain_id = get_chain_id(); + let configuration = get_config( + unique_vec![peer.id.clone()], + Some(chain_id.clone()), + Some(get_key_pair()), + ); let rt = Runtime::test(); - let genesis = GenesisNetwork::new(generate_genesis(1_000_000_u32), &{ + let genesis = GenesisNetwork::new(generate_genesis(1_000_000_u32), &chain_id, &{ let private_key = configuration .genesis .private_key diff --git a/client/src/client.rs b/client/src/client.rs index 66a7ada3373..c0614fc778a 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -31,7 +31,7 @@ use crate::{ prelude::*, query::{Pagination, Query, Sorting}, transaction::TransactionPayload, - BatchedResponse, ValidationFail, + BatchedResponse, ChainId, ValidationFail, }, http::{Method as HttpMethod, RequestBuilder, Response, StatusCode}, http_default::{self, DefaultRequestBuilder, WebSocketError, WebSocketMessage}, @@ -344,6 +344,8 @@ impl_query_output! { )] #[display(fmt = "{}@{torii_url}", "key_pair.public_key()")] pub struct Client { + /// Unique id of the blockchain. Used for simple replay attack protection. + pub chain_id: ChainId, /// Url for accessing iroha node pub torii_url: Url, /// Accounts keypair @@ -440,6 +442,7 @@ impl Client { } Ok(Self { + chain_id: configuration.chain_id.clone(), torii_url: configuration.torii_api_url.clone(), key_pair: KeyPair::new( configuration.public_key.clone(), @@ -466,7 +469,7 @@ impl Client { instructions: impl Into, metadata: UnlimitedMetadata, ) -> Result { - let tx_builder = TransactionBuilder::new(self.account_id.clone()); + let tx_builder = TransactionBuilder::new(self.chain_id.clone(), self.account_id.clone()); let mut tx_builder = match instructions.into() { Executable::Instructions(instructions) => tx_builder.with_instructions(instructions), @@ -1666,6 +1669,7 @@ mod tests { let (public_key, private_key) = KeyPair::generate().unwrap().into(); let cfg = ConfigurationProxy { + chain_id: Some(ChainId::new("0")), public_key: Some(public_key), private_key: Some(private_key), account_id: Some( @@ -1708,6 +1712,7 @@ mod tests { }; let cfg = ConfigurationProxy { + chain_id: Some(ChainId::new("0")), public_key: Some( "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" .parse() diff --git a/client/src/lib.rs b/client/src/lib.rs index 78a3cbeac13..239a6eb5a7f 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -12,12 +12,14 @@ pub mod samples { use crate::{ config::{torii::DEFAULT_API_ADDR, Configuration, ConfigurationProxy}, crypto::KeyPair, + data_model::ChainId, }; /// Get sample client configuration. - pub fn get_client_config(key_pair: &KeyPair) -> Configuration { + pub fn get_client_config(chain_id: ChainId, key_pair: &KeyPair) -> Configuration { let (public_key, private_key) = key_pair.clone().into(); ConfigurationProxy { + chain_id: Some(chain_id), public_key: Some(public_key), private_key: Some(private_key), account_id: Some( diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index 3b151b99ec8..beac868a71a 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -277,10 +277,12 @@ fn find_rate_and_make_exchange_isi_should_succeed() { alice_id.clone(), ); - let grant_asset_transfer_tx = TransactionBuilder::new(asset_id.account_id().clone()) - .with_instructions([allow_alice_to_transfer_asset]) - .sign(owner_keypair) - .expect("Failed to sign seller transaction"); + let chain_id = ChainId::new("0"); + let grant_asset_transfer_tx = + TransactionBuilder::new(chain_id, asset_id.account_id().clone()) + .with_instructions([allow_alice_to_transfer_asset]) + .sign(owner_keypair) + .expect("Failed to sign seller transaction"); test_client .submit_transaction_blocking(&grant_asset_transfer_tx) diff --git a/client/tests/integration/burn_public_keys.rs b/client/tests/integration/burn_public_keys.rs index f207894995d..9b73edb1b34 100644 --- a/client/tests/integration/burn_public_keys.rs +++ b/client/tests/integration/burn_public_keys.rs @@ -13,8 +13,10 @@ fn submit( HashOf, eyre::Result>, ) { + let chain_id = ChainId::new("0"); + let tx = if let Some((account_id, keypair)) = submitter { - TransactionBuilder::new(account_id) + TransactionBuilder::new(chain_id, account_id) .with_instructions(instructions) .sign(keypair) .unwrap() diff --git a/client/tests/integration/domain_owner.rs b/client/tests/integration/domain_owner.rs index eeeb881b324..b159fa34cbd 100644 --- a/client/tests/integration/domain_owner.rs +++ b/client/tests/integration/domain_owner.rs @@ -104,6 +104,7 @@ fn domain_owner_asset_definition_permissions() -> Result<()> { let (_rt, _peer, test_client) = ::new().with_port(11_085).start_with_runtime(); wait_for_genesis_committed(&[test_client.clone()], 0); + let chain_id = ChainId::new("0"); let kingdom_id: DomainId = "kingdom".parse()?; let bob_id: AccountId = "bob@kingdom".parse()?; let rabbit_id: AccountId = "rabbit@kingdom".parse()?; @@ -122,7 +123,7 @@ fn domain_owner_asset_definition_permissions() -> Result<()> { // register asset definitions by "bob@kingdom" so he is owner of it let coin = AssetDefinition::quantity(coin_id.clone()); - let transaction = TransactionBuilder::new(bob_id.clone()) + let transaction = TransactionBuilder::new(chain_id, bob_id.clone()) .with_instructions([Register::asset_definition(coin)]) .sign(bob_keypair)?; test_client.submit_transaction_blocking(&transaction)?; @@ -161,6 +162,8 @@ fn domain_owner_asset_definition_permissions() -> Result<()> { #[test] fn domain_owner_asset_permissions() -> Result<()> { + let chain_id = ChainId::new("0"); + let (_rt, _peer, test_client) = ::new().with_port(11_090).start_with_runtime(); wait_for_genesis_committed(&[test_client.clone()], 0); @@ -181,7 +184,7 @@ fn domain_owner_asset_permissions() -> Result<()> { // register asset definitions by "bob@kingdom" so he is owner of it let coin = AssetDefinition::quantity(coin_id.clone()); let store = AssetDefinition::store(store_id.clone()); - let transaction = TransactionBuilder::new(bob_id.clone()) + let transaction = TransactionBuilder::new(chain_id, bob_id.clone()) .with_instructions([ Register::asset_definition(coin), Register::asset_definition(store), diff --git a/client/tests/integration/permissions.rs b/client/tests/integration/permissions.rs index f9ff3c05cdf..beb8ffd6de0 100644 --- a/client/tests/integration/permissions.rs +++ b/client/tests/integration/permissions.rs @@ -62,6 +62,8 @@ fn get_assets(iroha_client: &Client, id: &AccountId) -> Vec { #[test] #[ignore = "ignore, more in #2851"] fn permissions_disallow_asset_transfer() { + let chain_id = ChainId::new("0"); + let (_rt, _peer, iroha_client) = ::new().with_port(10_730).start_with_runtime(); wait_for_genesis_committed(&[iroha_client.clone()], 0); @@ -94,7 +96,7 @@ fn permissions_disallow_asset_transfer() { quantity, alice_id.clone(), ); - let transfer_tx = TransactionBuilder::new(mouse_id) + let transfer_tx = TransactionBuilder::new(chain_id, mouse_id) .with_instructions([transfer_asset]) .sign(mouse_keypair) .expect("Failed to sign mouse transaction"); @@ -118,6 +120,8 @@ fn permissions_disallow_asset_transfer() { #[test] #[ignore = "ignore, more in #2851"] fn permissions_disallow_asset_burn() { + let chain_id = ChainId::new("0"); + let (_rt, _peer, iroha_client) = ::new().with_port(10_735).start_with_runtime(); let alice_id = "alice@wonderland".parse().expect("Valid"); @@ -144,7 +148,7 @@ fn permissions_disallow_asset_burn() { quantity, AssetId::new(asset_definition_id, mouse_id.clone()), ); - let burn_tx = TransactionBuilder::new(mouse_id) + let burn_tx = TransactionBuilder::new(chain_id, mouse_id) .with_instructions([burn_asset]) .sign(mouse_keypair) .expect("Failed to sign mouse transaction"); @@ -192,6 +196,8 @@ fn account_can_query_only_its_own_domain() -> Result<()> { #[test] fn permissions_differ_not_only_by_names() { + let chain_id = ChainId::new("0"); + let (_rt, _not_drop, client) = ::new().with_port(10_745).start_with_runtime(); let alice_id: AccountId = "alice@wonderland".parse().expect("Valid"); @@ -226,7 +232,7 @@ fn permissions_differ_not_only_by_names() { alice_id.clone(), ); - let grant_hats_access_tx = TransactionBuilder::new(mouse_id.clone()) + 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.clone()) .expect("Failed to sign mouse transaction"); @@ -263,7 +269,7 @@ fn permissions_differ_not_only_by_names() { alice_id, ); - let grant_shoes_access_tx = TransactionBuilder::new(mouse_id) + let grant_shoes_access_tx = TransactionBuilder::new(chain_id, mouse_id) .with_instructions([allow_alice_to_set_key_value_in_shoes]) .sign(mouse_keypair) .expect("Failed to sign mouse transaction"); @@ -281,6 +287,8 @@ fn permissions_differ_not_only_by_names() { #[test] #[allow(deprecated)] fn stored_vs_granted_token_payload() -> Result<()> { + let chain_id = ChainId::new("0"); + let (_rt, _peer, iroha_client) = ::new().with_port(10_730).start_with_runtime(); wait_for_genesis_committed(&[iroha_client.clone()], 0); @@ -313,7 +321,7 @@ fn stored_vs_granted_token_payload() -> Result<()> { alice_id, ); - let transaction = TransactionBuilder::new(mouse_id) + let transaction = TransactionBuilder::new(chain_id, mouse_id) .with_instructions([allow_alice_to_set_key_value_in_mouse_asset]) .sign(mouse_keypair) .expect("Failed to sign mouse transaction"); diff --git a/client/tests/integration/roles.rs b/client/tests/integration/roles.rs index 95245852db4..e91909837ef 100644 --- a/client/tests/integration/roles.rs +++ b/client/tests/integration/roles.rs @@ -46,6 +46,8 @@ fn register_role_with_empty_token_params() -> Result<()> { /// @s8sato added: This test represents #2081 case. #[test] fn register_and_grant_role_for_metadata_access() -> Result<()> { + let chain_id = ChainId::new("0"); + let (_rt, _peer, test_client) = ::new().with_port(10_700).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); @@ -76,7 +78,7 @@ fn register_and_grant_role_for_metadata_access() -> Result<()> { // Mouse grants role to Alice let grant_role = Grant::role(role_id.clone(), alice_id.clone()); - let grant_role_tx = TransactionBuilder::new(mouse_id.clone()) + let grant_role_tx = TransactionBuilder::new(chain_id, mouse_id.clone()) .with_instructions([grant_role]) .sign(mouse_key_pair)?; test_client.submit_transaction_blocking(&grant_role_tx)?; diff --git a/client/tests/integration/upgrade.rs b/client/tests/integration/upgrade.rs index 3ec49a84600..bc2e075ad7f 100644 --- a/client/tests/integration/upgrade.rs +++ b/client/tests/integration/upgrade.rs @@ -12,6 +12,8 @@ use test_network::*; #[test] fn executor_upgrade_should_work() -> Result<()> { + let chain_id = ChainId::new("0"); + let (_rt, _peer, client) = ::new().with_port(10_795).start_with_runtime(); wait_for_genesis_committed(&vec![client.clone()], 0); @@ -30,7 +32,7 @@ fn executor_upgrade_should_work() -> Result<()> { let alice_rose: AssetId = "rose##alice@wonderland".parse()?; let admin_rose: AccountId = "admin@admin".parse()?; let transfer_alice_rose = Transfer::asset_quantity(alice_rose, 1_u32, admin_rose); - let transfer_rose_tx = TransactionBuilder::new(admin_id.clone()) + let transfer_rose_tx = TransactionBuilder::new(chain_id.clone(), admin_id.clone()) .with_instructions([transfer_alice_rose.clone()]) .sign(admin_keypair.clone())?; let _ = client @@ -44,7 +46,7 @@ fn executor_upgrade_should_work() -> Result<()> { // Check that admin can transfer alice's rose now // Creating new transaction instead of cloning, because we need to update it's creation time - let transfer_rose_tx = TransactionBuilder::new(admin_id) + let transfer_rose_tx = TransactionBuilder::new(chain_id, admin_id) .with_instructions([transfer_alice_rose]) .sign(admin_keypair)?; client diff --git a/client_cli/pytests/README.md b/client_cli/pytests/README.md index fb740fc04b8..610261cf789 100644 --- a/client_cli/pytests/README.md +++ b/client_cli/pytests/README.md @@ -46,14 +46,14 @@ This test framework uses [Poetry](https://python-poetry.org/) for dependency man poetry install ``` 4. Activate the virtual environment: - ```bash - poetry shell - ``` - Now, you should be in the virtual environment with all the required dependencies installed. All the subsequent commands (e.g., pytest, allure) should be executed within this virtual environment. + ```bash + poetry shell + ``` + Now, you should be in the virtual environment with all the required dependencies installed. All the subsequent commands (e.g., pytest, allure) should be executed within this virtual environment. 5. When you're done working in the virtual environment, deactivate it by running: - ```bash - exit - ``` + ```bash + exit + ``` ## Run tests @@ -89,4 +89,4 @@ The framework is organized into the following directories: The framework also includes configuration files: `poetry.lock` and `pyproject.toml`: Configuration files for Poetry, the dependency management and virtual environment tool used in this framework. -`pytest.ini`: Configuration file for pytest, the testing framework used in this framework. \ No newline at end of file +`pytest.ini`: Configuration file for pytest, the testing framework used in this framework. diff --git a/config/iroha_test_config.json b/config/iroha_test_config.json index 7a180598bbb..53339579831 100644 --- a/config/iroha_test_config.json +++ b/config/iroha_test_config.json @@ -1,4 +1,5 @@ { + "CHAIN_ID": "00000000-0000-0000-0000-000000000000", "PUBLIC_KEY": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B", "PRIVATE_KEY": { "digest_function": "ed25519", diff --git a/config/src/client.rs b/config/src/client.rs index a9238879cac..41c5f30240d 100644 --- a/config/src/client.rs +++ b/config/src/client.rs @@ -6,13 +6,11 @@ use derive_more::Display; use eyre::{Result, WrapErr}; use iroha_config_base::derive::{Error as ConfigError, Proxy}; use iroha_crypto::prelude::*; -use iroha_data_model::{prelude::*, transaction::TransactionLimits}; +use iroha_data_model::{prelude::*, ChainId}; use iroha_primitives::small::SmallStr; use serde::{Deserialize, Serialize}; use url::Url; -use crate::wsv::default::DEFAULT_TRANSACTION_LIMITS; - #[allow(unsafe_code)] const DEFAULT_TRANSACTION_TIME_TO_LIVE_MS: NonZeroU64 = unsafe { NonZeroU64::new_unchecked(100_000) }; @@ -69,6 +67,8 @@ pub struct BasicAuth { #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "IROHA_")] pub struct Configuration { + /// Unique id of the blockchain. Used for simple replay attack protection. + pub chain_id: ChainId, /// Public key of the user account. #[config(serde_as_str)] pub public_key: PublicKey, @@ -84,13 +84,6 @@ pub struct Configuration { pub transaction_time_to_live_ms: Option, /// Transaction status wait timeout in milliseconds. pub transaction_status_timeout_ms: u64, - /// The limits to which transactions must adhere to - // NOTE: If you want this functionality, implement it in the app manually - #[deprecated( - note = "This parameter is not used and takes no effect and will be removed in future releases. \ - If you want this functionality, implement it in the app manually." - )] - pub transaction_limits: TransactionLimits, /// If `true` add nonce, which make different hashes for transactions which occur repeatedly and simultaneously pub add_transaction_nonce: bool, } @@ -98,6 +91,7 @@ pub struct Configuration { impl Default for ConfigurationProxy { fn default() -> Self { Self { + chain_id: None, public_key: None, private_key: None, account_id: None, @@ -105,7 +99,6 @@ impl Default for ConfigurationProxy { torii_api_url: None, transaction_time_to_live_ms: Some(Some(DEFAULT_TRANSACTION_TIME_TO_LIVE_MS)), transaction_status_timeout_ms: Some(DEFAULT_TRANSACTION_STATUS_TIMEOUT_MS), - transaction_limits: Some(DEFAULT_TRANSACTION_LIMITS), add_transaction_nonce: Some(DEFAULT_ADD_TRANSACTION_NONCE), } } @@ -208,17 +201,17 @@ mod tests { prop_compose! { fn arb_proxy() ( + chain_id in prop::option::of(Just(crate::iroha::tests::placeholder_chain_id())), (public_key, private_key) in arb_keys_with_option(), account_id in prop::option::of(Just(placeholder_account())), basic_auth in prop::option::of(Just(None)), torii_api_url in prop::option::of(Just(format!("http://{DEFAULT_API_ADDR}").parse().unwrap())), transaction_time_to_live_ms in prop::option::of(Just(Some(DEFAULT_TRANSACTION_TIME_TO_LIVE_MS))), transaction_status_timeout_ms in prop::option::of(Just(DEFAULT_TRANSACTION_STATUS_TIMEOUT_MS)), - transaction_limits in prop::option::of(Just(DEFAULT_TRANSACTION_LIMITS)), add_transaction_nonce in prop::option::of(Just(DEFAULT_ADD_TRANSACTION_NONCE)), ) -> ConfigurationProxy { - ConfigurationProxy { public_key, private_key, account_id, basic_auth, torii_api_url, transaction_time_to_live_ms, transaction_status_timeout_ms, transaction_limits, add_transaction_nonce } + ConfigurationProxy { chain_id, public_key, private_key, account_id, basic_auth, torii_api_url, transaction_time_to_live_ms, transaction_status_timeout_ms, add_transaction_nonce } } } @@ -236,10 +229,6 @@ mod tests { assert_eq!(arb_cfg.account_id, example_cfg.account_id); assert_eq!(arb_cfg.transaction_time_to_live_ms, example_cfg.transaction_time_to_live_ms); assert_eq!(arb_cfg.transaction_status_timeout_ms, example_cfg.transaction_status_timeout_ms); - #[allow(deprecated)] // For testing purposes only - { - assert_eq!(arb_cfg.transaction_limits, example_cfg.transaction_limits); - } assert_eq!(arb_cfg.add_transaction_nonce, example_cfg.add_transaction_nonce); } } diff --git a/config/src/iroha.rs b/config/src/iroha.rs index 1946b2571b1..b4be726e12b 100644 --- a/config/src/iroha.rs +++ b/config/src/iroha.rs @@ -3,6 +3,7 @@ use std::fmt::Debug; use iroha_config_base::derive::{view, Error as ConfigError, Proxy}; use iroha_crypto::prelude::*; +use iroha_data_model::ChainId; use serde::{Deserialize, Serialize}; use super::*; @@ -14,6 +15,9 @@ view! { #[serde(rename_all = "UPPERCASE")] #[config(env_prefix = "IROHA_")] pub struct Configuration { + /// Unique id of the blockchain. Used for simple replay attack protection. + #[config(serde_as_str)] + pub chain_id: ChainId, /// Public key of this peer #[config(serde_as_str)] pub public_key: PublicKey, @@ -64,6 +68,7 @@ view! { impl Default for ConfigurationProxy { fn default() -> Self { Self { + chain_id: None, public_key: None, private_key: None, kura: Some(Box::default()), @@ -165,7 +170,7 @@ impl ConfigurationProxy { } #[cfg(test)] -mod tests { +pub mod tests { use std::path::PathBuf; use proptest::prelude::*; @@ -200,8 +205,13 @@ mod tests { .boxed() } + pub fn placeholder_chain_id() -> ChainId { + ChainId::new("0") + } + prop_compose! { fn arb_proxy()( + chain_id in prop::option::of(Just(placeholder_chain_id())), (public_key, private_key) in arb_keys(), kura in prop::option::of(kura::tests::arb_proxy().prop_map(Box::new)), sumeragi in (prop::option::of(sumeragi::tests::arb_proxy().prop_map(Box::new))), @@ -216,7 +226,7 @@ mod tests { snapshot in prop::option::of(snapshot::tests::arb_proxy().prop_map(Box::new)), live_query_store in prop::option::of(live_query_store::tests::arb_proxy()), ) -> ConfigurationProxy { - ConfigurationProxy { public_key, private_key, kura, sumeragi, torii, block_sync, queue, + ConfigurationProxy { chain_id, public_key, private_key, kura, sumeragi, torii, block_sync, queue, logger, genesis, wsv, network, telemetry, snapshot, live_query_store } } } diff --git a/configs/client/config.json b/configs/client/config.json index 5ed2399d626..b8a507409ac 100644 --- a/configs/client/config.json +++ b/configs/client/config.json @@ -1,4 +1,5 @@ { + "CHAIN_ID": "00000000-0000-0000-0000-000000000000", "PUBLIC_KEY": "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0", "PRIVATE_KEY": { "digest_function": "ed25519", @@ -12,9 +13,5 @@ "TORII_API_URL": "http://127.0.0.1:8080/", "TRANSACTION_TIME_TO_LIVE_MS": 100000, "TRANSACTION_STATUS_TIMEOUT_MS": 15000, - "TRANSACTION_LIMITS": { - "max_instruction_number": 4096, - "max_wasm_size_bytes": 4194304 - }, "ADD_TRANSACTION_NONCE": false } diff --git a/configs/peer/config.json b/configs/peer/config.json index 3f0dc2f87a9..2695398702f 100644 --- a/configs/peer/config.json +++ b/configs/peer/config.json @@ -1,4 +1,5 @@ { + "CHAIN_ID": null, "PUBLIC_KEY": null, "PRIVATE_KEY": null, "KURA": { diff --git a/configs/peer/executor.wasm b/configs/peer/executor.wasm index ad6286a1ccb83d8066600ca9805b7fd5c72445b5..1e61546fd9a61bfec90e882c2b2c31a27bb9e472 100644 GIT binary patch delta 69363 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 delta 68497 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 H>(~DWRHZs= diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index 7aef12edd2d..f8969b2f16b 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -15,6 +15,7 @@ use iroha_data_model::{ isi::InstructionBox, prelude::*, transaction::TransactionLimits, + ChainId, }; use iroha_primitives::unique_vec::UniqueVec; use serde_json::json; @@ -26,7 +27,9 @@ pub fn create_block( account_id: AccountId, key_pair: KeyPair, ) -> CommittedBlock { - let transaction = TransactionBuilder::new(account_id) + let chain_id = ChainId::new("0"); + + let transaction = TransactionBuilder::new(chain_id.clone(), account_id) .with_instructions(instructions) .sign(key_pair.clone()) .unwrap(); @@ -34,7 +37,7 @@ pub fn create_block( let topology = Topology::new(UniqueVec::new()); let block = BlockBuilder::new( - vec![AcceptedTransaction::accept(transaction, &limits).unwrap()], + vec![AcceptedTransaction::accept(transaction, &chain_id, &limits).unwrap()], topology.clone(), Vec::new(), ) diff --git a/core/benches/kura.rs b/core/benches/kura.rs index a47f731e31d..a7508e9567c 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -19,21 +19,26 @@ use iroha_primitives::unique_vec::UniqueVec; use tokio::{fs, runtime::Runtime}; async fn measure_block_size_for_n_executors(n_executors: u32) { + let chain_id = ChainId::new("0"); + let alice_id = AccountId::from_str("alice@test").expect("tested"); let bob_id = AccountId::from_str("bob@test").expect("tested"); let xor_id = AssetDefinitionId::from_str("xor#test").expect("tested"); let alice_xor_id = AssetId::new(xor_id, alice_id); let transfer = Transfer::asset_quantity(alice_xor_id, 10_u32, bob_id); let keypair = KeyPair::generate().expect("Failed to generate KeyPair."); - let tx = TransactionBuilder::new(AccountId::from_str("alice@wonderland").expect("checked")) - .with_instructions([transfer]) - .sign(keypair.clone()) - .expect("Failed to sign."); + let tx = TransactionBuilder::new( + chain_id.clone(), + AccountId::from_str("alice@wonderland").expect("checked"), + ) + .with_instructions([transfer]) + .sign(keypair.clone()) + .expect("Failed to sign."); let transaction_limits = TransactionLimits { max_instruction_number: 4096, max_wasm_size_bytes: 0, }; - let tx = AcceptedTransaction::accept(tx, &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 = Configuration { diff --git a/core/benches/validation.rs b/core/benches/validation.rs index 3a5bcaefe23..a7c41a024c6 100644 --- a/core/benches/validation.rs +++ b/core/benches/validation.rs @@ -23,7 +23,7 @@ const TRANSACTION_LIMITS: TransactionLimits = TransactionLimits { max_wasm_size_bytes: 0, }; -fn build_test_transaction(keys: KeyPair) -> SignedTransaction { +fn build_test_transaction(keys: KeyPair, chain_id: ChainId) -> SignedTransaction { let domain_name = "domain"; let domain_id = DomainId::from_str(domain_name).expect("does not panic"); let create_domain: InstructionBox = Register::domain(Domain::new(domain_id)).into(); @@ -47,10 +47,13 @@ fn build_test_transaction(keys: KeyPair) -> SignedTransaction { Register::asset_definition(AssetDefinition::quantity(asset_definition_id)).into(); let instructions = [create_domain, create_account, create_asset]; - TransactionBuilder::new(AccountId::new( - START_ACCOUNT.parse().expect("Valid"), - START_DOMAIN.parse().expect("Valid"), - )) + TransactionBuilder::new( + chain_id, + AccountId::new( + START_ACCOUNT.parse().expect("Valid"), + START_DOMAIN.parse().expect("Valid"), + ), + ) .with_instructions(instructions) .sign(keys) .expect("Failed to sign.") @@ -93,24 +96,28 @@ fn build_test_and_transient_wsv(keys: KeyPair) -> WorldStateView { } fn accept_transaction(criterion: &mut Criterion) { + let chain_id = ChainId::new("0"); + let keys = KeyPair::generate().expect("Failed to generate keys"); - let transaction = build_test_transaction(keys); + let transaction = build_test_transaction(keys, chain_id.clone()); let mut success_count = 0; let mut failures_count = 0; let _ = criterion.bench_function("accept", |b| { - b.iter( - || match AcceptedTransaction::accept(transaction.clone(), &TRANSACTION_LIMITS) { + b.iter(|| { + match AcceptedTransaction::accept(transaction.clone(), &chain_id, &TRANSACTION_LIMITS) { Ok(_) => success_count += 1, Err(_) => failures_count += 1, - }, - ); + } + }); }); println!("Success count: {success_count}, Failures count: {failures_count}"); } fn sign_transaction(criterion: &mut Criterion) { + let chain_id = ChainId::new("0"); + let keys = KeyPair::generate().expect("Failed to generate keys"); - let transaction = build_test_transaction(keys); + let transaction = build_test_transaction(keys, chain_id); let key_pair = KeyPair::generate().expect("Failed to generate KeyPair."); let mut success_count = 0; let mut failures_count = 0; @@ -124,10 +131,15 @@ fn sign_transaction(criterion: &mut Criterion) { } fn validate_transaction(criterion: &mut Criterion) { + let chain_id = ChainId::new("0"); + let keys = KeyPair::generate().expect("Failed to generate keys"); - let transaction = - AcceptedTransaction::accept(build_test_transaction(keys.clone()), &TRANSACTION_LIMITS) - .expect("Failed to accept transaction."); + let transaction = AcceptedTransaction::accept( + build_test_transaction(keys.clone(), chain_id.clone()), + &chain_id, + &TRANSACTION_LIMITS, + ) + .expect("Failed to accept transaction."); let mut success_count = 0; let mut failure_count = 0; let wsv = build_test_and_transient_wsv(keys); @@ -145,10 +157,15 @@ fn validate_transaction(criterion: &mut Criterion) { } fn sign_blocks(criterion: &mut Criterion) { + let chain_id = ChainId::new("0"); + let keys = KeyPair::generate().expect("Failed to generate keys"); - let transaction = - AcceptedTransaction::accept(build_test_transaction(keys), &TRANSACTION_LIMITS) - .expect("Failed to accept transaction."); + let transaction = AcceptedTransaction::accept( + build_test_transaction(keys, chain_id.clone()), + &chain_id, + &TRANSACTION_LIMITS, + ) + .expect("Failed to accept transaction."); let key_pair = KeyPair::generate().expect("Failed to generate KeyPair."); let kura = iroha_core::kura::Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); diff --git a/core/src/block.rs b/core/src/block.rs index 164dc6b5456..3c79e5dd8f4 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -235,6 +235,8 @@ mod chained { } mod valid { + use iroha_data_model::ChainId; + use super::*; use crate::sumeragi::network_topology::Role; @@ -265,6 +267,7 @@ mod valid { pub fn validate( block: SignedBlock, topology: &Topology, + expected_chain_id: &ChainId, wsv: &mut WorldStateView, ) -> Result { if !block.payload().header.is_genesis() { @@ -326,7 +329,7 @@ mod valid { return Err((block, BlockValidationError::HasCommittedTransactions)); } - if let Err(error) = Self::validate_transactions(&block, wsv) { + if let Err(error) = Self::validate_transactions(&block, expected_chain_id, wsv) { return Err((block, error.into())); } @@ -342,6 +345,7 @@ mod valid { fn validate_transactions( block: &SignedBlock, + expected_chain_id: &ChainId, wsv: &mut WorldStateView, ) -> Result<(), TransactionValidationError> { let is_genesis = block.payload().header.is_genesis(); @@ -356,10 +360,10 @@ mod valid { let limits = &transaction_executor.transaction_limits; let tx = if is_genesis { - AcceptedTransaction::accept_genesis(GenesisTransaction(value)) + AcceptedTransaction::accept_genesis(GenesisTransaction(value), expected_chain_id) } else { - AcceptedTransaction::accept(value, limits)? - }; + AcceptedTransaction::accept(value, expected_chain_id, limits) + }?; if error.is_some() { match transaction_executor.validate(tx, wsv) { @@ -720,6 +724,8 @@ mod tests { #[tokio::test] async fn should_reject_due_to_repetition() { + let chain_id = ChainId::new("0"); + // Predefined world state let alice_id = AccountId::from_str("alice@wonderland").expect("Valid"); let alice_keys = KeyPair::generate().expect("Valid"); @@ -740,11 +746,11 @@ mod tests { // Making two transactions that have the same instruction let transaction_limits = &wsv.transaction_executor().transaction_limits; - let tx = TransactionBuilder::new(alice_id) + let tx = TransactionBuilder::new(chain_id.clone(), alice_id) .with_instructions([create_asset_definition]) .sign(alice_keys.clone()) .expect("Valid"); - let tx = AcceptedTransaction::accept(tx, transaction_limits).expect("Valid"); + let tx = AcceptedTransaction::accept(tx, &chain_id, transaction_limits).expect("Valid"); // Creating a block of two identical transactions and validating it let transactions = vec![tx.clone(), tx]; @@ -763,6 +769,8 @@ mod tests { #[tokio::test] async fn tx_order_same_in_validation_and_revalidation() { + let chain_id = ChainId::new("0"); + // Predefined world state let alice_id = AccountId::from_str("alice@wonderland").expect("Valid"); let alice_keys = KeyPair::generate().expect("Valid"); @@ -783,11 +791,11 @@ mod tests { // Making two transactions that have the same instruction let transaction_limits = &wsv.transaction_executor().transaction_limits; - let tx = TransactionBuilder::new(alice_id.clone()) + let tx = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([create_asset_definition]) .sign(alice_keys.clone()) .expect("Valid"); - let tx = AcceptedTransaction::accept(tx, transaction_limits).expect("Valid"); + let tx = AcceptedTransaction::accept(tx, &chain_id, transaction_limits).expect("Valid"); let quantity: u32 = 200; let fail_quantity: u32 = 20; @@ -802,17 +810,17 @@ mod tests { AssetId::new(asset_definition_id, alice_id.clone()), ); - let tx0 = TransactionBuilder::new(alice_id.clone()) + let tx0 = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([fail_mint]) .sign(alice_keys.clone()) .expect("Valid"); - let tx0 = AcceptedTransaction::accept(tx0, transaction_limits).expect("Valid"); + let tx0 = AcceptedTransaction::accept(tx0, &chain_id, transaction_limits).expect("Valid"); - let tx2 = TransactionBuilder::new(alice_id) + let tx2 = TransactionBuilder::new(chain_id.clone(), alice_id) .with_instructions([succeed_mint]) .sign(alice_keys.clone()) .expect("Valid"); - let tx2 = AcceptedTransaction::accept(tx2, transaction_limits).expect("Valid"); + let tx2 = AcceptedTransaction::accept(tx2, &chain_id, transaction_limits).expect("Valid"); // Creating a block of two identical transactions and validating it let transactions = vec![tx0, tx, tx2]; @@ -831,6 +839,8 @@ mod tests { #[tokio::test] async fn failed_transactions_revert() { + let chain_id = ChainId::new("0"); + // Predefined world state let alice_id = AccountId::from_str("alice@wonderland").expect("Valid"); let alice_keys = KeyPair::generate().expect("Valid"); @@ -858,16 +868,18 @@ mod tests { Fail::new("Always fail".to_owned()).into(), ]; let instructions_accept: [InstructionBox; 2] = [create_domain.into(), create_asset.into()]; - let tx_fail = TransactionBuilder::new(alice_id.clone()) + let tx_fail = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions(instructions_fail) .sign(alice_keys.clone()) .expect("Valid"); - let tx_fail = AcceptedTransaction::accept(tx_fail, transaction_limits).expect("Valid"); - let tx_accept = TransactionBuilder::new(alice_id) + 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_keys.clone()) .expect("Valid"); - let tx_accept = AcceptedTransaction::accept(tx_accept, transaction_limits).expect("Valid"); + let tx_accept = + AcceptedTransaction::accept(tx_accept, &chain_id, transaction_limits).expect("Valid"); // Creating a block of where first transaction must fail and second one fully executed let transactions = vec![tx_fail, tx_accept]; diff --git a/core/src/gossiper.rs b/core/src/gossiper.rs index 365ebb7ac7a..ccfe657f21b 100644 --- a/core/src/gossiper.rs +++ b/core/src/gossiper.rs @@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration}; use iroha_config::sumeragi::Configuration; -use iroha_data_model::transaction::SignedTransaction; +use iroha_data_model::{transaction::SignedTransaction, ChainId}; use iroha_p2p::Broadcast; use parity_scale_codec::{Decode, Encode}; use tokio::sync::mpsc; @@ -31,6 +31,8 @@ impl TransactionGossiperHandle { /// Actor to gossip transactions and receive transaction gossips pub struct TransactionGossiper { + /// Unique id of the blockchain. Used for simple replay attack protection. + chain_id: ChainId, /// The size of batch that is being gossiped. Smaller size leads /// to longer time to synchronise, useful if you have high packet loss. gossip_batch_size: u32, @@ -57,19 +59,21 @@ impl TransactionGossiper { /// Construct [`Self`] from configuration pub fn from_configuration( + chain_id: ChainId, // Currently we are using configuration parameters from sumeragi not to break configuration - configuartion: &Configuration, + configuration: &Configuration, network: IrohaNetwork, queue: Arc, sumeragi: SumeragiHandle, ) -> Self { let wsv = sumeragi.wsv_clone(); Self { + chain_id, queue, sumeragi, network, - gossip_batch_size: configuartion.gossip_batch_size, - gossip_period: Duration::from_millis(configuartion.gossip_period_ms), + gossip_batch_size: configuration.gossip_batch_size, + gossip_period: Duration::from_millis(configuration.gossip_period_ms), wsv, } } @@ -115,7 +119,7 @@ impl TransactionGossiper { for tx in txs { let transaction_limits = &self.wsv.config.transaction_limits; - match AcceptedTransaction::accept(tx, transaction_limits) { + match AcceptedTransaction::accept(tx, &self.chain_id, transaction_limits) { Ok(tx) => match self.queue.push(tx, &self.wsv) { Ok(()) => {} Err(crate::queue::Failure { diff --git a/core/src/queue.rs b/core/src/queue.rs index b06a3a6c82f..cf133e25751 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -392,19 +392,24 @@ mod tests { }; fn accepted_tx(account_id: &str, key: KeyPair) -> AcceptedTransaction { + let chain_id = ChainId::new("0"); + let message = std::iter::repeat_with(rand::random::) .take(16) .collect(); let instructions = [Fail { message }]; - let tx = TransactionBuilder::new(AccountId::from_str(account_id).expect("Valid")) - .with_instructions(instructions) - .sign(key) - .expect("Failed to sign."); + let tx = TransactionBuilder::new( + chain_id.clone(), + AccountId::from_str(account_id).expect("Valid"), + ) + .with_instructions(instructions) + .sign(key) + .expect("Failed to sign."); let limits = TransactionLimits { max_instruction_number: 4096, max_wasm_size_bytes: 0, }; - AcceptedTransaction::accept(tx, &limits).expect("Failed to accept Transaction.") + AcceptedTransaction::accept(tx, &chain_id, &limits).expect("Failed to accept Transaction.") } pub fn world_with_test_domains( @@ -481,6 +486,8 @@ mod tests { #[test] async fn push_multisignature_tx() { + let chain_id = ChainId::new("0"); + let max_txs_in_block = 2; let key_pairs = [KeyPair::generate().unwrap(), KeyPair::generate().unwrap()]; let kura = Kura::blank_kura_for_testing(); @@ -511,8 +518,9 @@ mod tests { .expect("Default queue config should always build") }); let instructions: [InstructionBox; 0] = []; - let tx = TransactionBuilder::new("alice@wonderland".parse().expect("Valid")) - .with_instructions(instructions); + let tx = + TransactionBuilder::new(chain_id.clone(), "alice@wonderland".parse().expect("Valid")) + .with_instructions(instructions); let tx_limits = TransactionLimits { max_instruction_number: 4096, max_wasm_size_bytes: 0, @@ -525,7 +533,7 @@ mod tests { for key_pair in &key_pairs[1..] { signed_tx = signed_tx.sign(key_pair.clone()).expect("Failed to sign"); } - AcceptedTransaction::accept(signed_tx, &tx_limits) + AcceptedTransaction::accept(signed_tx, &chain_id, &tx_limits) .expect("Failed to accept Transaction.") }; // Check that fully signed transaction pass signature check @@ -537,6 +545,7 @@ mod tests { let get_tx = |key_pair| { AcceptedTransaction::accept( tx.clone().sign(key_pair).expect("Failed to sign."), + &chain_id, &tx_limits, ) .expect("Failed to accept Transaction.") @@ -742,6 +751,8 @@ mod tests { #[test] async fn custom_expired_transaction_is_rejected() { + let chain_id = ChainId::new("0"); + let max_txs_in_block = 2; let alice_key = KeyPair::generate().expect("Failed to generate keypair."); let kura = Kura::blank_kura_for_testing(); @@ -761,16 +772,19 @@ mod tests { let instructions = [Fail { message: "expired".to_owned(), }]; - let mut tx = - TransactionBuilder::new(AccountId::from_str("alice@wonderland").expect("Valid")) - .with_instructions(instructions); + let mut tx = TransactionBuilder::new( + chain_id.clone(), + AccountId::from_str("alice@wonderland").expect("Valid"), + ) + .with_instructions(instructions); tx.set_ttl(Duration::from_millis(10)); let tx = tx.sign(alice_key).expect("Failed to sign."); let limits = TransactionLimits { max_instruction_number: 4096, max_wasm_size_bytes: 0, }; - let tx = AcceptedTransaction::accept(tx, &limits).expect("Failed to accept Transaction."); + let tx = AcceptedTransaction::accept(tx, &chain_id, &limits) + .expect("Failed to accept Transaction."); queue .push(tx.clone(), &wsv) .expect("Failed to push tx into queue"); diff --git a/core/src/smartcontracts/isi/query.rs b/core/src/smartcontracts/isi/query.rs index d14ed740d0b..2291930467e 100644 --- a/core/src/smartcontracts/isi/query.rs +++ b/core/src/smartcontracts/isi/query.rs @@ -249,6 +249,8 @@ mod tests { valid_tx_per_block: usize, invalid_tx_per_block: usize, ) -> Result { + let chain_id = ChainId::new("0"); + let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); let mut wsv = WorldStateView::new(world_with_test_domains(), kura.clone(), query_handle); @@ -266,17 +268,17 @@ mod tests { let valid_tx = { let instructions: [InstructionBox; 0] = []; - let tx = TransactionBuilder::new(ALICE_ID.clone()) + let tx = TransactionBuilder::new(chain_id.clone(), ALICE_ID.clone()) .with_instructions(instructions) .sign(ALICE_KEYS.clone())?; - AcceptedTransaction::accept(tx, &limits)? + AcceptedTransaction::accept(tx, &chain_id, &limits)? }; let invalid_tx = { let isi = Fail::new("fail".to_owned()); - let tx = TransactionBuilder::new(ALICE_ID.clone()) + let tx = TransactionBuilder::new(chain_id.clone(), ALICE_ID.clone()) .with_instructions([isi.clone(), isi]) .sign(ALICE_KEYS.clone())?; - AcceptedTransaction::accept(tx, &huge_limits)? + AcceptedTransaction::accept(tx, &chain_id, &huge_limits)? }; let mut transactions = vec![valid_tx; valid_tx_per_block]; @@ -409,17 +411,19 @@ mod tests { #[test] async fn find_transaction() -> Result<()> { + let chain_id = ChainId::new("0"); + let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); let mut wsv = WorldStateView::new(world_with_test_domains(), kura.clone(), query_handle); let instructions: [InstructionBox; 0] = []; - let tx = TransactionBuilder::new(ALICE_ID.clone()) + let tx = TransactionBuilder::new(chain_id.clone(), ALICE_ID.clone()) .with_instructions(instructions) .sign(ALICE_KEYS.clone())?; let tx_limits = &wsv.transaction_executor().transaction_limits; - let va_tx = AcceptedTransaction::accept(tx, tx_limits)?; + let va_tx = AcceptedTransaction::accept(tx, &chain_id, tx_limits)?; let topology = Topology::new(UniqueVec::new()); let vcb = BlockBuilder::new(vec![va_tx.clone()], topology.clone(), Vec::new()) @@ -431,7 +435,7 @@ mod tests { wsv.apply(&vcb)?; kura.store_block(vcb); - let unapplied_tx = TransactionBuilder::new(ALICE_ID.clone()) + let unapplied_tx = TransactionBuilder::new(chain_id, ALICE_ID.clone()) .with_instructions([Unregister::account("account@domain".parse().unwrap())]) .sign(ALICE_KEYS.clone())?; let wrong_hash = unapplied_tx.hash(); diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index fcca60b867b..97046f1d9ab 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -14,6 +14,8 @@ use crate::{block::*, sumeragi::tracing::instrument}; /// `Sumeragi` is the implementation of the consensus. pub struct Sumeragi { + /// Unique id of the blockchain. Used for simple replay attack protection. + pub chain_id: ChainId, /// The pair of keys used for communication given this Sumeragi instance. pub key_pair: KeyPair, /// Address of queue @@ -209,19 +211,23 @@ impl Sumeragi { } }; - let block = - match ValidBlock::validate(block, &self.current_topology, &mut new_wsv) - .and_then(|block| { - block - .commit(&self.current_topology) - .map_err(|(block, error)| (block.into(), error)) - }) { - Ok(block) => block, - Err((_, error)) => { - error!(?error, "Received invalid genesis block"); - continue; - } - }; + let block = match ValidBlock::validate( + block, + &self.current_topology, + &self.chain_id, + &mut new_wsv, + ) + .and_then(|block| { + block + .commit(&self.current_topology) + .map_err(|(block, error)| (block.into(), error)) + }) { + Ok(block) => block, + Err((_, error)) => { + error!(?error, "Received invalid genesis block"); + continue; + } + }; new_wsv.world_mut().trusted_peers_ids = block.payload().commit_topology.clone(); @@ -244,8 +250,9 @@ impl Sumeragi { let transactions: Vec<_> = genesis_network .into_transactions() .into_iter() - .map(AcceptedTransaction::accept_genesis) - .collect(); + .map(|tx| AcceptedTransaction::accept_genesis(tx, &self.chain_id)) + .collect::>() + .expect("Genesis invalid"); let mut new_wsv = self.wsv.clone(); let genesis = BlockBuilder::new(transactions, self.current_topology.clone(), vec![]) @@ -364,172 +371,177 @@ impl Sumeragi { self.transaction_cache .retain(|tx| !self.wsv.has_transaction(tx.hash()) && !self.queue.is_expired(tx)); } -} -fn suggest_view_change( - sumeragi: &Sumeragi, - view_change_proof_chain: &mut ProofChain, - current_view_change_index: u64, -) { - let suspect_proof = - ProofBuilder::new(sumeragi.wsv.latest_block_hash(), current_view_change_index) - .sign(sumeragi.key_pair.clone()) - .expect("Proof signing failed"); - - view_change_proof_chain - .insert_proof( - &sumeragi.current_topology.ordered_peers, - sumeragi.current_topology.max_faults(), - sumeragi.wsv.latest_block_hash(), - suspect_proof, - ) - .unwrap_or_else(|err| error!("{err}")); + fn vote_for_block( + &self, + topology: &Topology, + BlockCreated { block }: BlockCreated, + ) -> Option { + let block_hash = block.payload().hash(); + let addr = &self.peer_id.address; + let role = self.current_topology.role(&self.peer_id); + trace!(%addr, %role, block_hash=%block_hash, "Block received, voting..."); - let msg = MessagePacket::new(view_change_proof_chain.clone(), None); - sumeragi.broadcast_packet(msg); -} + let mut new_wsv = self.wsv.clone(); + let block = match ValidBlock::validate(block, topology, &self.chain_id, &mut new_wsv) { + Ok(block) => block, + Err((_, error)) => { + warn!(%addr, %role, ?error, "Block validation failed"); + return None; + } + }; -fn prune_view_change_proofs_and_calculate_current_index( - sumeragi: &Sumeragi, - view_change_proof_chain: &mut ProofChain, -) -> u64 { - view_change_proof_chain.prune(sumeragi.wsv.latest_block_hash()); - view_change_proof_chain.verify_with_state( - &sumeragi.current_topology.ordered_peers, - sumeragi.current_topology.max_faults(), - sumeragi.wsv.latest_block_hash(), - ) as u64 -} + let signed_block = block + .sign(self.key_pair.clone()) + .expect("Block signing failed"); -#[allow(clippy::too_many_lines)] -fn handle_message( - message: Message, - sumeragi: &mut Sumeragi, - voting_block: &mut Option, - current_view_change_index: u64, - view_change_proof_chain: &mut ProofChain, - voting_signatures: &mut Vec>, -) { - let current_topology = &sumeragi.current_topology; - let role = current_topology.role(&sumeragi.peer_id); - let addr = &sumeragi.peer_id.address; - - #[allow(clippy::suspicious_operation_groupings)] - match (message, role) { - (Message::BlockSyncUpdate(BlockSyncUpdate { block }), _) => { - let block_hash = block.hash(); - info!(%addr, %role, hash=%block_hash, "Block sync update received"); - - match handle_block_sync(block, &sumeragi.wsv, &sumeragi.finalized_wsv) { - Ok(BlockSyncOk::CommitBlock(block, new_wsv)) => { - sumeragi.commit_block(block, new_wsv) - } - Ok(BlockSyncOk::ReplaceTopBlock(block, new_wsv)) => { - warn!( - %addr, %role, - peer_latest_block_hash=?sumeragi.wsv.latest_block_hash(), - peer_latest_block_view_change_index=?sumeragi.wsv.latest_block_view_change_index(), - consensus_latest_block_hash=%block.hash(), - consensus_latest_block_view_change_index=%block.payload().header.view_change_index, - "Soft fork occurred: peer in inconsistent state. Rolling back and replacing top block." - ); - sumeragi.replace_top_block(block, new_wsv) - } - Err((_, BlockSyncError::BlockNotValid(error))) => { - error!(%addr, %role, %block_hash, ?error, "Block not valid.") - } - Err((_, BlockSyncError::SoftForkBlockNotValid(error))) => { - error!(%addr, %role, %block_hash, ?error, "Soft-fork block not valid.") - } - Err(( - _, - BlockSyncError::SoftForkBlockSmallViewChangeIndex { - peer_view_change_index, - block_view_change_index, - }, - )) => { - debug!( - %addr, %role, - peer_latest_block_hash=?sumeragi.wsv.latest_block_hash(), - peer_latest_block_view_change_index=?peer_view_change_index, - consensus_latest_block_hash=%block_hash, - consensus_latest_block_view_change_index=%block_view_change_index, - "Soft fork doesn't occurred: block has the same or smaller view change index" - ); - } - Err(( - _, - BlockSyncError::BlockNotProperHeight { - peer_height, - block_height, - }, - )) => { - 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`).") + Some(VotingBlock::new(signed_block, new_wsv)) + } + + fn suggest_view_change( + &self, + view_change_proof_chain: &mut ProofChain, + current_view_change_index: u64, + ) { + let suspect_proof = + ProofBuilder::new(self.wsv.latest_block_hash(), current_view_change_index) + .sign(self.key_pair.clone()) + .expect("Proof signing failed"); + + view_change_proof_chain + .insert_proof( + &self.current_topology.ordered_peers, + self.current_topology.max_faults(), + self.wsv.latest_block_hash(), + suspect_proof, + ) + .unwrap_or_else(|err| error!("{err}")); + + let msg = MessagePacket::new(view_change_proof_chain.clone(), None); + self.broadcast_packet(msg); + } + + fn prune_view_change_proofs_and_calculate_current_index( + &self, + view_change_proof_chain: &mut ProofChain, + ) -> u64 { + view_change_proof_chain.prune(self.wsv.latest_block_hash()); + view_change_proof_chain.verify_with_state( + &self.current_topology.ordered_peers, + self.current_topology.max_faults(), + self.wsv.latest_block_hash(), + ) as u64 + } + + #[allow(clippy::too_many_lines)] + fn handle_message( + &mut self, + message: Message, + voting_block: &mut Option, + current_view_change_index: u64, + view_change_proof_chain: &mut ProofChain, + voting_signatures: &mut Vec>, + ) { + let current_topology = &self.current_topology; + let role = current_topology.role(&self.peer_id); + let addr = &self.peer_id.address; + + #[allow(clippy::suspicious_operation_groupings)] + match (message, role) { + (Message::BlockSyncUpdate(BlockSyncUpdate { block }), _) => { + let block_hash = block.hash(); + info!(%addr, %role, hash=%block_hash, "Block sync update received"); + + match handle_block_sync(&self.chain_id, block, &self.wsv, &self.finalized_wsv) { + Ok(BlockSyncOk::CommitBlock(block, new_wsv)) => { + self.commit_block(block, new_wsv) + } + Ok(BlockSyncOk::ReplaceTopBlock(block, new_wsv)) => { + warn!( + %addr, %role, + peer_latest_block_hash=?self.wsv.latest_block_hash(), + peer_latest_block_view_change_index=?self.wsv.latest_block_view_change_index(), + consensus_latest_block_hash=%block.hash(), + consensus_latest_block_view_change_index=%block.payload().header.view_change_index, + "Soft fork occurred: peer in inconsistent state. Rolling back and replacing top block." + ); + self.replace_top_block(block, new_wsv) + } + Err((_, BlockSyncError::BlockNotValid(error))) => { + error!(%addr, %role, %block_hash, ?error, "Block not valid.") + } + Err((_, BlockSyncError::SoftForkBlockNotValid(error))) => { + error!(%addr, %role, %block_hash, ?error, "Soft-fork block not valid.") + } + Err(( + _, + BlockSyncError::SoftForkBlockSmallViewChangeIndex { + peer_view_change_index, + block_view_change_index, + }, + )) => { + debug!( + %addr, %role, + peer_latest_block_hash=?self.wsv.latest_block_hash(), + peer_latest_block_view_change_index=?peer_view_change_index, + consensus_latest_block_hash=%block_hash, + consensus_latest_block_view_change_index=%block_view_change_index, + "Soft fork doesn't occurred: block has the same or smaller view change index" + ); + } + Err(( + _, + BlockSyncError::BlockNotProperHeight { + peer_height, + block_height, + }, + )) => { + 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`).") + } } } - } - ( - Message::BlockCommitted(BlockCommitted { hash, signatures }), - Role::Leader | Role::ValidatingPeer | Role::ProxyTail | Role::ObservingPeer, - ) => { - let is_consensus_required = current_topology.is_consensus_required().is_some(); - if role == Role::ProxyTail && is_consensus_required - || role == Role::Leader && !is_consensus_required - { - error!(%addr, %role, "Received BlockCommitted message, but shouldn't"); - } else if let Some(voted_block) = voting_block.take() { - let voting_block_hash = voted_block.block.payload().hash(); - - if hash == voting_block_hash { - match voted_block - .block - .commit_with_signatures(current_topology, signatures) - { - Ok(committed_block) => { - sumeragi.commit_block(committed_block, voted_block.new_wsv) - } - Err((_, error)) => { - error!(%addr, %role, %hash, ?error, "Block failed to be committed") - } + ( + Message::BlockCommitted(BlockCommitted { hash, signatures }), + Role::Leader | Role::ValidatingPeer | Role::ProxyTail | Role::ObservingPeer, + ) => { + let is_consensus_required = current_topology.is_consensus_required().is_some(); + if role == Role::ProxyTail && is_consensus_required + || role == Role::Leader && !is_consensus_required + { + error!(%addr, %role, "Received BlockCommitted message, but shouldn't"); + } else if let Some(voted_block) = voting_block.take() { + let voting_block_hash = voted_block.block.payload().hash(); + + if hash == voting_block_hash { + match voted_block + .block + .commit_with_signatures(current_topology, signatures) + { + Ok(committed_block) => { + self.commit_block(committed_block, voted_block.new_wsv) + } + Err((_, error)) => { + error!(%addr, %role, %hash, ?error, "Block failed to be committed") + } + }; + } else { + error!( + %addr, %role, committed_block_hash=%hash, %voting_block_hash, + "The hash of the committed block does not match the hash of the block stored by the peer." + ); + + *voting_block = Some(voted_block); }; } else { - error!( - %addr, %role, committed_block_hash=%hash, %voting_block_hash, - "The hash of the committed block does not match the hash of the block stored by the peer." - ); - - *voting_block = Some(voted_block); - }; - } else { - error!(%addr, %role, %hash, "Peer missing voting block") + error!(%addr, %role, %hash, "Peer missing voting block") + } } - } - (Message::BlockCreated(block_created), Role::ValidatingPeer) => { - let current_topology = current_topology + (Message::BlockCreated(block_created), Role::ValidatingPeer) => { + let current_topology = current_topology .is_consensus_required() .expect("Peer has `ValidatingPeer` role, which mean that current topology require consensus"); - if let Some(v_block) = vote_for_block(sumeragi, ¤t_topology, block_created) { - let block_hash = v_block.block.payload().hash(); - - let msg = MessagePacket::new( - view_change_proof_chain.clone(), - Some(BlockSigned::from(v_block.block.clone()).into()), - ); - - sumeragi.broadcast_packet_to(msg, [current_topology.proxy_tail()]); - info!(%addr, %block_hash, "Block validated, signed and forwarded"); - - *voting_block = Some(v_block); - } - } - (Message::BlockCreated(block_created), Role::ObservingPeer) => { - let current_topology = current_topology.is_consensus_required().expect( - "Peer has `ObservingPeer` role, which mean that current topology require consensus", - ); - - if let Some(v_block) = vote_for_block(sumeragi, ¤t_topology, block_created) { - if current_view_change_index >= 1 { + if let Some(v_block) = self.vote_for_block(¤t_topology, block_created) { let block_hash = v_block.block.payload().hash(); let msg = MessagePacket::new( @@ -537,176 +549,198 @@ fn handle_message( Some(BlockSigned::from(v_block.block.clone()).into()), ); - sumeragi.broadcast_packet_to(msg, [current_topology.proxy_tail()]); + self.broadcast_packet_to(msg, [current_topology.proxy_tail()]); info!(%addr, %block_hash, "Block validated, signed and forwarded"); + *voting_block = Some(v_block); - } else { - error!(%addr, %role, "Received BlockCreated message, but shouldn't"); } } - } - (Message::BlockCreated(block_created), Role::ProxyTail) => { - if let Some(mut new_block) = vote_for_block(sumeragi, current_topology, 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); + (Message::BlockCreated(block_created), Role::ObservingPeer) => { + let current_topology = current_topology.is_consensus_required().expect( + "Peer has `ObservingPeer` role, which mean that current topology require consensus", + ); + + if let Some(v_block) = self.vote_for_block(¤t_topology, block_created) { + if current_view_change_index >= 1 { + let block_hash = v_block.block.payload().hash(); + + let msg = MessagePacket::new( + view_change_proof_chain.clone(), + Some(BlockSigned::from(v_block.block.clone()).into()), + ); + + self.broadcast_packet_to(msg, [current_topology.proxy_tail()]); + info!(%addr, %block_hash, "Block validated, signed and forwarded"); + *voting_block = Some(v_block); + } else { + error!(%addr, %role, "Received BlockCreated message, but shouldn't"); + } + } } - } - (Message::BlockSigned(BlockSigned { hash, signatures }), Role::ProxyTail) => { - trace!(block_hash=%hash, "Received block signatures"); + (Message::BlockCreated(block_created), Role::ProxyTail) => { + if let Some(mut new_block) = self.vote_for_block(current_topology, 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); + } + } + (Message::BlockSigned(BlockSigned { hash, signatures }), Role::ProxyTail) => { + trace!(block_hash=%hash, "Received block signatures"); - let roles: &[Role] = if current_view_change_index >= 1 { - &[Role::ValidatingPeer, Role::ObservingPeer] - } else { - &[Role::ValidatingPeer] - }; - let valid_signatures = current_topology.filter_signatures_by_roles(roles, &signatures); + let roles: &[Role] = if current_view_change_index >= 1 { + &[Role::ValidatingPeer, Role::ObservingPeer] + } else { + &[Role::ValidatingPeer] + }; + let valid_signatures = + current_topology.filter_signatures_by_roles(roles, &signatures); - if let Some(voted_block) = voting_block.as_mut() { - let voting_block_hash = voted_block.block.payload().hash(); + if let Some(voted_block) = voting_block.as_mut() { + let voting_block_hash = voted_block.block.payload().hash(); - if hash == voting_block_hash { - add_signatures::(voted_block, valid_signatures); + if hash == voting_block_hash { + add_signatures::(voted_block, valid_signatures); + } else { + debug!(%voting_block_hash, "Received signatures are not for the current block"); + } } else { - debug!(%voting_block_hash, "Received signatures are not for the current block"); + // 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); } - } 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); } - } - (msg, role) => { - trace!(%addr, %role, ?msg, "message not handled") + (msg, role) => { + trace!(%addr, %role, ?msg, "message not handled") + } } } -} -#[allow(clippy::too_many_lines)] -fn process_message_independent( - sumeragi: &mut Sumeragi, - voting_block: &mut Option, - current_view_change_index: u64, - view_change_proof_chain: &mut ProofChain, - round_start_time: &Instant, - #[cfg_attr(not(debug_assertions), allow(unused_variables))] is_genesis_peer: bool, -) { - let current_topology = &sumeragi.current_topology; - let role = current_topology.role(&sumeragi.peer_id); - let addr = &sumeragi.peer_id.address; - - match role { - Role::Leader => { - if voting_block.is_none() { - let cache_full = sumeragi.transaction_cache.len() >= sumeragi.max_txs_in_block; - let deadline_reached = round_start_time.elapsed() > sumeragi.block_time; - let cache_non_empty = !sumeragi.transaction_cache.is_empty(); - - if cache_full || (deadline_reached && cache_non_empty) { - let transactions = sumeragi.transaction_cache.clone(); - info!(%addr, txns=%transactions.len(), "Creating block..."); - - // TODO: properly process triggers! - let mut new_wsv = sumeragi.wsv.clone(); - let event_recommendations = Vec::new(); - let new_block = match BlockBuilder::new( - transactions, - sumeragi.current_topology.clone(), - event_recommendations, - ) - .chain(current_view_change_index, &mut new_wsv) - .sign(sumeragi.key_pair.clone()) - { - Ok(block) => block, - Err(error) => { - error!(?error, "Failed to sign block"); - return; - } - }; + #[allow(clippy::too_many_lines)] + fn process_message_independent( + &mut self, + voting_block: &mut Option, + current_view_change_index: u64, + view_change_proof_chain: &mut ProofChain, + 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; - if let Some(current_topology) = current_topology.is_consensus_required() { - info!(%addr, block_payload_hash=%new_block.payload().hash(), "Block created"); - *voting_block = Some(VotingBlock::new(new_block.clone(), new_wsv)); + 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(); - let msg = MessagePacket::new( - view_change_proof_chain.clone(), - Some(BlockCreated::from(new_block).into()), - ); - if current_view_change_index >= 1 { - sumeragi.broadcast_packet(msg); - } else { - sumeragi.broadcast_packet_to(msg, current_topology.voting_peers()); - } - } else { - match new_block.commit(current_topology) { - Ok(committed_block) => { - let msg = MessagePacket::new( - view_change_proof_chain.clone(), - Some(BlockCommitted::from(committed_block.clone()).into()), - ); + if cache_full || (deadline_reached && cache_non_empty) { + let transactions = self.transaction_cache.clone(); + info!(%addr, txns=%transactions.len(), "Creating block..."); - sumeragi.broadcast_packet(msg); - sumeragi.commit_block(committed_block, new_wsv); + // TODO: properly process triggers! + let mut new_wsv = self.wsv.clone(); + let event_recommendations = Vec::new(); + let new_block = match BlockBuilder::new( + transactions, + self.current_topology.clone(), + event_recommendations, + ) + .chain(current_view_change_index, &mut new_wsv) + .sign(self.key_pair.clone()) + { + Ok(block) => block, + Err(error) => { + error!(?error, "Failed to sign block"); + return; + } + }; + + if let Some(current_topology) = current_topology.is_consensus_required() { + info!(%addr, block_payload_hash=%new_block.payload().hash(), "Block created"); + *voting_block = Some(VotingBlock::new(new_block.clone(), new_wsv)); + + let msg = MessagePacket::new( + view_change_proof_chain.clone(), + Some(BlockCreated::from(new_block).into()), + ); + if current_view_change_index >= 1 { + self.broadcast_packet(msg); + } else { + self.broadcast_packet_to(msg, current_topology.voting_peers()); + } + } else { + match new_block.commit(current_topology) { + Ok(committed_block) => { + let msg = MessagePacket::new( + view_change_proof_chain.clone(), + Some(BlockCommitted::from(committed_block.clone()).into()), + ); + + self.broadcast_packet(msg); + self.commit_block(committed_block, new_wsv); + } + Err((_, error)) => error!(%addr, role=%Role::Leader, ?error), } - Err((_, error)) => error!(%addr, role=%Role::Leader, ?error), } } } } - } - Role::ProxyTail => { - if let Some(voted_block) = voting_block.take() { - let voted_at = voted_block.voted_at; - let new_wsv = voted_block.new_wsv; + Role::ProxyTail => { + if let Some(voted_block) = voting_block.take() { + let voted_at = voted_block.voted_at; + let new_wsv = voted_block.new_wsv; - match voted_block.block.commit(current_topology) { - Ok(committed_block) => { - info!(voting_block_hash = %committed_block.hash(), "Block reached required number of votes"); + match voted_block.block.commit(current_topology) { + Ok(committed_block) => { + info!(voting_block_hash = %committed_block.hash(), "Block reached required number of votes"); - let msg = MessagePacket::new( - view_change_proof_chain.clone(), - Some(BlockCommitted::from(committed_block.clone()).into()), - ); + let msg = MessagePacket::new( + view_change_proof_chain.clone(), + Some(BlockCommitted::from(committed_block.clone()).into()), + ); - let current_topology = current_topology + let current_topology = current_topology .is_consensus_required() .expect("Peer has `ProxyTail` role, which mean that current topology require consensus"); - #[cfg(debug_assertions)] - if is_genesis_peer && sumeragi.debug_force_soft_fork { - std::thread::sleep(sumeragi.pipeline_time() * 2); - } else if current_view_change_index >= 1 { - sumeragi.broadcast_packet(msg); - } else { - sumeragi.broadcast_packet_to(msg, current_topology.voting_peers()); - } - - #[cfg(not(debug_assertions))] - { - if current_view_change_index >= 1 { - sumeragi.broadcast_packet(msg); + #[cfg(debug_assertions)] + if is_genesis_peer && self.debug_force_soft_fork { + std::thread::sleep(self.pipeline_time() * 2); + } else if current_view_change_index >= 1 { + self.broadcast_packet(msg); } else { - sumeragi.broadcast_packet_to( - msg, - current_topology - .ordered_peers - .iter() - .take(current_topology.min_votes_for_commit()), - ); + self.broadcast_packet_to(msg, current_topology.voting_peers()); } + + #[cfg(not(debug_assertions))] + { + if current_view_change_index >= 1 { + self.broadcast_packet(msg); + } else { + self.broadcast_packet_to( + msg, + current_topology + .ordered_peers + .iter() + .take(current_topology.min_votes_for_commit()), + ); + } + } + self.commit_block(committed_block, new_wsv); + } + Err((block, error)) => { + // Restore the current voting block and continue the round + *voting_block = Some(VotingBlock::voted_at(block, new_wsv, voted_at)); + trace!(?error, "Not enough signatures, waiting for more..."); } - sumeragi.commit_block(committed_block, new_wsv); - } - Err((block, error)) => { - // Restore the current voting block and continue the round - *voting_block = Some(VotingBlock::voted_at(block, new_wsv, voted_at)); - trace!(?error, "Not enough signatures, waiting for more..."); } } } + _ => {} } - _ => {} } } @@ -857,10 +891,8 @@ pub(crate) fn run( ); sumeragi.send_events(expired_transactions.iter().map(expired_event)); - let current_view_change_index = prune_view_change_proofs_and_calculate_current_index( - &sumeragi, - &mut view_change_proof_chain, - ); + let current_view_change_index = sumeragi + .prune_view_change_proofs_and_calculate_current_index(&mut view_change_proof_chain); reset_state( &sumeragi.peer_id, @@ -893,11 +925,7 @@ pub(crate) fn run( warn!(peer_public_key=%sumeragi.peer_id.public_key, %role, "No block produced in due time, requesting view change..."); } - suggest_view_change( - &sumeragi, - &mut view_change_proof_chain, - current_view_change_index, - ); + sumeragi.suggest_view_change(&mut view_change_proof_chain, current_view_change_index); // NOTE: View change must be periodically suggested until it is accepted. // Must be initialized to pipeline time but can increase by chosen amount @@ -914,9 +942,8 @@ pub(crate) fn run( should_sleep = true; }, |message| { - handle_message( + sumeragi.handle_message( message, - &mut sumeragi, &mut voting_block, current_view_change_index, &mut view_change_proof_chain, @@ -926,10 +953,8 @@ pub(crate) fn run( ); // State could be changed after handling message so it is necessary to reset state before handling message independent step - let current_view_change_index = prune_view_change_proofs_and_calculate_current_index( - &sumeragi, - &mut view_change_proof_chain, - ); + let current_view_change_index = sumeragi + .prune_view_change_proofs_and_calculate_current_index(&mut view_change_proof_chain); reset_state( &sumeragi.peer_id, @@ -949,8 +974,7 @@ pub(crate) fn run( &mut view_change_time, ); - process_message_independent( - &mut sumeragi, + sumeragi.process_message_independent( &mut voting_block, current_view_change_index, &mut view_change_proof_chain, @@ -989,32 +1013,6 @@ fn expired_event(txn: &AcceptedTransaction) -> Event { .into() } -fn vote_for_block( - sumeragi: &Sumeragi, - topology: &Topology, - BlockCreated { block }: BlockCreated, -) -> Option { - let block_hash = block.payload().hash(); - let addr = &sumeragi.peer_id.address; - let role = sumeragi.current_topology.role(&sumeragi.peer_id); - trace!(%addr, %role, block_hash=%block_hash, "Block received, voting..."); - - let mut new_wsv = sumeragi.wsv.clone(); - let block = match ValidBlock::validate(block, topology, &mut new_wsv) { - Ok(block) => block, - Err((_, error)) => { - warn!(%addr, %role, ?error, "Block validation failed"); - return None; - } - }; - - let signed_block = block - .sign(sumeragi.key_pair.clone()) - .expect("Block signing failed"); - - Some(VotingBlock::new(signed_block, new_wsv)) -} - /// 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 @@ -1111,6 +1109,7 @@ enum BlockSyncError { } fn handle_block_sync( + chain_id: &ChainId, block: SignedBlock, wsv: &WorldStateView, finalized_wsv: &WorldStateView, @@ -1128,7 +1127,7 @@ fn handle_block_sync( let view_change_index = block.payload().header().view_change_index; Topology::recreate_topology(&last_committed_block, view_change_index, new_peers) }; - ValidBlock::validate(block, &topology, &mut new_wsv) + ValidBlock::validate(block, &topology, chain_id, &mut new_wsv) .and_then(|block| { block .commit(&topology) @@ -1148,7 +1147,7 @@ fn handle_block_sync( let view_change_index = block.payload().header().view_change_index; Topology::recreate_topology(&last_committed_block, view_change_index, new_peers) }; - ValidBlock::validate(block, &topology, &mut new_wsv) + ValidBlock::validate(block, &topology, chain_id, &mut new_wsv) .and_then(|block| { block .commit(&topology) @@ -1191,6 +1190,7 @@ mod tests { use crate::{query::store::LiveQueryStore, smartcontracts::Registrable}; fn create_data_for_test( + chain_id: &ChainId, topology: &Topology, leader_key_pair: KeyPair, ) -> (WorldStateView, Arc, SignedBlock) { @@ -1209,15 +1209,19 @@ mod tests { // Create "genesis" block // Creating an instruction - let fail_box: InstructionBox = Fail::new("Dummy isi".to_owned()).into(); + let fail_box = Fail::new("Dummy isi".to_owned()); // Making two transactions that have the same instruction - let tx = TransactionBuilder::new(alice_id.clone()) + let tx = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([fail_box]) .sign(alice_keys.clone()) .expect("Valid"); - let tx = AcceptedTransaction::accept(tx, &wsv.transaction_executor().transaction_limits) - .expect("Valid"); + let tx = AcceptedTransaction::accept( + tx, + chain_id, + &wsv.transaction_executor().transaction_limits, + ) + .expect("Valid"); // Creating a block of two identical transactions and validating it let block = BlockBuilder::new(vec![tx.clone(), tx], topology.clone(), Vec::new()) @@ -1237,20 +1241,28 @@ mod tests { "xor2#wonderland".parse().expect("Valid"), )); - let tx1 = TransactionBuilder::new(alice_id.clone()) + let tx1 = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([create_asset_definition1]) .sign(alice_keys.clone()) .expect("Valid"); - let tx1 = AcceptedTransaction::accept(tx1, &wsv.transaction_executor().transaction_limits) - .map(Into::into) - .expect("Valid"); - let tx2 = TransactionBuilder::new(alice_id) + let tx1 = AcceptedTransaction::accept( + tx1, + chain_id, + &wsv.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_keys) .expect("Valid"); - let tx2 = AcceptedTransaction::accept(tx2, &wsv.transaction_executor().transaction_limits) - .map(Into::into) - .expect("Valid"); + let tx2 = AcceptedTransaction::accept( + tx2, + chain_id, + &wsv.transaction_executor().transaction_limits, + ) + .map(Into::into) + .expect("Valid"); // Creating a block of two identical transactions and validating it let block = BlockBuilder::new(vec![tx1, tx2], topology.clone(), Vec::new()) @@ -1264,32 +1276,39 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_invalid_block() { + let chain_id = ChainId::new("0"); + let leader_key_pair = KeyPair::generate().unwrap(); let topology = Topology::new(unique_vec![PeerId::new( &"127.0.0.1:8080".parse().unwrap(), leader_key_pair.public_key(), )]); - let (finalized_wsv, _, mut block) = create_data_for_test(&topology, leader_key_pair); + let (finalized_wsv, _, mut block) = + create_data_for_test(&chain_id, &topology, leader_key_pair); let wsv = finalized_wsv.clone(); // Malform block to make it invalid block.payload_mut().commit_topology.clear(); - let result = handle_block_sync(block, &wsv, &finalized_wsv); + let result = handle_block_sync(&chain_id, block, &wsv, &finalized_wsv); assert!(matches!(result, Err((_, BlockSyncError::BlockNotValid(_))))) } #[test] async fn block_sync_invalid_soft_fork_block() { + let chain_id = ChainId::new("0"); + let leader_key_pair = KeyPair::generate().unwrap(); let topology = Topology::new(unique_vec![PeerId::new( &"127.0.0.1:8080".parse().unwrap(), leader_key_pair.public_key(), )]); - let (finalized_wsv, kura, mut block) = create_data_for_test(&topology, leader_key_pair); + let (finalized_wsv, kura, mut block) = + create_data_for_test(&chain_id, &topology, leader_key_pair); let mut wsv = finalized_wsv.clone(); - let validated_block = ValidBlock::validate(block.clone(), &topology, &mut wsv).unwrap(); + let validated_block = + ValidBlock::validate(block.clone(), &topology, &chain_id, &mut wsv).unwrap(); let committed_block = validated_block.commit(&topology).expect("Block is valid"); wsv.apply_without_execution(&committed_block) .expect("Failed to apply block"); @@ -1298,7 +1317,7 @@ mod tests { // Malform block to make it invalid block.payload_mut().commit_topology.clear(); - let result = handle_block_sync(block, &wsv, &finalized_wsv); + let result = handle_block_sync(&chain_id, block, &wsv, &finalized_wsv); assert!(matches!( result, Err((_, BlockSyncError::SoftForkBlockNotValid(_))) @@ -1308,15 +1327,18 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_not_proper_height() { + let chain_id = ChainId::new("0"); + let topology = Topology::new(UniqueVec::new()); let leader_key_pair = KeyPair::generate().unwrap(); - let (finalized_wsv, _, mut block) = create_data_for_test(&topology, leader_key_pair); + let (finalized_wsv, _, mut block) = + create_data_for_test(&chain_id, &topology, leader_key_pair); let wsv = finalized_wsv.clone(); // Change block height block.payload_mut().header.height = 42; - let result = handle_block_sync(block, &wsv, &finalized_wsv); + let result = handle_block_sync(&chain_id, block, &wsv, &finalized_wsv); assert!(matches!( result, Err(( @@ -1332,28 +1354,34 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_commit_block() { + let chain_id = ChainId::new("0"); + let leader_key_pair = KeyPair::generate().unwrap(); let topology = Topology::new(unique_vec![PeerId::new( &"127.0.0.1:8080".parse().unwrap(), leader_key_pair.public_key(), )]); - let (finalized_wsv, _, block) = create_data_for_test(&topology, leader_key_pair); + let (finalized_wsv, _, block) = create_data_for_test(&chain_id, &topology, leader_key_pair); let wsv = finalized_wsv.clone(); - let result = handle_block_sync(block, &wsv, &finalized_wsv); + let result = handle_block_sync(&chain_id, block, &wsv, &finalized_wsv); assert!(matches!(result, Ok(BlockSyncOk::CommitBlock(_, _)))) } #[test] async fn block_sync_replace_top_block() { + let chain_id = ChainId::new("0"); + let leader_key_pair = KeyPair::generate().unwrap(); let topology = Topology::new(unique_vec![PeerId::new( &"127.0.0.1:8080".parse().unwrap(), leader_key_pair.public_key(), )]); - let (finalized_wsv, kura, mut block) = create_data_for_test(&topology, leader_key_pair); + let (finalized_wsv, kura, mut block) = + create_data_for_test(&chain_id, &topology, leader_key_pair); let mut wsv = finalized_wsv.clone(); - let validated_block = ValidBlock::validate(block.clone(), &topology, &mut wsv).unwrap(); + let validated_block = + ValidBlock::validate(block.clone(), &topology, &chain_id, &mut wsv).unwrap(); let committed_block = validated_block.commit(&topology).expect("Block is valid"); wsv.apply_without_execution(&committed_block) .expect("Failed to apply block"); @@ -1363,24 +1391,28 @@ mod tests { // Increase block view change index block.payload_mut().header.view_change_index = 42; - let result = handle_block_sync(block, &wsv, &finalized_wsv); + let result = handle_block_sync(&chain_id, block, &wsv, &finalized_wsv); assert!(matches!(result, Ok(BlockSyncOk::ReplaceTopBlock(_, _)))) } #[test] async fn block_sync_small_view_change_index() { + let chain_id = ChainId::new("0"); + let leader_key_pair = KeyPair::generate().unwrap(); let topology = Topology::new(unique_vec![PeerId::new( &"127.0.0.1:8080".parse().unwrap(), leader_key_pair.public_key(), )]); - let (finalized_wsv, kura, mut block) = create_data_for_test(&topology, leader_key_pair); + let (finalized_wsv, kura, mut block) = + create_data_for_test(&chain_id, &topology, leader_key_pair); let mut wsv = finalized_wsv.clone(); // Increase block view change index block.payload_mut().header.view_change_index = 42; - let validated_block = ValidBlock::validate(block.clone(), &topology, &mut wsv).unwrap(); + let validated_block = + ValidBlock::validate(block.clone(), &topology, &chain_id, &mut wsv).unwrap(); let committed_block = validated_block.commit(&topology).expect("Block is valid"); wsv.apply_without_execution(&committed_block) .expect("Failed to apply block"); @@ -1390,7 +1422,7 @@ mod tests { // Decrease block view change index back block.payload_mut().header.view_change_index = 0; - let result = handle_block_sync(block, &wsv, &finalized_wsv); + let result = handle_block_sync(&chain_id, block, &wsv, &finalized_wsv); assert!(matches!( result, Err(( @@ -1406,9 +1438,12 @@ mod tests { #[test] #[allow(clippy::redundant_clone)] async fn block_sync_genesis_block_do_not_replace() { + let chain_id = ChainId::new("0"); + let topology = Topology::new(UniqueVec::new()); let leader_key_pair = KeyPair::generate().unwrap(); - let (finalized_wsv, _, mut block) = create_data_for_test(&topology, leader_key_pair); + let (finalized_wsv, _, mut block) = + create_data_for_test(&chain_id, &topology, leader_key_pair); let wsv = finalized_wsv.clone(); // Change block height and view change index @@ -1416,7 +1451,7 @@ mod tests { block.payload_mut().header.view_change_index = 42; block.payload_mut().header.height = 1; - let result = handle_block_sync(block, &wsv, &finalized_wsv); + let result = handle_block_sync(&chain_id, block, &wsv, &finalized_wsv); assert!(matches!( result, Err(( diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 2bafc67e9f2..883636b6c59 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -227,6 +227,7 @@ impl SumeragiHandle { } fn replay_block( + chain_id: &ChainId, block: &SignedBlock, wsv: &mut WorldStateView, mut current_topology: Topology, @@ -234,7 +235,7 @@ impl SumeragiHandle { // NOTE: topology need to be updated up to block's view_change_index current_topology.rotate_all_n(block.payload().header.view_change_index); - let block = ValidBlock::validate(block.clone(), ¤t_topology, wsv) + let block = ValidBlock::validate(block.clone(), ¤t_topology, chain_id, wsv) .expect("Kura blocks should be valid") .commit(¤t_topology) .expect("Kura blocks should be valid"); @@ -258,6 +259,7 @@ impl SumeragiHandle { #[allow(clippy::too_many_lines)] pub fn start( SumeragiStartArgs { + chain_id, configuration, events_sender, mut wsv, @@ -296,14 +298,14 @@ impl SumeragiHandle { let block_iter_except_last = (&mut blocks_iter).take(block_count.saturating_sub(skip_block_count + 1)); for block in block_iter_except_last { - current_topology = Self::replay_block(&block, &mut wsv, current_topology); + current_topology = Self::replay_block(&chain_id, &block, &mut wsv, current_topology); } // finalized_wsv is one block behind let finalized_wsv = wsv.clone(); if let Some(block) = blocks_iter.next() { - current_topology = Self::replay_block(&block, &mut wsv, current_topology); + current_topology = Self::replay_block(&chain_id, &block, &mut wsv, current_topology); } info!("Sumeragi has finished loading blocks and setting up the WSV"); @@ -318,6 +320,7 @@ impl SumeragiHandle { let debug_force_soft_fork = false; let sumeragi = main_loop::Sumeragi { + chain_id, key_pair: configuration.key_pair.clone(), queue: Arc::clone(&queue), peer_id: configuration.peer_id.clone(), @@ -418,6 +421,7 @@ impl VotingBlock { /// Arguments for [`SumeragiHandle::start`] function #[allow(missing_docs)] pub struct SumeragiStartArgs<'args> { + pub chain_id: ChainId, pub configuration: &'args Configuration, pub events_sender: EventsSender, pub wsv: WorldStateView, diff --git a/core/src/tx.rs b/core/src/tx.rs index 89bacca20e0..0cee36bc3d4 100644 --- a/core/src/tx.rs +++ b/core/src/tx.rs @@ -11,6 +11,7 @@ use eyre::Result; use iroha_crypto::{HashOf, SignatureVerificationFail, SignaturesOf}; pub use iroha_data_model::prelude::*; use iroha_data_model::{ + isi::error::Mismatch, query::error::FindError, transaction::{error::TransactionLimitError, TransactionLimits}, }; @@ -34,12 +35,30 @@ pub enum AcceptTransactionFail { 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 + ChainIdMismatch(Mismatch), } impl AcceptedTransaction { /// Accept genesis transaction. Transition from [`GenesisTransaction`] to [`AcceptedTransaction`]. - pub fn accept_genesis(tx: GenesisTransaction) -> Self { - Self(tx.0) + /// + /// # Errors + /// + /// - if transaction chain id doesn't match + pub fn accept_genesis( + tx: GenesisTransaction, + expected_chain_id: &ChainId, + ) -> Result { + let actual_chain_id = &tx.0.payload().chain_id; + + if expected_chain_id != actual_chain_id { + return Err(AcceptTransactionFail::ChainIdMismatch(Mismatch { + expected: expected_chain_id.clone(), + actual: actual_chain_id.clone(), + })); + } + + Ok(Self(tx.0)) } /// Accept transaction. Transition from [`SignedTransaction`] to [`AcceptedTransaction`]. @@ -48,14 +67,24 @@ impl AcceptedTransaction { /// /// - if it does not adhere to limits pub fn accept( - transaction: SignedTransaction, + tx: SignedTransaction, + expected_chain_id: &ChainId, limits: &TransactionLimits, ) -> Result { - if *iroha_genesis::GENESIS_ACCOUNT_ID == transaction.payload().authority { + let actual_chain_id = &tx.payload().chain_id; + + if expected_chain_id != actual_chain_id { + return Err(AcceptTransactionFail::ChainIdMismatch(Mismatch { + expected: expected_chain_id.clone(), + actual: actual_chain_id.clone(), + })); + } + + if *iroha_genesis::GENESIS_ACCOUNT_ID == tx.payload().authority { return Err(AcceptTransactionFail::UnexpectedGenesisAccountSignature); } - match &transaction.payload().instructions { + match &tx.payload().instructions { Executable::Instructions(instructions) => { let instruction_count = instructions.len(); if Self::len_u64(instruction_count) > limits.max_instruction_number { @@ -87,7 +116,7 @@ impl AcceptedTransaction { } } - Ok(Self(transaction)) + Ok(Self(tx)) } /// Transaction hash diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index 9b217a69d59..90686cde43f 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -19,6 +19,7 @@ use iroha_config::{ torii::Configuration as ToriiConfiguration, }; use iroha_crypto::prelude::*; +use iroha_data_model::ChainId; use iroha_genesis::{GenesisNetwork, RawGenesisBlock}; use iroha_logger::{Configuration as LoggerConfiguration, InstrumentFutures}; use iroha_primitives::{ @@ -49,21 +50,22 @@ pub struct Network { pub peers: BTreeMap, } +/// Get a standardized blockchain id +pub fn get_chain_id() -> ChainId { + ChainId::new("0") +} + /// Get a standardised key-pair from the hard-coded literals. -/// -/// # Panics -/// Programmer error. Given keys must be in proper format. pub fn get_key_pair() -> KeyPair { KeyPair::new( PublicKey::from_str( "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0", - ) - .expect("Public key not in mulithash format"), + ).unwrap(), PrivateKey::from_hex( Algorithm::Ed25519, "9AC47ABF59B356E0BD7DCBBBB4DEC080E302156A48CA907E47CB6AEA1D32719E7233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" - ).expect("Private key not hex encoded") - ).expect("Key pair mismatch") + ).unwrap() + ).unwrap() } /// Trait used to differentiate a test instance of `genesis`. @@ -129,12 +131,13 @@ impl TestGenesis for GenesisNetwork { first_transaction.append_instruction(isi); } + let chain_id = ChainId::new("0"); let key_pair = KeyPair::new( cfg.genesis.public_key.clone(), cfg.genesis.private_key.expect("Should be"), ) .expect("Genesis key pair should be valid"); - GenesisNetwork::new(genesis, &key_pair).expect("Failed to init genesis") + GenesisNetwork::new(genesis, &chain_id, &key_pair).expect("Failed to init genesis") } } @@ -768,8 +771,11 @@ impl TestRuntime for Runtime { impl TestConfiguration for Configuration { fn test() -> Self { - let mut sample_proxy = - iroha::samples::get_config_proxy(UniqueVec::new(), Some(get_key_pair())); + let mut sample_proxy = iroha::samples::get_config_proxy( + UniqueVec::new(), + Some(get_chain_id()), + Some(get_key_pair()), + ); let env_proxy = ConfigurationProxy::from_std_env().expect("Test env variables should parse properly"); let (public_key, private_key) = KeyPair::generate().unwrap().into(); @@ -791,7 +797,8 @@ impl TestConfiguration for Configuration { impl TestClientConfiguration for ClientConfiguration { fn test(api_url: &SocketAddr) -> Self { - let mut configuration = iroha_client::samples::get_client_config(&get_key_pair()); + let mut configuration = + iroha_client::samples::get_client_config(get_chain_id(), &get_key_pair()); configuration.torii_api_url = format!("http://{api_url}") .parse() .expect("Should be valid url"); diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index b6b232b9184..0ba15aefaca 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -582,6 +582,15 @@ pub mod parameter { pub mod model { use super::*; + /// Unique id of blockchain + #[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Deserialize, Serialize, IntoSchema, + )] + #[repr(transparent)] + #[serde(transparent)] + #[ffi_type(unsafe {robust})] + pub struct ChainId(Box); + /// Sized container for all possible identifications. #[derive( Debug, @@ -1000,6 +1009,22 @@ pub mod model { /// in the next request to continue fetching results of the original query pub cursor: crate::query::cursor::ForwardCursor, } + + impl ChainId { + /// Create new [`Self`] + pub fn new(inner: &str) -> Self { + Self(inner.into()) + } + } +} + +impl Decode for ChainId { + fn decode( + input: &mut I, + ) -> Result { + let boxed: String = parity_scale_codec::Decode::decode(input)?; + Ok(Self::new(&boxed)) + } } // TODO: think of a way to `impl Identifiable for IdentifiableBox`. @@ -1859,7 +1884,7 @@ pub mod prelude { account::prelude::*, asset::prelude::*, domain::prelude::*, events::prelude::*, executor::prelude::*, isi::prelude::*, metadata::prelude::*, name::prelude::*, parameter::prelude::*, peer::prelude::*, permission::prelude::*, query::prelude::*, - role::prelude::*, transaction::prelude::*, trigger::prelude::*, EnumTryAsError, + role::prelude::*, transaction::prelude::*, trigger::prelude::*, ChainId, EnumTryAsError, HasMetadata, IdBox, Identifiable, IdentifiableBox, LengthLimits, NumericValue, PredicateTrait, RegistrableBox, ToValue, TryAsMut, TryAsRef, TryToValue, UpgradableBox, ValidationFail, Value, diff --git a/data_model/src/transaction.rs b/data_model/src/transaction.rs index 9d288239636..c709ce53d88 100644 --- a/data_model/src/transaction.rs +++ b/data_model/src/transaction.rs @@ -25,7 +25,7 @@ use crate::{ isi::{Instruction, InstructionBox}, metadata::UnlimitedMetadata, name::Name, - Value, + ChainId, Value, }; #[model] @@ -101,6 +101,9 @@ pub mod model { #[getset(get = "pub")] #[ffi_type] pub struct TransactionPayload { + /// Unique id of the blockchain. Used for simple replay attack protection. + #[getset(skip)] + pub chain_id: ChainId, /// Creation timestamp (unix time in milliseconds). #[getset(skip)] pub creation_time_ms: u64, @@ -676,7 +679,7 @@ mod http { /// Construct [`Self`]. #[inline] #[cfg(feature = "std")] - pub fn new(authority: AccountId) -> Self { + pub fn new(chain_id: ChainId, authority: AccountId) -> Self { let creation_time_ms = crate::current_time() .as_millis() .try_into() @@ -684,6 +687,7 @@ mod http { Self { payload: TransactionPayload { + chain_id, authority, creation_time_ms, nonce: None, diff --git a/docker-compose.dev.local.yml b/docker-compose.dev.local.yml index b7e9c831877..42652c22646 100644 --- a/docker-compose.dev.local.yml +++ b/docker-compose.dev.local.yml @@ -7,6 +7,7 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' @@ -27,6 +28,7 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f"}' @@ -44,6 +46,7 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736"}' @@ -61,6 +64,7 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61"}' diff --git a/docker-compose.dev.single.yml b/docker-compose.dev.single.yml index a6d0af1cdae..2bef3e4873e 100644 --- a/docker-compose.dev.single.yml +++ b/docker-compose.dev.single.yml @@ -7,6 +7,7 @@ services: build: ./ platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index a483a1c1b16..c35f6a859db 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -7,6 +7,7 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed01208BA62848CF767D72E7F7F4B9D2D7BA07FEE33760F79ABE5597A51520E292A0CB IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8f4c15e5d664da3f13778801d23d4e89b76e94c1b94b389544168b6cb894f84f8ba62848cf767d72e7f7f4b9d2d7ba07fee33760f79abe5597a51520e292a0cb"}' @@ -27,6 +28,7 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120815BBDC9775D28C3633269B25F22D048E2AA2E36017CBE5AD85F15220BEB6F6F IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"c02ffad5e455e7ec620d74de5769681e4d8385906bce5a437eb67452a9efbbc2815bbdc9775d28c3633269b25f22d048e2aa2e36017cbe5ad85f15220beb6f6f"}' @@ -44,6 +46,7 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120F417E0371E6ADB32FD66749477402B1AB67F84A8E9B082E997980CC91F327736 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"29c5ed1409cb10fd791bc4ff8a6cb5e22a5fae7e36f448ef3ea2988b1319a88bf417e0371e6adb32fd66749477402b1ab67f84a8e9b082e997980cc91f327736"}' @@ -61,6 +64,7 @@ services: image: hyperledger/iroha2:dev platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120A66522370D60B9C09E79ADE2E9BB1EF2E78733A944B999B3A6AEE687CE476D61 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5eed4855fad183c451aac39dfc50831607e4cf408c98e2b977f3ce4a2df42ce2a66522370d60b9c09e79ade2e9bb1ef2e78733a944b999b3a6aee687ce476d61"}' diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index d6af0cf7e7e..c66139a6dda 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -816,6 +816,7 @@ } ] }, + "ChainId": "String", "ConfigurationEvent": { "Enum": [ { @@ -4117,6 +4118,10 @@ }, "TransactionPayload": { "Struct": [ + { + "name": "chain_id", + "type": "ChainId" + }, { "name": "creation_time_ms", "type": "u64" diff --git a/ffi/derive/src/lib.rs b/ffi/derive/src/lib.rs index aa2fd27550d..0d56d35a2dd 100644 --- a/ffi/derive/src/lib.rs +++ b/ffi/derive/src/lib.rs @@ -260,7 +260,7 @@ pub fn ffi_type_derive(input: TokenStream) -> TokenStream { /// /* function implementation */ /// FfiReturn::Ok /// } -/// extern "C" fn Foo__bar(handle: *const Foo, output: *mut SliceRef) -> FfiReturn { +/// extern "C" fn Foo__bar(handle: *const Foo, output: *mut RefSlice) -> FfiReturn { /// /* function implementation */ /// FfiReturn::Ok /// } diff --git a/ffi/derive/src/wrapper.rs b/ffi/derive/src/wrapper.rs index 01aec489542..eb8fab7d9b0 100644 --- a/ffi/derive/src/wrapper.rs +++ b/ffi/derive/src/wrapper.rs @@ -382,9 +382,9 @@ fn gen_impl_ffi(name: &Ident, generics: &syn2::Generics) -> TokenStream { type Ref<#lifetime> = &#lifetime iroha_ffi::Extern where #(#lifetime_bounded_where_clause),*; type RefMut<#lifetime> = &#lifetime mut iroha_ffi::Extern where #(#lifetime_bounded_where_clause),*; type Box = Box; - type SliceBox = Box<[iroha_ffi::Extern]>; - type SliceRef<#lifetime> = &#lifetime [iroha_ffi::ir::Transparent] where #(#lifetime_bounded_where_clause),*; - type SliceRefMut<#lifetime> = &#lifetime mut [iroha_ffi::ir::Transparent] where #(#lifetime_bounded_where_clause),*; + type BoxedSlice = Box<[iroha_ffi::Extern]>; + type RefSlice<#lifetime> = &#lifetime [iroha_ffi::ir::Transparent] where #(#lifetime_bounded_where_clause),*; + type RefMutSlice<#lifetime> = &#lifetime mut [iroha_ffi::ir::Transparent] where #(#lifetime_bounded_where_clause),*; type Vec = Vec; type Arr = iroha_ffi::ir::Transparent; } diff --git a/ffi/src/ir.rs b/ffi/src/ir.rs index 6c99c9dbf69..b8dfa64aa14 100644 --- a/ffi/src/ir.rs +++ b/ffi/src/ir.rs @@ -103,16 +103,16 @@ pub trait IrTypeFamily { type RefMut<'itm> where Self: 'itm; - /// [`Ir`] type that [`Box`] is mapped into + /// [`Ir`] type that [`Box`] is mapped into for any `T: Sized` type Box; /// [`Ir`] type that `Box<[T]>` is mapped into - type SliceBox; + type BoxedSlice; /// [`Ir`] type that `&[T]` is mapped into - type SliceRef<'itm> + type RefSlice<'itm> where Self: 'itm; /// [`Ir`] type that `&mut [T]` is mapped into - type SliceRefMut<'itm> + type RefMutSlice<'itm> where Self: 'itm; /// [`Ir`] type that [`Vec`] is mapped into @@ -126,10 +126,10 @@ impl IrTypeFamily for R { // NOTE: Unused type RefMut<'itm> = () where Self: 'itm; type Box = Box; - type SliceBox = Box<[Self]>; - type SliceRef<'itm> = &'itm [Self] where Self: 'itm; + type BoxedSlice = Box<[Self]>; + type RefSlice<'itm> = &'itm [Self] where Self: 'itm; // NOTE: Unused - type SliceRefMut<'itm> = () where Self: 'itm; + type RefMutSlice<'itm> = () where Self: 'itm; type Vec = Vec; type Arr = [Self; N]; } @@ -137,9 +137,9 @@ impl IrTypeFamily for Robust { type Ref<'itm> = Transparent; type RefMut<'itm> = Transparent; type Box = Box; - type SliceBox = Box<[Self]>; - type SliceRef<'itm> = &'itm [Self]; - type SliceRefMut<'itm> = &'itm mut [Self]; + type BoxedSlice = Box<[Self]>; + type RefSlice<'itm> = &'itm [Self]; + type RefMutSlice<'itm> = &'itm mut [Self]; type Vec = Vec; type Arr = Self; } @@ -147,9 +147,9 @@ impl IrTypeFamily for Opaque { type Ref<'itm> = Transparent; type RefMut<'itm> = Transparent; type Box = Box; - type SliceBox = Box<[Self]>; - type SliceRef<'itm> = &'itm [Self]; - type SliceRefMut<'itm> = &'itm mut [Self]; + type BoxedSlice = Box<[Self]>; + type RefSlice<'itm> = &'itm [Self]; + type RefMutSlice<'itm> = &'itm mut [Self]; type Vec = Vec; type Arr = [Self; N]; } @@ -157,9 +157,9 @@ impl IrTypeFamily for Transparent { type Ref<'itm> = Self; type RefMut<'itm> = Self; type Box = Box; - type SliceBox = Box<[Self]>; - type SliceRef<'itm> = &'itm [Self]; - type SliceRefMut<'itm> = &'itm mut [Self]; + type BoxedSlice = Box<[Self]>; + type RefSlice<'itm> = &'itm [Self]; + type RefMutSlice<'itm> = &'itm mut [Self]; type Vec = Vec; type Arr = Self; } @@ -167,9 +167,9 @@ impl IrTypeFamily for &Extern { type Ref<'itm> = &'itm Self where Self: 'itm; type RefMut<'itm> = &'itm mut Self where Self: 'itm; type Box = Box; - type SliceBox = Box<[Self]>; - type SliceRef<'itm> = &'itm [Self] where Self: 'itm; - type SliceRefMut<'itm> = &'itm mut [Self] where Self: 'itm; + type BoxedSlice = Box<[Self]>; + type RefSlice<'itm> = &'itm [Self] where Self: 'itm; + type RefMutSlice<'itm> = &'itm mut [Self] where Self: 'itm; type Vec = Vec; type Arr = [Self; N]; } @@ -177,9 +177,9 @@ impl IrTypeFamily for &mut Extern { type Ref<'itm> = &'itm Self where Self: 'itm; type RefMut<'itm> = &'itm mut Self where Self: 'itm; type Box = Box; - type SliceBox = Box<[Self]>; - type SliceRef<'itm> = &'itm [Self] where Self: 'itm; - type SliceRefMut<'itm> = &'itm mut [Self] where Self: 'itm; + type BoxedSlice = Box<[Self]>; + type RefSlice<'itm> = &'itm [Self] where Self: 'itm; + type RefMutSlice<'itm> = &'itm mut [Self] where Self: 'itm; type Vec = Vec; type Arr = [Self; N]; } @@ -221,27 +221,27 @@ impl Ir for Box<[R]> where R::Type: IrTypeFamily, { - type Type = ::SliceBox; + type Type = ::BoxedSlice; } impl<'itm, R: Ir> Ir for &'itm [R] where R::Type: IrTypeFamily, { - type Type = ::SliceRef<'itm>; + type Type = ::RefSlice<'itm>; } #[cfg(feature = "non_robust_ref_mut")] impl<'itm, R: Ir> Ir for &'itm mut [R] where R::Type: IrTypeFamily, { - type Type = ::SliceRefMut<'itm>; + type Type = ::RefMutSlice<'itm>; } #[cfg(not(feature = "non_robust_ref_mut"))] impl<'itm, R: Ir + InfallibleTransmute> Ir for &'itm mut [R] where R::Type: IrTypeFamily, { - type Type = ::SliceRefMut<'itm>; + type Type = ::RefMutSlice<'itm>; } impl Ir for Vec where diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 4590f55b1c7..8467a87c981 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -223,7 +223,7 @@ pub struct LocalRef<'data, R>(R, core::marker::PhantomData<&'data ()>); /// ``` #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] -pub struct LocalSlice<'data, R>(alloc::vec::Vec, core::marker::PhantomData<&'data ()>); +pub struct LocalSlice<'data, R>(alloc::boxed::Box<[R]>, core::marker::PhantomData<&'data ()>); /// Result of execution of an FFI function #[derive(Debug, Display, Clone, Copy, PartialEq, Eq)] diff --git a/ffi/src/primitives.rs b/ffi/src/primitives.rs index b6009b1f138..2b7301b15c3 100644 --- a/ffi/src/primitives.rs +++ b/ffi/src/primitives.rs @@ -30,8 +30,8 @@ mod wasm { type Ref<'itm> = Transparent; type RefMut<'itm> = Transparent; type Box = Box; - type SliceRef<'itm> = &'itm [Robust]; - type SliceRefMut<'itm> = &'itm mut [Robust]; + type RefSlice<'itm> = &'itm [Robust]; + type RefMutSlice<'itm> = &'itm mut [Robust]; type Vec = Vec; type Arr = Robust; } diff --git a/ffi/src/repr_c.rs b/ffi/src/repr_c.rs index 621b160b173..2a88e9657d6 100644 --- a/ffi/src/repr_c.rs +++ b/ffi/src/repr_c.rs @@ -10,7 +10,7 @@ use core::{mem::ManuallyDrop, ptr::addr_of_mut}; use crate::{ ir::{External, Ir, Opaque, Robust, Transmute, Transparent}, - slice::{OutBoxedSlice, SliceMut, SliceRef}, + slice::{OutBoxedSlice, RefMutSlice, RefSlice}, Extern, FfiConvert, FfiOutPtr, FfiOutPtrRead, FfiOutPtrWrite, FfiReturn, FfiType, FfiWrapperType, LocalRef, LocalSlice, ReprC, Result, WrapperTypeOf, }; @@ -115,11 +115,11 @@ pub trait COutPtrRead: COutPtr + Sized { /// # Example /// /// 1. `&[u8]` implements [`NonLocal`] -/// This type will be converted to [`SliceRef`] and during conversion will not make use -/// of the store (in any direction). The corresponding out-pointer will be `*mut SliceRef` +/// This type will be converted to [`RefSlice`] and during conversion will not make use +/// of the store (in any direction). The corresponding out-pointer will be `*mut RefSlice` /// /// 2. `&[Opaque]` doesn't implement [`NonLocal`] -/// This type will be converted to [`SliceRef<*const T>`] and during conversion will use the +/// This type will be converted to [`RefSlice<*const T>`] and during conversion will use the /// local store `Vec<*const T>`. The corresponding out-pointer will be `*mut OutBoxedSlice<*const T>`. /// /// 3. `&(u32, u32)` @@ -278,7 +278,7 @@ impl<'itm, R: NonLocal + CTypeConvert<'itm, S, R::ReprC> + Clone + 'itm, S: C COutPtrWrite> for Box { unsafe fn write_out(self, out_ptr: *mut Self::OutPtr) { - let mut store = <(Option, R::RustStore)>::default(); + let mut store = <(_, _)>::default(); // NOTE: Bypasses the erroneous lifetime check. // Correct as long as `R::into_repr_c` doesn't return a reference to the store (`R: NonLocal`) let store_borrow = &mut *addr_of_mut!(store); @@ -301,17 +301,101 @@ impl<'itm, R: NonLocal + CTypeConvert<'itm, S, R::ReprC> + Clone + 'itm, S: C } } +impl, S: Cloned> CType> for Box<[R]> { + type ReprC = RefMutSlice; +} +impl<'itm, R: CTypeConvert<'itm, S, C> + Clone, S: Cloned, C: ReprC> + CTypeConvert<'itm, Box<[S]>, RefMutSlice> for Box<[R]> +{ + type RustStore = (Box<[C]>, Box<[R::RustStore]>); + type FfiStore = Box<[R::FfiStore]>; + + fn into_repr_c(self, store: &'itm mut Self::RustStore) -> RefMutSlice { + let boxed_slice = self; + + store.1 = core::iter::repeat_with(Default::default) + .take(boxed_slice.len()) + .collect(); + + store.0 = Vec::from(boxed_slice) + .into_iter() + .zip(&mut *store.1) + .map(|(item, substore)| item.into_repr_c(substore)) + .collect(); + + RefMutSlice::from_slice(Some(&mut store.0)) + } + unsafe fn try_from_repr_c( + source: RefMutSlice, + store: &'itm mut Self::FfiStore, + ) -> Result { + let slice = source.into_rust().ok_or(FfiReturn::ArgIsNull)?; + + *store = core::iter::repeat_with(Default::default) + .take(slice.len()) + .collect(); + + let vec: Box<[_]> = slice + .iter() + .copied() + .zip(&mut **store) + .map(|(item, substore)| R::try_from_repr_c(item, substore).map(ManuallyDrop::new)) + .collect::>()?; + + Ok(vec.iter().cloned().map(ManuallyDrop::into_inner).collect()) + } +} + +impl, S: Cloned> CWrapperType> for Box<[R]> { + type InputType = Box<[R::InputType]>; + type ReturnType = Box<[R::ReturnType]>; +} +impl, S: Cloned> COutPtr> for Box<[R]> { + type OutPtr = OutBoxedSlice; +} +impl<'itm, R: NonLocal + CTypeConvert<'itm, S, R::ReprC> + Clone + 'itm, S: Cloned + 'itm> + COutPtrWrite> for Box<[R]> +{ + unsafe fn write_out(self, out_ptr: *mut Self::OutPtr) { + let mut store = <(_, _)>::default(); + // NOTE: Bypasses the erroneous lifetime check. + // Correct as long as `R::into_repr_c` doesn't return a reference to the store (`R: NonLocal`) + let store_borrow = &mut *addr_of_mut!(store); + CTypeConvert::, _>::into_repr_c(self, store_borrow); + out_ptr.write(OutBoxedSlice::from_boxed_slice(Some(store.0))); + } +} +impl<'itm, R: NonLocal + CTypeConvert<'itm, S, R::ReprC> + Clone + 'itm, S: Cloned + 'itm> + COutPtrRead> for Box<[R]> +{ + unsafe fn try_read_out(out_ptr: Self::OutPtr) -> Result { + let slice = RefMutSlice::from_raw_parts_mut(out_ptr.as_mut_ptr(), out_ptr.len()); + + let mut store = Box::default(); + // NOTE: Bypasses the erroneous lifetime check. + // Correct as long as `R::try_from_repr_c` doesn't return a reference to the store (`R: NonLocal`) + let store_borrow = &mut *addr_of_mut!(store); + let res = Self::try_from_repr_c(slice, store_borrow); + + if !out_ptr.deallocate() { + return Err(FfiReturn::TrapRepresentation); + } + + res + } +} + // NOTE: `CType` cannot be implemented for `&mut [T]` impl, S: Cloned> CType<&[S]> for &[R] { - type ReprC = SliceRef; + type ReprC = RefSlice; } impl<'slice, R: CTypeConvert<'slice, S, C> + Clone, S: Cloned, C: ReprC> - CTypeConvert<'slice, &'slice [S], SliceRef> for &'slice [R] + CTypeConvert<'slice, &'slice [S], RefSlice> for &'slice [R] { - type RustStore = (Vec, Vec); - type FfiStore = (Vec, Vec); + type RustStore = (Box<[C]>, Box<[R::RustStore]>); + type FfiStore = (Box<[R]>, Box<[R::FfiStore]>); - fn into_repr_c(self, store: &'slice mut Self::RustStore) -> SliceRef { + fn into_repr_c(self, store: &'slice mut Self::RustStore) -> RefSlice { let slice = self.to_vec(); store.1 = core::iter::repeat_with(Default::default) @@ -320,26 +404,26 @@ impl<'slice, R: CTypeConvert<'slice, S, C> + Clone, S: Cloned, C: ReprC> store.0 = slice .into_iter() - .zip(&mut store.1) + .zip(&mut *store.1) .map(|(item, substore)| item.into_repr_c(substore)) .collect(); - SliceRef::from_slice(Some(&store.0)) + RefSlice::from_slice(Some(&store.0)) } unsafe fn try_from_repr_c( - source: SliceRef, + source: RefSlice, store: &'slice mut Self::FfiStore, ) -> Result { store.1 = core::iter::repeat_with(Default::default) .take(source.len()) .collect(); - let source: Vec> = source + let source: Box<[_]> = source .into_rust() .ok_or(FfiReturn::ArgIsNull)? .iter() - .zip(&mut store.1) + .zip(&mut *store.1) .map(|(&item, substore)| R::try_from_repr_c(item, substore).map(ManuallyDrop::new)) .collect::>()?; @@ -364,21 +448,21 @@ impl<'itm, R: NonLocal + CTypeConvert<'itm, S, R::ReprC> + Clone, S: Cloned> COutPtrWrite<&'itm [S]> for &'itm [R] { unsafe fn write_out(self, out_ptr: *mut Self::OutPtr) { - let mut store = <(Vec, Vec)>::default(); + let mut store = <(_, _)>::default(); // NOTE: Bypasses the erroneous lifetime check. // Correct as long as `R::into_repr_c` doesn't return a reference to the store (`R: NonLocal`) let store_borrow = &mut *addr_of_mut!(store); CTypeConvert::<&[S], _>::into_repr_c(self, store_borrow); - out_ptr.write(OutBoxedSlice::from_vec(Some(store.0))); + out_ptr.write(OutBoxedSlice::from_boxed_slice(Some(store.0))); } } impl<'itm, R: NonLocal + CTypeConvert<'itm, S, R::ReprC> + Clone + 'itm, S: Cloned + 'itm> COutPtrRead<&'itm [S]> for LocalSlice<'itm, R> { unsafe fn try_read_out(out_ptr: OutBoxedSlice) -> Result { - let slice = SliceRef::from_raw_parts(out_ptr.as_mut_ptr(), out_ptr.len()); + let slice = RefSlice::from_raw_parts(out_ptr.as_mut_ptr(), out_ptr.len()); - let mut store = <(Vec, Vec)>::default(); + let mut store = <(_, _)>::default(); // NOTE: Bypasses the erroneous lifetime check. // Correct as long as `R::try_from_repr_c` doesn't return a reference to the store (`R: NonLocal`) let store_borrow = &mut *addr_of_mut!(store); @@ -394,15 +478,15 @@ impl<'itm, R: NonLocal + CTypeConvert<'itm, S, R::ReprC> + Clone + 'itm, S: C } impl, S: Cloned> CType> for Vec { - type ReprC = SliceMut; + type ReprC = RefMutSlice; } impl<'itm, R: CTypeConvert<'itm, S, C> + Clone, S: Cloned, C: ReprC> - CTypeConvert<'itm, Vec, SliceMut> for Vec + CTypeConvert<'itm, Vec, RefMutSlice> for Vec { - type RustStore = (Vec, Vec); - type FfiStore = Vec; + type RustStore = (Box<[C]>, Box<[R::RustStore]>); + type FfiStore = Box<[R::FfiStore]>; - fn into_repr_c(self, store: &'itm mut Self::RustStore) -> SliceMut { + fn into_repr_c(self, store: &'itm mut Self::RustStore) -> RefMutSlice { let vec = self; store.1 = core::iter::repeat_with(Default::default) @@ -411,14 +495,14 @@ impl<'itm, R: CTypeConvert<'itm, S, C> + Clone, S: Cloned, C: ReprC> store.0 = vec .into_iter() - .zip(&mut store.1) + .zip(&mut *store.1) .map(|(item, substore)| item.into_repr_c(substore)) .collect(); - SliceMut::from_slice(Some(&mut store.0)) + RefMutSlice::from_slice(Some(&mut store.0)) } unsafe fn try_from_repr_c( - source: SliceMut, + source: RefMutSlice, store: &'itm mut Self::FfiStore, ) -> Result { let slice = source.into_rust().ok_or(FfiReturn::ArgIsNull)?; @@ -427,18 +511,14 @@ impl<'itm, R: CTypeConvert<'itm, S, C> + Clone, S: Cloned, C: ReprC> .take(slice.len()) .collect(); - let vec: Vec> = slice + let vec: Box<[_]> = slice .iter() .copied() - .zip(store) + .zip(&mut **store) .map(|(item, substore)| R::try_from_repr_c(item, substore).map(ManuallyDrop::new)) .collect::>()?; - Ok(vec - .iter() - .cloned() - .map(ManuallyDrop::into_inner) - .collect::>()) + Ok(vec.iter().cloned().map(ManuallyDrop::into_inner).collect()) } } @@ -453,21 +533,21 @@ impl<'itm, R: NonLocal + CTypeConvert<'itm, S, R::ReprC> + Clone + 'itm, S: C COutPtrWrite> for Vec { unsafe fn write_out(self, out_ptr: *mut Self::OutPtr) { - let mut store = <(Vec, Vec)>::default(); + let mut store = <(_, _)>::default(); // NOTE: Bypasses the erroneous lifetime check. // Correct as long as `R::into_repr_c` doesn't return a reference to the store (`R: NonLocal`) let store_borrow = &mut *addr_of_mut!(store); CTypeConvert::, _>::into_repr_c(self, store_borrow); - out_ptr.write(OutBoxedSlice::from_vec(Some(store.0))); + out_ptr.write(OutBoxedSlice::from_boxed_slice(Some(store.0))); } } impl<'itm, R: NonLocal + CTypeConvert<'itm, S, R::ReprC> + Clone + 'itm, S: Cloned + 'itm> COutPtrRead> for Vec { unsafe fn try_read_out(out_ptr: Self::OutPtr) -> Result { - let slice = SliceMut::from_raw_parts_mut(out_ptr.as_mut_ptr(), out_ptr.len()); + let slice = RefMutSlice::from_raw_parts_mut(out_ptr.as_mut_ptr(), out_ptr.len()); - let mut store = Vec::default(); + let mut store = Box::default(); // NOTE: Bypasses the erroneous lifetime check. // Correct as long as `R::try_from_repr_c` doesn't return a reference to the store (`R: NonLocal`) let store_borrow = &mut *addr_of_mut!(store); @@ -688,18 +768,18 @@ impl COutPtrRead> for Box { } impl CType> for Box<[R]> { - type ReprC = SliceMut; + type ReprC = RefMutSlice; } -impl CTypeConvert<'_, Box<[Robust]>, SliceMut> for Box<[R]> { +impl CTypeConvert<'_, Box<[Robust]>, RefMutSlice> for Box<[R]> { type RustStore = Self; type FfiStore = (); - fn into_repr_c(self, store: &mut Self::RustStore) -> SliceMut { + fn into_repr_c(self, store: &mut Self::RustStore) -> RefMutSlice { *store = self; - SliceMut::from_slice(Some(store.as_mut())) + RefMutSlice::from_slice(Some(store)) } - unsafe fn try_from_repr_c(source: SliceMut, (): &mut ()) -> Result { + unsafe fn try_from_repr_c(source: RefMutSlice, (): &mut ()) -> Result { source .into_rust() .ok_or(FfiReturn::ArgIsNull) @@ -723,7 +803,7 @@ impl COutPtrWrite> for Box<[R]> { } impl COutPtrRead> for Box<[R]> { unsafe fn try_read_out(out_ptr: Self::OutPtr) -> Result { - let slice = SliceMut::from_raw_parts_mut(out_ptr.as_mut_ptr(), out_ptr.len()); + let slice = RefMutSlice::from_raw_parts_mut(out_ptr.as_mut_ptr(), out_ptr.len()); let res = CTypeConvert::, _>::try_from_repr_c(slice, &mut ()); if !out_ptr.deallocate() { @@ -735,17 +815,17 @@ impl COutPtrRead> for Box<[R]> { } impl CType<&[Robust]> for &[R] { - type ReprC = SliceRef; + type ReprC = RefSlice; } -impl CTypeConvert<'_, &[Robust], SliceRef> for &[R] { +impl CTypeConvert<'_, &[Robust], RefSlice> for &[R] { type RustStore = (); type FfiStore = (); - fn into_repr_c(self, (): &mut ()) -> SliceRef { - SliceRef::from_slice(Some(self)) + fn into_repr_c(self, (): &mut ()) -> RefSlice { + RefSlice::from_slice(Some(self)) } - unsafe fn try_from_repr_c(source: SliceRef, (): &mut ()) -> Result { + unsafe fn try_from_repr_c(source: RefSlice, (): &mut ()) -> Result { source.into_rust().ok_or(FfiReturn::ArgIsNull) } } @@ -773,17 +853,17 @@ impl CWrapperType<&mut [Robust]> for &mut [R] { type ReturnType = Self; } impl CType<&mut [Robust]> for &mut [R] { - type ReprC = SliceMut; + type ReprC = RefMutSlice; } -impl CTypeConvert<'_, &mut [Robust], SliceMut> for &mut [R] { +impl CTypeConvert<'_, &mut [Robust], RefMutSlice> for &mut [R] { type RustStore = (); type FfiStore = (); - fn into_repr_c(self, (): &mut ()) -> SliceMut { - SliceMut::from_slice(Some(self)) + fn into_repr_c(self, (): &mut ()) -> RefMutSlice { + RefMutSlice::from_slice(Some(self)) } - unsafe fn try_from_repr_c(source: SliceMut, (): &mut ()) -> Result { + unsafe fn try_from_repr_c(source: RefMutSlice, (): &mut ()) -> Result { source.into_rust().ok_or(FfiReturn::ArgIsNull) } } @@ -803,18 +883,18 @@ impl COutPtrRead<&mut [Robust]> for &mut [R] { } impl CType> for Vec { - type ReprC = SliceMut; + type ReprC = RefMutSlice; } -impl CTypeConvert<'_, Vec, SliceMut> for Vec { - type RustStore = Self; +impl CTypeConvert<'_, Vec, RefMutSlice> for Vec { + type RustStore = Box<[R]>; type FfiStore = (); - fn into_repr_c(self, store: &mut Self::RustStore) -> SliceMut { - *store = self; - SliceMut::from_slice(Some(store)) + fn into_repr_c(self, store: &mut Self::RustStore) -> RefMutSlice { + *store = self.into_boxed_slice(); + RefMutSlice::from_slice(Some(store)) } - unsafe fn try_from_repr_c(source: SliceMut, (): &mut ()) -> Result { + unsafe fn try_from_repr_c(source: RefMutSlice, (): &mut ()) -> Result { source .into_rust() .ok_or(FfiReturn::ArgIsNull) @@ -831,14 +911,14 @@ impl COutPtr> for Vec { } impl COutPtrWrite> for Vec { unsafe fn write_out(self, out_ptr: *mut Self::OutPtr) { - let mut store = Vec::default(); + let mut store = Box::default(); CTypeConvert::, _>::into_repr_c(self, &mut store); - out_ptr.write(OutBoxedSlice::from_vec(Some(store))); + out_ptr.write(OutBoxedSlice::from_boxed_slice(Some(store))); } } impl COutPtrRead> for Vec { unsafe fn try_read_out(out_ptr: Self::OutPtr) -> Result { - let slice = SliceMut::from_raw_parts_mut(out_ptr.as_mut_ptr(), out_ptr.len()); + let slice = RefMutSlice::from_raw_parts_mut(out_ptr.as_mut_ptr(), out_ptr.len()); let res = CTypeConvert::, _>::try_from_repr_c(slice, &mut ()); if !out_ptr.deallocate() { @@ -916,23 +996,23 @@ impl COutPtrWrite> for Box { } impl CType> for Box<[R]> { - type ReprC = SliceMut<*mut R>; + type ReprC = RefMutSlice<*mut R>; } -impl CTypeConvert<'_, Box<[Opaque]>, SliceMut<*mut R>> for Box<[R]> { +impl CTypeConvert<'_, Box<[Opaque]>, RefMutSlice<*mut R>> for Box<[R]> { type RustStore = Box<[*mut R]>; type FfiStore = (); - fn into_repr_c(self, store: &mut Self::RustStore) -> SliceMut<*mut R> { + fn into_repr_c(self, store: &mut Self::RustStore) -> RefMutSlice<*mut R> { *store = Vec::from(self) .into_iter() - .map(|a: R| Box::new(a)) + .map(Box::new) .map(Box::into_raw) .collect(); - SliceMut::from_slice(Some(store)) + RefMutSlice::from_slice(Some(store)) } - unsafe fn try_from_repr_c(source: SliceMut<*mut R>, (): &mut ()) -> Result { + unsafe fn try_from_repr_c(source: RefMutSlice<*mut R>, (): &mut ()) -> Result { source .into_rust() .ok_or(FfiReturn::ArgIsNull)? @@ -960,21 +1040,21 @@ impl COutPtrWrite> for Box<[R]> { } impl CType<&[Opaque]> for &[R] { - type ReprC = SliceRef<*const R>; + type ReprC = RefSlice<*const R>; } -impl<'slice, R: Clone> CTypeConvert<'slice, &'slice [Opaque], SliceRef<*const R>> +impl<'slice, R: Clone> CTypeConvert<'slice, &'slice [Opaque], RefSlice<*const R>> for &'slice [R] { - type RustStore = Vec<*const R>; - type FfiStore = Vec; + type RustStore = Box<[*const R]>; + type FfiStore = Box<[R]>; - fn into_repr_c(self, store: &mut Self::RustStore) -> SliceRef<*const R> { + fn into_repr_c(self, store: &mut Self::RustStore) -> RefSlice<*const R> { *store = self.iter().map(|item| item as *const R).collect(); - SliceRef::from_slice(Some(store)) + RefSlice::from_slice(Some(store)) } unsafe fn try_from_repr_c( - source: SliceRef<*const R>, + source: RefSlice<*const R>, store: &'slice mut Self::FfiStore, ) -> Result { let source = source.into_rust().ok_or(FfiReturn::ArgIsNull)?; @@ -1000,27 +1080,29 @@ impl COutPtr<&[Opaque]> for &[R] { } impl COutPtrWrite<&[Opaque]> for &[R] { unsafe fn write_out(self, out_ptr: *mut Self::OutPtr) { - let mut store = Vec::default(); + let mut store = Box::default(); CTypeConvert::<&[Opaque], _>::into_repr_c(self, &mut store); - out_ptr.write(OutBoxedSlice::from_vec(Some(store))); + out_ptr.write(OutBoxedSlice::from_boxed_slice(Some(store))); } } impl CType<&mut [Opaque]> for &mut [R] { - type ReprC = SliceMut<*mut R>; + type ReprC = RefMutSlice<*mut R>; } -impl<'slice, R: Clone> CTypeConvert<'slice, &mut [Opaque], SliceMut<*mut R>> for &'slice mut [R] { - type RustStore = Vec<*mut R>; - type FfiStore = Vec; +impl<'slice, R: Clone> CTypeConvert<'slice, &mut [Opaque], RefMutSlice<*mut R>> + for &'slice mut [R] +{ + type RustStore = Box<[*mut R]>; + type FfiStore = Box<[R]>; - fn into_repr_c(self, store: &mut Self::RustStore) -> SliceMut<*mut R> { + fn into_repr_c(self, store: &mut Self::RustStore) -> RefMutSlice<*mut R> { *store = self.iter_mut().map(|item| item as *mut R).collect(); - SliceMut::from_slice(Some(store)) + RefMutSlice::from_slice(Some(store)) } unsafe fn try_from_repr_c( - source: SliceMut<*mut R>, + source: RefMutSlice<*mut R>, store: &'slice mut Self::FfiStore, ) -> Result { let source = source.into_rust().ok_or(FfiReturn::ArgIsNull)?; @@ -1046,25 +1128,25 @@ impl COutPtr<&mut [Opaque]> for &mut [R] { } impl COutPtrWrite<&mut [Opaque]> for &mut [R] { unsafe fn write_out(self, out_ptr: *mut Self::OutPtr) { - let mut store = Vec::default(); + let mut store = Box::default(); CTypeConvert::<&mut [Opaque], _>::into_repr_c(self, &mut store); - out_ptr.write(OutBoxedSlice::from_vec(Some(store))); + out_ptr.write(OutBoxedSlice::from_boxed_slice(Some(store))); } } impl CType> for Vec { - type ReprC = SliceMut<*mut R>; + type ReprC = RefMutSlice<*mut R>; } -impl CTypeConvert<'_, Vec, SliceMut<*mut R>> for Vec { - type RustStore = Vec<*mut R>; +impl CTypeConvert<'_, Vec, RefMutSlice<*mut R>> for Vec { + type RustStore = Box<[*mut R]>; type FfiStore = (); - fn into_repr_c(self, store: &mut Self::RustStore) -> SliceMut<*mut R> { + fn into_repr_c(self, store: &mut Self::RustStore) -> RefMutSlice<*mut R> { *store = self.into_iter().map(Box::new).map(Box::into_raw).collect(); - SliceMut::from_slice(Some(store)) + RefMutSlice::from_slice(Some(store)) } - unsafe fn try_from_repr_c(source: SliceMut<*mut R>, (): &mut ()) -> Result { + unsafe fn try_from_repr_c(source: RefMutSlice<*mut R>, (): &mut ()) -> Result { source .into_rust() .ok_or(FfiReturn::ArgIsNull)? @@ -1085,9 +1167,9 @@ impl COutPtr> for Vec { } impl COutPtrWrite> for Vec { unsafe fn write_out(self, out_ptr: *mut Self::OutPtr) { - let mut store = Vec::default(); + let mut store = Box::default(); CTypeConvert::, _>::into_repr_c(self, &mut store); - out_ptr.write(OutBoxedSlice::from_vec(Some(store))); + out_ptr.write(OutBoxedSlice::from_boxed_slice(Some(store))); } } @@ -1440,12 +1522,12 @@ where type FfiStore = <&'slice [R::Target] as FfiConvert<'slice, C>>::FfiStore; fn into_repr_c(self, store: &'slice mut Self::RustStore) -> C { - transmute_into_target_slice_ref(self).into_ffi(store) + transmute_into_target_ref_slice(self).into_ffi(store) } unsafe fn try_from_repr_c(source: C, store: &'slice mut Self::FfiStore) -> Result { let slice = <&[R::Target]>::try_from_ffi(source, store)?; - transmute_from_target_slice_ref(slice) + transmute_from_target_ref_slice(slice) } } @@ -1471,7 +1553,7 @@ where &'slice [R::Target]: FfiOutPtrWrite, { unsafe fn write_out(self, out_ptr: *mut Self::OutPtr) { - FfiOutPtrWrite::write_out(transmute_into_target_slice_ref(self), out_ptr); + FfiOutPtrWrite::write_out(transmute_into_target_ref_slice(self), out_ptr); } } impl<'itm, R: Transmute> COutPtrRead<&'itm [Transparent]> for &'itm [R] @@ -1480,7 +1562,7 @@ where { unsafe fn try_read_out(out_ptr: Self::OutPtr) -> Result { <&[R::Target]>::try_read_out(out_ptr) - .and_then(|output| transmute_from_target_slice_ref(output)) + .and_then(|output| transmute_from_target_ref_slice(output)) } } @@ -1687,12 +1769,12 @@ unsafe fn transmute_from_target_boxed_slice( ))) } -fn transmute_into_target_slice_ref(source: &[R]) -> &[R::Target] { +fn transmute_into_target_ref_slice(source: &[R]) -> &[R::Target] { let (ptr, len) = (source.as_ptr().cast::(), source.len()); // SAFETY: `R` is guaranteed to be transmutable into `R::Target` unsafe { core::slice::from_raw_parts(ptr, len) } } -unsafe fn transmute_from_target_slice_ref(source: &[R::Target]) -> Result<&[R]> { +unsafe fn transmute_from_target_ref_slice(source: &[R::Target]) -> Result<&[R]> { if !source.iter().all(|item| R::is_valid(item)) { return Err(FfiReturn::TrapRepresentation); } diff --git a/ffi/src/slice.rs b/ffi/src/slice.rs index 8c3edfc9b8e..e47674cca94 100644 --- a/ffi/src/slice.rs +++ b/ffi/src/slice.rs @@ -11,13 +11,13 @@ crate::decl_ffi_fns! { dealloc } /// If the data pointer is set to `null`, the struct represents `Option<&[C]>`. #[repr(C)] #[derive(Debug)] -pub struct SliceRef(*const C, usize); +pub struct RefSlice(*const C, usize); /// Mutable slice `&mut [C]` with a defined C ABI layout. Consists of a data pointer and a length. /// If the data pointer is set to `null`, the struct represents `Option<&mut [C]>`. #[repr(C)] #[derive(Debug)] -pub struct SliceMut(*mut C, usize); +pub struct RefMutSlice(*mut C, usize); /// Owned slice `Box<[C]>` with a defined C ABI layout. Consists of a data pointer and a length. /// Used in place of a function out-pointer to transfer ownership of the slice to the caller. @@ -26,14 +26,14 @@ pub struct SliceMut(*mut C, usize); #[derive(Debug)] pub struct OutBoxedSlice(*mut C, usize); -impl Copy for SliceRef {} -impl Clone for SliceRef { +impl Copy for RefSlice {} +impl Clone for RefSlice { fn clone(&self) -> Self { *self } } -impl Copy for SliceMut {} -impl Clone for SliceMut { +impl Copy for RefMutSlice {} +impl Clone for RefMutSlice { fn clone(&self) -> Self { *self } @@ -45,7 +45,7 @@ impl Clone for OutBoxedSlice { } } -impl SliceRef { +impl RefSlice { /// Set the slice's data pointer to null pub const fn null() -> Self { // TODO: len could be uninitialized @@ -95,7 +95,7 @@ impl SliceRef { Some(slice::from_raw_parts(self.0, self.1)) } } -impl SliceMut { +impl RefMutSlice { /// Set the slice's data pointer to null pub const fn null_mut() -> Self { // TODO: len could be uninitialized @@ -165,7 +165,7 @@ impl OutBoxedSlice { self.1 } - /// Create [`Self`] from a `Box<[T]>` + /// Create [`Self`] from a [`Box<[T]>`] pub fn from_boxed_slice(source: Option>) -> Self { source.map_or_else( || Self(core::ptr::null_mut(), 0), @@ -176,17 +176,6 @@ impl OutBoxedSlice { ) } - /// Create [`Self`] from a `Vec` - pub fn from_vec(source: Option>) -> Self { - source.map_or_else( - || Self(core::ptr::null_mut(), 0), - |boxed_slice| { - let mut boxed_slice = core::mem::ManuallyDrop::new(boxed_slice.into_boxed_slice()); - Self(boxed_slice.as_mut_ptr(), boxed_slice.len()) - }, - ) - } - /// Create a `Vec` directly from the raw components of another vector. /// Unlike [`Vec::from_raw_parts`], data pointer is allowed to be null. /// @@ -218,8 +207,8 @@ impl OutBoxedSlice { } // SAFETY: Robust type with a defined C ABI -unsafe impl ReprC for SliceRef {} +unsafe impl ReprC for RefSlice {} // SAFETY: Robust type with a defined C ABI -unsafe impl ReprC for SliceMut {} +unsafe impl ReprC for RefMutSlice {} // SAFETY: Robust type with a defined C ABI unsafe impl ReprC for OutBoxedSlice {} diff --git a/ffi/src/std_impls.rs b/ffi/src/std_impls.rs index ee579c75fcb..d45d2ece14e 100644 --- a/ffi/src/std_impls.rs +++ b/ffi/src/std_impls.rs @@ -1,12 +1,11 @@ -// Triggered by `&mut str` expansion -#![allow(clippy::mut_mut, single_use_lifetimes)] +#![allow(single_use_lifetimes)] // NOTE: Triggered by &str implementation -use alloc::{string::String, vec::Vec}; +use alloc::{boxed::Box, string::String, vec::Vec}; use core::{mem::ManuallyDrop, ptr::NonNull}; use crate::{ ffi_type, - slice::{SliceMut, SliceRef}, + slice::{RefMutSlice, RefSlice}, ReprC, WrapperTypeOf, }; @@ -18,17 +17,25 @@ ffi_type! { type Target = Vec; validation_fn=unsafe {|target| core::str::from_utf8(target).is_ok()}, - niche_value=SliceMut::null_mut() + niche_value=RefMutSlice::null_mut() } } // NOTE: `core::str::as_bytes` uses transmute internally which means that // even though it's a string slice it can be transmuted into byte slice. +ffi_type! { + unsafe impl Transparent for Box { + type Target = Box<[u8]>; + + validation_fn=unsafe {|target| core::str::from_utf8(target).is_ok()}, + niche_value=RefMutSlice::null_mut() + } +} ffi_type! { unsafe impl<'slice> Transparent for &'slice str { type Target = &'slice [u8]; validation_fn=unsafe {|target| core::str::from_utf8(target).is_ok()}, - niche_value=SliceRef::null() + niche_value=RefSlice::null() } } #[cfg(feature = "non_robust_ref_mut")] @@ -37,7 +44,7 @@ ffi_type! { type Target = &'slice mut [u8]; validation_fn=unsafe {|target| core::str::from_utf8(target).is_ok()}, - niche_value=SliceMut::null_mut() + niche_value=RefMutSlice::null_mut() } } ffi_type! { diff --git a/ffi/tests/export_shared_fns.rs b/ffi/tests/export_shared_fns.rs index b254c657d42..49a09691c5e 100644 --- a/ffi/tests/export_shared_fns.rs +++ b/ffi/tests/export_shared_fns.rs @@ -42,7 +42,7 @@ fn export_shared_fns() { let ffi_struct1 = unsafe { let mut ffi_struct = MaybeUninit::new(core::ptr::null_mut()); - let mut store = Vec::new(); + let mut store = Box::default(); assert_eq! {FfiReturn::Ok, FfiStruct1__new(FfiConvert::into_ffi(name.clone(), &mut store), ffi_struct.as_mut_ptr())}; let ffi_struct = ffi_struct.assume_init(); assert!(!ffi_struct.is_null()); diff --git a/ffi/tests/ffi_export.rs b/ffi/tests/ffi_export.rs index da6ebaa4d20..a9d6ee3cabd 100644 --- a/ffi/tests/ffi_export.rs +++ b/ffi/tests/ffi_export.rs @@ -290,12 +290,12 @@ fn get_new_struct_with_params() -> OpaqueStruct { #[webassembly_test::webassembly_test] #[cfg(feature = "non_robust_ref_mut")] fn non_robust_ref_mut() { - use iroha_ffi::slice::SliceMut; + use iroha_ffi::slice::RefMutSlice; let mut owned = "queen".to_owned(); let ffi_struct: &mut str = owned.as_mut(); - let mut output = MaybeUninit::new(SliceMut::from_raw_parts_mut(core::ptr::null_mut(), 0)); - let ffi_type: SliceMut = FfiConvert::into_ffi(ffi_struct, &mut ()); + let mut output = MaybeUninit::new(RefMutSlice::from_raw_parts_mut(core::ptr::null_mut(), 0)); + let ffi_type: RefMutSlice = FfiConvert::into_ffi(ffi_struct, &mut ()); unsafe { assert_eq!( @@ -350,7 +350,7 @@ fn into_iter_item_impl_into() { ]; let mut ffi_struct = get_new_struct(); - let mut tokens_store = Vec::default(); + let mut tokens_store = Box::default(); let tokens_ffi = tokens.clone().into_ffi(&mut tokens_store); let mut output = MaybeUninit::new(core::ptr::null_mut()); diff --git a/ffi/tests/ffi_import.rs b/ffi/tests/ffi_import.rs index 4d560620ad9..7be795cf684 100644 --- a/ffi/tests/ffi_import.rs +++ b/ffi/tests/ffi_import.rs @@ -131,7 +131,7 @@ mod ffi { use std::alloc; use iroha_ffi::{ - slice::{OutBoxedSlice, SliceMut, SliceRef}, + slice::{OutBoxedSlice, RefMutSlice, RefSlice}, FfiOutPtr, FfiReturn, FfiTuple2, FfiType, }; @@ -157,17 +157,17 @@ mod ffi { #[no_mangle] unsafe extern "C" fn __freestanding_returns_local_slice( - input: SliceRef>, + input: RefSlice>, output: *mut OutBoxedSlice>, ) -> FfiReturn { - let input = input.into_rust().map(<[_]>::to_vec); - output.write(OutBoxedSlice::from_vec(input)); + let input = input.into_rust().map(Into::into); + output.write(OutBoxedSlice::from_boxed_slice(input)); FfiReturn::Ok } #[no_mangle] unsafe extern "C" fn __freestanding_returns_boxed_slice( - input: SliceMut, + input: RefMutSlice, output: *mut OutBoxedSlice, ) -> FfiReturn { let input = input.into_rust().map(|slice| (&*slice).into()); @@ -177,11 +177,11 @@ mod ffi { #[no_mangle] unsafe extern "C" fn __freestanding_returns_iterator( - input: SliceMut, + input: RefMutSlice, output: *mut OutBoxedSlice, ) -> FfiReturn { - let input = input.into_rust().map(|slice| slice.to_vec()); - output.write(OutBoxedSlice::from_vec(input)); + let input = input.into_rust().map(|slice| (&*slice).into()); + output.write(OutBoxedSlice::from_boxed_slice(input)); FfiReturn::Ok } diff --git a/ffi/tests/ffi_import_opaque.rs b/ffi/tests/ffi_import_opaque.rs index c9ffe7c4fb4..6b1be273f54 100644 --- a/ffi/tests/ffi_import_opaque.rs +++ b/ffi/tests/ffi_import_opaque.rs @@ -144,7 +144,7 @@ mod ffi { use std::{alloc, collections::BTreeMap}; use iroha_ffi::{ - def_ffi_fns, slice::SliceMut, FfiConvert, FfiOutPtr, FfiOutPtrWrite, FfiReturn, FfiType, + def_ffi_fns, slice::RefMutSlice, FfiConvert, FfiOutPtr, FfiOutPtrWrite, FfiReturn, FfiType, }; iroha_ffi::handles! {ExternOpaqueStruct, ExternValue} @@ -175,7 +175,7 @@ mod ffi { #[no_mangle] unsafe extern "C" fn Value__new( - input: SliceMut, + input: RefMutSlice, output: *mut *mut ExternValue, ) -> FfiReturn { let string = String::from_utf8(input.into_rust().expect("Defined").to_vec()); @@ -205,7 +205,7 @@ mod ffi { output: *mut *mut ExternOpaqueStruct, ) -> iroha_ffi::FfiReturn { let mut handle = *Box::from_raw(handle); - let mut store = Vec::default(); + let mut store = Box::default(); let params: Vec<(u8, ExternValue)> = FfiConvert::try_from_ffi(params, &mut store).expect("Valid"); handle.params = params.into_iter().collect(); diff --git a/ffi/tests/import_getset.rs b/ffi/tests/import_getset.rs index 56d5a733dae..1fa1a74a5b2 100644 --- a/ffi/tests/import_getset.rs +++ b/ffi/tests/import_getset.rs @@ -55,7 +55,7 @@ mod ffi { use std::alloc; use iroha_ffi::{ - def_ffi_fns, slice::SliceMut, FfiConvert, FfiOutPtr, FfiOutPtrWrite, FfiReturn, FfiType, + def_ffi_fns, slice::RefMutSlice, FfiConvert, FfiOutPtr, FfiOutPtrWrite, FfiReturn, FfiType, }; iroha_ffi::handles! {ExternName, ExternFfiStruct} @@ -84,7 +84,7 @@ mod ffi { #[no_mangle] unsafe extern "C" fn Name__new( - input1: SliceMut, + input1: RefMutSlice, output: *mut *mut ExternName, ) -> FfiReturn { let string = String::from_utf8(input1.into_rust().expect("Defined").to_vec()); @@ -95,7 +95,7 @@ mod ffi { #[no_mangle] unsafe extern "C" fn FfiStruct__new( - input1: SliceMut, + input1: RefMutSlice, input2: ::ReprC, output: *mut *mut ExternFfiStruct, ) -> FfiReturn { diff --git a/ffi/tests/import_shared_fns.rs b/ffi/tests/import_shared_fns.rs index cab3d5fd658..00210ef790e 100644 --- a/ffi/tests/import_shared_fns.rs +++ b/ffi/tests/import_shared_fns.rs @@ -35,7 +35,7 @@ fn import_shared_fns() { mod ffi { use std::alloc; - use iroha_ffi::{def_ffi_fns, slice::SliceMut, FfiReturn, FfiType}; + use iroha_ffi::{def_ffi_fns, slice::RefMutSlice, FfiReturn, FfiType}; iroha_ffi::handles! {ExternFfiStruct} @@ -56,7 +56,7 @@ mod ffi { #[no_mangle] unsafe extern "C" fn FfiStruct__new( - input: SliceMut, + input: RefMutSlice, output: *mut *mut ExternFfiStruct, ) -> FfiReturn { let string = String::from_utf8(input.into_rust().expect("Defined").to_vec()); diff --git a/ffi/tests/transparent.rs b/ffi/tests/transparent.rs index 01f93912add..49f6efd8d55 100644 --- a/ffi/tests/transparent.rs +++ b/ffi/tests/transparent.rs @@ -4,7 +4,7 @@ use std::{alloc, marker::PhantomData, mem::MaybeUninit}; use iroha_ffi::{ ffi_export, - slice::{OutBoxedSlice, SliceRef}, + slice::{OutBoxedSlice, RefSlice}, FfiConvert, FfiOutPtrRead, FfiReturn, FfiType, }; @@ -168,7 +168,7 @@ fn transparent_vec_to_vec() { TransparentStruct::new(GenericTransparentStruct::new(3)), ]; - let mut store = Vec::default(); + let mut store = Box::default(); let mut output = MaybeUninit::new(OutBoxedSlice::from_raw_parts(core::ptr::null_mut(), 0)); unsafe { @@ -196,7 +196,7 @@ fn transparent_slice_to_slice() { TransparentStruct::new(GenericTransparentStruct::new(2)), TransparentStruct::new(GenericTransparentStruct::new(3)), ]; - let mut output = MaybeUninit::new(SliceRef::from_raw_parts(core::ptr::null(), 0)); + let mut output = MaybeUninit::new(RefSlice::from_raw_parts(core::ptr::null(), 0)); unsafe { assert_eq!( diff --git a/genesis/src/lib.rs b/genesis/src/lib.rs index d32ebb22405..e36f2b904b3 100644 --- a/genesis/src/lib.rs +++ b/genesis/src/lib.rs @@ -14,6 +14,7 @@ use iroha_data_model::{ asset::AssetDefinition, executor::Executor, prelude::{Metadata, *}, + ChainId, }; use iroha_schema::IntoSchema; use once_cell::sync::Lazy; @@ -45,7 +46,11 @@ impl GenesisNetwork { /// - If fails to sign a transaction (which means that the `key_pair` is malformed rather /// than anything else) /// - If transactions set is empty - pub fn new(raw_block: RawGenesisBlock, genesis_key_pair: &KeyPair) -> Result { + pub fn new( + raw_block: RawGenesisBlock, + chain_id: &ChainId, + genesis_key_pair: &KeyPair, + ) -> Result { // First instruction should be Executor upgrade. // This makes possible to grant permissions to users in genesis. let transactions_iter = std::iter::once(GenesisTransactionBuilder { @@ -64,7 +69,7 @@ impl GenesisNetwork { // FIXME: fix underlying chain of `.sign` so that it doesn't // consume the key pair unnecessarily. It might be costly to clone // the key pair for a large genesis. - .sign(genesis_key_pair.clone()) + .sign(chain_id.clone(), genesis_key_pair.clone()) .map(GenesisTransaction) .wrap_err_with(|| eyre!("Failed to sign transaction at index {i}")) }) @@ -188,11 +193,12 @@ impl GenesisTransactionBuilder { /// /// # Errors /// Fails if signing or accepting fails. - pub fn sign( + fn sign( self, + chain_id: ChainId, genesis_key_pair: KeyPair, ) -> core::result::Result { - TransactionBuilder::new(GENESIS_ACCOUNT_ID.clone()) + TransactionBuilder::new(chain_id, GENESIS_ACCOUNT_ID.clone()) .with_instructions(self.isi) .sign(genesis_key_pair) } @@ -364,8 +370,11 @@ mod tests { #[test] fn load_new_genesis_block() -> Result<()> { + let chain_id = ChainId::new("0"); + let genesis_key_pair = KeyPair::generate()?; let (alice_public_key, _) = KeyPair::generate()?.into(); + let _genesis_block = GenesisNetwork::new( RawGenesisBlockBuilder::default() .domain("wonderland".parse()?) @@ -373,6 +382,7 @@ mod tests { .finish_domain() .executor(dummy_executor()) .build(), + &chain_id, &genesis_key_pair, )?; Ok(()) diff --git a/schema/src/lib.rs b/schema/src/lib.rs index 558dfca3796..0b803eaa68d 100644 --- a/schema/src/lib.rs +++ b/schema/src/lib.rs @@ -427,6 +427,28 @@ impl IntoSchema for Box { } } +impl TypeId for Box { + fn id() -> String { + "String".to_owned() + } +} +impl IntoSchema for Box { + fn type_name() -> String { + "String".to_owned() + } + fn update_schema_map(map: &mut MetaMap) { + if !map.contains_key::() { + if !map.contains_key::() { + String::update_schema_map(map); + } + + if let Some(schema) = map.get::() { + map.insert::(schema.clone()); + } + } + } +} + impl TypeId for Result { fn id() -> String { format!("Result<{}, {}>", T::id(), E::id()) diff --git a/scripts/test_env.py b/scripts/test_env.py index cd72d89c5aa..b6d3b858910 100755 --- a/scripts/test_env.py +++ b/scripts/test_env.py @@ -17,6 +17,7 @@ import time import urllib.error import urllib.request +import uuid class Network: @@ -46,6 +47,7 @@ def __init__(self, args: argparse.Namespace): sys.exit(1) copy_or_prompt_build_bin("iroha", args.root_dir, peers_dir) + self.shared_env["IROHA_CHAIN_ID"] = "00000000-0000-0000-0000-000000000000" self.shared_env["IROHA_CONFIG"] = str(peers_dir.joinpath("config.json")) self.shared_env["IROHA_GENESIS_PUBLIC_KEY"] = self.peers[0].public_key @@ -123,7 +125,7 @@ def run(self, shared_env: dict(), submit_genesis: bool = False): peer_env["KURA_BLOCK_STORE_PATH"] = str(self.peer_dir.joinpath("storage")) peer_env["SNAPSHOT_DIR_PATH"] = str(self.peer_dir.joinpath("storage")) peer_env["LOG_LEVEL"] = "INFO" - peer_env["LOG_FORMAT"] = "\"pretty\"" + peer_env["LOG_FORMAT"] = '"pretty"' peer_env["LOG_TOKIO_CONSOLE_ADDR"] = f"{self.host_ip}:{self.tokio_console_port}" peer_env["IROHA_PUBLIC_KEY"] = self.public_key peer_env["IROHA_PRIVATE_KEY"] = self.private_key diff --git a/tools/kagami/src/config.rs b/tools/kagami/src/config.rs index e36b53fcd18..10c9aab9255 100644 --- a/tools/kagami/src/config.rs +++ b/tools/kagami/src/config.rs @@ -41,6 +41,7 @@ mod client { impl RunArgs for Args { fn run(self, writer: &mut BufWriter) -> Outcome { let config = ConfigurationProxy { + chain_id: Some(ChainId::new("00000000-0000-0000-0000-000000000000")), torii_api_url: Some(format!("http://{DEFAULT_API_ADDR}").parse()?), account_id: Some("alice@wonderland".parse()?), basic_auth: Some(Some(BasicAuth { diff --git a/tools/swarm/src/compose.rs b/tools/swarm/src/compose.rs index a8e538ea328..d6db4acb1d8 100644 --- a/tools/swarm/src/compose.rs +++ b/tools/swarm/src/compose.rs @@ -11,7 +11,7 @@ use color_eyre::eyre::{eyre, Context, ContextCompat}; use iroha_crypto::{ error::Error as IrohaCryptoError, KeyGenConfiguration, KeyPair, PrivateKey, PublicKey, }; -use iroha_data_model::prelude::PeerId; +use iroha_data_model::{prelude::PeerId, ChainId}; use iroha_primitives::addr::SocketAddr; use peer_generator::Peer; use serde::{ser::Error as _, Serialize, Serializer}; @@ -103,6 +103,7 @@ pub struct DockerComposeService { impl DockerComposeService { pub fn new( + chain_id: ChainId, peer: &Peer, source: ServiceSource, volumes: Vec<(String, String)>, @@ -122,6 +123,7 @@ impl DockerComposeService { }; let compact_env = CompactPeerEnv { + chain_id, trusted_peers, genesis_public_key, genesis_private_key, @@ -209,6 +211,7 @@ pub enum ServiceSource { #[derive(Serialize, Debug)] #[serde(rename_all = "UPPERCASE")] struct FullPeerEnv { + iroha_chain_id: ChainId, iroha_config: String, iroha_public_key: PublicKey, iroha_private_key: SerializeAsJsonStr, @@ -224,6 +227,7 @@ struct FullPeerEnv { } struct CompactPeerEnv { + chain_id: ChainId, key_pair: KeyPair, genesis_public_key: PublicKey, /// Genesis private key is only needed for a peer that is submitting the genesis block @@ -246,6 +250,7 @@ impl From for FullPeerEnv { }); Self { + iroha_chain_id: value.chain_id, iroha_config: PATH_TO_CONFIG.to_string(), iroha_public_key: value.key_pair.public_key().clone(), iroha_private_key: SerializeAsJsonStr(value.key_pair.private_key().clone()), @@ -302,6 +307,7 @@ impl DockerComposeBuilder<'_> { ) })?; + let chain_id = ChainId::new("00000000-0000-0000-0000-000000000000"); let peers = peer_generator::generate_peers(self.peers, self.seed) .wrap_err("Failed to generate peers")?; let genesis_key_pair = generate_key_pair(self.seed, GENESIS_KEYPAIR_SEED) @@ -329,6 +335,7 @@ impl DockerComposeBuilder<'_> { let first_peer_service = { let (name, peer) = peers_iter.next().expect("There is non-zero count of peers"); let service = DockerComposeService::new( + chain_id.clone(), peer, service_source.clone(), volumes.clone(), @@ -347,6 +354,7 @@ impl DockerComposeBuilder<'_> { let services = peers_iter .map(|(name, peer)| { let service = DockerComposeService::new( + chain_id.clone(), peer, service_source.clone(), volumes.clone(), @@ -504,6 +512,7 @@ mod tests { } } + #[derive(Debug)] struct TestEnv { env: HashMap, /// Set of env variables that weren't fetched yet @@ -513,11 +522,22 @@ mod tests { impl From for TestEnv { fn from(peer_env: FullPeerEnv) -> Self { let json = serde_json::to_string(&peer_env).expect("Must be serializable"); - let env: HashMap<_, _> = + let env: HashMap<_, serde_json::Value> = serde_json::from_str(&json).expect("Must be deserializable into a hash map"); let untouched = env.keys().cloned().collect(); Self { - env, + env: env + .into_iter() + .map(|(k, v)| { + let s = if let serde_json::Value::String(s) = v { + s + } else { + v.to_string() + }; + + (k, s) + }) + .collect(), untouched: RefCell::new(untouched), } } @@ -557,6 +577,7 @@ mod tests { fn default_config_with_swarm_env_is_exhaustive() { let keypair = KeyPair::generate().unwrap(); let env: TestEnv = CompactPeerEnv { + chain_id: ChainId::new("00000000-0000-0000-0000-000000000000"), key_pair: keypair.clone(), genesis_public_key: keypair.public_key().clone(), genesis_private_key: Some(keypair.private_key().clone()), @@ -593,6 +614,7 @@ mod tests { services: { let mut map = BTreeMap::new(); + let chain_id = ChainId::new("00000000-0000-0000-0000-000000000000"); let key_pair = KeyPair::generate_with_configuration( KeyGenConfiguration::default().use_seed(vec![1, 5, 1, 2, 2, 3, 4, 1, 2, 3]), ) @@ -604,6 +626,7 @@ mod tests { platform: PlatformArchitecture, source: ServiceSource::Build(PathBuf::from(".")), environment: CompactPeerEnv { + chain_id, key_pair: key_pair.clone(), genesis_public_key: key_pair.public_key().clone(), genesis_private_key: Some(key_pair.private_key().clone()), @@ -638,6 +661,7 @@ mod tests { build: . platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed012039E5BF092186FACC358770792A493CA98A83740643A3D41389483CF334F748C8 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"db9d90d20f969177bd5882f9fe211d14d1399d5440d04e3468783d169bbc4a8e39e5bf092186facc358770792a493ca98a83740643a3d41389483cf334f748c8"}' @@ -660,11 +684,15 @@ mod tests { #[test] fn empty_genesis_public_key_is_skipped_in_env() { + let chain_id = ChainId::new("00000000-0000-0000-0000-000000000000"); + let key_pair = KeyPair::generate_with_configuration( KeyGenConfiguration::default().use_seed(vec![0, 1, 2]), ) .unwrap(); + let env: FullPeerEnv = CompactPeerEnv { + chain_id, key_pair: key_pair.clone(), genesis_public_key: key_pair.public_key().clone(), genesis_private_key: None, @@ -676,6 +704,7 @@ mod tests { let actual = serde_yaml::to_string(&env).unwrap(); let expected = expect_test::expect![[r#" + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120415388A90FA238196737746A70565D041CFB32EAA0C89FF8CB244C7F832A6EBD IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"6bf163fd75192b81a78cb20c5f8cb917f591ac6635f2577e6ca305c27a456a5d415388a90fa238196737746a70565d041cfb32eaa0c89ff8cb244c7f832a6ebd"}' @@ -715,6 +744,7 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120F0321EB4139163C35F88BF78520FF7071499D7F4E79854550028A196C7B49E13 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"5f8d1291bf6b762ee748a87182345d135fd167062857aa4f20ba39f25e74c4b0f0321eb4139163c35f88bf78520ff7071499d7f4e79854550028a196c7b49e13"}' @@ -735,6 +765,7 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120A88554AA5C86D28D0EEBEC497235664433E807881CD31E12A1AF6C4D8B0F026C IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"8d34d2c6a699c61e7a9d5aabbbd07629029dfb4f9a0800d65aa6570113edb465a88554aa5c86d28d0eebec497235664433e807881cd31e12a1af6c4d8b0f026c"}' @@ -752,6 +783,7 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120312C1B7B5DE23D366ADCF23CD6DB92CE18B2AA283C7D9F5033B969C2DC2B92F4 IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"cf4515a82289f312868027568c0da0ee3f0fde7fef1b69deb47b19fde7cbc169312c1b7b5de23d366adcf23cd6db92ce18b2aa283c7d9f5033b969c2dc2b92f4"}' @@ -769,6 +801,7 @@ mod tests { build: ./iroha-cloned platform: linux/amd64 environment: + IROHA_CHAIN_ID: 00000000-0000-0000-0000-000000000000 IROHA_CONFIG: /config/config.json IROHA_PUBLIC_KEY: ed0120854457B2E3D6082181DA73DC01C1E6F93A72D0C45268DC8845755287E98A5DEE IROHA_PRIVATE_KEY: '{"digest_function":"ed25519","payload":"ab0e99c2b845b4ac7b3e88d25a860793c7eb600a25c66c75cba0bae91e955aa6854457b2e3d6082181da73dc01c1e6f93a72d0c45268dc8845755287e98a5dee"}' diff --git a/torii/src/lib.rs b/torii/src/lib.rs index 83d9ce10a26..cdf4a46537e 100644 --- a/torii/src/lib.rs +++ b/torii/src/lib.rs @@ -23,6 +23,7 @@ use iroha_core::{ sumeragi::SumeragiHandle, EventsSender, }; +use iroha_data_model::ChainId; use iroha_primitives::addr::SocketAddr; use tokio::{sync::Notify, task}; use utils::*; @@ -41,6 +42,7 @@ mod stream; /// Main network handler and the only entrypoint of the Iroha. pub struct Torii { + chain_id: Arc, kiso: KisoHandle, queue: Arc, events: EventsSender, @@ -56,6 +58,7 @@ impl Torii { /// Construct `Torii`. #[allow(clippy::too_many_arguments)] pub fn new( + chain_id: ChainId, kiso: KisoHandle, config: &ToriiConfiguration, queue: Arc, @@ -66,6 +69,7 @@ impl Torii { kura: Arc, ) -> Self { Self { + chain_id: Arc::new(chain_id), kiso, queue, events, @@ -131,10 +135,10 @@ impl Torii { let post_router = warp::post() .and( - endpoint3( + endpoint4( routing::handle_transaction, warp::path(uri::TRANSACTION) - .and(add_state!(self.queue, self.sumeragi)) + .and(add_state!(self.chain_id, self.queue, self.sumeragi)) .and(warp::body::content_length_limit( self.transaction_max_content_length, )) diff --git a/torii/src/routing.rs b/torii/src/routing.rs index baf083998b6..d909c7faec6 100644 --- a/torii/src/routing.rs +++ b/torii/src/routing.rs @@ -76,13 +76,14 @@ fn fetch_size() -> impl warp::Filter, queue: Arc, sumeragi: SumeragiHandle, transaction: SignedTransaction, ) -> Result { let wsv = sumeragi.wsv_clone(); let transaction_limits = wsv.config.transaction_limits; - let transaction = AcceptedTransaction::accept(transaction, &transaction_limits) + let transaction = AcceptedTransaction::accept(transaction, &chain_id, &transaction_limits) .map_err(Error::AcceptTransaction)?; queue .push(transaction, &wsv)