diff --git a/Cargo.lock b/Cargo.lock index 21a001766e..3d6cc9b6ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -75,8 +75,8 @@ dependencies = [ name = "aggregator" version = "0.7.0" dependencies = [ - "aleph-bft-rmc 0.14.0", - "aleph-bft-types 0.14.0", + "aleph-bft-rmc 0.16.0", + "aleph-bft-types 0.16.0", "async-trait", "futures", "log", @@ -87,10 +87,10 @@ dependencies = [ [[package]] name = "aggregator" version = "0.7.0" -source = "git+https://github.com/Cardinal-Cryptography/aleph-node.git?tag=r-14.0.0#0788bc518ffefb7084cb3401149b696f1ba37a2c" +source = "git+https://github.com/Cardinal-Cryptography/aleph-node.git?tag=r-15.2.0#36ed9faa3fd6189a01437eb195a56ec8eccebb5b" dependencies = [ - "aleph-bft-rmc 0.13.0", - "aleph-bft-types 0.13.0", + "aleph-bft-rmc 0.14.0", + "aleph-bft-types 0.14.0", "async-trait", "futures", "log", @@ -133,33 +133,33 @@ dependencies = [ [[package]] name = "aleph-bft" -version = "0.36.5" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f930b4c0d4a6b66533422e7ee62f5bab08949ada6d275298de909e95bc75b4ce" +checksum = "6c89b5ad70909935424ee13421e1a98ddef96d24a9af4525461826e57328da6a" dependencies = [ - "aleph-bft-rmc 0.13.0", - "aleph-bft-types 0.13.0", + "aleph-bft-rmc 0.14.0", + "aleph-bft-types 0.14.0", "anyhow", "async-trait", "derivative", "futures", "futures-timer", - "itertools 0.12.1", + "itertools 0.13.0", "log", "parity-scale-codec", "parking_lot 0.12.3", "rand", - "thiserror 1.0.69", + "thiserror 2.0.3", ] [[package]] name = "aleph-bft" -version = "0.42.1" +version = "0.45.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c89b5ad70909935424ee13421e1a98ddef96d24a9af4525461826e57328da6a" +checksum = "86b3ab57299549d25f297d9c98310087288d7dd2ce3f091e435c7972db7b09cd" dependencies = [ - "aleph-bft-rmc 0.14.0", - "aleph-bft-types 0.14.0", + "aleph-bft-rmc 0.16.0", + "aleph-bft-types 0.16.0", "anyhow", "async-trait", "derivative", @@ -180,19 +180,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fdf0c124883ef234a6262e43b9ed1d214e9f9c8744a88f1f2451c2b6efe4290" dependencies = [ "async-trait", - "bit-vec", + "bit-vec 0.6.3", "derive_more 0.99.18", "log", "parity-scale-codec", ] +[[package]] +name = "aleph-bft-crypto" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8888d6bd5c727c23e88eca018da1567a2202222cd48d36fe874519134e796c93" +dependencies = [ + "async-trait", + "bit-vec 0.8.0", + "derive_more 1.0.0", + "log", + "parity-scale-codec", +] + [[package]] name = "aleph-bft-mock" -version = "0.15.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8222fc5e37b3341d6e4fa01054ddd20983fbae2f00d21e438876dc7caf9ed997" +checksum = "06219ccbdc2d3903335e3046e974103ad9f2ca80282feb937e165d2d6293fb88" dependencies = [ - "aleph-bft-types 0.14.0", + "aleph-bft-types 0.16.0", "async-trait", "futures", "log", @@ -204,12 +217,12 @@ dependencies = [ [[package]] name = "aleph-bft-rmc" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "878948f17ae09afc0b62162a22325b8828fa1f46c90109bde7261224e2e09e34" +checksum = "795fad15176131c019e71ae7319bff398ad00e4079922ad368b7014e9068f03c" dependencies = [ - "aleph-bft-crypto", - "aleph-bft-types 0.13.0", + "aleph-bft-crypto 0.9.0", + "aleph-bft-types 0.14.0", "async-trait", "futures", "futures-timer", @@ -219,12 +232,12 @@ dependencies = [ [[package]] name = "aleph-bft-rmc" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "795fad15176131c019e71ae7319bff398ad00e4079922ad368b7014e9068f03c" +checksum = "c3d5e788fe61f308be53f55afbbcf44be2d71f5d87764208deb3718c3e30fde6" dependencies = [ - "aleph-bft-crypto", - "aleph-bft-types 0.14.0", + "aleph-bft-crypto 0.11.0", + "aleph-bft-types 0.16.0", "async-trait", "futures", "futures-timer", @@ -234,11 +247,11 @@ dependencies = [ [[package]] name = "aleph-bft-types" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bca9d19f587215da2b6c50b5e71f9addbf2305153f850dad3f9496ed67e28bb" +checksum = "8986b69d48e1dd4ea1a6b0ca820321dddbf4089efca3301d479ec45dcb167dea" dependencies = [ - "aleph-bft-crypto", + "aleph-bft-crypto 0.9.0", "async-trait", "futures", "parity-scale-codec", @@ -246,11 +259,11 @@ dependencies = [ [[package]] name = "aleph-bft-types" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8986b69d48e1dd4ea1a6b0ca820321dddbf4089efca3301d479ec45dcb167dea" +checksum = "dc168a8f7c9a98e743ac446d2ec406b1122e8ac09b73a2e28ac63fd2dbfa9434" dependencies = [ - "aleph-bft-crypto", + "aleph-bft-crypto 0.11.0", "async-trait", "futures", "parity-scale-codec", @@ -841,6 +854,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitcoin-internals" version = "0.2.0" @@ -1371,6 +1390,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1797,7 +1825,7 @@ version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version 0.4.1", @@ -1819,6 +1847,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ + "convert_case 0.6.0", "proc-macro2", "quote", "syn 2.0.89", @@ -2280,12 +2309,12 @@ name = "finality-aleph" version = "0.11.0" dependencies = [ "aggregator 0.7.0", - "aggregator 0.7.0 (git+https://github.com/Cardinal-Cryptography/aleph-node.git?tag=r-14.0.0)", - "aleph-bft 0.36.5", + "aggregator 0.7.0 (git+https://github.com/Cardinal-Cryptography/aleph-node.git?tag=r-15.2.0)", "aleph-bft 0.42.1", - "aleph-bft-crypto", - "aleph-bft-rmc 0.13.0", + "aleph-bft 0.45.2", + "aleph-bft-crypto 0.11.0", "aleph-bft-rmc 0.14.0", + "aleph-bft-rmc 0.16.0", "array-bytes 6.2.3", "async-trait", "derive_more 1.0.0", @@ -3604,15 +3633,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -4825,7 +4845,7 @@ name = "network-clique" version = "0.6.0" dependencies = [ "aleph-bft-mock", - "aleph-bft-types 0.14.0", + "aleph-bft-types 0.16.0", "async-trait", "bytes", "env_logger", @@ -9759,6 +9779,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.1.14" diff --git a/Cargo.toml b/Cargo.toml index c76d1712d0..c8bb5ef679 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,10 +45,10 @@ homepage = "https://alephzero.org" repository = "https://github.com/Cardinal-Cryptography/aleph-node" [workspace.dependencies] -aleph-bft-crypto = { version = "0.9" } -aleph-bft-mock = { version = "0.15" } -aleph-bft-rmc = { version = "0.14" } -aleph-bft-types = { version = "0.14" } +aleph-bft-crypto = { version = "0.11" } +aleph-bft-mock = { version = "0.18" } +aleph-bft-rmc = { version = "0.16" } +aleph-bft-types = { version = "0.16" } async-trait = { version = "0.1" } array-bytes = { version = "6" } bytes = { version = "1.8" } diff --git a/bin/fake-runtime-api/Cargo.toml b/bin/fake-runtime-api/Cargo.toml index 41e4432c45..a2c42932ef 100644 --- a/bin/fake-runtime-api/Cargo.toml +++ b/bin/fake-runtime-api/Cargo.toml @@ -37,3 +37,5 @@ std = [ "pallet-aleph-runtime-api/std", ] short_session = [] +try-runtime = [] +runtime-benchmarks = [] diff --git a/bin/node/src/executor.rs b/bin/node/src/executor.rs index 5f00b3e377..dbdeee5e16 100644 --- a/bin/node/src/executor.rs +++ b/bin/node/src/executor.rs @@ -4,7 +4,7 @@ use sc_service::Configuration; -#[cfg(not(any(feature = "runtime-benchmarks", feature = "aleph-native-runtime",)))] +#[cfg(not(feature = "runtime-benchmarks"))] pub mod aleph_executor { use sc_executor::WasmExecutor; @@ -18,7 +18,7 @@ pub mod aleph_executor { } } -#[cfg(any(feature = "runtime-benchmarks", feature = "aleph-native-runtime",))] +#[cfg(feature = "runtime-benchmarks")] pub mod aleph_executor { use sc_executor::NativeElseWasmExecutor; diff --git a/bin/node/src/lib.rs b/bin/node/src/lib.rs index e4eeadd30d..047204c1ad 100644 --- a/bin/node/src/lib.rs +++ b/bin/node/src/lib.rs @@ -9,6 +9,6 @@ mod service; pub use cli::{Cli, Subcommand}; pub use config::Validator as ConfigValidator; -#[cfg(any(feature = "runtime-benchmarks", feature = "aleph-native-runtime"))] +#[cfg(feature = "runtime-benchmarks")] pub use executor::aleph_executor::ExecutorDispatch; pub use service::{new_authority, new_partial, ServiceComponents}; diff --git a/finality-aleph/Cargo.toml b/finality-aleph/Cargo.toml index 957569fe59..225e00593e 100644 --- a/finality-aleph/Cargo.toml +++ b/finality-aleph/Cargo.toml @@ -11,14 +11,14 @@ repository.workspace = true # fixed version to 'freeze' some types used in abft, mainly `SignatureSet` used in justification and signature aggregation aleph-bft-crypto = { workspace = true } -current-aleph-bft = { package = "aleph-bft", version = "0.42" } -current-aleph-bft-rmc = { package = "aleph-bft-rmc", version = "0.14" } -legacy-aleph-bft = { package = "aleph-bft", version = "0.36" } -legacy-aleph-bft-rmc = { package = "aleph-bft-rmc", version = "0.13" } +current-aleph-bft = { package = "aleph-bft", version = "0.45" } +current-aleph-bft-rmc = { package = "aleph-bft-rmc", version = "0.16" } +legacy-aleph-bft = { package = "aleph-bft", version = "0.42" } +legacy-aleph-bft-rmc = { package = "aleph-bft-rmc", version = "0.14" } network-clique = { workspace = true } primitives = { workspace = true } -legacy-aleph-aggregator = { package = "aggregator", git = "https://github.com/Cardinal-Cryptography/aleph-node.git", tag = "r-14.0.0" } +legacy-aleph-aggregator = { package = "aggregator", git = "https://github.com/Cardinal-Cryptography/aleph-node.git", tag = "r-15.2.0" } current-aleph-aggregator = { path = "../aggregator", package = "aggregator" } rate-limiter = { package = "rate-limiter", path = "../rate-limiter" } fake-runtime-api = { workspace = true, features = ["std"] } diff --git a/finality-aleph/src/abft/crypto.rs b/finality-aleph/src/abft/crypto.rs index e2a8d9aa32..a5619d50be 100644 --- a/finality-aleph/src/abft/crypto.rs +++ b/finality-aleph/src/abft/crypto.rs @@ -48,14 +48,18 @@ impl Keychain { } } -// Currently the traits for legacy and current match, so only one implementation needed. impl legacy_aleph_bft::Index for Keychain { fn index(&self) -> legacy_aleph_bft::NodeIndex { Keychain::index(self).into() } } -// Currently the traits for legacy and current match, so only one implementation needed. +impl current_aleph_bft::Index for Keychain { + fn index(&self) -> current_aleph_bft::NodeIndex { + Keychain::index(self).into() + } +} + impl legacy_aleph_bft::Keychain for Keychain { type Signature = Signature; @@ -72,7 +76,22 @@ impl legacy_aleph_bft::Keychain for Keychain { } } -// Currently the traits for legacy and current match, so only one implementation needed. +impl current_aleph_bft::Keychain for Keychain { + type Signature = Signature; + + fn node_count(&self) -> current_aleph_bft::NodeCount { + Keychain::node_count(self).into() + } + + fn sign(&self, msg: &[u8]) -> Signature { + Keychain::sign(self, msg) + } + + fn verify(&self, msg: &[u8], sgn: &Signature, index: current_aleph_bft::NodeIndex) -> bool { + Keychain::verify(self, msg, sgn, index) + } +} + impl legacy_aleph_bft::MultiKeychain for Keychain { // Using `SignatureSet` is slow, but Substrate has not yet implemented aggregation. // We probably should do this for them at some point. @@ -96,3 +115,27 @@ impl legacy_aleph_bft::MultiKeychain for Keychain { Keychain::is_complete(self, msg, partial) } } + +impl current_aleph_bft::MultiKeychain for Keychain { + // Using `SignatureSet` is slow, but Substrate has not yet implemented aggregation. + // We probably should do this for them at some point. + type PartialMultisignature = SignatureSet; + + fn bootstrap_multi( + &self, + signature: &Signature, + index: current_aleph_bft::NodeIndex, + ) -> Self::PartialMultisignature { + current_aleph_bft::PartialMultisignature::add_signature( + SignatureSet(aleph_bft_crypto::SignatureSet::with_size( + aleph_bft_crypto::Keychain::node_count(self), + )), + signature, + index, + ) + } + + fn is_complete(&self, msg: &[u8], partial: &Self::PartialMultisignature) -> bool { + Keychain::is_complete(self, msg, partial) + } +} diff --git a/finality-aleph/src/abft/legacy/mod.rs b/finality-aleph/src/abft/legacy/mod.rs index 8f43b74278..f667806ad7 100644 --- a/finality-aleph/src/abft/legacy/mod.rs +++ b/finality-aleph/src/abft/legacy/mod.rs @@ -5,9 +5,11 @@ use log::debug; use network_clique::SpawnHandleExt; mod network; +mod performance; mod traits; pub use network::NetworkData; +pub use performance::{Service as PerformanceService, ServiceIO as PerformanceServiceIO}; pub use crate::aleph_primitives::LEGACY_FINALITY_VERSION as VERSION; use crate::{ @@ -15,9 +17,9 @@ use crate::{ common::{unit_creation_delay_fn, MAX_ROUNDS, SESSION_LEN_LOWER_BOUND_MS}, NetworkWrapper, }, - block::{Header, HeaderBackend, HeaderVerifier}, + block::UnverifiedHeader, crypto::Signature, - data_io::{AlephData, OrderedDataInterpreter, SubstrateChainInfoProvider}, + data_io::AlephData, network::data::Network, oneshot, party::{ @@ -31,20 +33,21 @@ type WrappedNetwork = NetworkWrapper< legacy_aleph_bft::NetworkData, Signature, SignatureSet>, ADN, >; -pub fn run_member( +pub fn run_member( subtask_common: TaskCommon, multikeychain: Keychain, config: Config, - network: WrappedNetwork, - data_provider: impl legacy_aleph_bft::DataProvider> + 'static, - ordered_data_interpreter: OrderedDataInterpreter, H, V>, + network: WrappedNetwork, + data_provider: impl legacy_aleph_bft::DataProvider> + 'static, + ordered_data_interpreter: impl legacy_aleph_bft::UnitFinalizationHandler< + Data = AlephData, + Hasher = Hasher, + >, backup: ABFTBackup, ) -> Task where - H: Header, - C: HeaderBackend + 'static, - ADN: Network> + 'static, - V: HeaderVerifier, + UH: UnverifiedHeader, + ADN: Network> + 'static, { let TaskCommon { spawn_handle, @@ -52,7 +55,12 @@ where } = subtask_common; let (stop, exit) = oneshot::channel(); let member_terminator = Terminator::create_root(exit, "member"); - let local_io = LocalIO::new(data_provider, ordered_data_interpreter, backup.0, backup.1); + let local_io = LocalIO::new_with_unit_finalization_handler( + data_provider, + ordered_data_interpreter, + backup.0, + backup.1, + ); let task = { let spawn_handle = spawn_handle.clone(); diff --git a/finality-aleph/src/abft/legacy/performance/mod.rs b/finality-aleph/src/abft/legacy/performance/mod.rs new file mode 100644 index 0000000000..03d6d9bc00 --- /dev/null +++ b/finality-aleph/src/abft/legacy/performance/mod.rs @@ -0,0 +1,8 @@ +use crate::{data_io::AlephData, Hasher}; + +mod scorer; +mod service; + +pub use service::{Service, ServiceIO}; + +type Batch = Vec, Hasher>>; diff --git a/finality-aleph/src/abft/legacy/performance/scorer.rs b/finality-aleph/src/abft/legacy/performance/scorer.rs new file mode 100644 index 0000000000..5ad594f8ae --- /dev/null +++ b/finality-aleph/src/abft/legacy/performance/scorer.rs @@ -0,0 +1,152 @@ +use legacy_aleph_bft::{NodeCount, NodeMap, Round}; + +use crate::{abft::legacy::performance::Batch, aleph_primitives::RawScore, UnverifiedHeader}; + +/// Scoring ABFT performance based on returned ordered unit batches. +pub struct Scorer { + newest_unit_by: NodeMap, +} + +impl Scorer { + /// Create a new scorer for the provided node count. + pub fn new(node_count: NodeCount) -> Self { + Scorer { + newest_unit_by: NodeMap::with_size(node_count), + } + } + + /// Add a batch of ordered units and return a score consisting of numbers of rounds a specific + /// node is behind. + pub fn process_batch(&mut self, batch: Batch) -> RawScore { + let max_round = batch.last().expect("batches always contain a head").round; + for unit in batch { + // Units are always added in order, so any unit created by an honest node + // here has a round greater than any that was included earlier. + // This is not necessarily true for forkers, but punishing them is fine. + self.newest_unit_by.insert(unit.creator, unit.round) + } + let all_nodes = self.newest_unit_by.size().into_iterator(); + all_nodes + .map(|node_id| { + self.newest_unit_by + .get(node_id) + // All other units have lower round than head, so the saturating_sub is just + // subtraction. + .map(|unit_round| max_round.saturating_sub(*unit_round)) + // If we don't have a unit it's the same as having a unit of round equal to -1. + .unwrap_or(max_round.saturating_add(1)) + }) + .collect() + } +} + +#[cfg(test)] +mod tests { + use std::iter; + + use legacy_aleph_bft::{NodeCount, OrderedUnit, Round}; + + use super::Scorer; + use crate::{block::mock::MockHeader, data_io::AlephData, Hasher}; + + const NODE_COUNT: NodeCount = NodeCount(7); + + fn units_up_to(max_round: Round) -> Vec, Hasher>>> { + let mut result = Vec::new(); + for round in 0..=max_round { + let mut round_units = Vec::new(); + for creator in NODE_COUNT.into_iterator() { + round_units.push(OrderedUnit { + data: None, + // We ignore the parents, so just putting nothing here. + parents: Vec::new(), + hash: Hasher::random_hash(), + creator, + round, + }); + } + result.push(round_units); + } + result + } + + #[test] + fn processes_initial_batch() { + let mut scorer = Scorer::new(NODE_COUNT); + let unit = units_up_to(0) + .pop() + .expect("there is a round") + .pop() + .expect("there is a unit"); + assert_eq!(scorer.process_batch(vec![unit]), vec![1, 1, 1, 1, 1, 1, 0]); + } + + #[test] + fn processes_perfect_performance_batch() { + let mut scorer = Scorer::new(NODE_COUNT); + let mut all_units = units_up_to(1); + let mut round_one_units = all_units.pop().expect("just created"); + let mut round_zero_units = all_units.pop().expect("just created"); + let first_head = round_zero_units.pop().expect("there is a unit"); + assert_eq!( + scorer.process_batch(vec![first_head]), + vec![1, 1, 1, 1, 1, 1, 0] + ); + let second_head = round_one_units.pop().expect("there is a unit"); + round_zero_units.push(second_head); + assert_eq!( + scorer.process_batch(round_zero_units), + vec![1, 1, 1, 1, 1, 1, 0] + ); + } + + #[test] + fn processes_lacking_creator_batch() { + let mut scorer = Scorer::new(NODE_COUNT); + let mut all_units = units_up_to(1); + let mut round_one_units = all_units.pop().expect("just created"); + round_one_units.pop(); + let mut round_zero_units = all_units.pop().expect("just created"); + round_zero_units.pop(); + let first_head = round_zero_units.pop().expect("there is a unit"); + assert_eq!( + scorer.process_batch(vec![first_head]), + vec![1, 1, 1, 1, 1, 0, 1] + ); + let second_head = round_one_units.pop().expect("there is a unit"); + round_zero_units.push(second_head); + assert_eq!( + scorer.process_batch(round_zero_units), + vec![1, 1, 1, 1, 1, 0, 2] + ); + } + + #[test] + fn processes_lagging_creator_batch() { + let mut scorer = Scorer::new(NODE_COUNT); + let mut all_units = units_up_to(2); + let mut round_two_units = all_units.pop().expect("just created"); + round_two_units.pop(); + let mut round_one_units = all_units.pop().expect("just created"); + round_one_units.pop(); + let mut round_zero_units = all_units.pop().expect("just created"); + let lagged_unit = round_zero_units.pop().expect("just created"); + let first_head = round_zero_units.pop().expect("there is a unit"); + assert_eq!( + scorer.process_batch(vec![first_head]), + vec![1, 1, 1, 1, 1, 0, 1] + ); + let second_head = round_one_units.pop().expect("there is a unit"); + round_zero_units.push(second_head); + assert_eq!( + scorer.process_batch(round_zero_units), + vec![1, 1, 1, 1, 1, 0, 2] + ); + let third_head = round_two_units.pop().expect("there is a unit"); + let third_batch = iter::once(lagged_unit) + .chain(round_one_units) + .chain(iter::once(third_head)) + .collect(); + assert_eq!(scorer.process_batch(third_batch), vec![1, 1, 1, 1, 1, 0, 2]); + } +} diff --git a/finality-aleph/src/abft/legacy/performance/service.rs b/finality-aleph/src/abft/legacy/performance/service.rs new file mode 100644 index 0000000000..07a93a3624 --- /dev/null +++ b/finality-aleph/src/abft/legacy/performance/service.rs @@ -0,0 +1,210 @@ +use std::collections::HashMap; + +use futures::{ + channel::{mpsc, oneshot}, + StreamExt, +}; +use legacy_aleph_bft::NodeCount; +use log::{debug, error, warn}; +use parity_scale_codec::Encode; +use sp_runtime::traits::Hash as _; + +use crate::{ + abft::{ + legacy::performance::{scorer::Scorer, Batch}, + LOG_TARGET, + }, + aleph_primitives::{ + crypto::SignatureSet, AuthoritySignature, Hash, Hashing, RawScore, Score, ScoreNonce, + }, + data_io::AlephData, + metrics::ScoreMetrics, + party::manager::Runnable, + runtime_api::RuntimeApi, + Hasher, SessionId, UnverifiedHeader, +}; + +struct FinalizationWrapper +where + UH: UnverifiedHeader, + FH: legacy_aleph_bft::FinalizationHandler>, +{ + finalization_handler: FH, + batches_for_scorer: mpsc::UnboundedSender>, +} + +impl FinalizationWrapper +where + UH: UnverifiedHeader, + FH: legacy_aleph_bft::FinalizationHandler>, +{ + fn new(finalization_handler: FH, batches_for_scorer: mpsc::UnboundedSender>) -> Self { + FinalizationWrapper { + finalization_handler, + batches_for_scorer, + } + } +} + +impl legacy_aleph_bft::UnitFinalizationHandler for FinalizationWrapper +where + UH: UnverifiedHeader, + FH: legacy_aleph_bft::FinalizationHandler>, +{ + type Data = AlephData; + type Hasher = Hasher; + + fn batch_finalized(&mut self, batch: Batch) { + for unit in &batch { + if let Some(data) = &unit.data { + self.finalization_handler.data_finalized(data.clone()) + } + } + if let Err(err) = self.batches_for_scorer.unbounded_send(batch) { + warn!(target: LOG_TARGET, "Failed to send ABFT batch to performance scoring: {}.", err); + } + } +} + +/// A service computing the performance score of ABFT nodes based on batches of ordered units. +pub struct Service +where + UH: UnverifiedHeader, + RA: RuntimeApi, +{ + my_index: usize, + session_id: SessionId, + score_submission_period: u32, + batches_from_abft: mpsc::UnboundedReceiver>, + hashes_for_aggregator: mpsc::UnboundedSender, + signatures_from_aggregator: mpsc::UnboundedReceiver<(Hash, SignatureSet)>, + runtime_api: RA, + pending_scores: HashMap, + nonce: ScoreNonce, + scorer: Scorer, + metrics: ScoreMetrics, +} + +pub struct ServiceIO { + pub hashes_for_aggregator: mpsc::UnboundedSender, + pub signatures_from_aggregator: + mpsc::UnboundedReceiver<(Hash, SignatureSet)>, +} + +impl Service +where + UH: UnverifiedHeader, + RA: RuntimeApi, +{ + /// Create a new service, together with a unit finalization handler that should be passed to + /// ABFT. It will wrap the provided finalization handler and call it in the background. + #[allow(clippy::too_many_arguments)] + pub fn new( + my_index: usize, + n_members: usize, + session_id: SessionId, + score_submission_period: u32, + finalization_handler: FH, + io: ServiceIO, + runtime_api: RA, + metrics: ScoreMetrics, + ) -> ( + Self, + impl legacy_aleph_bft::UnitFinalizationHandler, Hasher = Hasher>, + ) + where + FH: legacy_aleph_bft::FinalizationHandler>, + { + let ServiceIO { + hashes_for_aggregator, + signatures_from_aggregator, + } = io; + let (batches_for_us, batches_from_abft) = mpsc::unbounded(); + ( + Service { + my_index, + session_id, + score_submission_period, + batches_from_abft, + hashes_for_aggregator, + signatures_from_aggregator, + runtime_api, + pending_scores: HashMap::new(), + nonce: 1, + scorer: Scorer::new(NodeCount(n_members)), + metrics, + }, + FinalizationWrapper::new(finalization_handler, batches_for_us), + ) + } + + fn make_score(&mut self, points: RawScore) -> Score { + let result = Score { + session_id: self.session_id.0, + nonce: self.nonce, + points, + }; + self.nonce += 1; + result + } +} + +#[async_trait::async_trait] +impl Runnable for Service +where + UH: UnverifiedHeader, + RA: RuntimeApi, +{ + async fn run(mut self, mut exit: oneshot::Receiver<()>) { + let mut batch_counter = 1; + loop { + tokio::select! { + maybe_batch = self.batches_from_abft.next() => { + let points = match maybe_batch { + Some(batch) => self.scorer.process_batch(batch), + None => { + error!(target: LOG_TARGET, "Batches' channel closed, ABFT performance scoring terminating."); + break; + }, + }; + self.metrics.report_score(points[self.my_index]); + if batch_counter % self.score_submission_period == 0 { + let score = self.make_score(points); + let score_hash = Hashing::hash_of(&score.encode()); + debug!(target: LOG_TARGET, "Gathering signature under ABFT score: {:?}.", score); + self.pending_scores.insert(score_hash, score); + if let Err(e) = self.hashes_for_aggregator.unbounded_send(score_hash) { + error!(target: LOG_TARGET, "Failed to send score hash to signature aggregation: {}.", e); + break; + } + } + batch_counter += 1; + } + maybe_signed = self.signatures_from_aggregator.next() => { + match maybe_signed { + Some((hash, signature)) => { + match self.pending_scores.remove(&hash) { + Some(score) => { + if let Err(e) = self.runtime_api.submit_abft_score(score, signature) { + warn!(target: LOG_TARGET, "Failed to submit performance score to chain: {}.", e); + } + }, + None => { + warn!(target: LOG_TARGET, "Received multisigned hash for unknown performance score, this shouldn't ever happen."); + }, + } + }, + None => { + error!(target: LOG_TARGET, "Signatures' channel closed, ABFT performance scoring terminating."); + break; + }, + } + } + _ = &mut exit => { + debug!(target: LOG_TARGET, "ABFT performance scoring task received exit signal. Terminating."); + break; + } + } + } + } +} diff --git a/finality-aleph/src/abft/legacy/traits.rs b/finality-aleph/src/abft/legacy/traits.rs index be691597cd..faf710ba72 100644 --- a/finality-aleph/src/abft/legacy/traits.rs +++ b/finality-aleph/src/abft/legacy/traits.rs @@ -5,7 +5,9 @@ use crate::{ }; #[async_trait::async_trait] -impl legacy_aleph_bft::DataProvider> for DataProvider { +impl legacy_aleph_bft::DataProvider for DataProvider { + type Output = AlephData; + async fn get_data(&mut self) -> Option> { DataProvider::get_data(self).await } diff --git a/finality-aleph/src/abft/mod.rs b/finality-aleph/src/abft/mod.rs index 8786ea1f51..d76e8b96ab 100644 --- a/finality-aleph/src/abft/mod.rs +++ b/finality-aleph/src/abft/mod.rs @@ -25,7 +25,8 @@ pub use current::{ }; pub use legacy::{ create_aleph_config as legacy_create_aleph_config, run_member as run_legacy_member, - NetworkData as LegacyNetworkData, VERSION as LEGACY_VERSION, + NetworkData as LegacyNetworkData, PerformanceService as LegacyPerformanceService, + PerformanceServiceIO as LegacyPerformanceServiceIO, VERSION as LEGACY_VERSION, }; pub use network::NetworkWrapper; use parity_scale_codec::{Decode, Encode}; @@ -95,7 +96,6 @@ impl From> for PrimitivesSignatureSet legacy_aleph_bft::PartialMultisignature for SignatureSet { type Signature = S; @@ -107,3 +107,15 @@ impl legacy_aleph_bft::PartialMultisignature for SignatureSet< SignatureSet::add_signature(self, signature, index.into()) } } + +impl current_aleph_bft::PartialMultisignature for SignatureSet { + type Signature = S; + + fn add_signature( + self, + signature: &Self::Signature, + index: current_aleph_bft::NodeIndex, + ) -> Self { + SignatureSet::add_signature(self, signature, index.into()) + } +} diff --git a/finality-aleph/src/abft/network.rs b/finality-aleph/src/abft/network.rs index 034c61958e..ed18d2354e 100644 --- a/finality-aleph/src/abft/network.rs +++ b/finality-aleph/src/abft/network.rs @@ -49,7 +49,7 @@ impl + 'static> current_aleph_bft::Network for Networ } #[async_trait::async_trait] -impl> legacy_aleph_bft::Network for NetworkWrapper { +impl + 'static> legacy_aleph_bft::Network for NetworkWrapper { fn send(&self, data: D, recipient: legacy_aleph_bft::Recipient) { NetworkWrapper::send(self, data, recipient) } diff --git a/finality-aleph/src/abft/types.rs b/finality-aleph/src/abft/types.rs index b8da5ca59a..89dccf51a2 100644 --- a/finality-aleph/src/abft/types.rs +++ b/finality-aleph/src/abft/types.rs @@ -49,48 +49,68 @@ impl From for Recipient { } } -// Currently the traits for legacy and current match, so only one implementation needed. +impl From for legacy_aleph_bft::Recipient { + fn from(recipient: Recipient) -> Self { + match recipient { + Recipient::Everyone => legacy_aleph_bft::Recipient::Everyone, + Recipient::Node(idx) => legacy_aleph_bft::Recipient::Node(idx.into()), + } + } +} + +impl From for current_aleph_bft::Recipient { + fn from(recipient: Recipient) -> Self { + match recipient { + Recipient::Everyone => current_aleph_bft::Recipient::Everyone, + Recipient::Node(idx) => current_aleph_bft::Recipient::Node(idx.into()), + } + } +} + impl From for legacy_aleph_bft::NodeCount { fn from(count: NodeCount) -> Self { legacy_aleph_bft::NodeCount(count.0) } } -// Currently the traits for legacy and current match, so only one implementation needed. +impl From for current_aleph_bft::NodeCount { + fn from(count: NodeCount) -> Self { + current_aleph_bft::NodeCount(count.0) + } +} + impl From for NodeCount { fn from(count: legacy_aleph_bft::NodeCount) -> Self { Self(count.0) } } -// Currently the traits for legacy and current match, so only one implementation needed. +impl From for NodeCount { + fn from(count: current_aleph_bft::NodeCount) -> Self { + Self(count.0) + } +} + impl From for legacy_aleph_bft::NodeIndex { fn from(idx: NodeIndex) -> Self { legacy_aleph_bft::NodeIndex(idx.0) } } -// Currently the traits for legacy and current match, so only one implementation needed. -impl From for NodeIndex { - fn from(idx: legacy_aleph_bft::NodeIndex) -> Self { - Self(idx.0) +impl From for current_aleph_bft::NodeIndex { + fn from(idx: NodeIndex) -> Self { + current_aleph_bft::NodeIndex(idx.0) } } -impl From for current_aleph_bft::Recipient { - fn from(recipient: Recipient) -> Self { - match recipient { - Recipient::Everyone => current_aleph_bft::Recipient::Everyone, - Recipient::Node(idx) => current_aleph_bft::Recipient::Node(idx.into()), - } +impl From for NodeIndex { + fn from(idx: legacy_aleph_bft::NodeIndex) -> Self { + Self(idx.0) } } -impl From for legacy_aleph_bft::Recipient { - fn from(recipient: Recipient) -> Self { - match recipient { - Recipient::Everyone => legacy_aleph_bft::Recipient::Everyone, - Recipient::Node(idx) => legacy_aleph_bft::Recipient::Node(idx.into()), - } +impl From for NodeIndex { + fn from(idx: current_aleph_bft::NodeIndex) -> Self { + Self(idx.0) } } diff --git a/finality-aleph/src/aggregation/mod.rs b/finality-aleph/src/aggregation/mod.rs index 8ec3f84672..120d401af0 100644 --- a/finality-aleph/src/aggregation/mod.rs +++ b/finality-aleph/src/aggregation/mod.rs @@ -39,12 +39,15 @@ impl AsRef<[u8]> for SignableTypedHash { } pub type LegacyRmcNetworkData = - legacy_aleph_aggregator::RmcNetworkData>; + legacy_aleph_aggregator::RmcNetworkData>; pub type CurrentRmcNetworkData = current_aleph_aggregator::RmcNetworkData>; -pub type LegacyAggregator = - legacy_aleph_aggregator::IO, Keychain>; +pub type LegacyAggregator = legacy_aleph_aggregator::IO< + SignableTypedHash, + NetworkWrapper, + Keychain, +>; pub type CurrentAggregator = current_aleph_aggregator::IO< SignableTypedHash, @@ -71,7 +74,7 @@ where agg: EitherAggregator, } -impl<'a, CN, LN> Aggregator +impl Aggregator where LN: Network, CN: Network, @@ -82,7 +85,7 @@ where ); let rmc_handler = legacy_aleph_bft_rmc::Handler::new(multikeychain.clone()); let rmc_service = legacy_aleph_bft_rmc::Service::new(scheduler, rmc_handler); - let aggregator = legacy_aleph_aggregator::BlockSignatureAggregator::new(); + let aggregator = legacy_aleph_aggregator::HashSignatureAggregator::new(); let aggregator_io = LegacyAggregator::::new(NetworkWrapper::new(rmc_network), rmc_service, aggregator); @@ -107,13 +110,9 @@ where } pub async fn start_aggregation(&mut self, h: SignableTypedHash) { - use SignableTypedHash::*; match &mut self.agg { EitherAggregator::Current(agg) => agg.start_aggregation(h).await, - EitherAggregator::Legacy(agg) => match h { - Block(h) => agg.start_aggregation(h).await, - Performance(_) => { /* should never happen, but ignoring is fine */ } - }, + EitherAggregator::Legacy(agg) => agg.start_aggregation(h).await, } } @@ -122,10 +121,7 @@ where ) -> Option<(SignableTypedHash, SignatureSet)> { match &mut self.agg { EitherAggregator::Current(agg) => agg.next_multisigned_hash().await, - EitherAggregator::Legacy(agg) => agg - .next_multisigned_hash() - .await - .map(|(h, sig)| (SignableTypedHash::Block(h), sig)), + EitherAggregator::Legacy(agg) => agg.next_multisigned_hash().await, } } diff --git a/finality-aleph/src/block/substrate/verification/cache.rs b/finality-aleph/src/block/substrate/verification/cache.rs index cb0040f95c..12ca88ce1b 100644 --- a/finality-aleph/src/block/substrate/verification/cache.rs +++ b/finality-aleph/src/block/substrate/verification/cache.rs @@ -416,8 +416,7 @@ where .map_err(|_| HeaderVerificationError::MissingAuthorityData)?; let (header, slot) = - Self::check_header_slot_and_seal(header, &authorities.aura_authorities) - .map_err(HeaderVerificationError::from)?; + Self::check_header_slot_and_seal(header, &authorities.aura_authorities)?; let (expected_author, maybe_account_id) = Self::slot_author( slot, diff --git a/finality-aleph/src/network/session/discovery.rs b/finality-aleph/src/network/session/discovery.rs index 032fa3db18..99c24aecb3 100644 --- a/finality-aleph/src/network/session/discovery.rs +++ b/finality-aleph/src/network/session/discovery.rs @@ -36,11 +36,7 @@ impl Discovery { &mut self, handler: &SessionHandler, ) -> Option> { - let authentication = match handler.authentication() { - Some(authentication) => authentication, - None => return None, - }; - + let authentication = handler.authentication()?; let missing_authorities = handler.missing_nodes(); let node_count = handler.node_count(); info!(target: "aleph-network", "{}/{} authorities known for session {}.", node_count.0-missing_authorities.len(), node_count.0, handler.session_id().0); diff --git a/finality-aleph/src/party/manager/mod.rs b/finality-aleph/src/party/manager/mod.rs index b3f38de47f..8fb9ed5f77 100644 --- a/finality-aleph/src/party/manager/mod.rs +++ b/finality-aleph/src/party/manager/mod.rs @@ -12,7 +12,8 @@ use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; use crate::{ abft::{ current_create_aleph_config, legacy_create_aleph_config, run_current_member, - run_legacy_member, CurrentPerformanceService, CurrentPerformanceServiceIO, SpawnHandle, + run_legacy_member, CurrentPerformanceService, CurrentPerformanceServiceIO, + LegacyPerformanceService, LegacyPerformanceServiceIO, SpawnHandle, }, aleph_primitives::{ crypto::SignatureSet, AuthoritySignature, BlockHash, BlockNumber, Hash, KEY_TYPE, @@ -48,7 +49,7 @@ mod authority; mod task; pub use authority::{Subtasks, Task as AuthorityTask}; -pub use task::{Handle, NoopRunnable, Runnable, Task, TaskCommon}; +pub use task::{Handle, Runnable, Task, TaskCommon}; use crate::{ abft::{CURRENT_VERSION, LEGACY_VERSION}, @@ -190,10 +191,13 @@ where n_members, node_id, session_id, + score_submission_period, data_network, session_boundaries, subtask_common, blocks_for_aggregator, + performance_for_aggregator, + signed_performance_from_aggregator, chain_info, aggregator_io, multikeychain, @@ -214,6 +218,19 @@ where self.verifier.clone(), session_boundaries.clone(), ); + let (abft_performance, abft_batch_handler) = LegacyPerformanceService::new( + node_id.into(), + n_members, + session_id, + score_submission_period, + ordered_data_interpreter, + LegacyPerformanceServiceIO { + hashes_for_aggregator: performance_for_aggregator, + signatures_from_aggregator: signed_performance_from_aggregator, + }, + self.runtime_api.clone(), + self.score_metrics.clone(), + ); let consensus_config = legacy_create_aleph_config(n_members, node_id, session_id, self.unit_creation_delay); let data_network = data_network.map(); @@ -237,14 +254,10 @@ where consensus_config, aleph_network.into(), data_provider, - ordered_data_interpreter, + abft_batch_handler, backup, ), - task::task( - subtask_common.clone(), - NoopRunnable, - "noop abft performance", - ), + task::task(subtask_common.clone(), abft_performance, "abft performance"), aggregator::task( subtask_common.clone(), self.header_backend.clone(), diff --git a/finality-aleph/src/sync/handler/mod.rs b/finality-aleph/src/sync/handler/mod.rs index 161f6268cd..7cea340108 100644 --- a/finality-aleph/src/sync/handler/mod.rs +++ b/finality-aleph/src/sync/handler/mod.rs @@ -70,7 +70,7 @@ where forest: &'a Forest, } -impl<'a, I, J> InterestProvider<'a, I, J> +impl InterestProvider<'_, I, J> where I: PeerId, J: Justification, @@ -1454,9 +1454,8 @@ mod tests { ); let (new_info, _, maybe_error) = handler.handle_request_response(response, 12); assert!(!new_info, "should not create new highest justified"); - match maybe_error { - None => panic!("should fail when it reaches the top finalized"), - Some(_) => (), + if maybe_error.is_none() { + panic!("should fail when it reaches the top finalized"); } // check that the fork is pruned @@ -1555,9 +1554,8 @@ mod tests { ); let (new_info, _, maybe_error) = handler.handle_request_response(response, 12); assert!(!new_info, "should not create new highest justified"); - match maybe_error { - None => panic!("should fail when it reaches the top finalized"), - Some(_) => (), + if maybe_error.is_none() { + panic!("should fail when it reaches the top finalized"); } // check that the fork is pruned diff --git a/finality-aleph/src/sync/handler/request_handler.rs b/finality-aleph/src/sync/handler/request_handler.rs index 6707670594..8f681b935d 100644 --- a/finality-aleph/src/sync/handler/request_handler.rs +++ b/finality-aleph/src/sync/handler/request_handler.rs @@ -302,7 +302,7 @@ where _phantom: PhantomData<(B, J)>, } -impl<'a, B, J, CS> HandlerTypes for RequestHandler<'a, B, J, CS> +impl HandlerTypes for RequestHandler<'_, B, J, CS> where J: Justification, B: Block>, diff --git a/finality-aleph/src/sync/service.rs b/finality-aleph/src/sync/service.rs index fc72af0c86..36547e6023 100644 --- a/finality-aleph/src/sync/service.rs +++ b/finality-aleph/src/sync/service.rs @@ -164,6 +164,8 @@ impl RequestBlocks for mpsc::UnboundedSender { } } +type HandlerResult = Result>; + impl Service where J: Justification, @@ -184,7 +186,7 @@ where metrics_registry: Option, slo_metrics: SloMetrics, favourite_block_request: mpsc::UnboundedReceiver>, - ) -> Result<(Self, impl RequestBlocks), HandlerError> { + ) -> HandlerResult<(Self, impl RequestBlocks), B, J, CS, V, F> { let IO { network, chain_events, diff --git a/pallets/aleph/src/lib.rs b/pallets/aleph/src/lib.rs index 3c67902e22..93ba6c8285 100644 --- a/pallets/aleph/src/lib.rs +++ b/pallets/aleph/src/lib.rs @@ -26,6 +26,7 @@ use sp_std::prelude::*; const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); pub(crate) const LOG_TARGET: &str = "pallet-aleph"; +#[expect(clippy::useless_conversion)] #[frame_support::pallet] #[pallet_doc("../README.md")] pub mod pallet { diff --git a/pallets/committee-management/README.md b/pallets/committee-management/README.md index b3cc091de7..035c4c0ec1 100644 --- a/pallets/committee-management/README.md +++ b/pallets/committee-management/README.md @@ -7,8 +7,9 @@ number of _underperformance_ sessions, which means that number of blocks produce validator is less than some predefined threshold. In other words, if a validator: * performance in a session is less or equal to a configurable threshold -`BanConfig::minimal_expected_performance` (from 0 to 100%), and, + `BanConfig::minimal_expected_performance` (from 0 to 100%), and, * it happened at least `BanConfig::underperformed_session_count_threshold` times, + then the validator is considered an underperformer and hence removed (ie _banned out_) from the committee. diff --git a/pallets/committee-management/src/manager.rs b/pallets/committee-management/src/manager.rs index 49b7ee7994..75a280fa59 100644 --- a/pallets/committee-management/src/manager.rs +++ b/pallets/committee-management/src/manager.rs @@ -27,7 +27,6 @@ use crate::{ /// 4. `new_session(S + 2)` is called. /// * If session `S+2` starts new era we emit fresh bans events /// * We rotate the validators for session `S + 2` using the information about reserved and non reserved validators. - impl pallet_authorship::EventHandler> for Pallet where T: Config, diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 6f4fa1f99a..f313080530 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -163,7 +163,7 @@ pub const DEFAULT_FINALITY_VERSION: Version = 0; pub const CURRENT_FINALITY_VERSION: u16 = LEGACY_FINALITY_VERSION + 1; /// Legacy version of abft. -pub const LEGACY_FINALITY_VERSION: u16 = 4; +pub const LEGACY_FINALITY_VERSION: u16 = 5; /// Percentage of validator performance that is treated as 100% performance pub const LENIENT_THRESHOLD: Perquintill = Perquintill::from_percent(90); @@ -444,6 +444,7 @@ pub mod staking { /// * `arg1: type1, arg2: type,...`is a list of arguments and will be passed as is, can be empty /// * `class_name`is a class that has non-self `method-name`,ie symbol `class_name::method_name` exists, /// * `return_type` is type returned from `method_name` + /// /// Example /// ```ignore /// wrap_methods!( diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6eb48e1d9d..93d43dca68 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.79.0" +channel = "1.85.1" targets = [ "wasm32-unknown-unknown" ] components = [ "rustfmt", "clippy", "rust-src" ] profile = "minimal"