diff --git a/Cargo.lock b/Cargo.lock index 398f5122bbfe..223375ce5ece 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1080,6 +1080,7 @@ dependencies = [ "impl-trait-for-tuples", "log", "pallet-asset-conversion", + "pallet-assets", "pallet-xcm", "parachains-common", "parity-scale-codec", @@ -2992,9 +2993,9 @@ dependencies = [ [[package]] name = "color-eyre" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" dependencies = [ "backtrace", "eyre", @@ -12567,6 +12568,7 @@ dependencies = [ "frame-try-runtime", "hex-literal", "log", + "pallet-asset-conversion", "pallet-asset-tx-payment", "pallet-assets", "pallet-aura", @@ -12585,6 +12587,7 @@ dependencies = [ "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-runtime-common", + "primitive-types", "scale-info", "smallvec", "sp-api", @@ -12989,7 +12992,9 @@ dependencies = [ "rand", "rand_chacha", "rand_core", + "sc-keystore", "schnorrkel 0.11.4", + "sp-application-crypto", "sp-authority-discovery", "sp-core", "sp-tracing 16.0.0", @@ -13802,14 +13807,17 @@ dependencies = [ "bitvec", "bounded-vec", "futures", + "futures-timer", "parity-scale-codec", "polkadot-erasure-coding", "polkadot-parachain-primitives", "polkadot-primitives", + "sc-keystore", "schnorrkel 0.11.4", "serde", "sp-application-crypto", "sp-consensus-babe", + "sp-consensus-slots", "sp-core", "sp-keystore", "sp-maybe-compressed-blob", diff --git a/Cargo.toml b/Cargo.toml index 378ba6b15ff0..69c6189cebc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -673,7 +673,7 @@ coarsetime = { version = "0.1.22" } codec = { version = "3.6.12", default-features = false, package = "parity-scale-codec" } collectives-westend-emulated-chain = { path = "cumulus/parachains/integration-tests/emulated/chains/parachains/collectives/collectives-westend" } collectives-westend-runtime = { path = "cumulus/parachains/runtimes/collectives/collectives-westend" } -color-eyre = { version = "0.6.1", default-features = false } +color-eyre = { version = "0.6.3", default-features = false } color-print = { version = "0.3.4" } colored = { version = "2.0.4" } comfy-table = { version = "7.1.0", default-features = false } diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs index d20e059f9fea..2876474e094c 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs @@ -27,6 +27,7 @@ use parachains_common::{AccountId, Balance}; pub const PARA_ID: u32 = 1000; pub const ED: Balance = testnet_parachains_constants::westend::currency::EXISTENTIAL_DEPOSIT; +pub const USDT_ED: Balance = 70_000; parameter_types! { pub AssetHubWestendAssetOwner: AccountId = get_account_id_from_seed::("Alice"); @@ -67,7 +68,7 @@ pub fn genesis() -> Storage { assets: asset_hub_westend_runtime::AssetsConfig { assets: vec![ (RESERVABLE_ASSET_ID, AssetHubWestendAssetOwner::get(), false, ED), - (USDT_ID, AssetHubWestendAssetOwner::get(), true, ED), + (USDT_ID, AssetHubWestendAssetOwner::get(), true, USDT_ED), ], ..Default::default() }, diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/genesis.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/genesis.rs index 38c94b34aa2e..2c34b7e96f5e 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/genesis.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/genesis.rs @@ -27,6 +27,7 @@ use penpal_runtime::xcm_config::{LocalReservableFromAssetHub, RelayLocation, Usd pub const PARA_ID_A: u32 = 2000; pub const PARA_ID_B: u32 = 2001; pub const ED: Balance = penpal_runtime::EXISTENTIAL_DEPOSIT; +pub const USDT_ED: Balance = 70_000; parameter_types! { pub PenpalSudoAccount: AccountId = get_account_id_from_seed::("Alice"); @@ -81,8 +82,8 @@ pub fn genesis(para_id: u32) -> Storage { (RelayLocation::get(), PenpalAssetOwner::get(), true, ED), // Sufficient AssetHub asset representation (LocalReservableFromAssetHub::get(), PenpalAssetOwner::get(), true, ED), - // USDT from Asset Hub - (UsdtFromAssetHub::get(), PenpalAssetOwner::get(), true, ED), + // USDT from AssetHub + (UsdtFromAssetHub::get(), PenpalAssetOwner::get(), true, USDT_ED), ], ..Default::default() }, diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/lib.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/lib.rs index 91793d33f304..92dfa30f2e83 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/testing/penpal/src/lib.rs @@ -53,6 +53,7 @@ decl_test_parachains! { PolkadotXcm: penpal_runtime::PolkadotXcm, Assets: penpal_runtime::Assets, ForeignAssets: penpal_runtime::ForeignAssets, + AssetConversion: penpal_runtime::AssetConversion, Balances: penpal_runtime::Balances, } }, @@ -76,6 +77,7 @@ decl_test_parachains! { PolkadotXcm: penpal_runtime::PolkadotXcm, Assets: penpal_runtime::Assets, ForeignAssets: penpal_runtime::ForeignAssets, + AssetConversion: penpal_runtime::AssetConversion, Balances: penpal_runtime::Balances, } }, diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs index 87a090bf1ae6..f4fe1478f3ed 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/lib.rs @@ -66,6 +66,7 @@ mod imports { CustomizableAssetFromSystemAssetHub as PenpalCustomizableAssetFromSystemAssetHub, LocalReservableFromAssetHub as PenpalLocalReservableFromAssetHub, LocalTeleportableToAssetHub as PenpalLocalTeleportableToAssetHub, + UsdtFromAssetHub as PenpalUsdtFromAssetHub, }, PenpalAParaPallet as PenpalAPallet, PenpalAssetOwner, PenpalBParaPallet as PenpalBPallet, ED as PENPAL_ED, diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reserve_transfer.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reserve_transfer.rs index 70dde03d75a2..faff5f7660c2 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reserve_transfer.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-rococo/src/tests/reserve_transfer.rs @@ -60,10 +60,10 @@ pub fn system_para_to_para_sender_assertions(t: SystemParaToParaTest) { AssetHubRococo::assert_xcm_pallet_attempted_complete(None); let sov_acc_of_dest = AssetHubRococo::sovereign_account_id_of(t.args.dest.clone()); - for (idx, asset) in t.args.assets.into_inner().into_iter().enumerate() { + for asset in t.args.assets.into_inner().into_iter() { let expected_id = asset.id.0.clone().try_into().unwrap(); let asset_amount = if let Fungible(a) = asset.fun { Some(a) } else { None }.unwrap(); - if idx == t.args.fee_asset_item as usize { + if asset.id == AssetId(Location::new(1, [])) { assert_expected_events!( AssetHubRococo, vec![ @@ -77,6 +77,23 @@ pub fn system_para_to_para_sender_assertions(t: SystemParaToParaTest) { }, ] ); + } else if matches!( + asset.id.0.unpack(), + (0, [PalletInstance(ASSETS_PALLET_ID), GeneralIndex(_)]) + ) { + assert_expected_events!( + AssetHubRococo, + vec![ + // Amount of trust-backed asset is transferred to Parachain's Sovereign account + RuntimeEvent::Assets( + pallet_assets::Event::Transferred { from, to, amount, .. }, + ) => { + from: *from == t.sender.account_id, + to: *to == sov_acc_of_dest, + amount: *amount == asset_amount, + }, + ] + ); } else { assert_expected_events!( AssetHubRococo, @@ -388,6 +405,38 @@ pub fn para_to_para_through_hop_sender_assertions(t: Test::RuntimeEvent; + let sov_penpal_a_on_ah = AssetHubRococo::sovereign_account_id_of( + AssetHubRococo::sibling_location_of(PenpalA::para_id()), + ); + let sov_penpal_b_on_ah = AssetHubRococo::sovereign_account_id_of( + AssetHubRococo::sibling_location_of(PenpalB::para_id()), + ); + + assert_expected_events!( + AssetHubRococo, + vec![ + // Withdrawn from sender parachain SA + RuntimeEvent::Assets( + pallet_assets::Event::Burned { owner, balance, .. } + ) => { + owner: *owner == sov_penpal_a_on_ah, + balance: *balance == t.args.amount, + }, + // Deposited to receiver parachain SA + RuntimeEvent::Assets( + pallet_assets::Event::Deposited { who, .. } + ) => { + who: *who == sov_penpal_b_on_ah, + }, + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + ] + ); +} + fn para_to_para_relay_hop_assertions(t: ParaToParaThroughRelayTest) { type RuntimeEvent = ::RuntimeEvent; let sov_penpal_a_on_rococo = @@ -469,6 +518,19 @@ fn system_para_to_para_reserve_transfer_assets(t: SystemParaToParaTest) -> Dispa ) } +fn para_to_para_through_asset_hub_limited_reserve_transfer_assets( + t: ParaToParaThroughAHTest, +) -> DispatchResult { + ::PolkadotXcm::limited_reserve_transfer_assets( + t.signed_origin, + bx!(t.args.dest.into()), + bx!(t.args.beneficiary.into()), + bx!(t.args.assets.into()), + t.args.fee_asset_item, + t.args.weight_limit, + ) +} + fn para_to_system_para_reserve_transfer_assets(t: ParaToSystemParaTest) -> DispatchResult { ::PolkadotXcm::limited_reserve_transfer_assets( t.signed_origin, @@ -1136,3 +1198,353 @@ fn reserve_transfer_native_asset_from_para_to_para_through_relay() { // Receiver's balance is increased assert!(receiver_assets_after > receiver_assets_before); } + +// ============================================================================ +// ==== Reserve Transfers USDT - AssetHub->Parachain - pay fees using pool ==== +// ============================================================================ +#[test] +fn reserve_transfer_usdt_from_asset_hub_to_para() { + let usdt_id = 1984u32; + let penpal_location = AssetHubRococo::sibling_location_of(PenpalA::para_id()); + let penpal_sov_account = AssetHubRococo::sovereign_account_id_of(penpal_location.clone()); + + // Create SA-of-Penpal-on-AHW with ED. + // This ED isn't reflected in any derivative in a PenpalA account. + AssetHubRococo::fund_accounts(vec![(penpal_sov_account.clone().into(), ASSET_HUB_ROCOCO_ED)]); + + let sender = AssetHubRococoSender::get(); + let receiver = PenpalAReceiver::get(); + let asset_amount_to_send = 1_000_000_000_000; + + AssetHubRococo::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type Assets = ::Assets; + assert_ok!(>::mint_into( + usdt_id.into(), + &AssetHubRococoSender::get(), + asset_amount_to_send + 10_000_000_000_000, // Make sure it has enough. + )); + }); + + let relay_asset_penpal_pov = RelayLocation::get(); + + let usdt_from_asset_hub = PenpalUsdtFromAssetHub::get(); + + // Setup the pool between `relay_asset_penpal_pov` and `usdt_from_asset_hub` on PenpalA. + // So we can swap the custom asset that comes from AssetHubRococo for native asset to pay for + // fees. + PenpalA::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_ok!(::ForeignAssets::mint( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + usdt_from_asset_hub.clone().into(), + PenpalASender::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(PenpalASender::get()), + Box::new(relay_asset_penpal_pov.clone()), + Box::new(usdt_from_asset_hub.clone()), + )); + + assert_expected_events!( + PenpalA, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(PenpalASender::get()), + Box::new(relay_asset_penpal_pov), + Box::new(usdt_from_asset_hub.clone()), + // `usdt_from_asset_hub` is worth a third of `relay_asset_penpal_pov` + 1_000_000_000_000, + 3_000_000_000_000, + 0, + 0, + PenpalASender::get().into() + )); + + assert_expected_events!( + PenpalA, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + let assets: Assets = vec![( + [PalletInstance(ASSETS_PALLET_ID), GeneralIndex(usdt_id.into())], + asset_amount_to_send, + ) + .into()] + .into(); + + let test_args = TestContext { + sender: sender.clone(), + receiver: receiver.clone(), + args: TestArgs::new_para( + penpal_location, + receiver.clone(), + asset_amount_to_send, + assets, + None, + 0, + ), + }; + let mut test = SystemParaToParaTest::new(test_args); + + let sender_initial_balance = AssetHubRococo::execute_with(|| { + type Assets = ::Assets; + >::balance(usdt_id, &sender) + }); + let sender_initial_native_balance = AssetHubRococo::execute_with(|| { + type Balances = ::Balances; + Balances::free_balance(&sender) + }); + let receiver_initial_balance = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &receiver) + }); + + test.set_assertion::(system_para_to_para_sender_assertions); + test.set_assertion::(system_para_to_para_receiver_assertions); + test.set_dispatchable::(system_para_to_para_reserve_transfer_assets); + test.assert(); + + let sender_after_balance = AssetHubRococo::execute_with(|| { + type Assets = ::Assets; + >::balance(usdt_id, &sender) + }); + let sender_after_native_balance = AssetHubRococo::execute_with(|| { + type Balances = ::Balances; + Balances::free_balance(&sender) + }); + let receiver_after_balance = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub, &receiver) + }); + + // TODO(https://github.com/paritytech/polkadot-sdk/issues/5160): When we allow payment with different assets locally, this should be the same, since + // they aren't used for fees. + assert!(sender_after_native_balance < sender_initial_native_balance); + // Sender account's balance decreases. + assert_eq!(sender_after_balance, sender_initial_balance - asset_amount_to_send); + // Receiver account's balance increases. + assert!(receiver_after_balance > receiver_initial_balance); + assert!(receiver_after_balance < receiver_initial_balance + asset_amount_to_send); +} + +// =================================================================================== +// == Reserve Transfers USDT - Parachain->AssetHub->Parachain - pay fees using pool == +// =================================================================================== +// +// Transfer USDT From Penpal A to Penpal B with AssetHub as the reserve, while paying fees using +// USDT by making use of existing USDT pools on AssetHub and destination. +#[test] +fn reserve_transfer_usdt_from_para_to_para_through_asset_hub() { + let destination = PenpalA::sibling_location_of(PenpalB::para_id()); + let sender = PenpalASender::get(); + let asset_amount_to_send: Balance = ROCOCO_ED * 10000; + let fee_amount_to_send: Balance = ROCOCO_ED * 10000; + let sender_chain_as_seen_by_asset_hub = AssetHubRococo::sibling_location_of(PenpalA::para_id()); + let sov_of_sender_on_asset_hub = + AssetHubRococo::sovereign_account_id_of(sender_chain_as_seen_by_asset_hub); + let receiver_as_seen_by_asset_hub = AssetHubRococo::sibling_location_of(PenpalB::para_id()); + let sov_of_receiver_on_asset_hub = + AssetHubRococo::sovereign_account_id_of(receiver_as_seen_by_asset_hub); + + // Create SA-of-Penpal-on-AHW with ED. + // This ED isn't reflected in any derivative in a PenpalA account. + AssetHubRococo::fund_accounts(vec![ + (sov_of_sender_on_asset_hub.clone().into(), ASSET_HUB_ROCOCO_ED), + (sov_of_receiver_on_asset_hub.clone().into(), ASSET_HUB_ROCOCO_ED), + ]); + + // Give USDT to sov account of sender. + let usdt_id = 1984; + AssetHubRococo::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type Assets = ::Assets; + assert_ok!(>::mint_into( + usdt_id.into(), + &sov_of_sender_on_asset_hub.clone().into(), + asset_amount_to_send + fee_amount_to_send, + )); + }); + + // We create a pool between WND and USDT in AssetHub. + let native_asset: Location = Parent.into(); + let usdt = Location::new( + 0, + [Junction::PalletInstance(ASSETS_PALLET_ID), Junction::GeneralIndex(usdt_id.into())], + ); + + // set up pool with USDT <> native pair + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_ok!(::Assets::mint( + ::RuntimeOrigin::signed(AssetHubRococoSender::get()), + usdt_id.into(), + AssetHubRococoSender::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(AssetHubRococoSender::get()), + Box::new(native_asset.clone()), + Box::new(usdt.clone()), + )); + + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(AssetHubRococoSender::get()), + Box::new(native_asset), + Box::new(usdt), + 1_000_000_000_000, + 2_000_000_000_000, // usdt is worth half of `native_asset` + 0, + 0, + AssetHubRococoSender::get().into() + )); + + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + let usdt_from_asset_hub = PenpalUsdtFromAssetHub::get(); + + // We also need a pool between WND and USDT on PenpalB. + PenpalB::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let relay_asset = RelayLocation::get(); + + assert_ok!(::ForeignAssets::mint( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + usdt_from_asset_hub.clone().into(), + PenpalBReceiver::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(PenpalBReceiver::get()), + Box::new(relay_asset.clone()), + Box::new(usdt_from_asset_hub.clone()), + )); + + assert_expected_events!( + PenpalB, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(PenpalBReceiver::get()), + Box::new(relay_asset), + Box::new(usdt_from_asset_hub.clone()), + 1_000_000_000_000, + 2_000_000_000_000, // `usdt_from_asset_hub` is worth half of `relay_asset` + 0, + 0, + PenpalBReceiver::get().into() + )); + + assert_expected_events!( + PenpalB, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + PenpalA::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type ForeignAssets = ::ForeignAssets; + assert_ok!(>::mint_into( + usdt_from_asset_hub.clone(), + &sender, + asset_amount_to_send + fee_amount_to_send, + )); + }); + + // Prepare assets to transfer. + let assets: Assets = + (usdt_from_asset_hub.clone(), asset_amount_to_send + fee_amount_to_send).into(); + // Just to be very specific we're not including anything other than USDT. + assert_eq!(assets.len(), 1); + + // Give the sender enough Relay tokens to pay for local delivery fees. + // TODO(https://github.com/paritytech/polkadot-sdk/issues/5160): When we support local delivery fee payment in other assets, we don't need this. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + RelayLocation::get(), + sender.clone(), + 10_000_000_000_000, // Large estimate to make sure it works. + ); + + // Init values for Parachain Destination + let receiver = PenpalBReceiver::get(); + + // Init Test + let fee_asset_index = 0; + let test_args = TestContext { + sender: sender.clone(), + receiver: receiver.clone(), + args: TestArgs::new_para( + destination, + receiver.clone(), + asset_amount_to_send, + assets, + None, + fee_asset_index, + ), + }; + let mut test = ParaToParaThroughAHTest::new(test_args); + + // Query initial balances + let sender_assets_before = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &sender) + }); + let receiver_assets_before = PenpalB::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &receiver) + }); + test.set_assertion::(para_to_para_through_hop_sender_assertions); + test.set_assertion::(para_to_para_asset_hub_hop_assertions); + test.set_assertion::(para_to_para_through_hop_receiver_assertions); + test.set_dispatchable::( + para_to_para_through_asset_hub_limited_reserve_transfer_assets, + ); + test.assert(); + + // Query final balances + let sender_assets_after = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &sender) + }); + let receiver_assets_after = PenpalB::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub, &receiver) + }); + + // Sender's balance is reduced by amount + assert!(sender_assets_after < sender_assets_before - asset_amount_to_send); + // Receiver's balance is increased + assert!(receiver_assets_after > receiver_assets_before); +} diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs index a887ee6a532a..f568fb4101db 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/lib.rs @@ -64,6 +64,7 @@ mod imports { CustomizableAssetFromSystemAssetHub as PenpalCustomizableAssetFromSystemAssetHub, LocalReservableFromAssetHub as PenpalLocalReservableFromAssetHub, LocalTeleportableToAssetHub as PenpalLocalTeleportableToAssetHub, + UsdtFromAssetHub as PenpalUsdtFromAssetHub, }, PenpalAParaPallet as PenpalAPallet, PenpalAssetOwner, PenpalBParaPallet as PenpalBPallet, diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/hybrid_transfers.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/hybrid_transfers.rs index 49dfe8d58394..975bacea7b4f 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/hybrid_transfers.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/hybrid_transfers.rs @@ -613,10 +613,10 @@ fn transfer_foreign_assets_from_para_to_para_through_asset_hub() { >::balance(roc_at_westend_parachains, &receiver) }); - // Sender's balance is reduced by amount sent plus delivery fees + // Sender's balance is reduced by amount sent. assert!(sender_wnds_after < sender_wnds_before - wnd_to_send); assert_eq!(sender_rocs_after, sender_rocs_before - roc_to_send); - // Sovereign accounts on reserve are changed accordingly + // Sovereign accounts on reserve are changed accordingly. assert_eq!( wnds_in_sender_reserve_on_ah_after, wnds_in_sender_reserve_on_ah_before - wnd_to_send @@ -630,7 +630,7 @@ fn transfer_foreign_assets_from_para_to_para_through_asset_hub() { rocs_in_receiver_reserve_on_ah_after, rocs_in_receiver_reserve_on_ah_before + roc_to_send ); - // Receiver's balance is increased + // Receiver's balance is increased by amount sent minus delivery fees. assert!(receiver_wnds_after > receiver_wnds_before); assert_eq!(receiver_rocs_after, receiver_rocs_before + roc_to_send); } diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reserve_transfer.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reserve_transfer.rs index 59f63d380590..53b6939298da 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reserve_transfer.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/reserve_transfer.rs @@ -60,10 +60,10 @@ pub fn system_para_to_para_sender_assertions(t: SystemParaToParaTest) { AssetHubWestend::assert_xcm_pallet_attempted_complete(None); let sov_acc_of_dest = AssetHubWestend::sovereign_account_id_of(t.args.dest.clone()); - for (idx, asset) in t.args.assets.into_inner().into_iter().enumerate() { + for asset in t.args.assets.into_inner().into_iter() { let expected_id = asset.id.0.clone().try_into().unwrap(); let asset_amount = if let Fungible(a) = asset.fun { Some(a) } else { None }.unwrap(); - if idx == t.args.fee_asset_item as usize { + if asset.id == AssetId(Location::new(1, [])) { assert_expected_events!( AssetHubWestend, vec![ @@ -77,6 +77,23 @@ pub fn system_para_to_para_sender_assertions(t: SystemParaToParaTest) { }, ] ); + } else if matches!( + asset.id.0.unpack(), + (0, [PalletInstance(ASSETS_PALLET_ID), GeneralIndex(_)]) + ) { + assert_expected_events!( + AssetHubWestend, + vec![ + // Amount of trust-backed asset is transferred to Parachain's Sovereign account + RuntimeEvent::Assets( + pallet_assets::Event::Transferred { from, to, amount, .. }, + ) => { + from: *from == t.sender.account_id, + to: *to == sov_acc_of_dest, + amount: *amount == asset_amount, + }, + ] + ); } else { assert_expected_events!( AssetHubWestend, @@ -418,6 +435,38 @@ fn para_to_para_relay_hop_assertions(t: ParaToParaThroughRelayTest) { ); } +fn para_to_para_asset_hub_hop_assertions(t: ParaToParaThroughAHTest) { + type RuntimeEvent = ::RuntimeEvent; + let sov_penpal_a_on_ah = AssetHubWestend::sovereign_account_id_of( + AssetHubWestend::sibling_location_of(PenpalA::para_id()), + ); + let sov_penpal_b_on_ah = AssetHubWestend::sovereign_account_id_of( + AssetHubWestend::sibling_location_of(PenpalB::para_id()), + ); + + assert_expected_events!( + AssetHubWestend, + vec![ + // Withdrawn from sender parachain SA + RuntimeEvent::Assets( + pallet_assets::Event::Burned { owner, balance, .. } + ) => { + owner: *owner == sov_penpal_a_on_ah, + balance: *balance == t.args.amount, + }, + // Deposited to receiver parachain SA + RuntimeEvent::Assets( + pallet_assets::Event::Deposited { who, .. } + ) => { + who: *who == sov_penpal_b_on_ah, + }, + RuntimeEvent::MessageQueue( + pallet_message_queue::Event::Processed { success: true, .. } + ) => {}, + ] + ); +} + pub fn para_to_para_through_hop_receiver_assertions(t: Test) { type RuntimeEvent = ::RuntimeEvent; @@ -493,6 +542,19 @@ fn para_to_para_through_relay_limited_reserve_transfer_assets( ) } +fn para_to_para_through_asset_hub_limited_reserve_transfer_assets( + t: ParaToParaThroughAHTest, +) -> DispatchResult { + ::PolkadotXcm::limited_reserve_transfer_assets( + t.signed_origin, + bx!(t.args.dest.into()), + bx!(t.args.beneficiary.into()), + bx!(t.args.assets.into()), + t.args.fee_asset_item, + t.args.weight_limit, + ) +} + /// Reserve Transfers of native asset from Relay Chain to the Asset Hub shouldn't work #[test] fn reserve_transfer_native_asset_from_relay_to_asset_hub_fails() { @@ -1133,8 +1195,360 @@ fn reserve_transfer_native_asset_from_para_to_para_through_relay() { >::balance(relay_native_asset_location, &receiver) }); - // Sender's balance is reduced by amount sent plus delivery fees + // Sender's balance is reduced by amount sent plus delivery fees. assert!(sender_assets_after < sender_assets_before - amount_to_send); + // Receiver's balance is increased by `amount_to_send` minus delivery fees. + assert!(receiver_assets_after > receiver_assets_before); + assert!(receiver_assets_after < receiver_assets_before + amount_to_send); +} + +// ============================================================================ +// ==== Reserve Transfers USDT - AssetHub->Parachain - pay fees using pool ==== +// ============================================================================ +#[test] +fn reserve_transfer_usdt_from_asset_hub_to_para() { + let usdt_id = 1984u32; + let penpal_location = AssetHubWestend::sibling_location_of(PenpalA::para_id()); + let penpal_sov_account = AssetHubWestend::sovereign_account_id_of(penpal_location.clone()); + + // Create SA-of-Penpal-on-AHW with ED. + // This ED isn't reflected in any derivative in a PenpalA account. + AssetHubWestend::fund_accounts(vec![(penpal_sov_account.clone().into(), ASSET_HUB_WESTEND_ED)]); + + let sender = AssetHubWestendSender::get(); + let receiver = PenpalAReceiver::get(); + let asset_amount_to_send = 1_000_000_000_000; + + AssetHubWestend::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type Assets = ::Assets; + assert_ok!(>::mint_into( + usdt_id.into(), + &AssetHubWestendSender::get(), + asset_amount_to_send + 10_000_000_000_000, // Make sure it has enough. + )); + }); + + let relay_asset_penpal_pov = RelayLocation::get(); + + let usdt_from_asset_hub = PenpalUsdtFromAssetHub::get(); + + // Setup the pool between `relay_asset_penpal_pov` and `usdt_from_asset_hub` on PenpalA. + // So we can swap the custom asset that comes from AssetHubWestend for native asset to pay for + // fees. + PenpalA::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_ok!(::ForeignAssets::mint( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + usdt_from_asset_hub.clone().into(), + PenpalASender::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(PenpalASender::get()), + Box::new(relay_asset_penpal_pov.clone()), + Box::new(usdt_from_asset_hub.clone()), + )); + + assert_expected_events!( + PenpalA, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(PenpalASender::get()), + Box::new(relay_asset_penpal_pov), + Box::new(usdt_from_asset_hub.clone()), + // `usdt_from_asset_hub` is worth a third of `relay_asset_penpal_pov` + 1_000_000_000_000, + 3_000_000_000_000, + 0, + 0, + PenpalASender::get().into() + )); + + assert_expected_events!( + PenpalA, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + let assets: Assets = vec![( + [PalletInstance(ASSETS_PALLET_ID), GeneralIndex(usdt_id.into())], + asset_amount_to_send, + ) + .into()] + .into(); + + let test_args = TestContext { + sender: sender.clone(), + receiver: receiver.clone(), + args: TestArgs::new_para( + penpal_location, + receiver.clone(), + asset_amount_to_send, + assets, + None, + 0, + ), + }; + let mut test = SystemParaToParaTest::new(test_args); + + let sender_initial_balance = AssetHubWestend::execute_with(|| { + type Assets = ::Assets; + >::balance(usdt_id, &sender) + }); + let sender_initial_native_balance = AssetHubWestend::execute_with(|| { + type Balances = ::Balances; + Balances::free_balance(&sender) + }); + let receiver_initial_balance = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &receiver) + }); + + test.set_assertion::(system_para_to_para_sender_assertions); + test.set_assertion::(system_para_to_para_receiver_assertions); + test.set_dispatchable::(system_para_to_para_reserve_transfer_assets); + test.assert(); + + let sender_after_balance = AssetHubWestend::execute_with(|| { + type Assets = ::Assets; + >::balance(usdt_id, &sender) + }); + let sender_after_native_balance = AssetHubWestend::execute_with(|| { + type Balances = ::Balances; + Balances::free_balance(&sender) + }); + let receiver_after_balance = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub, &receiver) + }); + + // TODO(https://github.com/paritytech/polkadot-sdk/issues/5160): When we allow payment with different assets locally, this should be the same, since + // they aren't used for fees. + assert!(sender_after_native_balance < sender_initial_native_balance); + // Sender account's balance decreases. + assert_eq!(sender_after_balance, sender_initial_balance - asset_amount_to_send); + // Receiver account's balance increases. + assert!(receiver_after_balance > receiver_initial_balance); + assert!(receiver_after_balance < receiver_initial_balance + asset_amount_to_send); +} + +// =================================================================================== +// == Reserve Transfers USDT - Parachain->AssetHub->Parachain - pay fees using pool == +// =================================================================================== +// +// Transfer USDT From Penpal A to Penpal B with AssetHub as the reserve, while paying fees using +// USDT by making use of existing USDT pools on AssetHub and destination. +#[test] +fn reserve_transfer_usdt_from_para_to_para_through_asset_hub() { + let destination = PenpalA::sibling_location_of(PenpalB::para_id()); + let sender = PenpalASender::get(); + let asset_amount_to_send: Balance = WESTEND_ED * 10000; + let fee_amount_to_send: Balance = WESTEND_ED * 10000; + let sender_chain_as_seen_by_asset_hub = + AssetHubWestend::sibling_location_of(PenpalA::para_id()); + let sov_of_sender_on_asset_hub = + AssetHubWestend::sovereign_account_id_of(sender_chain_as_seen_by_asset_hub); + let receiver_as_seen_by_asset_hub = AssetHubWestend::sibling_location_of(PenpalB::para_id()); + let sov_of_receiver_on_asset_hub = + AssetHubWestend::sovereign_account_id_of(receiver_as_seen_by_asset_hub); + + // Create SA-of-Penpal-on-AHW with ED. + // This ED isn't reflected in any derivative in a PenpalA account. + AssetHubWestend::fund_accounts(vec![ + (sov_of_sender_on_asset_hub.clone().into(), ASSET_HUB_WESTEND_ED), + (sov_of_receiver_on_asset_hub.clone().into(), ASSET_HUB_WESTEND_ED), + ]); + + // Give USDT to sov account of sender. + let usdt_id = 1984; + AssetHubWestend::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type Assets = ::Assets; + assert_ok!(>::mint_into( + usdt_id.into(), + &sov_of_sender_on_asset_hub.clone().into(), + asset_amount_to_send + fee_amount_to_send, + )); + }); + + // We create a pool between WND and USDT in AssetHub. + let native_asset: Location = Parent.into(); + let usdt = Location::new( + 0, + [Junction::PalletInstance(ASSETS_PALLET_ID), Junction::GeneralIndex(usdt_id.into())], + ); + + // set up pool with USDT <> native pair + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_ok!(::Assets::mint( + ::RuntimeOrigin::signed(AssetHubWestendSender::get()), + usdt_id.into(), + AssetHubWestendSender::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(AssetHubWestendSender::get()), + Box::new(native_asset.clone()), + Box::new(usdt.clone()), + )); + + assert_expected_events!( + AssetHubWestend, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(AssetHubWestendSender::get()), + Box::new(native_asset), + Box::new(usdt), + 1_000_000_000_000, + 2_000_000_000_000, // usdt is worth half of `native_asset` + 0, + 0, + AssetHubWestendSender::get().into() + )); + + assert_expected_events!( + AssetHubWestend, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + let usdt_from_asset_hub = PenpalUsdtFromAssetHub::get(); + + // We also need a pool between WND and USDT on PenpalB. + PenpalB::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let relay_asset = RelayLocation::get(); + + assert_ok!(::ForeignAssets::mint( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + usdt_from_asset_hub.clone().into(), + PenpalBReceiver::get().into(), + 10_000_000_000_000, // For it to have more than enough. + )); + + assert_ok!(::AssetConversion::create_pool( + ::RuntimeOrigin::signed(PenpalBReceiver::get()), + Box::new(relay_asset.clone()), + Box::new(usdt_from_asset_hub.clone()), + )); + + assert_expected_events!( + PenpalB, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + ::RuntimeOrigin::signed(PenpalBReceiver::get()), + Box::new(relay_asset), + Box::new(usdt_from_asset_hub.clone()), + 1_000_000_000_000, + 2_000_000_000_000, // `usdt_from_asset_hub` is worth half of `relay_asset` + 0, + 0, + PenpalBReceiver::get().into() + )); + + assert_expected_events!( + PenpalB, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded { .. }) => {}, + ] + ); + }); + + PenpalA::execute_with(|| { + use frame_support::traits::tokens::fungibles::Mutate; + type ForeignAssets = ::ForeignAssets; + assert_ok!(>::mint_into( + usdt_from_asset_hub.clone(), + &sender, + asset_amount_to_send + fee_amount_to_send, + )); + }); + + // Prepare assets to transfer. + let assets: Assets = + (usdt_from_asset_hub.clone(), asset_amount_to_send + fee_amount_to_send).into(); + // Just to be very specific we're not including anything other than USDT. + assert_eq!(assets.len(), 1); + + // Give the sender enough Relay tokens to pay for local delivery fees. + // TODO(https://github.com/paritytech/polkadot-sdk/issues/5160): When we support local delivery fee payment in other assets, we don't need this. + PenpalA::mint_foreign_asset( + ::RuntimeOrigin::signed(PenpalAssetOwner::get()), + RelayLocation::get(), + sender.clone(), + 10_000_000_000_000, // Large estimate to make sure it works. + ); + + // Init values for Parachain Destination + let receiver = PenpalBReceiver::get(); + + // Init Test + let fee_asset_index = 0; + let test_args = TestContext { + sender: sender.clone(), + receiver: receiver.clone(), + args: TestArgs::new_para( + destination, + receiver.clone(), + asset_amount_to_send, + assets, + None, + fee_asset_index, + ), + }; + let mut test = ParaToParaThroughAHTest::new(test_args); + + // Query initial balances + let sender_assets_before = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &sender) + }); + let receiver_assets_before = PenpalB::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &receiver) + }); + test.set_assertion::(para_to_para_through_hop_sender_assertions); + test.set_assertion::(para_to_para_asset_hub_hop_assertions); + test.set_assertion::(para_to_para_through_hop_receiver_assertions); + test.set_dispatchable::( + para_to_para_through_asset_hub_limited_reserve_transfer_assets, + ); + test.assert(); + + // Query final balances + let sender_assets_after = PenpalA::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub.clone(), &sender) + }); + let receiver_assets_after = PenpalB::execute_with(|| { + type ForeignAssets = ::ForeignAssets; + >::balance(usdt_from_asset_hub, &receiver) + }); + + // Sender's balance is reduced by amount + assert!(sender_assets_after < sender_assets_before - asset_amount_to_send); // Receiver's balance is increased assert!(receiver_assets_after > receiver_assets_before); } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs index 2d1914e059bf..f263baf4bef8 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs @@ -21,7 +21,7 @@ use super::{ XcmpQueue, }; use assets_common::{ - matching::{FromNetwork, FromSiblingParachain, IsForeignConcreteAsset}, + matching::{FromNetwork, FromSiblingParachain, IsForeignConcreteAsset, ParentLocation}, TrustBackedAssetsAsLocation, }; use frame_support::{ @@ -43,7 +43,7 @@ use parachains_common::{ use polkadot_parachain_primitives::primitives::Sibling; use polkadot_runtime_common::xcm_sender::ExponentialPrice; use snowbridge_router_primitives::inbound::GlobalConsensusEthereumConvertsFor; -use sp_runtime::traits::{AccountIdConversion, ConvertInto}; +use sp_runtime::traits::{AccountIdConversion, ConvertInto, TryConvertInto}; use testnet_parachains_constants::rococo::snowbridge::{ EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX, }; @@ -54,12 +54,13 @@ use xcm_builder::{ DenyReserveTransferToRelayChain, DenyThenTry, DescribeAllTerminal, DescribeFamily, EnsureXcmOrigin, FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, GlobalConsensusParachainConvertsFor, HashedDescription, IsConcrete, LocalMint, - NetworkExportTableItem, NoChecking, NonFungiblesAdapter, ParentAsSuperuser, ParentIsPreset, - RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignPaidRemoteExporter, + MatchedConvertedConcreteId, NetworkExportTableItem, NoChecking, NonFungiblesAdapter, + ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, + SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, + SignedToAccountId32, SingleAssetExchangeAdapter, SovereignPaidRemoteExporter, SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, - TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, - XcmFeeManagerFromComponents, + TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, + WithLatestLocationConverter, WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::XcmExecutor; @@ -326,6 +327,28 @@ pub type TrustedTeleporters = ( IsForeignConcreteAsset>>, ); +/// Asset converter for pool assets. +/// Used to convert one asset to another, when there is a pool available between the two. +/// This type thus allows paying fees with any asset as long as there is a pool between said +/// asset and the asset required for fee payment. +pub type PoolAssetsExchanger = SingleAssetExchangeAdapter< + crate::AssetConversion, + crate::NativeAndAssets, + ( + TrustBackedAssetsAsLocation, + ForeignAssetsConvertedConcreteId, + // `ForeignAssetsConvertedConcreteId` excludes the relay token, so we add it back here. + MatchedConvertedConcreteId< + xcm::v4::Location, + Balance, + Equals, + WithLatestLocationConverter, + TryConvertInto, + >, + ), + AccountId, +>; + pub struct XcmConfig; impl xcm_executor::Config for XcmConfig { type RuntimeCall = RuntimeCall; @@ -407,7 +430,7 @@ impl xcm_executor::Config for XcmConfig { type PalletInstancesInfo = AllPalletsWithSystem; type MaxAssetsIntoHolding = MaxAssetsIntoHolding; type AssetLocker = (); - type AssetExchanger = (); + type AssetExchanger = PoolAssetsExchanger; type FeeManager = XcmFeeManagerFromComponents< WaivedLocations, SendXcmFeeToAccount, diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs index d61381d3f50b..bc5d07f552b0 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs @@ -21,7 +21,7 @@ use super::{ XcmpQueue, }; use assets_common::{ - matching::{FromSiblingParachain, IsForeignConcreteAsset}, + matching::{FromSiblingParachain, IsForeignConcreteAsset, ParentLocation}, TrustBackedAssetsAsLocation, }; use frame_support::{ @@ -43,7 +43,7 @@ use parachains_common::{ use polkadot_parachain_primitives::primitives::Sibling; use polkadot_runtime_common::xcm_sender::ExponentialPrice; use snowbridge_router_primitives::inbound::GlobalConsensusEthereumConvertsFor; -use sp_runtime::traits::{AccountIdConversion, ConvertInto}; +use sp_runtime::traits::{AccountIdConversion, ConvertInto, TryConvertInto}; use xcm::latest::prelude::*; use xcm_builder::{ AccountId32Aliases, AllowExplicitUnpaidExecutionFrom, AllowHrmpNotificationsFromRelayChain, @@ -51,12 +51,13 @@ use xcm_builder::{ DenyReserveTransferToRelayChain, DenyThenTry, DescribeFamily, DescribePalletTerminal, EnsureXcmOrigin, FrameTransactionalProcessor, FungibleAdapter, FungiblesAdapter, GlobalConsensusParachainConvertsFor, HashedDescription, IsConcrete, LocalMint, - NetworkExportTableItem, NoChecking, NonFungiblesAdapter, ParentAsSuperuser, ParentIsPreset, - RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, - SignedAccountId32AsNative, SignedToAccountId32, SovereignPaidRemoteExporter, + MatchedConvertedConcreteId, NetworkExportTableItem, NoChecking, NonFungiblesAdapter, + ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, + SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, + SignedToAccountId32, SingleAssetExchangeAdapter, SovereignPaidRemoteExporter, SovereignSignedViaLocation, StartsWith, StartsWithExplicitGlobalConsensus, TakeWeightCredit, - TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, WithUniqueTopic, - XcmFeeManagerFromComponents, + TrailingSetTopicAsId, UsingComponents, WeightInfoBounds, WithComputedOrigin, + WithLatestLocationConverter, WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::XcmExecutor; @@ -350,6 +351,28 @@ pub type TrustedTeleporters = ( IsForeignConcreteAsset>>, ); +/// Asset converter for pool assets. +/// Used to convert one asset to another, when there is a pool available between the two. +/// This type thus allows paying fees with any asset as long as there is a pool between said +/// asset and the asset required for fee payment. +pub type PoolAssetsExchanger = SingleAssetExchangeAdapter< + crate::AssetConversion, + crate::NativeAndAssets, + ( + TrustBackedAssetsAsLocation, + ForeignAssetsConvertedConcreteId, + // `ForeignAssetsConvertedConcreteId` excludes the relay token, so we add it back here. + MatchedConvertedConcreteId< + xcm::v4::Location, + Balance, + Equals, + WithLatestLocationConverter, + TryConvertInto, + >, + ), + AccountId, +>; + pub struct XcmConfig; impl xcm_executor::Config for XcmConfig { type RuntimeCall = RuntimeCall; @@ -430,7 +453,7 @@ impl xcm_executor::Config for XcmConfig { type PalletInstancesInfo = AllPalletsWithSystem; type MaxAssetsIntoHolding = MaxAssetsIntoHolding; type AssetLocker = (); - type AssetExchanger = (); + type AssetExchanger = PoolAssetsExchanger; type FeeManager = XcmFeeManagerFromComponents< WaivedLocations, SendXcmFeeToAccount, diff --git a/cumulus/parachains/runtimes/assets/common/Cargo.toml b/cumulus/parachains/runtimes/assets/common/Cargo.toml index c6740269339d..fb66f0de2322 100644 --- a/cumulus/parachains/runtimes/assets/common/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/common/Cargo.toml @@ -19,6 +19,7 @@ impl-trait-for-tuples = { workspace = true } frame-support = { workspace = true } sp-api = { workspace = true } sp-runtime = { workspace = true } +pallet-assets = { workspace = true } pallet-asset-conversion = { workspace = true } # Polkadot @@ -42,6 +43,7 @@ std = [ "frame-support/std", "log/std", "pallet-asset-conversion/std", + "pallet-assets/std", "pallet-xcm/std", "parachains-common/std", "scale-info/std", @@ -56,6 +58,7 @@ runtime-benchmarks = [ "cumulus-primitives-core/runtime-benchmarks", "frame-support/runtime-benchmarks", "pallet-asset-conversion/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", "pallet-xcm/runtime-benchmarks", "parachains-common/runtime-benchmarks", "sp-runtime/runtime-benchmarks", diff --git a/cumulus/parachains/runtimes/assets/common/src/lib.rs b/cumulus/parachains/runtimes/assets/common/src/lib.rs index 4bb593f98929..deda5fa4ab9c 100644 --- a/cumulus/parachains/runtimes/assets/common/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/common/src/lib.rs @@ -29,7 +29,7 @@ use crate::matching::{LocalLocationPattern, ParentLocation}; use frame_support::traits::{Equals, EverythingBut}; use parachains_common::{AssetIdForTrustBackedAssets, CollectionId, ItemId}; use sp_runtime::traits::TryConvertInto; -use xcm::latest::Location; +use xcm::prelude::*; use xcm_builder::{ AsPrefixedGeneralIndex, MatchedConvertedConcreteId, StartsWith, WithLatestLocationConverter, }; @@ -138,7 +138,6 @@ pub type PoolAssetsConvertedConcreteId = mod tests { use super::*; use sp_runtime::traits::MaybeEquivalence; - use xcm::prelude::*; use xcm_builder::{StartsWithExplicitGlobalConsensus, WithLatestLocationConverter}; use xcm_executor::traits::{Error as MatchError, MatchesFungibles}; diff --git a/cumulus/parachains/runtimes/testing/penpal/Cargo.toml b/cumulus/parachains/runtimes/testing/penpal/Cargo.toml index e16629302be6..96338b645581 100644 --- a/cumulus/parachains/runtimes/testing/penpal/Cargo.toml +++ b/cumulus/parachains/runtimes/testing/penpal/Cargo.toml @@ -42,6 +42,7 @@ pallet-transaction-payment = { workspace = true } pallet-transaction-payment-rpc-runtime-api = { workspace = true } pallet-asset-tx-payment = { workspace = true } pallet-assets = { workspace = true } +pallet-asset-conversion = { workspace = true } sp-api = { workspace = true } sp-block-builder = { workspace = true } sp-consensus-aura = { workspace = true } @@ -79,6 +80,8 @@ parachain-info = { workspace = true } parachains-common = { workspace = true } assets-common = { workspace = true } +primitive-types = { version = "0.12.1", default-features = false, features = ["codec", "num-traits", "scale-info"] } + [features] default = ["std"] std = [ @@ -99,6 +102,7 @@ std = [ "frame-system/std", "frame-try-runtime?/std", "log/std", + "pallet-asset-conversion/std", "pallet-asset-tx-payment/std", "pallet-assets/std", "pallet-aura/std", @@ -117,6 +121,7 @@ std = [ "polkadot-parachain-primitives/std", "polkadot-primitives/std", "polkadot-runtime-common/std", + "primitive-types/std", "scale-info/std", "sp-api/std", "sp-block-builder/std", @@ -149,6 +154,7 @@ runtime-benchmarks = [ "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", "hex-literal", + "pallet-asset-conversion/runtime-benchmarks", "pallet-asset-tx-payment/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", @@ -176,6 +182,7 @@ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "frame-try-runtime/try-runtime", + "pallet-asset-conversion/try-runtime", "pallet-asset-tx-payment/try-runtime", "pallet-assets/try-runtime", "pallet-aura/try-runtime", diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs index bf39c02a3f59..7d19c0ed8d85 100644 --- a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs @@ -35,6 +35,10 @@ pub mod xcm_config; extern crate alloc; use alloc::{vec, vec::Vec}; +use assets_common::{ + local_and_foreign_assets::{LocalFromLeft, TargetFromLeft}, + AssetIdForTrustBackedAssetsConvert, +}; use codec::Encode; use cumulus_pallet_parachain_system::RelayNumberStrictlyIncreases; use cumulus_primitives_core::{AggregateMessageOrigin, ParaId}; @@ -42,10 +46,13 @@ use frame_support::{ construct_runtime, derive_impl, dispatch::DispatchClass, genesis_builder_helper::{build_state, get_preset}, + ord_parameter_types, pallet_prelude::Weight, parameter_types, traits::{ - AsEnsureOriginWithArg, ConstBool, ConstU32, ConstU64, ConstU8, Everything, TransformOrigin, + tokens::{fungible, fungibles, imbalance::ResolveAssetTo}, + AsEnsureOriginWithArg, ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, Everything, + TransformOrigin, }, weights::{ constants::WEIGHT_REF_TIME_PER_SECOND, ConstantMultiplier, FeePolynomial, WeightToFee as _, @@ -55,7 +62,7 @@ use frame_support::{ }; use frame_system::{ limits::{BlockLength, BlockWeights}, - EnsureRoot, EnsureSigned, + EnsureRoot, EnsureSigned, EnsureSignedBy, }; use parachains_common::{ impls::{AssetsToBlockAuthor, NonZeroIssuance}, @@ -67,7 +74,7 @@ pub use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, - traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, Dispatchable}, + traits::{AccountIdConversion, AccountIdLookup, BlakeTwo256, Block as BlockT, Dispatchable}, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, }; @@ -442,7 +449,9 @@ parameter_types! { // pub type AssetsForceOrigin = // EnsureOneOf, EnsureXcm>>; -impl pallet_assets::Config for Runtime { +pub type TrustBackedAssetsInstance = pallet_assets::Instance1; + +impl pallet_assets::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; type AssetId = AssetId; @@ -500,6 +509,106 @@ impl pallet_assets::Config for Runtime { type BenchmarkHelper = xcm_config::XcmBenchmarkHelper; } +parameter_types! { + pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon"); + pub const LiquidityWithdrawalFee: Permill = Permill::from_percent(0); +} + +ord_parameter_types! { + pub const AssetConversionOrigin: sp_runtime::AccountId32 = + AccountIdConversion::::into_account_truncating(&AssetConversionPalletId::get()); +} + +pub type AssetsForceOrigin = EnsureRoot; + +pub type PoolAssetsInstance = pallet_assets::Instance3; +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type RemoveItemsLimit = ConstU32<1000>; + type AssetId = u32; + type AssetIdParameter = u32; + type Currency = Balances; + type CreateOrigin = + AsEnsureOriginWithArg>; + type ForceOrigin = AssetsForceOrigin; + type AssetDeposit = ConstU128<0>; + type AssetAccountDeposit = ConstU128<0>; + type MetadataDepositBase = ConstU128<0>; + type MetadataDepositPerByte = ConstU128<0>; + type ApprovalDeposit = ConstU128<0>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type Extra = (); + type WeightInfo = pallet_assets::weights::SubstrateWeight; + type CallbackHandle = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +/// Union fungibles implementation for `Assets` and `ForeignAssets`. +pub type LocalAndForeignAssets = fungibles::UnionOf< + Assets, + ForeignAssets, + LocalFromLeft< + AssetIdForTrustBackedAssetsConvert< + xcm_config::TrustBackedAssetsPalletLocation, + xcm::latest::Location, + >, + parachains_common::AssetIdForTrustBackedAssets, + xcm::latest::Location, + >, + xcm::latest::Location, + AccountId, +>; + +/// Union fungibles implementation for [`LocalAndForeignAssets`] and `Balances`. +pub type NativeAndAssets = fungible::UnionOf< + Balances, + LocalAndForeignAssets, + TargetFromLeft, + xcm::latest::Location, + AccountId, +>; + +pub type PoolIdToAccountId = pallet_asset_conversion::AccountIdConverter< + AssetConversionPalletId, + (xcm::latest::Location, xcm::latest::Location), +>; + +impl pallet_asset_conversion::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type HigherPrecisionBalance = sp_core::U256; + type AssetKind = xcm::latest::Location; + type Assets = NativeAndAssets; + type PoolId = (Self::AssetKind, Self::AssetKind); + type PoolLocator = pallet_asset_conversion::WithFirstAsset< + xcm_config::RelayLocation, + AccountId, + Self::AssetKind, + PoolIdToAccountId, + >; + type PoolAssetId = u32; + type PoolAssets = PoolAssets; + type PoolSetupFee = ConstU128<0>; // Asset class deposit fees are sufficient to prevent spam + type PoolSetupFeeAsset = xcm_config::RelayLocation; + type PoolSetupFeeTarget = ResolveAssetTo; + type LiquidityWithdrawalFee = LiquidityWithdrawalFee; + type LPFee = ConstU32<3>; + type PalletId = AssetConversionPalletId; + type MaxSwapPathLength = ConstU32<3>; + type MintMinLiquidity = ConstU128<100>; + type WeightInfo = (); + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = assets_common::benchmarks::AssetPairFactory< + xcm_config::RelayLocation, + parachain_info::Pallet, + xcm_config::TrustBackedAssetsPalletIndex, + xcm::latest::Location, + >; +} + parameter_types! { pub const ReservedXcmpWeight: Weight = MAXIMUM_BLOCK_WEIGHT.saturating_div(4); pub const ReservedDmpWeight: Weight = MAXIMUM_BLOCK_WEIGHT.saturating_div(4); @@ -642,9 +751,9 @@ impl pallet_asset_tx_payment::Config for Runtime { Balances, Runtime, ConvertInto, - pallet_assets::Instance1, + TrustBackedAssetsInstance, >, - AssetsToBlockAuthor, + AssetsToBlockAuthor, >; } @@ -685,6 +794,8 @@ construct_runtime!( // The main stage. Assets: pallet_assets:: = 50, ForeignAssets: pallet_assets:: = 51, + PoolAssets: pallet_assets:: = 52, + AssetConversion: pallet_asset_conversion = 53, Sudo: pallet_sudo = 255, } diff --git a/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs b/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs index d0c421bccaf8..99aadb33b840 100644 --- a/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs @@ -24,15 +24,19 @@ //! soon. use super::{ AccountId, AllPalletsWithSystem, AssetId as AssetIdPalletAssets, Assets, Authorship, Balance, - Balances, ForeignAssets, ForeignAssetsInstance, NonZeroIssuance, ParachainInfo, - ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, WeightToFee, - XcmpQueue, + Balances, CollatorSelection, ForeignAssets, ForeignAssetsInstance, NonZeroIssuance, + ParachainInfo, ParachainSystem, PolkadotXcm, Runtime, RuntimeCall, RuntimeEvent, RuntimeOrigin, + WeightToFee, XcmpQueue, }; use crate::{BaseDeliveryFee, FeeAssetId, TransactionByteFee}; +use assets_common::TrustBackedAssetsAsLocation; use core::marker::PhantomData; use frame_support::{ parameter_types, - traits::{ConstU32, Contains, ContainsPair, Everything, EverythingBut, Get, Nothing}, + traits::{ + tokens::imbalance::ResolveAssetTo, ConstU32, Contains, ContainsPair, Everything, + EverythingBut, Get, Nothing, PalletInfoAccess, + }, weights::Weight, }; use frame_system::EnsureRoot; @@ -49,15 +53,15 @@ use xcm_builder::{ FungibleAdapter, FungiblesAdapter, IsConcrete, LocalMint, NativeAsset, NoChecking, ParentAsSuperuser, ParentIsPreset, RelayChainAsNative, SendXcmFeeToAccount, SiblingParachainAsNative, SiblingParachainConvertsVia, SignedAccountId32AsNative, - SignedToAccountId32, SovereignSignedViaLocation, StartsWith, TakeWeightCredit, - TrailingSetTopicAsId, UsingComponents, WithComputedOrigin, WithUniqueTopic, + SignedToAccountId32, SingleAssetExchangeAdapter, SovereignSignedViaLocation, StartsWith, + TakeWeightCredit, TrailingSetTopicAsId, UsingComponents, WithComputedOrigin, WithUniqueTopic, XcmFeeManagerFromComponents, }; use xcm_executor::{traits::JustTry, XcmExecutor}; parameter_types! { pub const RelayLocation: Location = Location::parent(); - // Local native currency which is stored in `pallet_balances`` + // Local native currency which is stored in `pallet_balances` pub const PenpalNativeCurrency: Location = Location::here(); // The Penpal runtime is utilized for testing with various environment setups. // This storage item allows us to customize the `NetworkId` where Penpal is deployed. @@ -70,6 +74,10 @@ parameter_types! { Parachain(ParachainInfo::parachain_id().into()) ].into(); pub TreasuryAccount: AccountId = TREASURY_PALLET_ID.into_account_truncating(); + pub StakingPot: AccountId = CollatorSelection::account_id(); + pub TrustBackedAssetsPalletIndex: u8 = ::index() as u8; + pub TrustBackedAssetsPalletLocation: Location = + PalletInstance(TrustBackedAssetsPalletIndex::get()).into(); } /// Type for specifying how a `Location` can be converted into an `AccountId`. This is used @@ -265,6 +273,8 @@ pub const TELEPORTABLE_ASSET_ID: u32 = 2; pub const ASSETS_PALLET_ID: u8 = 50; pub const ASSET_HUB_ID: u32 = 1000; +pub const USDT_ASSET_ID: u128 = 1984; + parameter_types! { /// The location that this chain recognizes as the Relay network's Asset Hub. pub SystemAssetHubLocation: Location = Location::new(1, [Parachain(ASSET_HUB_ID)]); @@ -284,7 +294,7 @@ parameter_types! { ); pub UsdtFromAssetHub: Location = Location::new( 1, - [Parachain(ASSET_HUB_ID), PalletInstance(ASSETS_PALLET_ID), GeneralIndex(1984)] + [Parachain(ASSET_HUB_ID), PalletInstance(ASSETS_PALLET_ID), GeneralIndex(USDT_ASSET_ID)], ); /// The Penpal runtime is utilized for testing with various environment setups. @@ -316,6 +326,28 @@ pub type TrustedReserves = ( pub type TrustedTeleporters = (AssetFromChain,); +/// `AssetId`/`Balance` converter for `TrustBackedAssets`. +pub type TrustBackedAssetsConvertedConcreteId = + assets_common::TrustBackedAssetsConvertedConcreteId; + +/// Asset converter for pool assets. +/// Used to convert assets in pools to the asset required for fee payment. +/// The pool must be between the first asset and the one required for fee payment. +/// This type allows paying fees with any asset in a pool with the asset required for fee payment. +pub type PoolAssetsExchanger = SingleAssetExchangeAdapter< + crate::AssetConversion, + crate::NativeAndAssets, + ( + TrustBackedAssetsAsLocation< + TrustBackedAssetsPalletLocation, + Balance, + xcm::latest::Location, + >, + ForeignAssetsConvertedConcreteId, + ), + AccountId, +>; + pub struct XcmConfig; impl xcm_executor::Config for XcmConfig { type RuntimeCall = RuntimeCall; @@ -331,18 +363,21 @@ impl xcm_executor::Config for XcmConfig { type Weigher = FixedWeightBounds; type Trader = ( UsingComponents>, - // This trader allows to pay with `is_sufficient=true` "Foreign" assets from dedicated - // `pallet_assets` instance - `ForeignAssets`. - cumulus_primitives_utility::TakeFirstAssetTrader< + cumulus_primitives_utility::SwapFirstAssetTrader< + RelayLocation, + crate::AssetConversion, + WeightToFee, + crate::NativeAndAssets, + ( + TrustBackedAssetsAsLocation< + TrustBackedAssetsPalletLocation, + Balance, + xcm::latest::Location, + >, + ForeignAssetsConvertedConcreteId, + ), + ResolveAssetTo, AccountId, - ForeignAssetFeeAsExistentialDepositMultiplierFeeCharger, - ForeignAssetsConvertedConcreteId, - ForeignAssets, - cumulus_primitives_utility::XcmFeesTo32ByteAccount< - ForeignFungiblesTransactor, - AccountId, - XcmAssetFeesReceiver, - >, >, ); type ResponseHandler = PolkadotXcm; @@ -352,7 +387,7 @@ impl xcm_executor::Config for XcmConfig { type PalletInstancesInfo = AllPalletsWithSystem; type MaxAssetsIntoHolding = MaxAssetsIntoHolding; type AssetLocker = (); - type AssetExchanger = (); + type AssetExchanger = PoolAssetsExchanger; type FeeManager = XcmFeeManagerFromComponents< (), SendXcmFeeToAccount, diff --git a/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs b/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs index 41418bcc511f..0b03f1127ee8 100644 --- a/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs +++ b/polkadot/node/core/approval-voting/benches/approval-voting-regression-bench.rs @@ -82,8 +82,8 @@ fn main() -> Result<(), String> { ("Sent to peers", 63995.2200, 0.01), ])); messages.extend(average_usage.check_cpu_usage(&[ - ("approval-distribution", 6.3912, 0.1), - ("approval-voting", 10.0578, 0.1), + ("approval-distribution", 12.2736, 0.1), + ("approval-voting", 2.7174, 0.1), ])); if messages.is_empty() { diff --git a/polkadot/node/core/approval-voting/src/approval_checking.rs b/polkadot/node/core/approval-voting/src/approval_checking.rs index 96eb25626de8..3774edc69981 100644 --- a/polkadot/node/core/approval-voting/src/approval_checking.rs +++ b/polkadot/node/core/approval-voting/src/approval_checking.rs @@ -22,9 +22,9 @@ use polkadot_primitives::ValidatorIndex; use crate::{ persisted_entries::{ApprovalEntry, CandidateEntry, TrancheEntry}, - time::Tick, MAX_RECORDED_NO_SHOW_VALIDATORS_PER_CANDIDATE, }; +use polkadot_node_primitives::approval::time::Tick; /// Result of counting the necessary tranches needed for approving a block. #[derive(Debug, PartialEq, Clone)] @@ -1195,9 +1195,9 @@ mod tests { struct NoShowTest { assignments: Vec<(ValidatorIndex, Tick)>, approvals: Vec, - clock_drift: crate::time::Tick, - no_show_duration: crate::time::Tick, - drifted_tick_now: crate::time::Tick, + clock_drift: Tick, + no_show_duration: Tick, + drifted_tick_now: Tick, exp_no_shows: usize, exp_next_no_show: Option, } diff --git a/polkadot/node/core/approval-voting/src/criteria.rs b/polkadot/node/core/approval-voting/src/criteria.rs index fb9d281e43bc..669b6001538e 100644 --- a/polkadot/node/core/approval-voting/src/criteria.rs +++ b/polkadot/node/core/approval-voting/src/criteria.rs @@ -16,8 +16,11 @@ //! Assignment criteria VRF generation and checking. -use codec::{Decode, Encode}; +use codec::Encode; use itertools::Itertools; +pub use polkadot_node_primitives::approval::criteria::{ + AssignmentCriteria, Config, InvalidAssignment, InvalidAssignmentReason, OurAssignment, +}; use polkadot_node_primitives::approval::{ self as approval_types, v1::{AssignmentCert, AssignmentCertKind, DelayTranche, RelayVRFStory}, @@ -25,9 +28,9 @@ use polkadot_node_primitives::approval::{ AssignmentCertKindV2, AssignmentCertV2, CoreBitfield, VrfPreOutput, VrfProof, VrfSignature, }, }; + use polkadot_primitives::{ - AssignmentId, AssignmentPair, CandidateHash, CoreIndex, GroupIndex, IndexedVec, SessionInfo, - ValidatorIndex, + AssignmentPair, CandidateHash, CoreIndex, GroupIndex, IndexedVec, ValidatorIndex, }; use rand::{seq::SliceRandom, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -44,56 +47,19 @@ use std::{ use super::LOG_TARGET; -/// Details pertaining to our assignment on a block. -#[derive(Debug, Clone, Encode, Decode, PartialEq)] -pub struct OurAssignment { - cert: AssignmentCertV2, - tranche: DelayTranche, - validator_index: ValidatorIndex, - // Whether the assignment has been triggered already. - triggered: bool, -} - -impl OurAssignment { - pub fn cert(&self) -> &AssignmentCertV2 { - &self.cert - } - - pub fn tranche(&self) -> DelayTranche { - self.tranche - } - - pub(crate) fn validator_index(&self) -> ValidatorIndex { - self.validator_index - } - - pub(crate) fn triggered(&self) -> bool { - self.triggered - } - - pub(crate) fn mark_triggered(&mut self) { - self.triggered = true; - } -} - impl From for OurAssignment { fn from(entry: crate::approval_db::v2::OurAssignment) -> Self { - OurAssignment { - cert: entry.cert, - tranche: entry.tranche, - validator_index: entry.validator_index, - triggered: entry.triggered, - } + OurAssignment::new(entry.cert, entry.tranche, entry.validator_index, entry.triggered) } } impl From for crate::approval_db::v2::OurAssignment { fn from(entry: OurAssignment) -> Self { Self { - cert: entry.cert, - tranche: entry.tranche, - validator_index: entry.validator_index, - triggered: entry.triggered, + tranche: entry.tranche(), + validator_index: entry.validator_index(), + triggered: entry.triggered(), + cert: entry.into_cert(), } } } @@ -223,60 +189,7 @@ fn assigned_core_transcript(core_index: CoreIndex) -> Transcript { t } -/// Information about the world assignments are being produced in. -#[derive(Clone, Debug)] -pub struct Config { - /// The assignment public keys for validators. - assignment_keys: Vec, - /// The groups of validators assigned to each core. - validator_groups: IndexedVec>, - /// The number of availability cores used by the protocol during this session. - n_cores: u32, - /// The zeroth delay tranche width. - zeroth_delay_tranche_width: u32, - /// The number of samples we do of `relay_vrf_modulo`. - relay_vrf_modulo_samples: u32, - /// The number of delay tranches in total. - n_delay_tranches: u32, -} - -impl<'a> From<&'a SessionInfo> for Config { - fn from(s: &'a SessionInfo) -> Self { - Config { - assignment_keys: s.assignment_keys.clone(), - validator_groups: s.validator_groups.clone(), - n_cores: s.n_cores, - zeroth_delay_tranche_width: s.zeroth_delay_tranche_width, - relay_vrf_modulo_samples: s.relay_vrf_modulo_samples, - n_delay_tranches: s.n_delay_tranches, - } - } -} - -/// A trait for producing and checking assignments. Used to mock. -pub(crate) trait AssignmentCriteria { - fn compute_assignments( - &self, - keystore: &LocalKeystore, - relay_vrf_story: RelayVRFStory, - config: &Config, - leaving_cores: Vec<(CandidateHash, CoreIndex, GroupIndex)>, - enable_v2_assignments: bool, - ) -> HashMap; - - fn check_assignment_cert( - &self, - claimed_core_bitfield: CoreBitfield, - validator_index: ValidatorIndex, - config: &Config, - relay_vrf_story: RelayVRFStory, - assignment: &AssignmentCertV2, - // Backing groups for each "leaving core". - backing_groups: Vec, - ) -> Result; -} - -pub(crate) struct RealAssignmentCriteria; +pub struct RealAssignmentCriteria; impl AssignmentCriteria for RealAssignmentCriteria { fn compute_assignments( @@ -469,12 +382,12 @@ fn compute_relay_vrf_modulo_assignments_v1( }; // All assignments of type RelayVRFModulo have tranche 0. - assignments.entry(core).or_insert(OurAssignment { - cert: cert.into(), - tranche: 0, + assignments.entry(core).or_insert(OurAssignment::new( + cert.into(), + 0, validator_index, - triggered: false, - }); + false, + )); } } } @@ -549,7 +462,7 @@ fn compute_relay_vrf_modulo_assignments_v2( }; // All assignments of type RelayVRFModulo have tranche 0. - OurAssignment { cert, tranche: 0, validator_index, triggered: false } + OurAssignment::new(cert, 0, validator_index, false) }) { for core_index in assigned_cores { assignments.insert(core_index, assignment.clone()); @@ -583,7 +496,7 @@ fn compute_relay_vrf_delay_assignments( }, }; - let our_assignment = OurAssignment { cert, tranche, validator_index, triggered: false }; + let our_assignment = OurAssignment::new(cert, tranche, validator_index, false); let used = match assignments.entry(core) { Entry::Vacant(e) => { @@ -591,7 +504,7 @@ fn compute_relay_vrf_delay_assignments( true }, Entry::Occupied(mut e) => - if e.get().tranche > our_assignment.tranche { + if e.get().tranche() > our_assignment.tranche() { e.insert(our_assignment); true } else { @@ -612,35 +525,6 @@ fn compute_relay_vrf_delay_assignments( } } -/// Assignment invalid. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct InvalidAssignment(pub(crate) InvalidAssignmentReason); - -impl std::fmt::Display for InvalidAssignment { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "Invalid Assignment: {:?}", self.0) - } -} - -impl std::error::Error for InvalidAssignment {} - -/// Failure conditions when checking an assignment cert. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum InvalidAssignmentReason { - ValidatorIndexOutOfBounds, - SampleOutOfBounds, - CoreIndexOutOfBounds, - InvalidAssignmentKey, - IsInBackingGroup, - VRFModuloCoreIndexMismatch, - VRFModuloOutputMismatch, - VRFDelayCoreIndexMismatch, - VRFDelayOutputMismatch, - InvalidArguments, - /// Assignment vrf check resulted in 0 assigned cores. - NullAssignment, -} - /// Checks the crypto of an assignment cert. Failure conditions: /// * Validator index out of bounds /// * VRF signature check fails @@ -820,13 +704,13 @@ fn is_in_backing_group( /// Migration helpers. impl From for OurAssignment { fn from(value: crate::approval_db::v1::OurAssignment) -> Self { - Self { - cert: value.cert.into(), - tranche: value.tranche, - validator_index: value.validator_index, + Self::new( + value.cert.into(), + value.tranche, + value.validator_index, // Whether the assignment has been triggered already. - triggered: value.triggered, - } + value.triggered, + ) } } @@ -834,7 +718,7 @@ impl From for OurAssignment { mod tests { use super::*; use crate::import::tests::garbage_vrf_signature; - use polkadot_primitives::{Hash, ASSIGNMENT_KEY_TYPE_ID}; + use polkadot_primitives::{AssignmentId, Hash, ASSIGNMENT_KEY_TYPE_ID}; use sp_application_crypto::sr25519; use sp_core::crypto::Pair as PairT; use sp_keyring::sr25519::Keyring as Sr25519Keyring; @@ -1053,7 +937,7 @@ mod tests { let mut counted = 0; for (core, assignment) in assignments { - let cores = match assignment.cert.kind.clone() { + let cores = match assignment.cert().kind.clone() { AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } => core_bitfield, AssignmentCertKindV2::RelayVRFModulo { sample: _ } => core.into(), AssignmentCertKindV2::RelayVRFDelay { core_index } => core_index.into(), @@ -1062,7 +946,7 @@ mod tests { let mut mutated = MutatedAssignment { cores: cores.clone(), groups: cores.iter_ones().map(|core| group_for_core(core)).collect(), - cert: assignment.cert, + cert: assignment.into_cert(), own_group: GroupIndex(0), val_index: ValidatorIndex(0), config: config.clone(), diff --git a/polkadot/node/core/approval-voting/src/import.rs b/polkadot/node/core/approval-voting/src/import.rs index 3ddef1e01c45..b163d718eb25 100644 --- a/polkadot/node/core/approval-voting/src/import.rs +++ b/polkadot/node/core/approval-voting/src/import.rs @@ -62,9 +62,10 @@ use crate::{ criteria::{AssignmentCriteria, OurAssignment}, get_extended_session_info, get_session_info, persisted_entries::CandidateEntry, - time::{slot_number_to_tick, Tick}, }; +use polkadot_node_primitives::approval::time::{slot_number_to_tick, Tick}; + use super::{State, LOG_TARGET}; #[derive(Debug)] @@ -574,9 +575,13 @@ pub(crate) async fn handle_new_head( hash: block_hash, number: block_header.number, parent_hash: block_header.parent_hash, - candidates: included_candidates.iter().map(|(hash, _, _, _)| *hash).collect(), + candidates: included_candidates + .iter() + .map(|(hash, _, core_index, group_index)| (*hash, *core_index, *group_index)) + .collect(), slot, session: session_index, + vrf_story: relay_vrf_story, }); imported_candidates.push(BlockImportedCandidates { @@ -609,6 +614,7 @@ pub(crate) mod tests { approval_db::common::{load_block_entry, DbBackend}, RuntimeInfo, RuntimeInfoConfig, MAX_BLOCKS_WITH_ASSIGNMENT_TIMESTAMPS, }; + use approval_types::time::Clock; use assert_matches::assert_matches; use polkadot_node_primitives::{ approval::v1::{VrfSignature, VrfTranscript}, @@ -642,7 +648,7 @@ pub(crate) mod tests { #[derive(Default)] struct MockClock; - impl crate::time::Clock for MockClock { + impl Clock for MockClock { fn tick_now(&self) -> Tick { 42 // chosen by fair dice roll } diff --git a/polkadot/node/core/approval-voting/src/lib.rs b/polkadot/node/core/approval-voting/src/lib.rs index d4b6855a44d0..942922cba6df 100644 --- a/polkadot/node/core/approval-voting/src/lib.rs +++ b/polkadot/node/core/approval-voting/src/lib.rs @@ -40,8 +40,9 @@ use polkadot_node_subsystem::{ ApprovalCheckError, ApprovalCheckResult, ApprovalDistributionMessage, ApprovalVotingMessage, AssignmentCheckError, AssignmentCheckResult, AvailabilityRecoveryMessage, BlockDescription, CandidateValidationMessage, ChainApiMessage, - ChainSelectionMessage, DisputeCoordinatorMessage, HighestApprovedAncestorBlock, - RuntimeApiMessage, RuntimeApiRequest, + ChainSelectionMessage, CheckedIndirectAssignment, CheckedIndirectSignedApprovalVote, + DisputeCoordinatorMessage, HighestApprovedAncestorBlock, RuntimeApiMessage, + RuntimeApiRequest, }, overseer, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, SubsystemResult, SubsystemSender, @@ -55,9 +56,8 @@ use polkadot_node_subsystem_util::{ }; use polkadot_primitives::{ ApprovalVoteMultipleCandidates, ApprovalVotingParams, BlockNumber, CandidateHash, - CandidateIndex, CandidateReceipt, CoreIndex, DisputeStatement, ExecutorParams, GroupIndex, - Hash, PvfExecKind, SessionIndex, SessionInfo, ValidDisputeStatementKind, ValidatorId, - ValidatorIndex, ValidatorPair, ValidatorSignature, + CandidateIndex, CandidateReceipt, CoreIndex, ExecutorParams, GroupIndex, Hash, PvfExecKind, + SessionIndex, SessionInfo, ValidatorId, ValidatorIndex, ValidatorPair, ValidatorSignature, }; use sc_keystore::LocalKeystore; use sp_application_crypto::Pair; @@ -91,9 +91,11 @@ use schnellru::{ByLength, LruMap}; use approval_checking::RequiredTranches; use bitvec::{order::Lsb0, vec::BitVec}; -use criteria::{AssignmentCriteria, RealAssignmentCriteria}; +pub use criteria::{AssignmentCriteria, Config as AssignmentConfig, RealAssignmentCriteria}; use persisted_entries::{ApprovalEntry, BlockEntry, CandidateEntry}; -use time::{slot_number_to_tick, Clock, ClockExt, DelayedApprovalTimer, SystemClock, Tick}; +use polkadot_node_primitives::approval::time::{ + slot_number_to_tick, Clock, ClockExt, DelayedApprovalTimer, SystemClock, Tick, +}; mod approval_checking; pub mod approval_db; @@ -102,7 +104,6 @@ pub mod criteria; mod import; mod ops; mod persisted_entries; -pub mod time; use crate::{ approval_checking::{Check, TranchesToApproveResult}, @@ -123,7 +124,6 @@ const APPROVAL_CHECKING_TIMEOUT: Duration = Duration::from_secs(120); const WAIT_FOR_SIGS_TIMEOUT: Duration = Duration::from_millis(500); const APPROVAL_CACHE_SIZE: u32 = 1024; -const TICK_TOO_FAR_IN_FUTURE: Tick = 20; // 10 seconds. const APPROVAL_DELAY: Tick = 2; pub(crate) const LOG_TARGET: &str = "parachain::approval-voting"; @@ -1607,9 +1607,30 @@ async fn distribution_messages_for_activation( hash: block_hash, number: block_entry.block_number(), parent_hash: block_entry.parent_hash(), - candidates: block_entry.candidates().iter().map(|(_, c_hash)| *c_hash).collect(), + candidates: block_entry + .candidates() + .iter() + .map(|(core_index, c_hash)| { + let candidate = db.load_candidate_entry(c_hash).ok().flatten(); + let group_index = candidate + .and_then(|entry| { + entry.approval_entry(&block_hash).map(|entry| entry.backing_group()) + }) + .unwrap_or_else(|| { + gum::warn!( + target: LOG_TARGET, + ?block_hash, + ?c_hash, + "Missing candidate entry or approval entry", + ); + GroupIndex::default() + }); + (*c_hash, *core_index, group_index) + }) + .collect(), slot: block_entry.slot(), session: block_entry.session(), + vrf_story: block_entry.relay_vrf_story(), }); let mut signatures_queued = HashSet::new(); for (core_index, candidate_hash) in block_entry.candidates() { @@ -1872,35 +1893,45 @@ async fn handle_from_overseer( vec![Action::Conclude] }, FromOrchestra::Communication { msg } => match msg { - ApprovalVotingMessage::CheckAndImportAssignment(a, claimed_cores, res) => { - let (check_outcome, actions) = check_and_import_assignment( + ApprovalVotingMessage::ImportAssignment(checked_assignment, tx) => { + let (check_outcome, actions) = import_assignment( ctx.sender(), state, db, session_info_provider, - a, - claimed_cores, + checked_assignment, ) .await?; - let _ = res.send(check_outcome); - + // approval-distribution makes sure this assignment is valid and expected, + // so this import should never fail, if it does it might mean one of two things, + // there is a bug in the code or the two subsystems got out of sync. + if let AssignmentCheckResult::Bad(ref err) = check_outcome { + gum::debug!(target: LOG_TARGET, ?err, "Unexpected fail when importing an assignment"); + } + let _ = tx.map(|tx| tx.send(check_outcome)); actions }, - ApprovalVotingMessage::CheckAndImportApproval(a, res) => - check_and_import_approval( + ApprovalVotingMessage::ImportApproval(a, tx) => { + let result = import_approval( ctx.sender(), state, db, session_info_provider, metrics, a, - |r| { - let _ = res.send(r); - }, &wakeups, ) - .await? - .0, + .await?; + // approval-distribution makes sure this vote is valid and expected, + // so this import should never fail, if it does it might mean one of two things, + // there is a bug in the code or the two subsystems got out of sync. + if let ApprovalCheckResult::Bad(ref err) = result.1 { + gum::debug!(target: LOG_TARGET, ?err, "Unexpected fail when importing an approval"); + } + let _ = tx.map(|tx| tx.send(result.1)); + + result.0 + }, ApprovalVotingMessage::ApprovedAncestor(target, lower_bound, res) => { let mut approved_ancestor_span = state .spans @@ -2439,29 +2470,30 @@ fn schedule_wakeup_action( maybe_action } -async fn check_and_import_assignment( +async fn import_assignment( sender: &mut Sender, state: &State, db: &mut OverlayedBackend<'_, impl Backend>, session_info_provider: &mut RuntimeInfo, - assignment: IndirectAssignmentCertV2, - candidate_indices: CandidateBitfield, + checked_assignment: CheckedIndirectAssignment, ) -> SubsystemResult<(AssignmentCheckResult, Vec)> where Sender: SubsystemSender, { let tick_now = state.clock.tick_now(); - - let mut check_and_import_assignment_span = state + let assignment = checked_assignment.assignment(); + let candidate_indices = checked_assignment.candidate_indices(); + let tranche = checked_assignment.tranche(); + let mut import_assignment_span = state .spans .get(&assignment.block_hash) - .map(|span| span.child("check-and-import-assignment")) - .unwrap_or_else(|| jaeger::Span::new(assignment.block_hash, "check-and-import-assignment")) + .map(|span| span.child("import-assignment")) + .unwrap_or_else(|| jaeger::Span::new(assignment.block_hash, "import-assignment")) .with_relay_parent(assignment.block_hash) .with_stage(jaeger::Stage::ApprovalChecking); for candidate_index in candidate_indices.iter_ones() { - check_and_import_assignment_span.add_uint_tag("candidate-index", candidate_index as u64); + import_assignment_span.add_uint_tag("candidate-index", candidate_index as u64); } let block_entry = match db.load_block_entry(&assignment.block_hash)? { @@ -2514,8 +2546,6 @@ where )) } - // The Compact VRF modulo assignment cert has multiple core assignments. - let mut backing_groups = Vec::new(); let mut claimed_core_indices = Vec::new(); let mut assigned_candidate_hashes = Vec::new(); @@ -2544,26 +2574,23 @@ where )), // no candidate at core. }; - check_and_import_assignment_span + import_assignment_span .add_string_tag("candidate-hash", format!("{:?}", assigned_candidate_hash)); - check_and_import_assignment_span.add_string_tag( + import_assignment_span.add_string_tag( "traceID", format!("{:?}", jaeger::hash_to_trace_identifier(assigned_candidate_hash.0)), ); - let approval_entry = match candidate_entry.approval_entry_mut(&assignment.block_hash) { - Some(a) => a, - None => - return Ok(( - AssignmentCheckResult::Bad(AssignmentCheckError::Internal( - assignment.block_hash, - assigned_candidate_hash, - )), - Vec::new(), + if candidate_entry.approval_entry_mut(&assignment.block_hash).is_none() { + return Ok(( + AssignmentCheckResult::Bad(AssignmentCheckError::Internal( + assignment.block_hash, + assigned_candidate_hash, )), + Vec::new(), + )); }; - backing_groups.push(approval_entry.backing_group()); claimed_core_indices.push(claimed_core_index); assigned_candidate_hashes.push(assigned_candidate_hash); } @@ -2579,42 +2606,6 @@ where )) } - // Check the assignment certificate. - let res = state.assignment_criteria.check_assignment_cert( - claimed_core_indices - .clone() - .try_into() - .expect("Checked for null assignment above; qed"), - assignment.validator, - &criteria::Config::from(session_info), - block_entry.relay_vrf_story(), - &assignment.cert, - backing_groups, - ); - - let tranche = match res { - Err(crate::criteria::InvalidAssignment(reason)) => - return Ok(( - AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCert( - assignment.validator, - format!("{:?}", reason), - )), - Vec::new(), - )), - Ok(tranche) => { - let current_tranche = - state.clock.tranche_now(state.slot_duration_millis, block_entry.slot()); - - let too_far_in_future = current_tranche + TICK_TOO_FAR_IN_FUTURE as DelayTranche; - - if tranche >= too_far_in_future { - return Ok((AssignmentCheckResult::TooFarInFuture, Vec::new())) - } - - tranche - }, - }; - let mut actions = Vec::new(); let res = { let mut is_duplicate = true; @@ -2647,7 +2638,7 @@ where }; is_duplicate &= approval_entry.is_assigned(assignment.validator); approval_entry.import_assignment(tranche, assignment.validator, tick_now); - check_and_import_assignment_span.add_uint_tag("tranche", tranche as u64); + import_assignment_span.add_uint_tag("tranche", tranche as u64); // We've imported a new assignment, so we need to schedule a wake-up for when that might // no-show. @@ -2704,30 +2695,28 @@ where Ok((res, actions)) } -async fn check_and_import_approval( +async fn import_approval( sender: &mut Sender, state: &mut State, db: &mut OverlayedBackend<'_, impl Backend>, session_info_provider: &mut RuntimeInfo, metrics: &Metrics, - approval: IndirectSignedApprovalVoteV2, - with_response: impl FnOnce(ApprovalCheckResult) -> T, + approval: CheckedIndirectSignedApprovalVote, wakeups: &Wakeups, -) -> SubsystemResult<(Vec, T)> +) -> SubsystemResult<(Vec, ApprovalCheckResult)> where Sender: SubsystemSender, { macro_rules! respond_early { ($e: expr) => {{ - let t = with_response($e); - return Ok((Vec::new(), t)) + return Ok((Vec::new(), $e)) }}; } let mut span = state .spans .get(&approval.block_hash) - .map(|span| span.child("check-and-import-approval")) - .unwrap_or_else(|| jaeger::Span::new(approval.block_hash, "check-and-import-approval")) + .map(|span| span.child("import-approval")) + .unwrap_or_else(|| jaeger::Span::new(approval.block_hash, "import-approval")) .with_string_fmt_debug_tag("candidate-index", approval.candidate_indices.clone()) .with_relay_parent(approval.block_hash) .with_stage(jaeger::Stage::ApprovalChecking); @@ -2774,67 +2763,11 @@ where ), ); - { - let session_info = match get_session_info( - session_info_provider, - sender, - approval.block_hash, - block_entry.session(), - ) - .await - { - Some(s) => s, - None => { - respond_early!(ApprovalCheckResult::Bad(ApprovalCheckError::UnknownSessionIndex( - block_entry.session() - ),)) - }, - }; - - let pubkey = match session_info.validators.get(approval.validator) { - Some(k) => k, - None => respond_early!(ApprovalCheckResult::Bad( - ApprovalCheckError::InvalidValidatorIndex(approval.validator), - )), - }; - - gum::trace!( - target: LOG_TARGET, - "Received approval for num_candidates {:}", - approval.candidate_indices.count_ones() - ); - - let candidate_hashes: Vec = - approved_candidates_info.iter().map(|candidate| candidate.1).collect(); - // Signature check: - match DisputeStatement::Valid( - ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates(candidate_hashes.clone()), - ) - .check_signature( - &pubkey, - if let Some(candidate_hash) = candidate_hashes.first() { - *candidate_hash - } else { - respond_early!(ApprovalCheckResult::Bad(ApprovalCheckError::InvalidValidatorIndex( - approval.validator - ),)) - }, - block_entry.session(), - &approval.signature, - ) { - Err(_) => { - gum::error!( - target: LOG_TARGET, - "Error while checking signature {:}", - approval.candidate_indices.count_ones() - ); - respond_early!(ApprovalCheckResult::Bad(ApprovalCheckError::InvalidSignature( - approval.validator - ),)) - }, - Ok(()) => {}, - }; - } + gum::trace!( + target: LOG_TARGET, + "Received approval for num_candidates {:}", + approval.candidate_indices.count_ones() + ); let mut actions = Vec::new(); for (approval_candidate_index, approved_candidate_hash) in approved_candidates_info { @@ -2898,9 +2831,7 @@ where } // importing the approval can be heavy as it may trigger acceptance for a series of blocks. - let t = with_response(ApprovalCheckResult::Accepted); - - Ok((actions, t)) + Ok((actions, ApprovalCheckResult::Accepted)) } #[derive(Debug)] diff --git a/polkadot/node/core/approval-voting/src/persisted_entries.rs b/polkadot/node/core/approval-voting/src/persisted_entries.rs index 59a461810051..16e231aa1a2d 100644 --- a/polkadot/node/core/approval-voting/src/persisted_entries.rs +++ b/polkadot/node/core/approval-voting/src/persisted_entries.rs @@ -36,7 +36,9 @@ use std::collections::BTreeMap; use crate::approval_db::v2::Bitfield; -use super::{criteria::OurAssignment, time::Tick}; +use super::criteria::OurAssignment; + +use polkadot_node_primitives::approval::time::Tick; /// Metadata regarding a specific tranche of assignments for a specific candidate. #[derive(Debug, Clone, PartialEq)] diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs index b912449baa4d..7126f209a94f 100644 --- a/polkadot/node/core/approval-voting/src/tests.rs +++ b/polkadot/node/core/approval-voting/src/tests.rs @@ -41,8 +41,9 @@ use polkadot_node_subsystem_test_helpers as test_helpers; use polkadot_node_subsystem_util::TimeoutExt; use polkadot_overseer::HeadSupportsParachains; use polkadot_primitives::{ - ApprovalVote, CandidateCommitments, CandidateEvent, CoreIndex, GroupIndex, Header, - Id as ParaId, IndexedVec, NodeFeatures, ValidationCode, ValidatorSignature, + ApprovalVote, CandidateCommitments, CandidateEvent, CoreIndex, DisputeStatement, GroupIndex, + Header, Id as ParaId, IndexedVec, NodeFeatures, ValidDisputeStatementKind, ValidationCode, + ValidatorSignature, }; use std::{cmp::max, time::Duration}; @@ -139,8 +140,8 @@ impl HeadSupportsParachains for MockSupportsParachains { } } -fn slot_to_tick(t: impl Into) -> crate::time::Tick { - crate::time::slot_number_to_tick(SLOT_DURATION_MILLIS, t.into()) +fn slot_to_tick(t: impl Into) -> Tick { + slot_number_to_tick(SLOT_DURATION_MILLIS, t.into()) } #[derive(Default, Clone)] @@ -647,7 +648,7 @@ fn make_candidate(para_id: ParaId, hash: &Hash) -> CandidateReceipt { r } -async fn check_and_import_approval( +async fn import_approval( overseer: &mut VirtualOverseer, block_hash: Hash, candidate_index: CandidateIndex, @@ -666,14 +667,14 @@ async fn check_and_import_approval( overseer_send( overseer, FromOrchestra::Communication { - msg: ApprovalVotingMessage::CheckAndImportApproval( - IndirectSignedApprovalVoteV2 { + msg: ApprovalVotingMessage::ImportApproval( + CheckedIndirectSignedApprovalVote::from_checked(IndirectSignedApprovalVoteV2 { block_hash, candidate_indices: candidate_index.into(), validator, signature, - }, - tx, + }), + Some(tx), ), }, ) @@ -689,25 +690,31 @@ async fn check_and_import_approval( rx } -async fn check_and_import_assignment( +async fn import_assignment( overseer: &mut VirtualOverseer, block_hash: Hash, candidate_index: CandidateIndex, validator: ValidatorIndex, + tranche: DelayTranche, ) -> oneshot::Receiver { let (tx, rx) = oneshot::channel(); overseer_send( overseer, FromOrchestra::Communication { - msg: ApprovalVotingMessage::CheckAndImportAssignment( - IndirectAssignmentCertV2 { - block_hash, - validator, - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }) + msg: ApprovalVotingMessage::ImportAssignment( + CheckedIndirectAssignment::from_checked( + IndirectAssignmentCertV2 { + block_hash, + validator, + cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { + sample: 0, + }) .into(), - }, - candidate_index.into(), - tx, + }, + candidate_index.into(), + tranche, + ), + Some(tx), ), }, ) @@ -715,7 +722,7 @@ async fn check_and_import_assignment( rx } -async fn check_and_import_assignment_v2( +async fn import_assignment_v2( overseer: &mut VirtualOverseer, block_hash: Hash, core_indices: Vec, @@ -725,22 +732,27 @@ async fn check_and_import_assignment_v2( overseer_send( overseer, FromOrchestra::Communication { - msg: ApprovalVotingMessage::CheckAndImportAssignment( - IndirectAssignmentCertV2 { - block_hash, - validator, - cert: garbage_assignment_cert_v2(AssignmentCertKindV2::RelayVRFModuloCompact { - core_bitfield: core_indices - .clone() - .into_iter() - .map(|c| CoreIndex(c)) - .collect::>() - .try_into() - .unwrap(), - }), - }, - core_indices.try_into().unwrap(), - tx, + msg: ApprovalVotingMessage::ImportAssignment( + CheckedIndirectAssignment::from_checked( + IndirectAssignmentCertV2 { + block_hash, + validator, + cert: garbage_assignment_cert_v2( + AssignmentCertKindV2::RelayVRFModuloCompact { + core_bitfield: core_indices + .clone() + .into_iter() + .map(|c| CoreIndex(c)) + .collect::>() + .try_into() + .unwrap(), + }, + ), + }, + core_indices.try_into().unwrap(), + 0, + ), + Some(tx), ), }, ) @@ -1121,26 +1133,18 @@ fn subsystem_rejects_bad_assignment_ok_criteria() { ); builder.build(&mut virtual_overseer).await; - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; + let rx = + import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0) + .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),); // unknown hash let unknown_hash = Hash::repeat_byte(0x02); - let rx = check_and_import_assignment( - &mut virtual_overseer, - unknown_hash, - candidate_index, - validator, - ) - .await; + let rx = + import_assignment(&mut virtual_overseer, unknown_hash, candidate_index, validator, 0) + .await; assert_eq!( rx.await, @@ -1151,59 +1155,6 @@ fn subsystem_rejects_bad_assignment_ok_criteria() { }); } -#[test] -fn subsystem_rejects_bad_assignment_err_criteria() { - let assignment_criteria = Box::new(MockAssignmentCriteria::check_only(move |_| { - Err(criteria::InvalidAssignment( - criteria::InvalidAssignmentReason::ValidatorIndexOutOfBounds, - )) - })); - let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build(); - test_harness(config, |test_harness| async move { - let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } = - test_harness; - assert_matches!( - overseer_recv(&mut virtual_overseer).await, - AllMessages::ChainApi(ChainApiMessage::FinalizedBlockNumber(rx)) => { - rx.send(Ok(0)).unwrap(); - } - ); - - let block_hash = Hash::repeat_byte(0x01); - let candidate_index = 0; - let validator = ValidatorIndex(0); - - let head: Hash = ChainBuilder::GENESIS_HASH; - let mut builder = ChainBuilder::new(); - let slot = Slot::from(1 as u64); - builder.add_block( - block_hash, - head, - 1, - BlockConfig { slot, candidates: None, session_info: None, end_syncing: false }, - ); - builder.build(&mut virtual_overseer).await; - - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; - - assert_eq!( - rx.await, - Ok(AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCert( - ValidatorIndex(0), - "ValidatorIndexOutOfBounds".to_string(), - ))), - ); - - virtual_overseer - }); -} - #[test] fn blank_subsystem_act_on_bad_block() { test_harness(HarnessConfig::default(), |test_harness| async move { @@ -1222,17 +1173,20 @@ fn blank_subsystem_act_on_bad_block() { overseer_send( &mut virtual_overseer, FromOrchestra::Communication { - msg: ApprovalVotingMessage::CheckAndImportAssignment( - IndirectAssignmentCertV2 { - block_hash: bad_block_hash, - validator: 0u32.into(), - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { - sample: 0, - }) - .into(), - }, - 0u32.into(), - tx, + msg: ApprovalVotingMessage::ImportAssignment( + CheckedIndirectAssignment::from_checked( + IndirectAssignmentCertV2 { + block_hash: bad_block_hash, + validator: 0u32.into(), + cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { + sample: 0, + }) + .into(), + }, + 0u32.into(), + 0, + ), + Some(tx), ), }, ) @@ -1295,7 +1249,7 @@ fn subsystem_rejects_approval_if_no_candidate_entry() { }); let session_index = 1; - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -1336,7 +1290,7 @@ fn subsystem_rejects_approval_if_no_block_entry() { let candidate_hash = dummy_candidate_receipt(block_hash).hash(); let session_index = 1; - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -1401,7 +1355,7 @@ fn subsystem_rejects_approval_before_assignment() { .build(&mut virtual_overseer) .await; - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -1424,68 +1378,6 @@ fn subsystem_rejects_approval_before_assignment() { }); } -#[test] -fn subsystem_rejects_assignment_in_future() { - let assignment_criteria = - Box::new(MockAssignmentCriteria::check_only(|_| Ok(TICK_TOO_FAR_IN_FUTURE as _))); - let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build(); - test_harness(config, |test_harness| async move { - let TestHarness { mut virtual_overseer, clock, sync_oracle_handle: _sync_oracle_handle } = - test_harness; - assert_matches!( - overseer_recv(&mut virtual_overseer).await, - AllMessages::ChainApi(ChainApiMessage::FinalizedBlockNumber(rx)) => { - rx.send(Ok(0)).unwrap(); - } - ); - - let block_hash = Hash::repeat_byte(0x01); - let candidate_index = 0; - let validator = ValidatorIndex(0); - - // Add block hash 00. - ChainBuilder::new() - .add_block( - block_hash, - ChainBuilder::GENESIS_HASH, - 1, - BlockConfig { - slot: Slot::from(0), - candidates: None, - session_info: None, - end_syncing: false, - }, - ) - .build(&mut virtual_overseer) - .await; - - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; - - assert_eq!(rx.await, Ok(AssignmentCheckResult::TooFarInFuture)); - - // Advance clock to make assignment reasonably near. - clock.inner.lock().set_tick(9); - - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; - - assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); - - virtual_overseer - }); -} - #[test] fn subsystem_accepts_duplicate_assignment() { test_harness(HarnessConfig::default(), |test_harness| async move { @@ -1535,7 +1427,7 @@ fn subsystem_accepts_duplicate_assignment() { .await; // Initial assignment. - let rx = check_and_import_assignment_v2( + let rx = import_assignment_v2( &mut virtual_overseer, block_hash, vec![candidate_index1, candidate_index2], @@ -1546,19 +1438,15 @@ fn subsystem_accepts_duplicate_assignment() { assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); // Test with single assigned core. - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; + let rx = + import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0) + .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::AcceptedDuplicate)); // Test with multiple assigned cores. This cannot happen in practice, as tranche0 // assignments are sent first, but we should still ensure correct behavior. - let rx = check_and_import_assignment_v2( + let rx = import_assignment_v2( &mut virtual_overseer, block_hash, vec![candidate_index1, candidate_index2], @@ -1604,13 +1492,9 @@ fn subsystem_rejects_assignment_with_unknown_candidate() { .build(&mut virtual_overseer) .await; - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; + let rx = + import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0) + .await; assert_eq!( rx.await, @@ -1654,13 +1538,9 @@ fn subsystem_rejects_oversized_bitfields() { .build(&mut virtual_overseer) .await; - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; + let rx = + import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0) + .await; assert_eq!( rx.await, @@ -1669,13 +1549,9 @@ fn subsystem_rejects_oversized_bitfields() { ))), ); - let rx = check_and_import_assignment_v2( - &mut virtual_overseer, - block_hash, - vec![1, 2, 10, 50], - validator, - ) - .await; + let rx = + import_assignment_v2(&mut virtual_overseer, block_hash, vec![1, 2, 10, 50], validator) + .await; assert_eq!( rx.await, @@ -1727,17 +1603,13 @@ fn subsystem_accepts_and_imports_approval_after_assignment() { .build(&mut virtual_overseer) .await; - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; + let rx = + import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0) + .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -1819,19 +1691,15 @@ fn subsystem_second_approval_import_only_schedules_wakeups() { .build(&mut virtual_overseer) .await; - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; + let rx = + import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0) + .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); assert!(clock.inner.lock().current_wakeup_is(APPROVAL_DELAY + 2)); - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -1848,7 +1716,7 @@ fn subsystem_second_approval_import_only_schedules_wakeups() { futures_timer::Delay::new(Duration::from_millis(100)).await; assert!(clock.inner.lock().current_wakeup_is(APPROVAL_DELAY + 2)); - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -1907,13 +1775,9 @@ fn subsystem_assignment_import_updates_candidate_entry_and_schedules_wakeup() { .build(&mut virtual_overseer) .await; - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; + let rx = + import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0) + .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); @@ -2029,20 +1893,17 @@ fn test_approvals_on_fork_are_always_considered_after_no_show( .await; // Send assignments for the same candidate on both forks - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; + let rx = + import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0) + .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); - let rx = check_and_import_assignment( + let rx = import_assignment( &mut virtual_overseer, block_hash_fork, candidate_index, validator, + 0, ) .await; @@ -2072,7 +1933,7 @@ fn test_approvals_on_fork_are_always_considered_after_no_show( futures_timer::Delay::new(Duration::from_millis(100)).await; // Send the approval for candidate just in the context of 0x01 block. - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -2142,13 +2003,9 @@ fn subsystem_process_wakeup_schedules_wakeup() { .build(&mut virtual_overseer) .await; - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; + let rx = + import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0) + .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); @@ -2201,17 +2058,20 @@ fn linear_import_act_on_leaf() { overseer_send( &mut virtual_overseer, FromOrchestra::Communication { - msg: ApprovalVotingMessage::CheckAndImportAssignment( - IndirectAssignmentCertV2 { - block_hash: head, - validator: 0u32.into(), - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { - sample: 0, - }) - .into(), - }, - 0u32.into(), - tx, + msg: ApprovalVotingMessage::ImportAssignment( + CheckedIndirectAssignment::from_checked( + IndirectAssignmentCertV2 { + block_hash: head, + validator: 0u32.into(), + cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { + sample: 0, + }) + .into(), + }, + 0u32.into(), + 0, + ), + Some(tx), ), }, ) @@ -2272,17 +2132,20 @@ fn forkful_import_at_same_height_act_on_leaf() { overseer_send( &mut virtual_overseer, FromOrchestra::Communication { - msg: ApprovalVotingMessage::CheckAndImportAssignment( - IndirectAssignmentCertV2 { - block_hash: head, - validator: 0u32.into(), - cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { - sample: 0, - }) - .into(), - }, - 0u32.into(), - tx, + msg: ApprovalVotingMessage::ImportAssignment( + CheckedIndirectAssignment::from_checked( + IndirectAssignmentCertV2 { + block_hash: head, + validator: 0u32.into(), + cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { + sample: 0, + }) + .into(), + }, + 0u32.into(), + 0, + ), + Some(tx), ), }, ) @@ -2439,21 +2302,23 @@ fn import_checked_approval_updates_entries_and_schedules() { let candidate_index = 0; - let rx = check_and_import_assignment( + let rx = import_assignment( &mut virtual_overseer, block_hash, candidate_index, validator_index_a, + 0, ) .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),); - let rx = check_and_import_assignment( + let rx = import_assignment( &mut virtual_overseer, block_hash, candidate_index, validator_index_b, + 0, ) .await; @@ -2462,7 +2327,7 @@ fn import_checked_approval_updates_entries_and_schedules() { let session_index = 1; let sig_a = sign_approval(Sr25519Keyring::Alice, candidate_hash, session_index); - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -2489,7 +2354,7 @@ fn import_checked_approval_updates_entries_and_schedules() { clock.inner.lock().wakeup_all(2); let sig_b = sign_approval(Sr25519Keyring::Bob, candidate_hash, session_index); - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -2602,13 +2467,9 @@ fn subsystem_import_checked_approval_sets_one_block_bit_at_a_time() { ]; for (candidate_index, validator) in assignments { - let rx = check_and_import_assignment( - &mut virtual_overseer, - block_hash, - candidate_index, - validator, - ) - .await; + let rx = + import_assignment(&mut virtual_overseer, block_hash, candidate_index, validator, 0) + .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); } @@ -2629,7 +2490,7 @@ fn subsystem_import_checked_approval_sets_one_block_bit_at_a_time() { } else { sign_approval(Sr25519Keyring::Bob, *candidate_hash, session_index) }; - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, *candidate_index, @@ -2887,11 +2748,12 @@ fn approved_ancestor_test( for (i, (block_hash, candidate_hash)) in block_hashes.iter().zip(candidate_hashes).enumerate() { - let rx = check_and_import_assignment( + let rx = import_assignment( &mut virtual_overseer, *block_hash, candidate_index, validator, + 0, ) .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); @@ -2900,7 +2762,7 @@ fn approved_ancestor_test( continue } - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, *block_hash, candidate_index, @@ -3355,7 +3217,8 @@ where F1: 'static + Fn(ValidatorIndex) -> Result + Send - + Sync, + + Sync + + Clone, F2: Fn(Tick) -> bool, { let TriggersAssignmentConfig { @@ -3384,7 +3247,7 @@ where ); assignments }, - assign_validator_tranche, + assign_validator_tranche.clone(), )); let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build(); let store = config.backend(); @@ -3445,11 +3308,12 @@ where .await; for validator in assignments_to_import { - let rx = check_and_import_assignment( + let rx = import_assignment( &mut virtual_overseer, block_hash, candidate_index, ValidatorIndex(validator), + assign_validator_tranche(ValidatorIndex(validator)).unwrap(), ) .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted)); @@ -3458,7 +3322,7 @@ where let n_validators = validators.len(); for (i, &validator_index) in approvals_to_import.iter().enumerate() { let expect_chain_approved = 3 * (i + 1) > n_validators; - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -3763,31 +3627,34 @@ fn pre_covers_dont_stall_approval() { let candidate_index = 0; - let rx = check_and_import_assignment( + let rx = import_assignment( &mut virtual_overseer, block_hash, candidate_index, validator_index_a, + 0, ) .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),); - let rx = check_and_import_assignment( + let rx = import_assignment( &mut virtual_overseer, block_hash, candidate_index, validator_index_b, + 0, ) .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),); - let rx = check_and_import_assignment( + let rx = import_assignment( &mut virtual_overseer, block_hash, candidate_index, validator_index_c, + 1, ) .await; @@ -3796,7 +3663,7 @@ fn pre_covers_dont_stall_approval() { let session_index = 1; let sig_b = sign_approval(Sr25519Keyring::Bob, candidate_hash, session_index); - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -3811,7 +3678,7 @@ fn pre_covers_dont_stall_approval() { assert_eq!(rx.await, Ok(ApprovalCheckResult::Accepted),); let sig_c = sign_approval(Sr25519Keyring::Charlie, candidate_hash, session_index); - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -3941,21 +3808,23 @@ fn waits_until_approving_assignments_are_old_enough() { let candidate_index = 0; - let rx = check_and_import_assignment( + let rx = import_assignment( &mut virtual_overseer, block_hash, candidate_index, validator_index_a, + 0, ) .await; assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted),); - let rx = check_and_import_assignment( + let rx = import_assignment( &mut virtual_overseer, block_hash, candidate_index, validator_index_b, + 0, ) .await; @@ -3966,7 +3835,7 @@ fn waits_until_approving_assignments_are_old_enough() { let session_index = 1; let sig_a = sign_approval(Sr25519Keyring::Alice, candidate_hash, session_index); - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -3982,7 +3851,7 @@ fn waits_until_approving_assignments_are_old_enough() { let sig_b = sign_approval(Sr25519Keyring::Bob, candidate_hash, session_index); - let rx = check_and_import_approval( + let rx = import_approval( &mut virtual_overseer, block_hash, candidate_index, @@ -4992,7 +4861,6 @@ fn subsystem_sends_pending_approvals_on_approval_restart() { })); } ); - assert_matches!( overseer_recv(&mut virtual_overseer).await, AllMessages::RuntimeApi( diff --git a/polkadot/node/network/approval-distribution/Cargo.toml b/polkadot/node/network/approval-distribution/Cargo.toml index 1bd3d51b5c93..51478dfa4a4f 100644 --- a/polkadot/node/network/approval-distribution/Cargo.toml +++ b/polkadot/node/network/approval-distribution/Cargo.toml @@ -26,6 +26,8 @@ gum = { workspace = true, default-features = true } bitvec = { features = ["alloc"], workspace = true } [dev-dependencies] +sc-keystore = { workspace = true } +sp-application-crypto = { workspace = true, default-features = true } sp-authority-discovery = { workspace = true, default-features = true } sp-core = { features = ["std"], workspace = true, default-features = true } diff --git a/polkadot/node/network/approval-distribution/src/lib.rs b/polkadot/node/network/approval-distribution/src/lib.rs index a1bdc47e9fb2..971b6de5f8f6 100644 --- a/polkadot/node/network/approval-distribution/src/lib.rs +++ b/polkadot/node/network/approval-distribution/src/lib.rs @@ -24,7 +24,7 @@ #![warn(missing_docs)] use self::metrics::Metrics; -use futures::{channel::oneshot, select, FutureExt as _}; +use futures::{select, FutureExt as _}; use itertools::Itertools; use net_protocol::peer_set::{ProtocolVersion, ValidationVersion}; use polkadot_node_jaeger as jaeger; @@ -35,29 +35,41 @@ use polkadot_node_network_protocol::{ v1 as protocol_v1, v2 as protocol_v2, v3 as protocol_v3, PeerId, UnifiedReputationChange as Rep, Versioned, View, }; -use polkadot_node_primitives::approval::{ - v1::{ - AssignmentCertKind, BlockApprovalMeta, IndirectAssignmentCert, IndirectSignedApprovalVote, - }, - v2::{ - AsBitIndex, AssignmentCertKindV2, CandidateBitfield, IndirectAssignmentCertV2, - IndirectSignedApprovalVoteV2, +use polkadot_node_primitives::{ + approval::{ + criteria::{AssignmentCriteria, InvalidAssignment}, + time::{Clock, ClockExt, SystemClock, TICK_TOO_FAR_IN_FUTURE}, + v1::{ + AssignmentCertKind, BlockApprovalMeta, DelayTranche, IndirectAssignmentCert, + IndirectSignedApprovalVote, RelayVRFStory, + }, + v2::{ + AsBitIndex, AssignmentCertKindV2, CandidateBitfield, IndirectAssignmentCertV2, + IndirectSignedApprovalVoteV2, + }, }, + DISPUTE_WINDOW, }; use polkadot_node_subsystem::{ messages::{ - ApprovalCheckResult, ApprovalDistributionMessage, ApprovalVotingMessage, - AssignmentCheckResult, NetworkBridgeEvent, NetworkBridgeTxMessage, + ApprovalDistributionMessage, ApprovalVotingMessage, CheckedIndirectAssignment, + CheckedIndirectSignedApprovalVote, NetworkBridgeEvent, NetworkBridgeTxMessage, + RuntimeApiMessage, }, overseer, FromOrchestra, OverseerSignal, SpawnedSubsystem, SubsystemError, }; -use polkadot_node_subsystem_util::reputation::{ReputationAggregator, REPUTATION_CHANGE_INTERVAL}; +use polkadot_node_subsystem_util::{ + reputation::{ReputationAggregator, REPUTATION_CHANGE_INTERVAL}, + runtime::{Config as RuntimeInfoConfig, ExtendedSessionInfo, RuntimeInfo}, +}; use polkadot_primitives::{ - BlockNumber, CandidateIndex, Hash, SessionIndex, ValidatorIndex, ValidatorSignature, + BlockNumber, CandidateHash, CandidateIndex, CoreIndex, DisputeStatement, GroupIndex, Hash, + SessionIndex, Slot, ValidDisputeStatementKind, ValidatorIndex, ValidatorSignature, }; use rand::{CryptoRng, Rng, SeedableRng}; use std::{ collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}, + sync::Arc, time::Duration, }; @@ -86,6 +98,9 @@ const MAX_BITFIELD_SIZE: usize = 500; /// The Approval Distribution subsystem. pub struct ApprovalDistribution { metrics: Metrics, + slot_duration_millis: u64, + clock: Box, + assignment_criteria: Arc, } /// Contains recently finalized @@ -354,6 +369,9 @@ pub struct State { /// Aggregated reputation change reputation: ReputationAggregator, + + /// Slot duration in millis + slot_duration_millis: u64, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -488,11 +506,17 @@ struct BlockEntry { knowledge: Knowledge, /// A votes entry for each candidate indexed by [`CandidateIndex`]. candidates: Vec, + /// Information about candidate metadata. + candidates_metadata: Vec<(CandidateHash, CoreIndex, GroupIndex)>, /// The session index of this block. session: SessionIndex, /// Approval entries for whole block. These also contain all approvals in the case of multiple /// candidates being claimed by assignments. approval_entries: HashMap<(ValidatorIndex, CandidateBitfield), ApprovalEntry>, + /// The block vrf story. + vrf_story: RelayVRFStory, + /// The block slot. + slot: Slot, } impl BlockEntry { @@ -646,6 +670,41 @@ enum MessageSource { Local, } +// Encountered error while validating an assignment. +#[derive(Debug)] +enum InvalidAssignmentError { + // The vrf check for the assignment failed. + #[allow(dead_code)] + CryptoCheckFailed(InvalidAssignment), + // The assignment did not claim any valid candidate. + NoClaimedCandidates, + // Claimed invalid candidate. + #[allow(dead_code)] + ClaimedInvalidCandidateIndex { + claimed_index: usize, + max_index: usize, + }, + // The assignment claimes more candidates than the maximum allowed. + OversizedClaimedBitfield, + // `SessionInfo` was not found for the block hash in the assignment. + #[allow(dead_code)] + SessionInfoNotFound(polkadot_node_subsystem_util::runtime::Error), +} + +// Encountered error while validating an approval. +#[derive(Debug)] +enum InvalidVoteError { + // The candidate index was out of bounds. + CandidateIndexOutOfBounds, + // The validator index was out of bounds. + ValidatorIndexOutOfBounds, + // The signature of the vote was invalid. + InvalidSignature, + // `SessionInfo` was not found for the block hash in the approval. + #[allow(dead_code)] + SessionInfoNotFound(polkadot_node_subsystem_util::runtime::Error), +} + impl MessageSource { fn peer_id(&self) -> Option { match self { @@ -662,16 +721,26 @@ enum PendingMessage { #[overseer::contextbounds(ApprovalDistribution, prefix = self::overseer)] impl State { + /// Build State with specified slot duration. + pub fn with_config(slot_duration_millis: u64) -> Self { + Self { slot_duration_millis, ..Default::default() } + } + async fn handle_network_msg< N: overseer::SubsystemSender, A: overseer::SubsystemSender, + RA: overseer::SubsystemSender, >( &mut self, approval_voting_sender: &mut A, network_sender: &mut N, + runtime_api_sender: &mut RA, metrics: &Metrics, event: NetworkBridgeEvent, rng: &mut (impl CryptoRng + Rng), + assignment_criteria: &(impl AssignmentCriteria + ?Sized), + clock: &(impl Clock + ?Sized), + session_info_provider: &mut RuntimeInfo, ) { match event { NetworkBridgeEvent::PeerConnected(peer_id, role, version, authority_ids) => { @@ -727,10 +796,14 @@ impl State { self.process_incoming_peer_message( approval_voting_sender, network_sender, + runtime_api_sender, metrics, peer_id, message, rng, + assignment_criteria, + clock, + session_info_provider, ) .await; }, @@ -776,16 +849,28 @@ impl State { async fn handle_new_blocks< N: overseer::SubsystemSender, A: overseer::SubsystemSender, + RA: overseer::SubsystemSender, >( &mut self, approval_voting_sender: &mut A, network_sender: &mut N, + runtime_api_sender: &mut RA, metrics: &Metrics, metas: Vec, rng: &mut (impl CryptoRng + Rng), + assignment_criteria: &(impl AssignmentCriteria + ?Sized), + clock: &(impl Clock + ?Sized), + session_info_provider: &mut RuntimeInfo, ) { let mut new_hashes = HashSet::new(); - for meta in &metas { + + gum::debug!( + target: LOG_TARGET, + "Got new blocks {:?}", + metas.iter().map(|m| (m.hash, m.number)).collect::>(), + ); + + for meta in metas { let mut span = self .spans .get(&meta.hash) @@ -809,6 +894,9 @@ impl State { candidates, session: meta.session, approval_entries: HashMap::new(), + candidates_metadata: meta.candidates, + vrf_story: meta.vrf_story, + slot: meta.slot, }); self.topologies.inc_session_refs(meta.session); @@ -823,12 +911,6 @@ impl State { } } - gum::debug!( - target: LOG_TARGET, - "Got new blocks {:?}", - metas.iter().map(|m| (m.hash, m.number)).collect::>(), - ); - { for (peer_id, PeerEntry { view, version }) in self.peer_views.iter() { let intersection = view.iter().filter(|h| new_hashes.contains(h)); @@ -883,11 +965,15 @@ impl State { self.import_and_circulate_assignment( approval_voting_sender, network_sender, + runtime_api_sender, metrics, MessageSource::Peer(peer_id), assignment, claimed_indices, rng, + assignment_criteria, + clock, + session_info_provider, ) .await; }, @@ -895,9 +981,11 @@ impl State { self.import_and_circulate_approval( approval_voting_sender, network_sender, + runtime_api_sender, metrics, MessageSource::Peer(peer_id), approval_vote, + session_info_provider, ) .await; }, @@ -943,17 +1031,22 @@ impl State { .await; } - async fn process_incoming_assignments( + async fn process_incoming_assignments( &mut self, approval_voting_sender: &mut A, network_sender: &mut N, + runtime_api_sender: &mut RA, metrics: &Metrics, peer_id: PeerId, assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)>, rng: &mut R, + assignment_criteria: &(impl AssignmentCriteria + ?Sized), + clock: &(impl Clock + ?Sized), + session_info_provider: &mut RuntimeInfo, ) where A: overseer::SubsystemSender, N: overseer::SubsystemSender, + RA: overseer::SubsystemSender, R: CryptoRng + Rng, { for (assignment, claimed_indices) in assignments { @@ -978,11 +1071,15 @@ impl State { self.import_and_circulate_assignment( approval_voting_sender, network_sender, + runtime_api_sender, metrics, MessageSource::Peer(peer_id), assignment, claimed_indices, rng, + assignment_criteria, + clock, + session_info_provider, ) .await; } @@ -992,13 +1089,16 @@ impl State { async fn process_incoming_approvals< N: overseer::SubsystemSender, A: overseer::SubsystemSender, + RA: overseer::SubsystemSender, >( &mut self, approval_voting_sender: &mut A, network_sender: &mut N, + runtime_api_sender: &mut RA, metrics: &Metrics, peer_id: PeerId, approvals: Vec, + session_info_provider: &mut RuntimeInfo, ) { gum::trace!( target: LOG_TARGET, @@ -1028,18 +1128,21 @@ impl State { self.import_and_circulate_approval( approval_voting_sender, network_sender, + runtime_api_sender, metrics, MessageSource::Peer(peer_id), approval_vote, + session_info_provider, ) .await; } } - async fn process_incoming_peer_message( + async fn process_incoming_peer_message( &mut self, approval_voting_sender: &mut A, network_sender: &mut N, + runtime_api_sender: &mut RA, metrics: &Metrics, peer_id: PeerId, msg: Versioned< @@ -1048,9 +1151,13 @@ impl State { protocol_v3::ApprovalDistributionMessage, >, rng: &mut R, + assignment_criteria: &(impl AssignmentCriteria + ?Sized), + clock: &(impl Clock + ?Sized), + session_info_provider: &mut RuntimeInfo, ) where A: overseer::SubsystemSender, N: overseer::SubsystemSender, + RA: overseer::SubsystemSender, R: CryptoRng + Rng, { match msg { @@ -1067,10 +1174,14 @@ impl State { self.process_incoming_assignments( approval_voting_sender, network_sender, + runtime_api_sender, metrics, peer_id, sanitized_assignments, rng, + assignment_criteria, + clock, + session_info_provider, ) .await; }, @@ -1089,10 +1200,14 @@ impl State { self.process_incoming_assignments( approval_voting_sender, network_sender, + runtime_api_sender, metrics, peer_id, sanitized_assignments, rng, + assignment_criteria, + clock, + session_info_provider, ) .await; }, @@ -1102,9 +1217,11 @@ impl State { self.process_incoming_approvals( approval_voting_sender, network_sender, + runtime_api_sender, metrics, peer_id, sanitized_approvals, + session_info_provider, ) .await; }, @@ -1115,9 +1232,11 @@ impl State { self.process_incoming_approvals( approval_voting_sender, network_sender, + runtime_api_sender, metrics, peer_id, sanitized_approvals, + session_info_provider, ) .await; }, @@ -1218,18 +1337,23 @@ impl State { self.enable_aggression(network_sender, Resend::No, metrics).await; } - async fn import_and_circulate_assignment( + async fn import_and_circulate_assignment( &mut self, approval_voting_sender: &mut A, network_sender: &mut N, + runtime_api_sender: &mut RA, metrics: &Metrics, source: MessageSource, assignment: IndirectAssignmentCertV2, claimed_candidate_indices: CandidateBitfield, rng: &mut R, + assignment_criteria: &(impl AssignmentCriteria + ?Sized), + clock: &(impl Clock + ?Sized), + session_info_provider: &mut RuntimeInfo, ) where A: overseer::SubsystemSender, N: overseer::SubsystemSender, + RA: overseer::SubsystemSender, R: CryptoRng + Rng, { let _span = self @@ -1355,35 +1479,47 @@ impl State { return } - let (tx, rx) = oneshot::channel(); + let result = Self::check_assignment_valid( + assignment_criteria, + &entry, + &assignment, + &claimed_candidate_indices, + session_info_provider, + runtime_api_sender, + ) + .await; - approval_voting_sender - .send_message(ApprovalVotingMessage::CheckAndImportAssignment( - assignment.clone(), - claimed_candidate_indices.clone(), - tx, - )) - .await; + match result { + Ok(checked_assignment) => { + let current_tranche = clock.tranche_now(self.slot_duration_millis, entry.slot); + let too_far_in_future = + current_tranche + TICK_TOO_FAR_IN_FUTURE as DelayTranche; - let timer = metrics.time_awaiting_approval_voting(); - let result = match rx.await { - Ok(result) => result, - Err(_) => { - gum::debug!(target: LOG_TARGET, "The approval voting subsystem is down"); - return - }, - }; - drop(timer); + if checked_assignment.tranche() >= too_far_in_future { + gum::debug!( + target: LOG_TARGET, + hash = ?block_hash, + ?peer_id, + "Got an assignment too far in the future", + ); + modify_reputation( + &mut self.reputation, + network_sender, + peer_id, + COST_ASSIGNMENT_TOO_FAR_IN_THE_FUTURE, + ) + .await; + metrics.on_assignment_far(); - gum::trace!( - target: LOG_TARGET, - ?source, - ?message_subject, - ?result, - "Checked assignment", - ); - match result { - AssignmentCheckResult::Accepted => { + return + } + + approval_voting_sender + .send_message(ApprovalVotingMessage::ImportAssignment( + checked_assignment, + None, + )) + .await; modify_reputation( &mut self.reputation, network_sender, @@ -1396,47 +1532,12 @@ impl State { peer_knowledge.received.insert(message_subject.clone(), message_kind); } }, - AssignmentCheckResult::AcceptedDuplicate => { - // "duplicate" assignments aren't necessarily equal. - // There is more than one way each validator can be assigned to each core. - // cf. https://github.com/paritytech/polkadot/pull/2160#discussion_r557628699 - if let Some(peer_knowledge) = entry.known_by.get_mut(&peer_id) { - peer_knowledge.received.insert(message_subject.clone(), message_kind); - } - gum::debug!( - target: LOG_TARGET, - hash = ?block_hash, - ?peer_id, - "Got an `AcceptedDuplicate` assignment", - ); - metrics.on_assignment_duplicatevoting(); - - return - }, - AssignmentCheckResult::TooFarInFuture => { - gum::debug!( - target: LOG_TARGET, - hash = ?block_hash, - ?peer_id, - "Got an assignment too far in the future", - ); - modify_reputation( - &mut self.reputation, - network_sender, - peer_id, - COST_ASSIGNMENT_TOO_FAR_IN_THE_FUTURE, - ) - .await; - metrics.on_assignment_far(); - - return - }, - AssignmentCheckResult::Bad(error) => { + Err(error) => { gum::info!( target: LOG_TARGET, hash = ?block_hash, ?peer_id, - %error, + ?error, "Got a bad assignment from peer", ); modify_reputation( @@ -1577,6 +1678,64 @@ impl State { } } + async fn check_assignment_valid>( + assignment_criteria: &(impl AssignmentCriteria + ?Sized), + entry: &BlockEntry, + assignment: &IndirectAssignmentCertV2, + claimed_candidate_indices: &CandidateBitfield, + runtime_info: &mut RuntimeInfo, + runtime_api_sender: &mut RA, + ) -> Result { + let ExtendedSessionInfo { ref session_info, .. } = runtime_info + .get_session_info_by_index(runtime_api_sender, assignment.block_hash, entry.session) + .await + .map_err(|err| InvalidAssignmentError::SessionInfoNotFound(err))?; + + if claimed_candidate_indices.len() > session_info.n_cores as usize { + return Err(InvalidAssignmentError::OversizedClaimedBitfield) + } + + let claimed_cores: Vec = claimed_candidate_indices + .iter_ones() + .map(|candidate_index| { + entry.candidates_metadata.get(candidate_index).map(|(_, core, _)| *core).ok_or( + InvalidAssignmentError::ClaimedInvalidCandidateIndex { + claimed_index: candidate_index, + max_index: entry.candidates_metadata.len(), + }, + ) + }) + .collect::, InvalidAssignmentError>>()?; + + let Ok(claimed_cores) = claimed_cores.try_into() else { + return Err(InvalidAssignmentError::NoClaimedCandidates) + }; + + let backing_groups = claimed_candidate_indices + .iter_ones() + .flat_map(|candidate_index| { + entry.candidates_metadata.get(candidate_index).map(|(_, _, group)| *group) + }) + .collect::>(); + + assignment_criteria + .check_assignment_cert( + claimed_cores, + assignment.validator, + &polkadot_node_primitives::approval::criteria::Config::from(session_info), + entry.vrf_story.clone(), + &assignment.cert, + backing_groups, + ) + .map_err(|err| InvalidAssignmentError::CryptoCheckFailed(err)) + .map(|tranche| { + CheckedIndirectAssignment::from_checked( + assignment.clone(), + claimed_candidate_indices.clone(), + tranche, + ) + }) + } // Checks if an approval can be processed. // Returns true if we can continue with processing the approval and false otherwise. async fn check_approval_can_be_processed< @@ -1666,13 +1825,16 @@ impl State { async fn import_and_circulate_approval< N: overseer::SubsystemSender, A: overseer::SubsystemSender, + RA: overseer::SubsystemSender, >( &mut self, approval_voting_sender: &mut A, network_sender: &mut N, + runtime_api_sender: &mut RA, metrics: &Metrics, source: MessageSource, vote: IndirectSignedApprovalVoteV2, + session_info_provider: &mut RuntimeInfo, ) { let _span = self .spans @@ -1740,31 +1902,16 @@ impl State { return } - let (tx, rx) = oneshot::channel(); - - approval_voting_sender - .send_message(ApprovalVotingMessage::CheckAndImportApproval(vote.clone(), tx)) - .await; - - let timer = metrics.time_awaiting_approval_voting(); - let result = match rx.await { - Ok(result) => result, - Err(_) => { - gum::debug!(target: LOG_TARGET, "The approval voting subsystem is down"); - return - }, - }; - drop(timer); + let result = + Self::check_vote_valid(&vote, &entry, session_info_provider, runtime_api_sender) + .await; - gum::trace!( - target: LOG_TARGET, - ?peer_id, - ?result, - ?vote, - "Checked approval", - ); match result { - ApprovalCheckResult::Accepted => { + Ok(vote) => { + approval_voting_sender + .send_message(ApprovalVotingMessage::ImportApproval(vote, None)) + .await; + modify_reputation( &mut self.reputation, network_sender, @@ -1782,7 +1929,7 @@ impl State { .insert(approval_knwowledge_key.0.clone(), approval_knwowledge_key.1); } }, - ApprovalCheckResult::Bad(error) => { + Err(err) => { modify_reputation( &mut self.reputation, network_sender, @@ -1790,10 +1937,11 @@ impl State { COST_INVALID_MESSAGE, ) .await; + gum::info!( target: LOG_TARGET, ?peer_id, - %error, + ?err, "Got a bad approval from peer", ); metrics.on_approval_bad(); @@ -1891,6 +2039,50 @@ impl State { } } + // Checks if the approval vote is valid. + async fn check_vote_valid>( + vote: &IndirectSignedApprovalVoteV2, + entry: &BlockEntry, + runtime_info: &mut RuntimeInfo, + runtime_api_sender: &mut RA, + ) -> Result { + if vote.candidate_indices.len() > entry.candidates_metadata.len() { + return Err(InvalidVoteError::CandidateIndexOutOfBounds) + } + + let candidate_hashes = vote + .candidate_indices + .iter_ones() + .flat_map(|candidate_index| { + entry + .candidates_metadata + .get(candidate_index) + .map(|(candidate_hash, _, _)| *candidate_hash) + }) + .collect::>(); + + let ExtendedSessionInfo { ref session_info, .. } = runtime_info + .get_session_info_by_index(runtime_api_sender, vote.block_hash, entry.session) + .await + .map_err(|err| InvalidVoteError::SessionInfoNotFound(err))?; + + let pubkey = session_info + .validators + .get(vote.validator) + .ok_or(InvalidVoteError::ValidatorIndexOutOfBounds)?; + DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates( + candidate_hashes.clone(), + )) + .check_signature( + &pubkey, + *candidate_hashes.first().unwrap(), + entry.session, + &vote.signature, + ) + .map_err(|_| InvalidVoteError::InvalidSignature) + .map(|_| CheckedIndirectSignedApprovalVote::from_checked(vote.clone())) + } + /// Retrieve approval signatures from state for the given relay block/indices: fn get_approval_signatures( &mut self, @@ -2468,16 +2660,48 @@ async fn modify_reputation( #[overseer::contextbounds(ApprovalDistribution, prefix = self::overseer)] impl ApprovalDistribution { /// Create a new instance of the [`ApprovalDistribution`] subsystem. - pub fn new(metrics: Metrics) -> Self { - Self { metrics } + pub fn new( + metrics: Metrics, + slot_duration_millis: u64, + assignment_criteria: Arc, + ) -> Self { + Self::new_with_clock( + metrics, + slot_duration_millis, + Box::new(SystemClock), + assignment_criteria, + ) + } + + /// Create a new instance of the [`ApprovalDistribution`] subsystem, with a custom clock. + pub fn new_with_clock( + metrics: Metrics, + slot_duration_millis: u64, + clock: Box, + assignment_criteria: Arc, + ) -> Self { + Self { metrics, slot_duration_millis, clock, assignment_criteria } } async fn run(self, ctx: Context) { - let mut state = State::default(); + let mut state = + State { slot_duration_millis: self.slot_duration_millis, ..Default::default() }; // According to the docs of `rand`, this is a ChaCha12 RNG in practice // and will always be chosen for strong performance and security properties. let mut rng = rand::rngs::StdRng::from_entropy(); - self.run_inner(ctx, &mut state, REPUTATION_CHANGE_INTERVAL, &mut rng).await + let mut session_info_provider = RuntimeInfo::new_with_config(RuntimeInfoConfig { + keystore: None, + session_cache_lru_size: DISPUTE_WINDOW.get(), + }); + + self.run_inner( + ctx, + &mut state, + REPUTATION_CHANGE_INTERVAL, + &mut rng, + &mut session_info_provider, + ) + .await } /// Used for testing. @@ -2487,11 +2711,14 @@ impl ApprovalDistribution { state: &mut State, reputation_interval: Duration, rng: &mut (impl CryptoRng + Rng), + session_info_provider: &mut RuntimeInfo, ) { let new_reputation_delay = || futures_timer::Delay::new(reputation_interval).fuse(); let mut reputation_delay = new_reputation_delay(); let mut approval_voting_sender = ctx.sender().clone(); let mut network_sender = ctx.sender().clone(); + let mut runtime_api_sender = ctx.sender().clone(); + loop { select! { _ = reputation_delay => { @@ -2507,8 +2734,7 @@ impl ApprovalDistribution { }, }; - - if self.handle_from_orchestra(message, &mut approval_voting_sender, &mut network_sender, state, rng).await { + if self.handle_from_orchestra(message, &mut approval_voting_sender, &mut network_sender, &mut runtime_api_sender, state, rng, session_info_provider).await { return; } @@ -2523,23 +2749,30 @@ impl ApprovalDistribution { pub async fn handle_from_orchestra< N: overseer::SubsystemSender, A: overseer::SubsystemSender, + RA: overseer::SubsystemSender, >( &self, message: FromOrchestra, approval_voting_sender: &mut A, network_sender: &mut N, + runtime_api_sender: &mut RA, state: &mut State, rng: &mut (impl CryptoRng + Rng), + session_info_provider: &mut RuntimeInfo, ) -> bool { match message { FromOrchestra::Communication { msg } => Self::handle_incoming( approval_voting_sender, network_sender, + runtime_api_sender, state, msg, &self.metrics, rng, + self.assignment_criteria.as_ref(), + self.clock.as_ref(), + session_info_provider, ) .await, FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => { @@ -2567,23 +2800,48 @@ impl ApprovalDistribution { async fn handle_incoming< N: overseer::SubsystemSender, A: overseer::SubsystemSender, + RA: overseer::SubsystemSender, >( approval_voting_sender: &mut A, network_sender: &mut N, + runtime_api_sender: &mut RA, state: &mut State, msg: ApprovalDistributionMessage, metrics: &Metrics, rng: &mut (impl CryptoRng + Rng), + assignment_criteria: &(impl AssignmentCriteria + ?Sized), + clock: &(impl Clock + ?Sized), + session_info_provider: &mut RuntimeInfo, ) { match msg { ApprovalDistributionMessage::NetworkBridgeUpdate(event) => { state - .handle_network_msg(approval_voting_sender, network_sender, metrics, event, rng) + .handle_network_msg( + approval_voting_sender, + network_sender, + runtime_api_sender, + metrics, + event, + rng, + assignment_criteria, + clock, + session_info_provider, + ) .await; }, ApprovalDistributionMessage::NewBlocks(metas) => { state - .handle_new_blocks(approval_voting_sender, network_sender, metrics, metas, rng) + .handle_new_blocks( + approval_voting_sender, + network_sender, + runtime_api_sender, + metrics, + metas, + rng, + assignment_criteria, + clock, + session_info_provider, + ) .await; }, ApprovalDistributionMessage::DistributeAssignment(cert, candidate_indices) => { @@ -2607,11 +2865,15 @@ impl ApprovalDistribution { .import_and_circulate_assignment( approval_voting_sender, network_sender, + runtime_api_sender, &metrics, MessageSource::Local, cert, candidate_indices, rng, + assignment_criteria, + clock, + session_info_provider, ) .await; }, @@ -2627,9 +2889,11 @@ impl ApprovalDistribution { .import_and_circulate_approval( approval_voting_sender, network_sender, + runtime_api_sender, metrics, MessageSource::Local, vote, + session_info_provider, ) .await; }, diff --git a/polkadot/node/network/approval-distribution/src/metrics.rs b/polkadot/node/network/approval-distribution/src/metrics.rs index 60c7f2f6d3b8..10553c352966 100644 --- a/polkadot/node/network/approval-distribution/src/metrics.rs +++ b/polkadot/node/network/approval-distribution/src/metrics.rs @@ -30,7 +30,6 @@ struct MetricsInner { aggression_l2_messages_total: prometheus::Counter, time_unify_with_peer: prometheus::Histogram, time_import_pending_now_known: prometheus::Histogram, - time_awaiting_approval_voting: prometheus::Histogram, assignments_received_result: prometheus::CounterVec, approvals_received_result: prometheus::CounterVec, } @@ -206,14 +205,6 @@ impl Metrics { } } - pub(crate) fn time_awaiting_approval_voting( - &self, - ) -> Option { - self.0 - .as_ref() - .map(|metrics| metrics.time_awaiting_approval_voting.start_timer()) - } - pub(crate) fn on_aggression_l1(&self) { if let Some(metrics) = &self.0 { metrics.aggression_l1_messages_total.inc(); @@ -288,13 +279,6 @@ impl MetricsTrait for Metrics { ).buckets(vec![0.0001, 0.0004, 0.0016, 0.0064, 0.0256, 0.1024, 0.4096, 1.6384, 3.2768, 4.9152, 6.5536,]))?, registry, )?, - time_awaiting_approval_voting: prometheus::register( - prometheus::Histogram::with_opts(prometheus::HistogramOpts::new( - "polkadot_parachain_time_awaiting_approval_voting", - "Time spent awaiting a reply from the Approval Voting Subsystem.", - ).buckets(vec![0.0001, 0.0004, 0.0016, 0.0064, 0.0256, 0.1024, 0.4096, 1.6384, 3.2768, 4.9152, 6.5536,]))?, - registry, - )?, assignments_received_result: prometheus::register( prometheus::CounterVec::new( prometheus::Opts::new( diff --git a/polkadot/node/network/approval-distribution/src/tests.rs b/polkadot/node/network/approval-distribution/src/tests.rs index 1ca571721ea9..4ee9320e0e45 100644 --- a/polkadot/node/network/approval-distribution/src/tests.rs +++ b/polkadot/node/network/approval-distribution/src/tests.rs @@ -16,7 +16,7 @@ use super::*; use assert_matches::assert_matches; -use futures::{executor, future, Future}; +use futures::{channel::oneshot, executor, future, Future}; use polkadot_node_network_protocol::{ grid_topology::{SessionGridTopology, TopologyPeerInfo}, our_view, @@ -24,6 +24,7 @@ use polkadot_node_network_protocol::{ view, ObservedRole, }; use polkadot_node_primitives::approval::{ + criteria, v1::{ AssignmentCert, AssignmentCertKind, IndirectAssignmentCert, IndirectSignedApprovalVote, VrfPreOutput, VrfProof, VrfSignature, @@ -34,12 +35,17 @@ use polkadot_node_primitives::approval::{ }, }; use polkadot_node_subsystem::messages::{ - network_bridge_event, AllMessages, ApprovalCheckError, ReportPeerMessage, + network_bridge_event, AllMessages, ReportPeerMessage, RuntimeApiRequest, }; use polkadot_node_subsystem_util::{reputation::add_reputation, TimeoutExt as _}; -use polkadot_primitives::{AuthorityDiscoveryId, BlakeTwo256, CoreIndex, HashT}; +use polkadot_primitives::{ + ApprovalVoteMultipleCandidates, AuthorityDiscoveryId, BlakeTwo256, CoreIndex, ExecutorParams, + HashT, NodeFeatures, SessionInfo, ValidatorId, +}; use polkadot_primitives_test_helpers::dummy_signature; use rand::SeedableRng; +use sc_keystore::{Keystore, LocalKeystore}; +use sp_application_crypto::AppCrypto; use sp_authority_discovery::AuthorityPair as AuthorityDiscoveryPair; use sp_core::crypto::Pair as PairT; use std::time::Duration; @@ -47,6 +53,8 @@ type VirtualOverseer = polkadot_node_subsystem_test_helpers::TestSubsystemContextHandle; fn test_harness>( + assignment_criteria: Arc, + clock: Box, mut state: State, test_fn: impl FnOnce(VirtualOverseer) -> T, ) -> State { @@ -56,13 +64,29 @@ fn test_harness>( let (context, virtual_overseer) = polkadot_node_subsystem_test_helpers::make_subsystem_context(pool.clone()); - let subsystem = ApprovalDistribution::new(Default::default()); + let subsystem = ApprovalDistribution::new_with_clock( + Metrics::default(), + Default::default(), + clock, + assignment_criteria, + ); { let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(12345); + let mut session_info_provider = RuntimeInfo::new_with_config(RuntimeInfoConfig { + keystore: None, + session_cache_lru_size: DISPUTE_WINDOW.get(), + }); + let (tx, rx) = oneshot::channel(); let subsystem = async { subsystem - .run_inner(context, &mut state, REPUTATION_CHANGE_TEST_INTERVAL, &mut rng) + .run_inner( + context, + &mut state, + REPUTATION_CHANGE_TEST_INTERVAL, + &mut rng, + &mut session_info_provider, + ) .await; tx.send(()).expect("Fail to notify subystem is done"); }; @@ -121,6 +145,41 @@ async fn overseer_recv(overseer: &mut VirtualOverseer) -> AllMessages { msg } +async fn provide_session(virtual_overseer: &mut VirtualOverseer, session_info: SessionInfo) { + assert_matches!( + overseer_recv(virtual_overseer).await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request( + _, + RuntimeApiRequest::SessionInfo(_, si_tx), + ) + ) => { + si_tx.send(Ok(Some(session_info.clone()))).unwrap(); + } + ); + assert_matches!( + overseer_recv(virtual_overseer).await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request( + _, + RuntimeApiRequest::SessionExecutorParams(_, si_tx), + ) + ) => { + // Make sure all SessionExecutorParams calls are not made for the leaf (but for its relay parent) + si_tx.send(Ok(Some(ExecutorParams::default()))).unwrap(); + } + ); + + assert_matches!( + overseer_recv(virtual_overseer).await, + AllMessages::RuntimeApi( + RuntimeApiMessage::Request(_, RuntimeApiRequest::NodeFeatures(_, si_tx), ) + ) => { + si_tx.send(Ok(NodeFeatures::EMPTY)).unwrap(); + } + ); +} + fn make_peers_and_authority_ids(n: usize) -> Vec<(PeerId, AuthorityDiscoveryId)> { (0..n) .map(|_| { @@ -335,6 +394,30 @@ fn fake_assignment_cert_v2( } } +fn fake_assignment_cert_delay( + block_hash: Hash, + validator: ValidatorIndex, + core_bitfield: CoreBitfield, +) -> IndirectAssignmentCertV2 { + let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT); + let msg = b"WhenParachains?"; + let mut prng = rand_core::OsRng; + let keypair = schnorrkel::Keypair::generate_with(&mut prng); + let (inout, proof, _) = keypair.vrf_sign(ctx.bytes(msg)); + let preout = inout.to_preout(); + + IndirectAssignmentCertV2 { + block_hash, + validator, + cert: AssignmentCertV2 { + kind: AssignmentCertKindV2::RelayVRFDelay { + core_index: CoreIndex(core_bitfield.iter_ones().next().unwrap() as u32), + }, + vrf: VrfSignature { pre_output: VrfPreOutput(preout), proof: VrfProof(proof) }, + }, + } +} + async fn expect_reputation_change( virtual_overseer: &mut VirtualOverseer, peer_id: &PeerId, @@ -378,6 +461,85 @@ fn state_with_reputation_delay() -> State { State { reputation: ReputationAggregator::new(|_| false), ..Default::default() } } +fn dummy_session_info_valid( + index: SessionIndex, + keystore: &mut LocalKeystore, + num_validators: usize, +) -> SessionInfo { + let keys = (0..num_validators) + .map(|_| { + keystore + .sr25519_generate_new(ValidatorId::ID, Some("//Node")) + .expect("Insert key into keystore") + }) + .collect_vec(); + + SessionInfo { + validators: keys.clone().into_iter().map(|key| key.into()).collect(), + discovery_keys: keys.clone().into_iter().map(|key| key.into()).collect(), + assignment_keys: keys.clone().into_iter().map(|key| key.into()).collect(), + validator_groups: Default::default(), + n_cores: 20, + zeroth_delay_tranche_width: index as _, + relay_vrf_modulo_samples: index as _, + n_delay_tranches: index as _, + no_show_slots: index as _, + needed_approvals: index as _, + active_validator_indices: Vec::new(), + dispute_period: 6, + random_seed: [0u8; 32], + } +} + +fn signature_for( + keystore: &LocalKeystore, + session: &SessionInfo, + candidate_hashes: Vec, + validator_index: ValidatorIndex, +) -> ValidatorSignature { + let payload = ApprovalVoteMultipleCandidates(&candidate_hashes).signing_payload(1); + let sign_key = session.validators.get(validator_index).unwrap().clone(); + let signature = keystore + .sr25519_sign(ValidatorId::ID, &sign_key.into(), &payload[..]) + .unwrap() + .unwrap(); + signature.into() +} + +struct MockAssignmentCriteria { + tranche: + Result, +} + +impl AssignmentCriteria for MockAssignmentCriteria { + fn compute_assignments( + &self, + _keystore: &LocalKeystore, + _relay_vrf_story: polkadot_node_primitives::approval::v1::RelayVRFStory, + _config: &criteria::Config, + _leaving_cores: Vec<( + CandidateHash, + polkadot_primitives::CoreIndex, + polkadot_primitives::GroupIndex, + )>, + _enable_assignments_v2: bool, + ) -> HashMap { + HashMap::new() + } + + fn check_assignment_cert( + &self, + _claimed_core_bitfield: polkadot_node_primitives::approval::v2::CoreBitfield, + _validator_index: polkadot_primitives::ValidatorIndex, + _config: &criteria::Config, + _relay_vrf_story: polkadot_node_primitives::approval::v1::RelayVRFStory, + _assignment: &polkadot_node_primitives::approval::v2::AssignmentCertV2, + _backing_groups: Vec, + ) -> Result { + self.tranche + } +} + /// import an assignment /// connect a new peer /// the new peer sends us the same assignment @@ -391,89 +553,98 @@ fn try_import_the_same_assignment() { let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // setup peers - setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await; - setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await; - setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V1).await; - - // Set up a gossip topology, where a, b, c and d are topology neighbors to the node under - // testing. - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - setup_gossip_topology( - overseer, - make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), - ) - .await; + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // setup peers + setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await; + setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await; + setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V1).await; + + // Set up a gossip topology, where a, b, c and d are topology neighbors to the node + // under testing. + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), + ) + .await; - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 2, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 2, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // send the assignment related to `hash` + let validator_index = ValidatorIndex(0); + let cert = fake_assignment_cert(hash, validator_index); + let assignments = vec![(cert.clone(), 0u32)]; - // send the assignment related to `hash` - let validator_index = ValidatorIndex(0); - let cert = fake_assignment_cert(hash, validator_index); - let assignments = vec![(cert.clone(), 0u32)]; + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer(overseer, &peer_a, msg).await; - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); - send_message_from_peer(overseer, &peer_a, msg).await; + expect_reputation_change(overseer, &peer_a, COST_UNEXPECTED_MESSAGE).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; + // send an `Accept` message from the Approval Voting subsystem + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, + _, + )) => { + assert_eq!(assignment.candidate_indices(), &0u32.into()); + assert_eq!(assignment.assignment(), &cert.into()); + assert_eq!(assignment.tranche(), 0); + } + ); - expect_reputation_change(overseer, &peer_a, COST_UNEXPECTED_MESSAGE).await; + expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await; - // send an `Accept` message from the Approval Voting subsystem - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - assignment, - claimed_indices, - tx, - )) => { - assert_eq!(claimed_indices, 0u32.into()); - assert_eq!(assignment, cert.into()); - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); - - expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers.len(), 2); - assert_eq!(assignments.len(), 1); - } - ); + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 2); + assert_eq!(assignments.len(), 1); + } + ); - // setup new peer with V2 - setup_peer_with_view(overseer, &peer_d, view![], ValidationVersion::V3).await; + // setup new peer with V2 + setup_peer_with_view(overseer, &peer_d, view![], ValidationVersion::V3).await; - // send the same assignment from peer_d - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments); - send_message_from_peer(overseer, &peer_d, msg).await; + // send the same assignment from peer_d + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments); + send_message_from_peer(overseer, &peer_d, msg).await; - expect_reputation_change(overseer, &peer_d, COST_UNEXPECTED_MESSAGE).await; - expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE).await; + expect_reputation_change(overseer, &peer_d, COST_UNEXPECTED_MESSAGE).await; + expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE).await; - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); } /// Just like `try_import_the_same_assignment` but use `VRFModuloCompact` assignments for multiple @@ -488,97 +659,106 @@ fn try_import_the_same_assignment_v2() { let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // setup peers - setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V3).await; - setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await; - setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await; - - // Set up a gossip topology, where a, b, c and d are topology neighbors to the node under - // testing. - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - setup_gossip_topology( - overseer, - make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), - ) - .await; + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // setup peers + setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V3).await; + setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await; + setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await; + + // Set up a gossip topology, where a, b, c and d are topology neighbors to the node + // under testing. + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), + ) + .await; - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 2, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - // send the assignment related to `hash` - let validator_index = ValidatorIndex(0); - let cores = vec![1, 2, 3, 4]; - let core_bitfield: CoreBitfield = cores - .iter() - .map(|index| CoreIndex(*index)) - .collect::>() - .try_into() - .unwrap(); - - let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield.clone()); - let assignments = vec![(cert.clone(), cores.clone().try_into().unwrap())]; - - let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone()); - send_message_from_peer_v3(overseer, &peer_a, msg).await; - - expect_reputation_change(overseer, &peer_a, COST_UNEXPECTED_MESSAGE).await; - - // send an `Accept` message from the Approval Voting subsystem - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - assignment, - claimed_indices, - tx, - )) => { - assert_eq!(claimed_indices, cores.try_into().unwrap()); - assert_eq!(assignment, cert.into()); - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); - - expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers.len(), 2); - assert_eq!(assignments.len(), 1); - } - ); + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 2, + candidates: vec![Default::default(); 5], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // send the assignment related to `hash` + let validator_index = ValidatorIndex(0); + let cores = vec![1, 2, 3, 4]; + let core_bitfield: CoreBitfield = cores + .iter() + .map(|index| CoreIndex(*index)) + .collect::>() + .try_into() + .unwrap(); + + let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield.clone()); + let assignments = vec![(cert.clone(), cores.clone().try_into().unwrap())]; + + let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer_v3(overseer, &peer_a, msg).await; + + expect_reputation_change(overseer, &peer_a, COST_UNEXPECTED_MESSAGE).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; + // send an `Accept` message from the Approval Voting subsystem + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, + _, + )) => { + assert_eq!(assignment.candidate_indices(), &cores.try_into().unwrap()); + assert_eq!(assignment.assignment(), &cert.into()); + assert_eq!(assignment.tranche(), 0); + } + ); + + expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 2); + assert_eq!(assignments.len(), 1); + } + ); - // setup new peer - setup_peer_with_view(overseer, &peer_d, view![], ValidationVersion::V3).await; + // setup new peer + setup_peer_with_view(overseer, &peer_d, view![], ValidationVersion::V3).await; - // send the same assignment from peer_d - let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments); - send_message_from_peer_v3(overseer, &peer_d, msg).await; + // send the same assignment from peer_d + let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments); + send_message_from_peer_v3(overseer, &peer_d, msg).await; - expect_reputation_change(overseer, &peer_d, COST_UNEXPECTED_MESSAGE).await; - expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE).await; + expect_reputation_change(overseer, &peer_d, COST_UNEXPECTED_MESSAGE).await; + expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE).await; - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); } /// import an assignment @@ -590,55 +770,65 @@ fn delay_reputation_change() { let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); - let _ = test_harness(state_with_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - - // Setup peers - setup_peer_with_view(overseer, &peer, view![], ValidationVersion::V1).await; - - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 2, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - // send the assignment related to `hash` - let validator_index = ValidatorIndex(0); - let cert = fake_assignment_cert(hash, validator_index); - let assignments = vec![(cert.clone(), 0u32)]; + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_with_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + + // Setup peers + setup_peer_with_view(overseer, &peer, view![], ValidationVersion::V1).await; + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 2, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // send the assignment related to `hash` + let validator_index = ValidatorIndex(0); + let cert = fake_assignment_cert(hash, validator_index); + let assignments = vec![(cert.clone(), 0u32)]; - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); - send_message_from_peer(overseer, &peer, msg).await; + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer(overseer, &peer, msg).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; - // send an `Accept` message from the Approval Voting subsystem - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - assignment, - claimed_candidates, - tx, - )) => { - assert_eq!(assignment.cert, cert.cert.into()); - assert_eq!(claimed_candidates, vec![0u32].try_into().unwrap()); - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); - expect_reputation_changes( - overseer, - &peer, - vec![COST_UNEXPECTED_MESSAGE, BENEFIT_VALID_MESSAGE_FIRST], - ) - .await; - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + // send an `Accept` message from the Approval Voting subsystem + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, + _, + )) => { + assert_eq!(assignment.assignment().cert, cert.cert.into()); + assert_eq!(assignment.candidate_indices(), &vec![0u32].try_into().unwrap()); + assert_eq!(assignment.tranche(), 0); + } + ); + expect_reputation_changes( + overseer, + &peer, + vec![COST_UNEXPECTED_MESSAGE, BENEFIT_VALID_MESSAGE_FIRST], + ) + .await; + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); + virtual_overseer + }, + ); } /// @@ -653,77 +843,88 @@ fn spam_attack_results_in_negative_reputation_change() { let peer_a = PeerId::random(); let hash_b = Hash::repeat_byte(0xBB); - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - let peer = &peer_a; - setup_peer_with_view(overseer, peer, view![], ValidationVersion::V1).await; - - // new block `hash_b` with 20 candidates - let candidates_count = 20; - let meta = BlockApprovalMeta { - hash: hash_b, - parent_hash, - number: 2, - candidates: vec![Default::default(); candidates_count], - slot: 1.into(), - session: 1, - }; - - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - // send 20 assignments related to `hash_b` - // to populate our knowledge - let assignments: Vec<_> = (0..candidates_count) - .map(|candidate_index| { - let validator_index = ValidatorIndex(candidate_index as u32); - let cert = fake_assignment_cert(hash_b, validator_index); - (cert, candidate_index as u32) - }) - .collect(); - - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); - send_message_from_peer(overseer, peer, msg.clone()).await; - - for i in 0..candidates_count { - expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await; + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + let peer = &peer_a; + setup_peer_with_view(overseer, peer, view![], ValidationVersion::V1).await; + + // new block `hash_b` with 20 candidates + let candidates_count = 20; + let meta = BlockApprovalMeta { + hash: hash_b, + parent_hash, + number: 2, + candidates: vec![Default::default(); candidates_count], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // send 20 assignments related to `hash_b` + // to populate our knowledge + let assignments: Vec<_> = (0..candidates_count) + .map(|candidate_index| { + let validator_index = ValidatorIndex(candidate_index as u32); + let cert = fake_assignment_cert(hash_b, validator_index); + (cert, candidate_index as u32) + }) + .collect(); - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - assignment, - claimed_candidate_index, - tx, - )) => { - assert_eq!(assignment, assignments[i].0.clone().into()); - assert_eq!(claimed_candidate_index, assignments[i].1.into()); - tx.send(AssignmentCheckResult::Accepted).unwrap(); + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer(overseer, peer, msg.clone()).await; + + for i in 0..candidates_count { + expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await; + if i == 0 { + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; } - ); + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, + _, + )) => { + assert_eq!(assignment.assignment(), &assignments[i].0.clone().into()); + assert_eq!(assignment.candidate_indices(), &assignments[i].1.into()); + assert_eq!(assignment.tranche(), 0); + } + ); - expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await; - } + expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await; + } - // send a view update that removes block B from peer's view by bumping the finalized_number - overseer_send( - overseer, - ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerViewChange( - *peer, - View::with_finalized(2), - )), - ) - .await; + // send a view update that removes block B from peer's view by bumping the + // finalized_number + overseer_send( + overseer, + ApprovalDistributionMessage::NetworkBridgeUpdate( + NetworkBridgeEvent::PeerViewChange(*peer, View::with_finalized(2)), + ), + ) + .await; - // send the assignments again - send_message_from_peer(overseer, peer, msg.clone()).await; + // send the assignments again + send_message_from_peer(overseer, peer, msg.clone()).await; - // each of them will incur `COST_UNEXPECTED_MESSAGE`, not only the first one - for _ in 0..candidates_count { - expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await; - expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE).await; - } - virtual_overseer - }); + // each of them will incur `COST_UNEXPECTED_MESSAGE`, not only the first one + for _ in 0..candidates_count { + expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await; + expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE).await; + } + virtual_overseer + }, + ); } /// Imagine we send a message to peer A and peer B. @@ -739,86 +940,94 @@ fn peer_sending_us_the_same_we_just_sent_them_is_ok() { let peers = make_peers_and_authority_ids(8); let peer_a = peers.first().unwrap().0; - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - let peer = &peer_a; - setup_peer_with_view(overseer, peer, view![], ValidationVersion::V1).await; - - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Setup a topology where peer_a is neighbor to current node. - setup_gossip_topology( - overseer, - make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1), - ) - .await; + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + let peer = &peer_a; + setup_peer_with_view(overseer, peer, view![], ValidationVersion::V1).await; + + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Setup a topology where peer_a is neighbor to current node. + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1), + ) + .await; - // new block `hash` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - // import an assignment related to `hash` locally - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; - let cert = fake_assignment_cert(hash, validator_index); - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment( - cert.clone().into(), - candidate_index.into(), - ), - ) - .await; + // new block `hash` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // import an assignment related to `hash` locally + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; + let cert = fake_assignment_cert(hash, validator_index); + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment( + cert.clone().into(), + candidate_index.into(), + ), + ) + .await; - // update peer view to include the hash - overseer_send( - overseer, - ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerViewChange( - *peer, - view![hash], - )), - ) - .await; + // update peer view to include the hash + overseer_send( + overseer, + ApprovalDistributionMessage::NetworkBridgeUpdate( + NetworkBridgeEvent::PeerViewChange(*peer, view![hash]), + ), + ) + .await; - // we should send them the assignment - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers.len(), 1); - assert_eq!(assignments.len(), 1); - } - ); + // we should send them the assignment + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(assignments.len(), 1); + } + ); - // but if someone else is sending it the same assignment - // the peer could send us it as well - let assignments = vec![(cert, candidate_index)]; - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments); - send_message_from_peer(overseer, peer, msg.clone()).await; + // but if someone else is sending it the same assignment + // the peer could send us it as well + let assignments = vec![(cert, candidate_index)]; + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments); + send_message_from_peer(overseer, peer, msg.clone()).await; - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "we should not punish the peer"); + assert!( + overseer.recv().timeout(TIMEOUT).await.is_none(), + "we should not punish the peer" + ); - // send the assignments again - send_message_from_peer(overseer, peer, msg).await; + // send the assignments again + send_message_from_peer(overseer, peer, msg).await; - // now we should - expect_reputation_change(overseer, peer, COST_DUPLICATE_MESSAGE).await; - virtual_overseer - }); + // now we should + expect_reputation_change(overseer, peer, COST_DUPLICATE_MESSAGE).await; + virtual_overseer + }, + ); } #[test] @@ -830,116 +1039,134 @@ fn import_approval_happy_path_v1_v2_peers() { let peer_c = peers.get(2).unwrap().0; let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); + let candidate_hash = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB)); + + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // setup peers with V1 and V2 protocol versions + setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await; + setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await; + setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V1).await; + + let mut keystore = LocalKeystore::in_memory(); + let session = dummy_session_info_valid(1, &mut keystore, 1); + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![(candidate_hash, 0.into(), 0.into()); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Set up a gossip topology, where a, b, and c are topology neighbors to the node. + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), + ) + .await; - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // setup peers with V1 and V2 protocol versions - setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await; - setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await; - setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V1).await; - - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Set up a gossip topology, where a, b, and c are topology neighbors to the node. - setup_gossip_topology( - overseer, - make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), - ) - .await; - - // import an assignment related to `hash` locally - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; - let cert = fake_assignment_cert(hash, validator_index); - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment( - cert.clone().into(), - candidate_index.into(), - ), - ) - .await; + // import an assignment related to `hash` locally + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; + let cert = fake_assignment_cert(hash, validator_index); + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment( + cert.clone().into(), + candidate_index.into(), + ), + ) + .await; - // 1 peer is v1 - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers.len(), 1); - assert_eq!(assignments.len(), 1); - } - ); - - // 1 peer is v2 - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers.len(), 1); - assert_eq!(assignments.len(), 1); - } - ); - - // send the an approval from peer_b - let approval = IndirectSignedApprovalVoteV2 { - block_hash: hash, - candidate_indices: candidate_index.into(), - validator: validator_index, - signature: dummy_signature(), - }; - let msg: protocol_v3::ApprovalDistributionMessage = - protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); - send_message_from_peer_v3(overseer, &peer_b, msg).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval( - vote, - tx, - )) => { - assert_eq!(vote, approval); - tx.send(ApprovalCheckResult::Accepted).unwrap(); - } - ); - - expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Approvals(approvals) - )) - )) => { - assert_eq!(peers.len(), 1); - assert_eq!(approvals.len(), 1); - } - ); - virtual_overseer - }); + // 1 peer is v1 + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(assignments.len(), 1); + } + ); + + // 1 peer is v2 + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(assignments.len(), 1); + } + ); + + // send the an approval from peer_b + let approval = IndirectSignedApprovalVoteV2 { + block_hash: hash, + candidate_indices: candidate_index.into(), + validator: validator_index, + signature: signature_for( + &keystore, + &session, + vec![candidate_hash], + validator_index, + ), + }; + let msg: protocol_v3::ApprovalDistributionMessage = + protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); + send_message_from_peer_v3(overseer, &peer_b, msg).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval( + vote, _, + )) => { + assert_eq!(Into::::into(vote), approval); + } + ); + + expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Approvals(approvals) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(approvals.len(), 1); + } + ); + virtual_overseer + }, + ); } // Test a v2 approval that signs multiple candidate is correctly processed. @@ -952,103 +1179,123 @@ fn import_approval_happy_path_v2() { let peer_c = peers.get(2).unwrap().0; let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); + let candidate_hash_first = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB)); + let candidate_hash_second = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xCC)); + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // setup peers with V2 protocol versions + setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V3).await; + setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await; + setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await; + let mut keystore = LocalKeystore::in_memory(); + let session = dummy_session_info_valid(1, &mut keystore, 1); + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![ + (candidate_hash_first, 0.into(), 0.into()), + (candidate_hash_second, 1.into(), 1.into()), + ], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Set up a gossip topology, where a, b, and c are topology neighbors to the node. + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), + ) + .await; - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // setup peers with V2 protocol versions - setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V3).await; - setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await; - setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await; - - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 2], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Set up a gossip topology, where a, b, and c are topology neighbors to the node. - setup_gossip_topology( - overseer, - make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), - ) - .await; + // import an assignment related to `hash` locally + let validator_index = ValidatorIndex(0); + let candidate_indices: CandidateBitfield = + vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap(); + let candidate_bitfields = vec![CoreIndex(0), CoreIndex(1)].try_into().unwrap(); + let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields); + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment( + cert.clone().into(), + candidate_indices.clone(), + ), + ) + .await; - // import an assignment related to `hash` locally - let validator_index = ValidatorIndex(0); - let candidate_indices: CandidateBitfield = - vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap(); - let candidate_bitfields = vec![CoreIndex(0), CoreIndex(1)].try_into().unwrap(); - let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields); - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment( - cert.clone().into(), - candidate_indices.clone(), - ), - ) - .await; + // 1 peer is v2 + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 2); + assert_eq!(assignments.len(), 1); + } + ); - // 1 peer is v2 - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers.len(), 2); - assert_eq!(assignments.len(), 1); - } - ); - - // send the an approval from peer_b - let approval = IndirectSignedApprovalVoteV2 { - block_hash: hash, - candidate_indices, - validator: validator_index, - signature: dummy_signature(), - }; - let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); - send_message_from_peer_v3(overseer, &peer_b, msg).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval( - vote, - tx, - )) => { - assert_eq!(vote, approval); - tx.send(ApprovalCheckResult::Accepted).unwrap(); - } - ); - - expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Approvals(approvals) - )) - )) => { - assert_eq!(peers.len(), 1); - assert_eq!(approvals.len(), 1); - } - ); - virtual_overseer - }); + // send the an approval from peer_b + let approval = IndirectSignedApprovalVoteV2 { + block_hash: hash, + candidate_indices, + validator: validator_index, + signature: signature_for( + &keystore, + &session, + vec![candidate_hash_first, candidate_hash_second], + validator_index, + ), + }; + let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); + send_message_from_peer_v3(overseer, &peer_b, msg).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval( + vote, _, + )) => { + assert_eq!(Into::::into(vote), approval); + } + ); + + expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Approvals(approvals) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(approvals.len(), 1); + } + ); + virtual_overseer + }, + ); } // Tests that votes that cover multiple assignments candidates are correctly processed on importing @@ -1062,187 +1309,203 @@ fn multiple_assignments_covered_with_one_approval_vote() { let peer_d = peers.get(4).unwrap().0; let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); + let candidate_hash_first = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB)); + let candidate_hash_second = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xCC)); + + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // setup peers with V2 protocol versions + setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await; + setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await; + setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await; + setup_peer_with_view(overseer, &peer_d, view![hash], ValidationVersion::V3).await; + + let mut keystore = LocalKeystore::in_memory(); + let session = dummy_session_info_valid(1, &mut keystore, 5); + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![ + (candidate_hash_first, 0.into(), 0.into()), + (candidate_hash_second, 1.into(), 1.into()), + ], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Set up a gossip topology, where a, b, and c, d are topology neighbors to the node. + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), + ) + .await; - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // setup peers with V2 protocol versions - setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await; - setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await; - setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V3).await; - setup_peer_with_view(overseer, &peer_d, view![hash], ValidationVersion::V3).await; - - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 2], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Set up a gossip topology, where a, b, and c, d are topology neighbors to the node. - setup_gossip_topology( - overseer, - make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), - ) - .await; + // import an assignment related to `hash` locally + let validator_index = ValidatorIndex(2); // peer_c is the originator + let candidate_indices: CandidateBitfield = + vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap(); - // import an assignment related to `hash` locally - let validator_index = ValidatorIndex(2); // peer_c is the originator - let candidate_indices: CandidateBitfield = - vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap(); + let core_bitfields = vec![CoreIndex(0)].try_into().unwrap(); + let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfields); - let core_bitfields = vec![CoreIndex(0)].try_into().unwrap(); - let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfields); + // send the candidate 0 assignment from peer_b + let assignment = IndirectAssignmentCertV2 { + block_hash: hash, + validator: validator_index, + cert: cert.cert, + }; + let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![( + assignment, + (0 as CandidateIndex).into(), + )]); + send_message_from_peer_v3(overseer, &peer_d, msg).await; + provide_session(overseer, session.clone()).await; - // send the candidate 0 assignment from peer_b - let assignment = IndirectAssignmentCertV2 { - block_hash: hash, - validator: validator_index, - cert: cert.cert, - }; - let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![( - assignment, - (0 as CandidateIndex).into(), - )]); - send_message_from_peer_v3(overseer, &peer_d, msg).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - _, _, - tx, - )) => { - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); - expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert!(peers.len() >= 2); - assert!(peers.contains(&peer_a)); - assert!(peers.contains(&peer_b)); - assert_eq!(assignments.len(), 1); - } - ); + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, + _, + )) => { + assert_eq!(assignment.tranche(), 0); + } + ); + expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert!(peers.len() >= 2); + assert!(peers.contains(&peer_a)); + assert!(peers.contains(&peer_b)); + assert_eq!(assignments.len(), 1); + } + ); - let candidate_bitfields = vec![CoreIndex(1)].try_into().unwrap(); - let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields); + let candidate_bitfields = vec![CoreIndex(1)].try_into().unwrap(); + let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields); - // send the candidate 1 assignment from peer_c - let assignment = IndirectAssignmentCertV2 { - block_hash: hash, - validator: validator_index, - cert: cert.cert, - }; - let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![( - assignment, - (1 as CandidateIndex).into(), - )]); - - send_message_from_peer_v3(overseer, &peer_c, msg).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - _, _, - tx, - )) => { - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); - expect_reputation_change(overseer, &peer_c, BENEFIT_VALID_MESSAGE_FIRST).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert!(peers.len() >= 2); - assert!(peers.contains(&peer_b)); - assert!(peers.contains(&peer_a)); - assert_eq!(assignments.len(), 1); - } - ); - - // send an approval from peer_b - let approval = IndirectSignedApprovalVoteV2 { - block_hash: hash, - candidate_indices, - validator: validator_index, - signature: dummy_signature(), - }; - let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); - send_message_from_peer_v3(overseer, &peer_d, msg).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval( - vote, - tx, - )) => { - assert_eq!(vote, approval); - tx.send(ApprovalCheckResult::Accepted).unwrap(); - } - ); - - expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Approvals(approvals) - )) - )) => { - assert!(peers.len() >= 2); - assert!(peers.contains(&peer_b)); - assert!(peers.contains(&peer_a)); - assert_eq!(approvals.len(), 1); - } - ); - for candidate_index in 0..1 { - let (tx_distribution, rx_distribution) = oneshot::channel(); - let mut candidates_requesting_signatures = HashSet::new(); - candidates_requesting_signatures.insert((hash, candidate_index)); - overseer_send( - overseer, - ApprovalDistributionMessage::GetApprovalSignatures( - candidates_requesting_signatures, - tx_distribution, + // send the candidate 1 assignment from peer_c + let assignment = IndirectAssignmentCertV2 { + block_hash: hash, + validator: validator_index, + cert: cert.cert, + }; + let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![( + assignment, + (1 as CandidateIndex).into(), + )]); + + send_message_from_peer_v3(overseer, &peer_c, msg).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, _, + )) => { + assert_eq!(assignment.tranche(), 0); + } + ); + expect_reputation_change(overseer, &peer_c, BENEFIT_VALID_MESSAGE_FIRST).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert!(peers.len() >= 2); + assert!(peers.contains(&peer_b)); + assert!(peers.contains(&peer_a)); + assert_eq!(assignments.len(), 1); + } + ); + + // send an approval from peer_b + let approval = IndirectSignedApprovalVoteV2 { + block_hash: hash, + candidate_indices, + validator: validator_index, + signature: signature_for( + &keystore, + &session, + vec![candidate_hash_first, candidate_hash_second], + validator_index, ), - ) - .await; - let signatures = rx_distribution.await.unwrap(); + }; + let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); + send_message_from_peer_v3(overseer, &peer_d, msg).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval( + vote, _, + )) => { + assert_eq!(Into::::into(vote), approval); + } + ); - assert_eq!(signatures.len(), 1); - for (signing_validator, signature) in signatures { - assert_eq!(validator_index, signing_validator); - assert_eq!(signature.0, hash); - assert_eq!(signature.2, approval.signature); - assert_eq!(signature.1, vec![0, 1]); + expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Approvals(approvals) + )) + )) => { + assert!(peers.len() >= 2); + assert!(peers.contains(&peer_b)); + assert!(peers.contains(&peer_a)); + assert_eq!(approvals.len(), 1); + } + ); + for candidate_index in 0..1 { + let (tx_distribution, rx_distribution) = oneshot::channel(); + let mut candidates_requesting_signatures = HashSet::new(); + candidates_requesting_signatures.insert((hash, candidate_index)); + overseer_send( + overseer, + ApprovalDistributionMessage::GetApprovalSignatures( + candidates_requesting_signatures, + tx_distribution, + ), + ) + .await; + let signatures = rx_distribution.await.unwrap(); + + assert_eq!(signatures.len(), 1); + for (signing_validator, signature) in signatures { + assert_eq!(validator_index, signing_validator); + assert_eq!(signature.0, hash); + assert_eq!(signature.2, approval.signature); + assert_eq!(signature.1, vec![0, 1]); + } } - } - virtual_overseer - }); + virtual_overseer + }, + ); } // Tests that votes that cover multiple assignments candidates are correctly processed when unify @@ -1256,305 +1519,335 @@ fn unify_with_peer_multiple_assignments_covered_with_one_approval_vote() { let peer_d = peers.get(4).unwrap().0; let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); + let candidate_hash_first = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB)); + let candidate_hash_second = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xCC)); + + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + setup_peer_with_view(overseer, &peer_d, view![hash], ValidationVersion::V3).await; + let mut keystore = LocalKeystore::in_memory(); + let session = dummy_session_info_valid(1, &mut keystore, 5); + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![ + (candidate_hash_first, 0.into(), 0.into()), + (candidate_hash_second, 1.into(), 1.into()), + ], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Set up a gossip topology, where a, b, and c, d are topology neighbors to the node. + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), + ) + .await; - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - setup_peer_with_view(overseer, &peer_d, view![hash], ValidationVersion::V3).await; - - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 2], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Set up a gossip topology, where a, b, and c, d are topology neighbors to the node. - setup_gossip_topology( - overseer, - make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), - ) - .await; - - // import an assignment related to `hash` locally - let validator_index = ValidatorIndex(2); // peer_c is the originator - let candidate_indices: CandidateBitfield = - vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap(); + // import an assignment related to `hash` locally + let validator_index = ValidatorIndex(2); // peer_c is the originator + let candidate_indices: CandidateBitfield = + vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap(); - let core_bitfields = vec![CoreIndex(0)].try_into().unwrap(); - let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfields); + let core_bitfields = vec![CoreIndex(0)].try_into().unwrap(); + let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfields); - // send the candidate 0 assignment from peer_b - let assignment = IndirectAssignmentCertV2 { - block_hash: hash, - validator: validator_index, - cert: cert.cert, - }; - let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![( - assignment, - (0 as CandidateIndex).into(), - )]); - send_message_from_peer_v3(overseer, &peer_d, msg).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - _, _, - tx, - )) => { - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); - expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await; + // send the candidate 0 assignment from peer_b + let assignment = IndirectAssignmentCertV2 { + block_hash: hash, + validator: validator_index, + cert: cert.cert, + }; + let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![( + assignment, + (0 as CandidateIndex).into(), + )]); + send_message_from_peer_v3(overseer, &peer_d, msg).await; + provide_session(overseer, session.clone()).await; - let candidate_bitfields = vec![CoreIndex(1)].try_into().unwrap(); - let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields); + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, _, + )) => { + assert_eq!(assignment.tranche(), 0); + } + ); + expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await; - // send the candidate 1 assignment from peer_c - let assignment = IndirectAssignmentCertV2 { - block_hash: hash, - validator: validator_index, - cert: cert.cert, - }; - let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![( - assignment, - (1 as CandidateIndex).into(), - )]); - - send_message_from_peer_v3(overseer, &peer_d, msg).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - _, _, - tx, - )) => { - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); - expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await; - - // send an approval from peer_b - let approval = IndirectSignedApprovalVoteV2 { - block_hash: hash, - candidate_indices, - validator: validator_index, - signature: dummy_signature(), - }; - let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); - send_message_from_peer_v3(overseer, &peer_d, msg).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval( - vote, - tx, - )) => { - assert_eq!(vote, approval); - tx.send(ApprovalCheckResult::Accepted).unwrap(); - } - ); - - expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await; - - // setup peers with V2 protocol versions - setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await; - setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await; - let mut expected_peers_assignments = vec![peer_a, peer_b]; - let mut expected_peers_approvals = vec![peer_a, peer_b]; - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert!(peers.len() == 1); - assert!(expected_peers_assignments.contains(peers.first().unwrap())); - expected_peers_assignments.retain(|peer| peer != peers.first().unwrap()); - assert_eq!(assignments.len(), 2); - } - ); - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Approvals(approvals) - )) - )) => { - assert!(peers.len() == 1); - assert!(expected_peers_approvals.contains(peers.first().unwrap())); - expected_peers_approvals.retain(|peer| peer != peers.first().unwrap()); - assert_eq!(approvals.len(), 1); - } - ); - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert!(peers.len() == 1); - assert!(expected_peers_assignments.contains(peers.first().unwrap())); - expected_peers_assignments.retain(|peer| peer != peers.first().unwrap()); - assert_eq!(assignments.len(), 2); - } - ); - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Approvals(approvals) - )) - )) => { - assert!(peers.len() == 1); - assert!(expected_peers_approvals.contains(peers.first().unwrap())); - expected_peers_approvals.retain(|peer| peer != peers.first().unwrap()); - assert_eq!(approvals.len(), 1); - } - ); + let candidate_bitfields = vec![CoreIndex(1)].try_into().unwrap(); + let cert = fake_assignment_cert_v2(hash, validator_index, candidate_bitfields); - virtual_overseer - }); -} + // send the candidate 1 assignment from peer_c + let assignment = IndirectAssignmentCertV2 { + block_hash: hash, + validator: validator_index, + cert: cert.cert, + }; + let msg = protocol_v3::ApprovalDistributionMessage::Assignments(vec![( + assignment, + (1 as CandidateIndex).into(), + )]); -#[test] -fn import_approval_bad() { - let peer_a = PeerId::random(); - let peer_b = PeerId::random(); - let parent_hash = Hash::repeat_byte(0xFF); - let hash = Hash::repeat_byte(0xAA); + send_message_from_peer_v3(overseer, &peer_d, msg).await; - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // setup peers - setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await; - setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await; - - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; - let cert = fake_assignment_cert(hash, validator_index); - - // send the an approval from peer_b, we don't have an assignment yet - let approval = IndirectSignedApprovalVoteV2 { - block_hash: hash, - candidate_indices: candidate_index.into(), - validator: validator_index, - signature: dummy_signature(), - }; - let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); - send_message_from_peer_v3(overseer, &peer_b, msg).await; + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, _, + )) => { + assert_eq!(assignment.tranche(), 0); + } + ); + expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await; - expect_reputation_change(overseer, &peer_b, COST_UNEXPECTED_MESSAGE).await; + // send an approval from peer_b + let approval = IndirectSignedApprovalVoteV2 { + block_hash: hash, + candidate_indices, + validator: validator_index, + signature: signature_for( + &keystore, + &session, + vec![candidate_hash_first, candidate_hash_second], + validator_index, + ), + }; + let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); + send_message_from_peer_v3(overseer, &peer_d, msg).await; - // now import an assignment from peer_b - let assignments = vec![(cert.clone(), candidate_index)]; - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments); - send_message_from_peer(overseer, &peer_b, msg).await; + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval( + vote, _, + )) => { + assert_eq!(Into::::into(vote), approval); + } + ); - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - assignment, - i, - tx, - )) => { - assert_eq!(assignment, cert.into()); - assert_eq!(i, candidate_index.into()); - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); - - expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await; - - // and try again - let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); - send_message_from_peer_v3(overseer, &peer_b, msg).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval( - vote, - tx, - )) => { - assert_eq!(vote, approval); - tx.send(ApprovalCheckResult::Bad(ApprovalCheckError::UnknownBlock(hash))).unwrap(); - } - ); + expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE_FIRST).await; - expect_reputation_change(overseer, &peer_b, COST_INVALID_MESSAGE).await; - virtual_overseer - }); -} + // setup peers with V2 protocol versions + setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await; + setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await; + let mut expected_peers_assignments = vec![peer_a, peer_b]; + let mut expected_peers_approvals = vec![peer_a, peer_b]; + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert!(peers.len() == 1); + assert!(expected_peers_assignments.contains(peers.first().unwrap())); + expected_peers_assignments.retain(|peer| peer != peers.first().unwrap()); + assert_eq!(assignments.len(), 2); + } + ); -/// make sure we clean up the state on block finalized -#[test] -fn update_our_view() { - let parent_hash = Hash::repeat_byte(0xFF); + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Approvals(approvals) + )) + )) => { + assert!(peers.len() == 1); + assert!(expected_peers_approvals.contains(peers.first().unwrap())); + expected_peers_approvals.retain(|peer| peer != peers.first().unwrap()); + assert_eq!(approvals.len(), 1); + } + ); + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert!(peers.len() == 1); + assert!(expected_peers_assignments.contains(peers.first().unwrap())); + expected_peers_assignments.retain(|peer| peer != peers.first().unwrap()); + assert_eq!(assignments.len(), 2); + } + ); + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Approvals(approvals) + )) + )) => { + assert!(peers.len() == 1); + assert!(expected_peers_approvals.contains(peers.first().unwrap())); + expected_peers_approvals.retain(|peer| peer != peers.first().unwrap()); + assert_eq!(approvals.len(), 1); + } + ); + + virtual_overseer + }, + ); +} + +#[test] +fn import_approval_bad() { + let peer_a = PeerId::random(); + let peer_b = PeerId::random(); + let parent_hash = Hash::repeat_byte(0xFF); + let hash = Hash::repeat_byte(0xAA); + let candidate_hash = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB)); + + let diff_candidate_hash = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xCC)); + + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // setup peers + setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await; + setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await; + let mut keystore = LocalKeystore::in_memory(); + let session = dummy_session_info_valid(1, &mut keystore, 1); + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![(candidate_hash, 0.into(), 0.into()); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; + let cert = fake_assignment_cert(hash, validator_index); + + // Sign a different candidate hash. + let payload = + ApprovalVoteMultipleCandidates(&vec![diff_candidate_hash]).signing_payload(1); + let sign_key = session.validators.get(ValidatorIndex(0)).unwrap().clone(); + let signature = keystore + .sr25519_sign(ValidatorId::ID, &sign_key.into(), &payload[..]) + .unwrap() + .unwrap(); + + // send the an approval from peer_b, we don't have an assignment yet + let approval = IndirectSignedApprovalVoteV2 { + block_hash: hash, + candidate_indices: candidate_index.into(), + validator: validator_index, + signature: signature.into(), + }; + let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); + send_message_from_peer_v3(overseer, &peer_b, msg).await; + + expect_reputation_change(overseer, &peer_b, COST_UNEXPECTED_MESSAGE).await; + + // now import an assignment from peer_b + let assignments = vec![(cert.clone(), candidate_index)]; + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments); + send_message_from_peer(overseer, &peer_b, msg).await; + provide_session(overseer, session.clone()).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, + _, + )) => { + assert_eq!(assignment.assignment(), &cert.into()); + assert_eq!(assignment.candidate_indices(), &candidate_index.into()); + assert_eq!(assignment.tranche(), 0); + } + ); + + expect_reputation_change(overseer, &peer_b, BENEFIT_VALID_MESSAGE_FIRST).await; + + // and try again + let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); + send_message_from_peer_v3(overseer, &peer_b, msg).await; + + expect_reputation_change(overseer, &peer_b, COST_INVALID_MESSAGE).await; + virtual_overseer + }, + ); +} + +/// make sure we clean up the state on block finalized +#[test] +fn update_our_view() { + let parent_hash = Hash::repeat_byte(0xFF); let hash_a = Hash::repeat_byte(0xAA); let hash_b = Hash::repeat_byte(0xBB); let hash_c = Hash::repeat_byte(0xCC); - let state = test_harness(State::default(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // new block `hash_a` with 1 candidates - let meta_a = BlockApprovalMeta { - hash: hash_a, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let meta_b = BlockApprovalMeta { - hash: hash_b, - parent_hash: hash_a, - number: 2, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let meta_c = BlockApprovalMeta { - hash: hash_c, - parent_hash: hash_b, - number: 3, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]); - overseer_send(overseer, msg).await; - virtual_overseer - }); + let state = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + State::default(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // new block `hash_a` with 1 candidates + let meta_a = BlockApprovalMeta { + hash: hash_a, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let meta_b = BlockApprovalMeta { + hash: hash_b, + parent_hash: hash_a, + number: 2, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let meta_c = BlockApprovalMeta { + hash: hash_c, + parent_hash: hash_b, + number: 3, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]); + overseer_send(overseer, msg).await; + virtual_overseer + }, + ); assert!(state.blocks_by_number.get(&1).is_some()); assert!(state.blocks_by_number.get(&2).is_some()); @@ -1563,12 +1856,17 @@ fn update_our_view() { assert!(state.blocks.get(&hash_b).is_some()); assert!(state.blocks.get(&hash_c).is_some()); - let state = test_harness(state, |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // finalize a block - overseer_signal_block_finalized(overseer, 2).await; - virtual_overseer - }); + let state = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state, + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // finalize a block + overseer_signal_block_finalized(overseer, 2).await; + virtual_overseer + }, + ); assert!(state.blocks_by_number.get(&1).is_none()); assert!(state.blocks_by_number.get(&2).is_none()); @@ -1577,12 +1875,17 @@ fn update_our_view() { assert!(state.blocks.get(&hash_b).is_none()); assert!(state.blocks.get(&hash_c).is_some()); - let state = test_harness(state, |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // finalize a very high block - overseer_signal_block_finalized(overseer, 4_000_000_000).await; - virtual_overseer - }); + let state = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state, + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // finalize a very high block + overseer_signal_block_finalized(overseer, 4_000_000_000).await; + virtual_overseer + }, + ); assert!(state.blocks_by_number.get(&3).is_none()); assert!(state.blocks.get(&hash_c).is_none()); @@ -1600,81 +1903,89 @@ fn update_peer_view() { let peer_a = peers.first().unwrap().0; let peer = &peer_a; - let state = test_harness(State::default(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // new block `hash_a` with 1 candidates - let meta_a = BlockApprovalMeta { - hash: hash_a, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let meta_b = BlockApprovalMeta { - hash: hash_b, - parent_hash: hash_a, - number: 2, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let meta_c = BlockApprovalMeta { - hash: hash_c, - parent_hash: hash_b, - number: 3, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; + let state = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + State::default(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // new block `hash_a` with 1 candidates + let meta_a = BlockApprovalMeta { + hash: hash_a, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let meta_b = BlockApprovalMeta { + hash: hash_b, + parent_hash: hash_a, + number: 2, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let meta_c = BlockApprovalMeta { + hash: hash_c, + parent_hash: hash_b, + number: 3, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]); + overseer_send(overseer, msg).await; + + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Setup a topology where peer_a is neighbor to current node. + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1), + ) + .await; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]); - overseer_send(overseer, msg).await; - - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Setup a topology where peer_a is neighbor to current node. - setup_gossip_topology( - overseer, - make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1), - ) - .await; + let cert_a = fake_assignment_cert(hash_a, ValidatorIndex(0)); + let cert_b = fake_assignment_cert(hash_b, ValidatorIndex(0)); - let cert_a = fake_assignment_cert(hash_a, ValidatorIndex(0)); - let cert_b = fake_assignment_cert(hash_b, ValidatorIndex(0)); + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment(cert_a.into(), 0.into()), + ) + .await; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment(cert_a.into(), 0.into()), - ) - .await; + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment(cert_b.into(), 0.into()), + ) + .await; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment(cert_b.into(), 0.into()), - ) - .await; + // connect a peer + setup_peer_with_view(overseer, peer, view![hash_a], ValidationVersion::V1).await; - // connect a peer - setup_peer_with_view(overseer, peer, view![hash_a], ValidationVersion::V1).await; - - // we should send relevant assignments to the peer - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers.len(), 1); - assert_eq!(assignments.len(), 1); - } - ); - virtual_overseer - }); + // we should send relevant assignments to the peer + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(assignments.len(), 1); + } + ); + virtual_overseer + }, + ); assert_eq!(state.peer_views.get(peer).map(|v| v.view.finalized_number), Some(0)); assert_eq!( @@ -1691,42 +2002,49 @@ fn update_peer_view() { 1, ); - let state = test_harness(state, |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // update peer's view - overseer_send( - overseer, - ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerViewChange( - *peer, - View::new(vec![hash_b, hash_c, hash_d], 2), - )), - ) - .await; + let state = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state, + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // update peer's view + overseer_send( + overseer, + ApprovalDistributionMessage::NetworkBridgeUpdate( + NetworkBridgeEvent::PeerViewChange( + *peer, + View::new(vec![hash_b, hash_c, hash_d], 2), + ), + ), + ) + .await; - let cert_c = fake_assignment_cert(hash_c, ValidatorIndex(0)); + let cert_c = fake_assignment_cert(hash_c, ValidatorIndex(0)); - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment(cert_c.clone().into(), 0.into()), - ) - .await; + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment(cert_c.clone().into(), 0.into()), + ) + .await; - // we should send relevant assignments to the peer - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers.len(), 1); - assert_eq!(assignments.len(), 1); - assert_eq!(assignments[0].0, cert_c); - } - ); - virtual_overseer - }); + // we should send relevant assignments to the peer + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(assignments.len(), 1); + assert_eq!(assignments[0].0, cert_c); + } + ); + virtual_overseer + }, + ); assert_eq!(state.peer_views.get(peer).map(|v| v.view.finalized_number), Some(2)); assert_eq!( @@ -1744,19 +2062,26 @@ fn update_peer_view() { ); let finalized_number = 4_000_000_000; - let state = test_harness(state, |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // update peer's view - overseer_send( - overseer, - ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerViewChange( - *peer, - View::with_finalized(finalized_number), - )), - ) - .await; - virtual_overseer - }); + let state = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state, + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // update peer's view + overseer_send( + overseer, + ApprovalDistributionMessage::NetworkBridgeUpdate( + NetworkBridgeEvent::PeerViewChange( + *peer, + View::with_finalized(finalized_number), + ), + ), + ) + .await; + virtual_overseer + }, + ); assert_eq!(state.peer_views.get(peer).map(|v| v.view.finalized_number), Some(finalized_number)); assert!(state.blocks.get(&hash_c).unwrap().known_by.get(peer).is_none()); @@ -1779,164 +2104,176 @@ fn update_peer_authority_id() { // Y neighbour, we simulate that PeerId is not known in the beginning. let neighbour_y = peers.get(neighbour_y_index).unwrap().0; - let _state = test_harness(State::default(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // new block `hash_a` with 1 candidates - let meta_a = BlockApprovalMeta { - hash: hash_a, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let meta_b = BlockApprovalMeta { - hash: hash_b, - parent_hash: hash_a, - number: 2, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let meta_c = BlockApprovalMeta { - hash: hash_c, - parent_hash: hash_b, - number: 3, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; + let _state = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + State::default(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // new block `hash_a` with 1 candidates + let meta_a = BlockApprovalMeta { + hash: hash_a, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let meta_b = BlockApprovalMeta { + hash: hash_b, + parent_hash: hash_a, + number: 2, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let meta_c = BlockApprovalMeta { + hash: hash_c, + parent_hash: hash_b, + number: 3, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]); + overseer_send(overseer, msg).await; + + let peers_with_optional_peer_id = peers + .iter() + .enumerate() + .map(|(index, (peer_id, authority))| { + (if index == 0 { None } else { Some(*peer_id) }, authority.clone()) + }) + .collect_vec(); + + // Setup a topology where peer_a is neighbor to current node. + setup_gossip_topology( + overseer, + make_gossip_topology( + 1, + &peers_with_optional_peer_id, + &[neighbour_x_index], + &[neighbour_y_index], + local_index, + ), + ) + .await; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta_a, meta_b, meta_c]); - overseer_send(overseer, msg).await; + let cert_a = fake_assignment_cert(hash_a, ValidatorIndex(local_index as u32)); + let cert_b = fake_assignment_cert(hash_b, ValidatorIndex(local_index as u32)); - let peers_with_optional_peer_id = peers - .iter() - .enumerate() - .map(|(index, (peer_id, authority))| { - (if index == 0 { None } else { Some(*peer_id) }, authority.clone()) - }) - .collect_vec(); - - // Setup a topology where peer_a is neighbor to current node. - setup_gossip_topology( - overseer, - make_gossip_topology( - 1, - &peers_with_optional_peer_id, - &[neighbour_x_index], - &[neighbour_y_index], - local_index, - ), - ) - .await; + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment(cert_a.into(), 0.into()), + ) + .await; - let cert_a = fake_assignment_cert(hash_a, ValidatorIndex(local_index as u32)); - let cert_b = fake_assignment_cert(hash_b, ValidatorIndex(local_index as u32)); + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment(cert_b.into(), 0.into()), + ) + .await; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment(cert_a.into(), 0.into()), - ) - .await; + // connect a peer + setup_peer_with_view(overseer, &neighbour_x, view![hash_a], ValidationVersion::V1) + .await; + setup_peer_with_view(overseer, &neighbour_y, view![hash_a], ValidationVersion::V1) + .await; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment(cert_b.into(), 0.into()), - ) - .await; + setup_peer_with_view(overseer, &neighbour_x, view![hash_b], ValidationVersion::V1) + .await; + setup_peer_with_view(overseer, &neighbour_y, view![hash_b], ValidationVersion::V1) + .await; - // connect a peer - setup_peer_with_view(overseer, &neighbour_x, view![hash_a], ValidationVersion::V1).await; - setup_peer_with_view(overseer, &neighbour_y, view![hash_a], ValidationVersion::V1).await; - - setup_peer_with_view(overseer, &neighbour_x, view![hash_b], ValidationVersion::V1).await; - setup_peer_with_view(overseer, &neighbour_y, view![hash_b], ValidationVersion::V1).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers.len(), 1); - assert_eq!(assignments.len(), 1); - assert_eq!(peers.get(0), Some(&neighbour_y)); - } - ); - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers.len(), 1); - assert_eq!(assignments.len(), 1); - assert_eq!(peers.get(0), Some(&neighbour_y)); - } - ); - - overseer_send( - overseer, - ApprovalDistributionMessage::NetworkBridgeUpdate( - NetworkBridgeEvent::UpdatedAuthorityIds( - peers[neighbour_x_index].0, - [peers[neighbour_x_index].1.clone()].into_iter().collect(), + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(assignments.len(), 1); + assert_eq!(peers.get(0), Some(&neighbour_y)); + } + ); + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(assignments.len(), 1); + assert_eq!(peers.get(0), Some(&neighbour_y)); + } + ); + + overseer_send( + overseer, + ApprovalDistributionMessage::NetworkBridgeUpdate( + NetworkBridgeEvent::UpdatedAuthorityIds( + peers[neighbour_x_index].0, + [peers[neighbour_x_index].1.clone()].into_iter().collect(), + ), ), - ), - ) - .await; + ) + .await; - // we should send relevant assignments to the peer, after we found it's peer id. - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - gum::info!(target: LOG_TARGET, ?peers, ?assignments); - assert_eq!(peers.len(), 1); - assert_eq!(assignments.len(), 2); - assert_eq!(assignments.get(0).unwrap().0.block_hash, hash_a); - assert_eq!(assignments.get(1).unwrap().0.block_hash, hash_b); - assert_eq!(peers.get(0), Some(&neighbour_x)); - } - ); - - overseer_send( - overseer, - ApprovalDistributionMessage::NetworkBridgeUpdate( - NetworkBridgeEvent::UpdatedAuthorityIds( - peers[neighbour_y_index].0, - [peers[neighbour_y_index].1.clone()].into_iter().collect(), + // we should send relevant assignments to the peer, after we found it's peer id. + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + gum::info!(target: LOG_TARGET, ?peers, ?assignments); + assert_eq!(peers.len(), 1); + assert_eq!(assignments.len(), 2); + assert_eq!(assignments.get(0).unwrap().0.block_hash, hash_a); + assert_eq!(assignments.get(1).unwrap().0.block_hash, hash_b); + assert_eq!(peers.get(0), Some(&neighbour_x)); + } + ); + + overseer_send( + overseer, + ApprovalDistributionMessage::NetworkBridgeUpdate( + NetworkBridgeEvent::UpdatedAuthorityIds( + peers[neighbour_y_index].0, + [peers[neighbour_y_index].1.clone()].into_iter().collect(), + ), ), - ), - ) - .await; - overseer_send( - overseer, - ApprovalDistributionMessage::NetworkBridgeUpdate( - NetworkBridgeEvent::UpdatedAuthorityIds( - peers[neighbour_x_index].0, - [peers[neighbour_x_index].1.clone()].into_iter().collect(), + ) + .await; + overseer_send( + overseer, + ApprovalDistributionMessage::NetworkBridgeUpdate( + NetworkBridgeEvent::UpdatedAuthorityIds( + peers[neighbour_x_index].0, + [peers[neighbour_x_index].1.clone()].into_iter().collect(), + ), ), - ), - ) - .await; - assert!( - overseer.recv().timeout(TIMEOUT).await.is_none(), - "no message should be sent peers are already known" - ); + ) + .await; + assert!( + overseer.recv().timeout(TIMEOUT).await.is_none(), + "no message should be sent peers are already known" + ); - virtual_overseer - }); + virtual_overseer + }, + ); } /// E.g. if someone copies the keys... @@ -1945,89 +2282,105 @@ fn import_remotely_then_locally() { let peer_a = PeerId::random(); let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); + let candidate_hash = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB)); let peer = &peer_a; - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // setup the peer - setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; - - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - // import the assignment remotely first - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; - let cert = fake_assignment_cert(hash, validator_index); - let assignments = vec![(cert.clone(), candidate_index)]; - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); - send_message_from_peer(overseer, peer, msg).await; - - // send an `Accept` message from the Approval Voting subsystem - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - assignment, - i, - tx, - )) => { - assert_eq!(assignment, cert.clone().into()); - assert_eq!(i, candidate_index.into()); - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // setup the peer + setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; + let mut keystore = LocalKeystore::in_memory(); + + let session = dummy_session_info_valid(1, &mut keystore, 1); + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![(candidate_hash, 0.into(), 0.into()); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + let payload = ApprovalVoteMultipleCandidates(&vec![candidate_hash]).signing_payload(1); + let sign_key = session.validators.get(ValidatorIndex(0)).unwrap().clone(); + let signature = keystore + .sr25519_sign(ValidatorId::ID, &sign_key.into(), &payload[..]) + .unwrap() + .unwrap(); + + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // import the assignment remotely first + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; + let cert = fake_assignment_cert(hash, validator_index); + let assignments = vec![(cert.clone(), candidate_index)]; + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer(overseer, peer, msg).await; + provide_session(overseer, session.clone()).await; + + // send an `Accept` message from the Approval Voting subsystem + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, + _, + )) => { + assert_eq!(assignment.assignment(), &cert.clone().into()); + assert_eq!(assignment.candidate_indices(), &candidate_index.into()); + assert_eq!(assignment.tranche(), 0); + } + ); - expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await; + expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await; - // import the same assignment locally - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment( - cert.clone().into(), - candidate_index.into(), - ), - ) - .await; + // import the same assignment locally + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment( + cert.clone().into(), + candidate_index.into(), + ), + ) + .await; - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - // send the approval remotely - let approval = IndirectSignedApprovalVoteV2 { - block_hash: hash, - candidate_indices: candidate_index.into(), - validator: validator_index, - signature: dummy_signature(), - }; - let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); - send_message_from_peer_v3(overseer, peer, msg).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval( - vote, - tx, - )) => { - assert_eq!(vote, approval); - tx.send(ApprovalCheckResult::Accepted).unwrap(); - } - ); - expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await; + // send the approval remotely + let approval = IndirectSignedApprovalVoteV2 { + block_hash: hash, + candidate_indices: candidate_index.into(), + validator: validator_index, + signature: signature.into(), + }; + let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); + send_message_from_peer_v3(overseer, peer, msg).await; - // import the same approval locally - overseer_send(overseer, ApprovalDistributionMessage::DistributeApproval(approval)).await; + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval( + vote, _, + )) => { + assert_eq!(Into::::into(vote), approval); + } + ); + expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await; - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); + // import the same approval locally + overseer_send(overseer, ApprovalDistributionMessage::DistributeApproval(approval)) + .await; + + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); } #[test] @@ -2038,94 +2391,100 @@ fn sends_assignments_even_when_state_is_approved() { let hash = Hash::repeat_byte(0xAA); let peer = &peer_a; - let _ = test_harness(State::default(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + State::default(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Setup a topology where peer_a is neighbor to current node. + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1), + ) + .await; - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Setup a topology where peer_a is neighbor to current node. - setup_gossip_topology( - overseer, - make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1), - ) - .await; + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; + // import an assignment and approval locally. + let cert = fake_assignment_cert(hash, validator_index); + let approval = IndirectSignedApprovalVote { + block_hash: hash, + candidate_index, + validator: validator_index, + signature: dummy_signature(), + }; - // import an assignment and approval locally. - let cert = fake_assignment_cert(hash, validator_index); - let approval = IndirectSignedApprovalVote { - block_hash: hash, - candidate_index, - validator: validator_index, - signature: dummy_signature(), - }; + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment( + cert.clone().into(), + candidate_index.into(), + ), + ) + .await; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment( - cert.clone().into(), - candidate_index.into(), - ), - ) - .await; + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeApproval(approval.clone().into()), + ) + .await; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeApproval(approval.clone().into()), - ) - .await; + // connect the peer. + setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; - // connect the peer. - setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; - - let assignments = vec![(cert.clone(), candidate_index)]; - let approvals = vec![approval.clone()]; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) - )) - )) => { - assert_eq!(peers, vec![*peer]); - assert_eq!(sent_assignments, assignments); - } - ); - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) - )) - )) => { - assert_eq!(peers, vec![*peer]); - assert_eq!(sent_approvals, approvals); - } - ); + let assignments = vec![(cert.clone(), candidate_index)]; + let approvals = vec![approval.clone()]; - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + )) + )) => { + assert_eq!(peers, vec![*peer]); + assert_eq!(sent_assignments, assignments); + } + ); + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) + )) + )) => { + assert_eq!(peers, vec![*peer]); + assert_eq!(sent_approvals, approvals); + } + ); + + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); } /// Same as `sends_assignments_even_when_state_is_approved_v2` but with `VRFModuloCompact` @@ -2138,393 +2497,589 @@ fn sends_assignments_even_when_state_is_approved_v2() { let hash = Hash::repeat_byte(0xAA); let peer = &peer_a; - let _ = test_harness(State::default(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + State::default(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![Default::default(); 4], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Setup a topology where peer_a is neighbor to current node. + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1), + ) + .await; - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 4], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Setup a topology where peer_a is neighbor to current node. - setup_gossip_topology( - overseer, - make_gossip_topology(1, &peers_with_optional_peer_id, &[0], &[2], 1), - ) - .await; + let validator_index = ValidatorIndex(0); + let cores = vec![0, 1, 2, 3]; + let candidate_bitfield: CandidateBitfield = cores.clone().try_into().unwrap(); + + let core_bitfield: CoreBitfield = cores + .iter() + .map(|index| CoreIndex(*index)) + .collect::>() + .try_into() + .unwrap(); + + let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield.clone()); + + // Assumes candidate index == core index. + let approvals = cores + .iter() + .map(|core| IndirectSignedApprovalVoteV2 { + block_hash: hash, + candidate_indices: (*core).into(), + validator: validator_index, + signature: dummy_signature(), + }) + .collect::>(); + + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment( + cert.clone().into(), + candidate_bitfield.clone(), + ), + ) + .await; + + for approval in &approvals { + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeApproval(approval.clone()), + ) + .await; + } + + // connect the peer. + setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V3).await; + + let assignments = vec![(cert.clone(), candidate_bitfield.clone())]; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Assignments(sent_assignments) + )) + )) => { + assert_eq!(peers, vec![*peer]); + assert_eq!(sent_assignments, assignments); + } + ); + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Approvals(sent_approvals) + )) + )) => { + // Construct a hashmaps of approvals for comparison. Approval distribution reorders messages because they are kept in a + // hashmap as well. + let sent_approvals = sent_approvals.into_iter().map(|approval| (approval.candidate_indices.clone(), approval)).collect::>(); + let approvals = approvals.into_iter().map(|approval| (approval.candidate_indices.clone(), approval)).collect::>(); + + assert_eq!(peers, vec![*peer]); + assert_eq!(sent_approvals, approvals); + } + ); + + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); +} + +/// +/// +/// 1. Receive remote peer view update with an unknown head +/// 2. Receive assignments for that unknown head +/// 3. Update our view and import the new block +/// 4. Expect that no reputation with `COST_UNEXPECTED_MESSAGE` is applied +#[test] +fn race_condition_in_local_vs_remote_view_update() { + let parent_hash = Hash::repeat_byte(0xFF); + let peer_a = PeerId::random(); + let hash_b = Hash::repeat_byte(0xBB); - let validator_index = ValidatorIndex(0); - let cores = vec![0, 1, 2, 3]; - let candidate_bitfield: CandidateBitfield = cores.clone().try_into().unwrap(); + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + let peer = &peer_a; + + // Test a small number of candidates + let candidates_count = 1; + let meta = BlockApprovalMeta { + hash: hash_b, + parent_hash, + number: 2, + candidates: vec![Default::default(); candidates_count], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + // This will send a peer view that is ahead of our view + setup_peer_with_view(overseer, peer, view![hash_b], ValidationVersion::V1).await; + + // Send our view update to include a new head + overseer_send( + overseer, + ApprovalDistributionMessage::NetworkBridgeUpdate( + NetworkBridgeEvent::OurViewChange(our_view![hash_b]), + ), + ) + .await; - let core_bitfield: CoreBitfield = cores - .iter() - .map(|index| CoreIndex(*index)) - .collect::>() - .try_into() - .unwrap(); + // send assignments related to `hash_b` but they will come to the MessagesPending + let assignments: Vec<_> = (0..candidates_count) + .map(|candidate_index| { + let validator_index = ValidatorIndex(candidate_index as u32); + let cert = fake_assignment_cert(hash_b, validator_index); + (cert, candidate_index as u32) + }) + .collect(); - let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield.clone()); + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer(overseer, peer, msg.clone()).await; + + // This will handle pending messages being processed + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; + + for i in 0..candidates_count { + // Previously, this has caused out-of-view assignments/approvals + //expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, + _, + )) => { + assert_eq!(assignment.assignment(), &assignments[i].0.clone().into()); + assert_eq!(assignment.candidate_indices(), &assignments[i].1.into()); + assert_eq!(assignment.tranche(), 0); + } + ); + + // Since we have a valid statement pending, this should always occur + expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await; + } + virtual_overseer + }, + ); +} - // Assumes candidate index == core index. - let approvals = cores - .iter() - .map(|core| IndirectSignedApprovalVoteV2 { +// Tests that local messages propagate to both dimensions. +#[test] +fn propagates_locally_generated_assignment_to_both_dimensions() { + let parent_hash = Hash::repeat_byte(0xFF); + let hash = Hash::repeat_byte(0xAA); + + let peers = make_peers_and_authority_ids(100); + + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + State::default(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + + // Connect all peers. + for (peer, _) in &peers { + setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; + } + + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + + // Set up a gossip topology. + setup_gossip_topology( + overseer, + make_gossip_topology( + 1, + &peers_with_optional_peer_id, + &[0, 10, 20, 30, 40, 60, 70, 80], + &[50, 51, 52, 53, 54, 55, 56, 57], + 1, + ), + ) + .await; + + let expected_indices = [ + // Both dimensions in the gossip topology + 0, 10, 20, 30, 40, 60, 70, 80, 50, 51, 52, 53, 54, 55, 56, 57, + ]; + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; + + // import an assignment and approval locally. + let cert = fake_assignment_cert(hash, validator_index); + let approval = IndirectSignedApprovalVote { block_hash: hash, - candidate_indices: (*core).into(), + candidate_index, validator: validator_index, - signature: dummy_signature(), - }) - .collect::>(); - - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment( - cert.clone().into(), - candidate_bitfield.clone(), - ), - ) - .await; + signature: dummy_signature(), + }; - for approval in &approvals { overseer_send( overseer, - ApprovalDistributionMessage::DistributeApproval(approval.clone()), + ApprovalDistributionMessage::DistributeAssignment( + cert.clone().into(), + candidate_index.into(), + ), ) .await; - } - - // connect the peer. - setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V3).await; - - let assignments = vec![(cert.clone(), candidate_bitfield.clone())]; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Assignments(sent_assignments) - )) - )) => { - assert_eq!(peers, vec![*peer]); - assert_eq!(sent_assignments, assignments); - } - ); - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( - protocol_v3::ApprovalDistributionMessage::Approvals(sent_approvals) - )) - )) => { - // Construct a hashmaps of approvals for comparison. Approval distribution reorders messages because they are kept in a - // hashmap as well. - let sent_approvals = sent_approvals.into_iter().map(|approval| (approval.candidate_indices.clone(), approval)).collect::>(); - let approvals = approvals.into_iter().map(|approval| (approval.candidate_indices.clone(), approval)).collect::>(); - - assert_eq!(peers, vec![*peer]); - assert_eq!(sent_approvals, approvals); - } - ); - - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); -} - -/// -/// -/// 1. Receive remote peer view update with an unknown head -/// 2. Receive assignments for that unknown head -/// 3. Update our view and import the new block -/// 4. Expect that no reputation with `COST_UNEXPECTED_MESSAGE` is applied -#[test] -fn race_condition_in_local_vs_remote_view_update() { - let parent_hash = Hash::repeat_byte(0xFF); - let peer_a = PeerId::random(); - let hash_b = Hash::repeat_byte(0xBB); - - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - let peer = &peer_a; - - // Test a small number of candidates - let candidates_count = 1; - let meta = BlockApprovalMeta { - hash: hash_b, - parent_hash, - number: 2, - candidates: vec![Default::default(); candidates_count], - slot: 1.into(), - session: 1, - }; - - // This will send a peer view that is ahead of our view - setup_peer_with_view(overseer, peer, view![hash_b], ValidationVersion::V1).await; - - // Send our view update to include a new head - overseer_send( - overseer, - ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::OurViewChange( - our_view![hash_b], - )), - ) - .await; - - // send assignments related to `hash_b` but they will come to the MessagesPending - let assignments: Vec<_> = (0..candidates_count) - .map(|candidate_index| { - let validator_index = ValidatorIndex(candidate_index as u32); - let cert = fake_assignment_cert(hash_b, validator_index); - (cert, candidate_index as u32) - }) - .collect(); - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); - send_message_from_peer(overseer, peer, msg.clone()).await; + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeApproval(approval.clone().into()), + ) + .await; - // This will handle pending messages being processed - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; + let assignments = vec![(cert.clone(), candidate_index)]; + let approvals = vec![approval.clone()]; - for i in 0..candidates_count { - // Previously, this has caused out-of-view assignments/approvals - //expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await; + let mut assignment_sent_peers = assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + )) + )) => { + assert_eq!(sent_peers.len(), expected_indices.len() + 4); + for &i in &expected_indices { + assert!( + sent_peers.contains(&peers[i].0), + "Message not sent to expected peer {}", + i, + ); + } + assert_eq!(sent_assignments, assignments); + sent_peers + } + ); assert_matches!( overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - assignment, - claimed_candidate_index, - tx, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + mut sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) + )) )) => { - assert_eq!(assignment, assignments[i].0.clone().into()); - assert_eq!(claimed_candidate_index, assignments[i].1.into()); - tx.send(AssignmentCheckResult::Accepted).unwrap(); + // Random sampling is reused from the assignment. + sent_peers.sort(); + assignment_sent_peers.sort(); + assert_eq!(sent_peers, assignment_sent_peers); + assert_eq!(sent_approvals, approvals); } ); - // Since we have a valid statement pending, this should always occur - expect_reputation_change(overseer, peer, BENEFIT_VALID_MESSAGE_FIRST).await; - } - virtual_overseer - }); + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); } -// Tests that local messages propagate to both dimensions. +// Tests that messages propagate to the unshared dimension. #[test] -fn propagates_locally_generated_assignment_to_both_dimensions() { +fn propagates_assignments_along_unshared_dimension() { let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); let peers = make_peers_and_authority_ids(100); - let _ = test_harness(State::default(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; - // Connect all peers. - for (peer, _) in &peers { - setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; - } + // Connect all peers. + for (peer, _) in &peers { + setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; + } - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - - // Set up a gossip topology. - setup_gossip_topology( - overseer, - make_gossip_topology( - 1, - &peers_with_optional_peer_id, - &[0, 10, 20, 30, 40, 60, 70, 80], - &[50, 51, 52, 53, 54, 55, 56, 57], - 1, - ), - ) - .await; + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); - let expected_indices = [ - // Both dimensions in the gossip topology - 0, 10, 20, 30, 40, 60, 70, 80, 50, 51, 52, 53, 54, 55, 56, 57, - ]; - - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; + // Set up a gossip topology. + setup_gossip_topology( + overseer, + make_gossip_topology( + 1, + &peers_with_optional_peer_id, + &[0, 10, 20, 30], + &[50, 51, 52, 53], + 1, + ), + ) + .await; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // Test messages from X direction go to Y peers + { + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; + + // import an assignment and approval locally. + let cert = fake_assignment_cert(hash, validator_index); + let assignments = vec![(cert.clone(), candidate_index)]; + + let msg = + protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + + // Issuer of the message is important, not the peer we receive from. + // 99 deliberately chosen because it's not in X or Y. + send_message_from_peer(overseer, &peers[99].0, msg).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, _, + )) => { + assert_eq!(assignment.tranche(), 0); + } + ); + expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await; - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; + let expected_y = [50, 51, 52, 53]; - // import an assignment and approval locally. - let cert = fake_assignment_cert(hash, validator_index); - let approval = IndirectSignedApprovalVote { - block_hash: hash, - candidate_index, - validator: validator_index, - signature: dummy_signature(), - }; + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + )) + )) => { + assert_eq!(sent_peers.len(), expected_y.len() + 4); + for &i in &expected_y { + assert!( + sent_peers.contains(&peers[i].0), + "Message not sent to expected peer {}", + i, + ); + } + assert_eq!(sent_assignments, assignments); + } + ); + }; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment( - cert.clone().into(), - candidate_index.into(), - ), - ) - .await; + // Test messages from X direction go to Y peers + { + let validator_index = ValidatorIndex(50); + let candidate_index = 0u32; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeApproval(approval.clone().into()), - ) - .await; + // import an assignment and approval locally. + let cert = fake_assignment_cert(hash, validator_index); + let assignments = vec![(cert.clone(), candidate_index)]; - let assignments = vec![(cert.clone(), candidate_index)]; - let approvals = vec![approval.clone()]; - - let mut assignment_sent_peers = assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) - )) - )) => { - assert_eq!(sent_peers.len(), expected_indices.len() + 4); - for &i in &expected_indices { - assert!( - sent_peers.contains(&peers[i].0), - "Message not sent to expected peer {}", - i, - ); - } - assert_eq!(sent_assignments, assignments); - sent_peers - } - ); - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - mut sent_peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) - )) - )) => { - // Random sampling is reused from the assignment. - sent_peers.sort(); - assignment_sent_peers.sort(); - assert_eq!(sent_peers, assignment_sent_peers); - assert_eq!(sent_approvals, approvals); - } - ); + let msg = + protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); + // Issuer of the message is important, not the peer we receive from. + // 99 deliberately chosen because it's not in X or Y. + send_message_from_peer(overseer, &peers[99].0, msg).await; + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, _, + )) => { + assert_eq!(assignment.tranche(), 0); + } + ); + expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await; + + let expected_x = [0, 10, 20, 30]; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + )) + )) => { + assert_eq!(sent_peers.len(), expected_x.len() + 4); + for &i in &expected_x { + assert!( + sent_peers.contains(&peers[i].0), + "Message not sent to expected peer {}", + i, + ); + } + assert_eq!(sent_assignments, assignments); + } + ); + }; + + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); } -// Tests that messages propagate to the unshared dimension. +// tests that messages are propagated to necessary peers after they connect #[test] -fn propagates_assignments_along_unshared_dimension() { +fn propagates_to_required_after_connect() { let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); let peers = make_peers_and_authority_ids(100); - let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - - // Connect all peers. - for (peer, _) in &peers { - setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; - } + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + State::default(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - - // Set up a gossip topology. - setup_gossip_topology( - overseer, - make_gossip_topology( - 1, - &peers_with_optional_peer_id, - &[0, 10, 20, 30], - &[50, 51, 52, 53], - 1, - ), - ) - .await; + let omitted = [0, 10, 50, 51]; - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; + // Connect all peers except omitted. + for (i, (peer, _)) in peers.iter().enumerate() { + if !omitted.contains(&i) { + setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; + } + } + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Set up a gossip topology. + setup_gossip_topology( + overseer, + make_gossip_topology( + 1, + &peers_with_optional_peer_id, + &[0, 10, 20, 30, 40, 60, 70, 80], + &[50, 51, 52, 53, 54, 55, 56, 57], + 1, + ), + ) + .await; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; + let expected_indices = [ + // Both dimensions in the gossip topology, minus omitted. + 20, 30, 40, 60, 70, 80, 52, 53, 54, 55, 56, 57, + ]; + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; - // Test messages from X direction go to Y peers - { let validator_index = ValidatorIndex(0); let candidate_index = 0u32; // import an assignment and approval locally. let cert = fake_assignment_cert(hash, validator_index); - let assignments = vec![(cert.clone(), candidate_index)]; + let approval = IndirectSignedApprovalVote { + block_hash: hash, + candidate_index, + validator: validator_index, + signature: dummy_signature(), + }; - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment( + cert.clone().into(), + candidate_index.into(), + ), + ) + .await; - // Issuer of the message is important, not the peer we receive from. - // 99 deliberately chosen because it's not in X or Y. - send_message_from_peer(overseer, &peers[99].0, msg).await; - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - _, - _, - tx, - )) => { - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); - expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await; + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeApproval(approval.clone().into()), + ) + .await; - let expected_y = [50, 51, 52, 53]; + let assignments = vec![(cert.clone(), candidate_index)]; + let approvals = vec![approval.clone()]; - assert_matches!( + let mut assignment_sent_peers = assert_matches!( overseer_recv(overseer).await, AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( sent_peers, @@ -2532,8 +3087,8 @@ fn propagates_assignments_along_unshared_dimension() { protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) )) )) => { - assert_eq!(sent_peers.len(), expected_y.len() + 4); - for &i in &expected_y { + assert_eq!(sent_peers.len(), expected_indices.len() + 4); + for &i in &expected_indices { assert!( sent_peers.contains(&peers[i].0), "Message not sent to expected peer {}", @@ -2541,1123 +3096,1029 @@ fn propagates_assignments_along_unshared_dimension() { ); } assert_eq!(sent_assignments, assignments); + sent_peers } ); - }; - // Test messages from X direction go to Y peers - { - let validator_index = ValidatorIndex(50); + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + mut sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) + )) + )) => { + // Random sampling is reused from the assignment. + sent_peers.sort(); + assignment_sent_peers.sort(); + assert_eq!(sent_peers, assignment_sent_peers); + assert_eq!(sent_approvals, approvals); + } + ); + + for i in omitted.iter().copied() { + setup_peer_with_view(overseer, &peers[i].0, view![hash], ValidationVersion::V1) + .await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + )) + )) => { + assert_eq!(sent_peers.len(), 1); + assert_eq!(&sent_peers[0], &peers[i].0); + assert_eq!(sent_assignments, assignments); + } + ); + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) + )) + )) => { + assert_eq!(sent_peers.len(), 1); + assert_eq!(&sent_peers[0], &peers[i].0); + assert_eq!(sent_approvals, approvals); + } + ); + } + + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); +} + +// test that new gossip topology triggers send of messages. +#[test] +fn sends_to_more_peers_after_getting_topology() { + let parent_hash = Hash::repeat_byte(0xFF); + let hash = Hash::repeat_byte(0xAA); + + let peers = make_peers_and_authority_ids(100); + + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + State::default(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + + // Connect all peers except omitted. + for (peer, _) in &peers { + setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; + } + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + let validator_index = ValidatorIndex(0); let candidate_index = 0u32; // import an assignment and approval locally. let cert = fake_assignment_cert(hash, validator_index); + let approval = IndirectSignedApprovalVote { + block_hash: hash, + candidate_index, + validator: validator_index, + signature: dummy_signature(), + }; + + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment( + cert.clone().into(), + candidate_index.into(), + ), + ) + .await; + + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeApproval(approval.clone().into()), + ) + .await; + let assignments = vec![(cert.clone(), candidate_index)]; + let approvals = vec![approval.clone()]; + + let expected_indices = vec![0, 10, 20, 30, 50, 51, 52, 53]; + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Set up a gossip topology. + setup_gossip_topology( + overseer, + make_gossip_topology( + 1, + &peers_with_optional_peer_id, + &[0, 10, 20, 30], + &[50, 51, 52, 53], + 1, + ), + ) + .await; + + let mut expected_indices_assignments = expected_indices.clone(); + let mut expected_indices_approvals = expected_indices.clone(); + + for _ in 0..expected_indices_assignments.len() { + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + )) + )) => { + // Sends to all expected peers. + assert_eq!(sent_peers.len(), 1); + assert_eq!(sent_assignments, assignments); - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + let pos = expected_indices_assignments.iter() + .position(|i| &peers[*i].0 == &sent_peers[0]) + .unwrap(); + expected_indices_assignments.remove(pos); + } + ); + } - // Issuer of the message is important, not the peer we receive from. - // 99 deliberately chosen because it's not in X or Y. - send_message_from_peer(overseer, &peers[99].0, msg).await; - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - _, - _, - tx, - )) => { - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); - expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await; + for _ in 0..expected_indices_approvals.len() { + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) + )) + )) => { + // Sends to all expected peers. + assert_eq!(sent_peers.len(), 1); + assert_eq!(sent_approvals, approvals); - let expected_x = [0, 10, 20, 30]; + let pos = expected_indices_approvals.iter() + .position(|i| &peers[*i].0 == &sent_peers[0]) + .unwrap(); - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) - )) - )) => { - assert_eq!(sent_peers.len(), expected_x.len() + 4); - for &i in &expected_x { - assert!( - sent_peers.contains(&peers[i].0), - "Message not sent to expected peer {}", - i, - ); + expected_indices_approvals.remove(pos); } - assert_eq!(sent_assignments, assignments); - } - ); - }; + ); + } - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); } -// tests that messages are propagated to necessary peers after they connect +// test aggression L1 #[test] -fn propagates_to_required_after_connect() { +fn originator_aggression_l1() { let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); let peers = make_peers_and_authority_ids(100); - let _ = test_harness(State::default(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; + let mut state = State::default(); + state.aggression_config.resend_unfinalized_period = None; + let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap(); - let omitted = [0, 10, 50, 51]; + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state, + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; - // Connect all peers except omitted. - for (i, (peer, _)) in peers.iter().enumerate() { - if !omitted.contains(&i) { + // Connect all peers except omitted. + for (peer, _) in &peers { setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; } - } - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Set up a gossip topology. - setup_gossip_topology( - overseer, - make_gossip_topology( - 1, - &peers_with_optional_peer_id, - &[0, 10, 20, 30, 40, 60, 70, 80], - &[50, 51, 52, 53, 54, 55, 56, 57], - 1, - ), - ) - .await; - - let expected_indices = [ - // Both dimensions in the gossip topology, minus omitted. - 20, 30, 40, 60, 70, 80, 52, 53, 54, 55, 56, 57, - ]; - - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; - // import an assignment and approval locally. - let cert = fake_assignment_cert(hash, validator_index); - let approval = IndirectSignedApprovalVote { - block_hash: hash, - candidate_index, - validator: validator_index, - signature: dummy_signature(), - }; + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment( - cert.clone().into(), - candidate_index.into(), - ), - ) - .await; + // import an assignment and approval locally. + let cert = fake_assignment_cert(hash, validator_index); + let approval = IndirectSignedApprovalVote { + block_hash: hash, + candidate_index, + validator: validator_index, + signature: dummy_signature(), + }; + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Set up a gossip topology. + setup_gossip_topology( + overseer, + make_gossip_topology( + 1, + &peers_with_optional_peer_id, + &[0, 10, 20, 30], + &[50, 51, 52, 53], + 1, + ), + ) + .await; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeApproval(approval.clone().into()), - ) - .await; + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment( + cert.clone().into(), + candidate_index.into(), + ), + ) + .await; - let assignments = vec![(cert.clone(), candidate_index)]; - let approvals = vec![approval.clone()]; - - let mut assignment_sent_peers = assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) - )) - )) => { - assert_eq!(sent_peers.len(), expected_indices.len() + 4); - for &i in &expected_indices { - assert!( - sent_peers.contains(&peers[i].0), - "Message not sent to expected peer {}", - i, - ); - } - assert_eq!(sent_assignments, assignments); - sent_peers - } - ); - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - mut sent_peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) - )) - )) => { - // Random sampling is reused from the assignment. - sent_peers.sort(); - assignment_sent_peers.sort(); - assert_eq!(sent_peers, assignment_sent_peers); - assert_eq!(sent_approvals, approvals); - } - ); + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeApproval(approval.clone().into()), + ) + .await; - for i in omitted.iter().copied() { - setup_peer_with_view(overseer, &peers[i].0, view![hash], ValidationVersion::V1).await; + let assignments = vec![(cert.clone(), candidate_index)]; + let approvals = vec![approval.clone()]; - assert_matches!( + let prev_sent_indices = assert_matches!( overseer_recv(overseer).await, AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( sent_peers, Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + protocol_v1::ApprovalDistributionMessage::Assignments(_) )) )) => { - assert_eq!(sent_peers.len(), 1); - assert_eq!(&sent_peers[0], &peers[i].0); - assert_eq!(sent_assignments, assignments); + sent_peers.into_iter() + .filter_map(|sp| peers.iter().position(|p| &p.0 == &sp)) + .collect::>() } ); assert_matches!( overseer_recv(overseer).await, AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, + _, Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) + protocol_v1::ApprovalDistributionMessage::Approvals(_) )) - )) => { - assert_eq!(sent_peers.len(), 1); - assert_eq!(&sent_peers[0], &peers[i].0); - assert_eq!(sent_approvals, approvals); - } + )) => { } ); - } - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); + // Add blocks until aggression L1 is triggered. + { + let mut parent_hash = hash; + for level in 0..aggression_l1_threshold { + let number = 1 + level + 1; // first block had number 1 + let hash = BlakeTwo256::hash_of(&(parent_hash, number)); + let meta = BlockApprovalMeta { + hash, + parent_hash, + number, + candidates: vec![], + slot: (level as u64).into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(level + 1); + overseer_send(overseer, msg).await; + + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + parent_hash = hash; + } + } + + let unsent_indices = + (0..peers.len()).filter(|i| !prev_sent_indices.contains(&i)).collect::>(); + + for _ in 0..unsent_indices.len() { + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + )) + )) => { + // Sends to all expected peers. + assert_eq!(sent_peers.len(), 1); + assert_eq!(sent_assignments, assignments); + + assert!(unsent_indices.iter() + .any(|i| &peers[*i].0 == &sent_peers[0])); + } + ); + } + + for _ in 0..unsent_indices.len() { + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) + )) + )) => { + // Sends to all expected peers. + assert_eq!(sent_peers.len(), 1); + assert_eq!(sent_approvals, approvals); + + assert!(unsent_indices.iter() + .any(|i| &peers[*i].0 == &sent_peers[0])); + } + ); + } + + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); } -// test that new gossip topology triggers send of messages. +// test aggression L1 #[test] -fn sends_to_more_peers_after_getting_topology() { +fn non_originator_aggression_l1() { let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); let peers = make_peers_and_authority_ids(100); - let _ = test_harness(State::default(), |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - - // Connect all peers except omitted. - for (peer, _) in &peers { - setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; - } + let mut state = state_without_reputation_delay(); + state.aggression_config.resend_unfinalized_period = None; + let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap(); - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state, + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; + // Connect all peers except omitted. + for (peer, _) in &peers { + setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; + } - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; - // import an assignment and approval locally. - let cert = fake_assignment_cert(hash, validator_index); - let approval = IndirectSignedApprovalVote { - block_hash: hash, - candidate_index, - validator: validator_index, - signature: dummy_signature(), - }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment( - cert.clone().into(), - candidate_index.into(), - ), - ) - .await; + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeApproval(approval.clone().into()), - ) - .await; + // import an assignment and approval locally. + let cert = fake_assignment_cert(hash, validator_index); + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Set up a gossip topology. + setup_gossip_topology( + overseer, + make_gossip_topology( + 1, + &peers_with_optional_peer_id, + &[0, 10, 20, 30], + &[50, 51, 52, 53], + 1, + ), + ) + .await; - let assignments = vec![(cert.clone(), candidate_index)]; - let approvals = vec![approval.clone()]; - - let expected_indices = vec![0, 10, 20, 30, 50, 51, 52, 53]; - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Set up a gossip topology. - setup_gossip_topology( - overseer, - make_gossip_topology( - 1, - &peers_with_optional_peer_id, - &[0, 10, 20, 30], - &[50, 51, 52, 53], - 1, - ), - ) - .await; + let assignments = vec![(cert.clone().into(), candidate_index)]; + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); - let mut expected_indices_assignments = expected_indices.clone(); - let mut expected_indices_approvals = expected_indices.clone(); + // Issuer of the message is important, not the peer we receive from. + // 99 deliberately chosen because it's not in X or Y. + send_message_from_peer(overseer, &peers[99].0, msg).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; - for _ in 0..expected_indices_assignments.len() { assert_matches!( overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) - )) + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, _, )) => { - // Sends to all expected peers. - assert_eq!(sent_peers.len(), 1); - assert_eq!(sent_assignments, assignments); - - let pos = expected_indices_assignments.iter() - .position(|i| &peers[*i].0 == &sent_peers[0]) - .unwrap(); - expected_indices_assignments.remove(pos); + assert_eq!(assignment.tranche(), 0); } ); - } - for _ in 0..expected_indices_approvals.len() { + expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await; + assert_matches!( overseer_recv(overseer).await, AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, + _, Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) + protocol_v1::ApprovalDistributionMessage::Assignments(_) )) - )) => { - // Sends to all expected peers. - assert_eq!(sent_peers.len(), 1); - assert_eq!(sent_approvals, approvals); - - let pos = expected_indices_approvals.iter() - .position(|i| &peers[*i].0 == &sent_peers[0]) - .unwrap(); - - expected_indices_approvals.remove(pos); - } + )) => { } ); - } - - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); -} -// test aggression L1 -#[test] -fn originator_aggression_l1() { - let parent_hash = Hash::repeat_byte(0xFF); - let hash = Hash::repeat_byte(0xAA); + // Add blocks until aggression L1 is triggered. + { + let mut parent_hash = hash; + for level in 0..aggression_l1_threshold { + let number = 1 + level + 1; // first block had number 1 + let hash = BlakeTwo256::hash_of(&(parent_hash, number)); + let meta = BlockApprovalMeta { + hash, + parent_hash, + number, + candidates: vec![], + slot: (level as u64).into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; - let peers = make_peers_and_authority_ids(100); + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; - let mut state = State::default(); - state.aggression_config.resend_unfinalized_period = None; - let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap(); + parent_hash = hash; + } + } - let _ = test_harness(state, |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; + // No-op on non-originator - // Connect all peers except omitted. - for (peer, _) in &peers { - setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; - } + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); +} - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; +// test aggression L2 on non-originator +#[test] +fn non_originator_aggression_l2() { + let parent_hash = Hash::repeat_byte(0xFF); + let hash = Hash::repeat_byte(0xAA); - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; + let peers = make_peers_and_authority_ids(100); - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; + let mut state = state_without_reputation_delay(); + state.aggression_config.resend_unfinalized_period = None; - // import an assignment and approval locally. - let cert = fake_assignment_cert(hash, validator_index); - let approval = IndirectSignedApprovalVote { - block_hash: hash, - candidate_index, - validator: validator_index, - signature: dummy_signature(), - }; - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Set up a gossip topology. - setup_gossip_topology( - overseer, - make_gossip_topology( - 1, - &peers_with_optional_peer_id, - &[0, 10, 20, 30], - &[50, 51, 52, 53], - 1, - ), - ) - .await; + let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap(); + let aggression_l2_threshold = state.aggression_config.l2_threshold.unwrap(); + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state, + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + + // Connect all peers except omitted. + for (peer, _) in &peers { + setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; + } - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment( - cert.clone().into(), - candidate_index.into(), - ), - ) - .await; + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeApproval(approval.clone().into()), - ) - .await; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; - let assignments = vec![(cert.clone(), candidate_index)]; - let approvals = vec![approval.clone()]; - - let prev_sent_indices = assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(_) - )) - )) => { - sent_peers.into_iter() - .filter_map(|sp| peers.iter().position(|p| &p.0 == &sp)) - .collect::>() - } - ); + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - _, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Approvals(_) - )) - )) => { } - ); - - // Add blocks until aggression L1 is triggered. - { - let mut parent_hash = hash; - for level in 0..aggression_l1_threshold { - let number = 1 + level + 1; // first block had number 1 - let hash = BlakeTwo256::hash_of(&(parent_hash, number)); - let meta = BlockApprovalMeta { - hash, - parent_hash, - number, - candidates: vec![], - slot: (level as u64).into(), - session: 1, - }; - - let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(level + 1); - overseer_send(overseer, msg).await; - - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - parent_hash = hash; - } - } + // import an assignment and approval locally. + let cert = fake_assignment_cert(hash, validator_index); + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Set up a gossip topology. + setup_gossip_topology( + overseer, + make_gossip_topology( + 1, + &peers_with_optional_peer_id, + &[0, 10, 20, 30], + &[50, 51, 52, 53], + 1, + ), + ) + .await; - let unsent_indices = - (0..peers.len()).filter(|i| !prev_sent_indices.contains(&i)).collect::>(); + let assignments = vec![(cert.clone(), candidate_index)]; + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); - for _ in 0..unsent_indices.len() { + // Issuer of the message is important, not the peer we receive from. + // 99 deliberately chosen because it's not in X or Y. + send_message_from_peer(overseer, &peers[99].0, msg).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; assert_matches!( overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) - )) + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, _, )) => { - // Sends to all expected peers. - assert_eq!(sent_peers.len(), 1); - assert_eq!(sent_assignments, assignments); - - assert!(unsent_indices.iter() - .any(|i| &peers[*i].0 == &sent_peers[0])); + assert_eq!(assignment.tranche(), 0); } ); - } - for _ in 0..unsent_indices.len() { - assert_matches!( + expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await; + + let prev_sent_indices = assert_matches!( overseer_recv(overseer).await, AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( sent_peers, Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Approvals(sent_approvals) + protocol_v1::ApprovalDistributionMessage::Assignments(_) )) )) => { - // Sends to all expected peers. - assert_eq!(sent_peers.len(), 1); - assert_eq!(sent_approvals, approvals); - - assert!(unsent_indices.iter() - .any(|i| &peers[*i].0 == &sent_peers[0])); + sent_peers.into_iter() + .filter_map(|sp| peers.iter().position(|p| &p.0 == &sp)) + .collect::>() } ); - } - - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); -} -// test aggression L1 -#[test] -fn non_originator_aggression_l1() { - let parent_hash = Hash::repeat_byte(0xFF); - let hash = Hash::repeat_byte(0xAA); + // Add blocks until aggression L1 is triggered. + let chain_head = { + let mut parent_hash = hash; + for level in 0..aggression_l1_threshold { + let number = 1 + level + 1; // first block had number 1 + let hash = BlakeTwo256::hash_of(&(parent_hash, number)); + let meta = BlockApprovalMeta { + hash, + parent_hash, + number, + candidates: vec![], + slot: (level as u64).into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; - let peers = make_peers_and_authority_ids(100); + let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(level + 1); + overseer_send(overseer, msg).await; - let mut state = state_without_reputation_delay(); - state.aggression_config.resend_unfinalized_period = None; - let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap(); + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; - let _ = test_harness(state, |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; + parent_hash = hash; + } - // Connect all peers except omitted. - for (peer, _) in &peers { - setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; - } + parent_hash + }; - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; + // No-op on non-originator - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; - - // import an assignment and approval locally. - let cert = fake_assignment_cert(hash, validator_index); - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Set up a gossip topology. - setup_gossip_topology( - overseer, - make_gossip_topology( - 1, - &peers_with_optional_peer_id, - &[0, 10, 20, 30], - &[50, 51, 52, 53], - 1, - ), - ) - .await; + // Add blocks until aggression L2 is triggered. + { + let mut parent_hash = chain_head; + for level in 0..aggression_l2_threshold - aggression_l1_threshold { + let number = aggression_l1_threshold + level + 1 + 1; // first block had number 1 + let hash = BlakeTwo256::hash_of(&(parent_hash, number)); + let meta = BlockApprovalMeta { + hash, + parent_hash, + number, + candidates: vec![], + slot: (level as u64).into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; - let assignments = vec![(cert.clone().into(), candidate_index)]; - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate( + aggression_l1_threshold + level + 1, + ); + overseer_send(overseer, msg).await; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; - // Issuer of the message is important, not the peer we receive from. - // 99 deliberately chosen because it's not in X or Y. - send_message_from_peer(overseer, &peers[99].0, msg).await; - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - _, - _, - tx, - )) => { - tx.send(AssignmentCheckResult::Accepted).unwrap(); + parent_hash = hash; + } } - ); - expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await; + // XY dimension - previously sent. + let unsent_indices = [0, 10, 20, 30, 50, 51, 52, 53] + .iter() + .cloned() + .filter(|i| !prev_sent_indices.contains(&i)) + .collect::>(); - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - _, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(_) - )) - )) => { } - ); - - // Add blocks until aggression L1 is triggered. - { - let mut parent_hash = hash; - for level in 0..aggression_l1_threshold { - let number = 1 + level + 1; // first block had number 1 - let hash = BlakeTwo256::hash_of(&(parent_hash, number)); - let meta = BlockApprovalMeta { - hash, - parent_hash, - number, - candidates: vec![], - slot: (level as u64).into(), - session: 1, - }; - - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - parent_hash = hash; - } - } + for _ in 0..unsent_indices.len() { + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + )) + )) => { + // Sends to all expected peers. + assert_eq!(sent_peers.len(), 1); + assert_eq!(sent_assignments, assignments); - // No-op on non-originator + assert!(unsent_indices.iter() + .any(|i| &peers[*i].0 == &sent_peers[0])); + } + ); + } - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); } -// test aggression L2 on non-originator +// Tests that messages propagate to the unshared dimension. #[test] -fn non_originator_aggression_l2() { +fn resends_messages_periodically() { let parent_hash = Hash::repeat_byte(0xFF); let hash = Hash::repeat_byte(0xAA); let peers = make_peers_and_authority_ids(100); let mut state = state_without_reputation_delay(); - state.aggression_config.resend_unfinalized_period = None; + state.aggression_config.l1_threshold = None; + state.aggression_config.l2_threshold = None; + state.aggression_config.resend_unfinalized_period = Some(2); + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state, + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + + // Connect all peers. + for (peer, _) in &peers { + setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; + } + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + // Set up a gossip topology. + setup_gossip_topology( + overseer, + make_gossip_topology( + 1, + &peers_with_optional_peer_id, + &[0, 10, 20, 30], + &[50, 51, 52, 53], + 1, + ), + ) + .await; - let aggression_l1_threshold = state.aggression_config.l1_threshold.unwrap(); - let aggression_l2_threshold = state.aggression_config.l2_threshold.unwrap(); - let _ = test_harness(state, |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; - // Connect all peers except omitted. - for (peer, _) in &peers { - setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; - } + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; - - // import an assignment and approval locally. - let cert = fake_assignment_cert(hash, validator_index); - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Set up a gossip topology. - setup_gossip_topology( - overseer, - make_gossip_topology( - 1, - &peers_with_optional_peer_id, - &[0, 10, 20, 30], - &[50, 51, 52, 53], - 1, - ), - ) - .await; + // import an assignment and approval locally. + let cert = fake_assignment_cert(hash, validator_index); + let assignments = vec![(cert.clone(), candidate_index)]; - let assignments = vec![(cert.clone(), candidate_index)]; - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + { + let msg = + protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + + // Issuer of the message is important, not the peer we receive from. + // 99 deliberately chosen because it's not in X or Y. + send_message_from_peer(overseer, &peers[99].0, msg).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; - // Issuer of the message is important, not the peer we receive from. - // 99 deliberately chosen because it's not in X or Y. - send_message_from_peer(overseer, &peers[99].0, msg).await; - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - _, - _, - tx, - )) => { - tx.send(AssignmentCheckResult::Accepted).unwrap(); - } - ); - - expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await; - - let prev_sent_indices = assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(_) - )) - )) => { - sent_peers.into_iter() - .filter_map(|sp| peers.iter().position(|p| &p.0 == &sp)) - .collect::>() - } - ); - - // Add blocks until aggression L1 is triggered. - let chain_head = { - let mut parent_hash = hash; - for level in 0..aggression_l1_threshold { - let number = 1 + level + 1; // first block had number 1 - let hash = BlakeTwo256::hash_of(&(parent_hash, number)); - let meta = BlockApprovalMeta { - hash, - parent_hash, - number, - candidates: vec![], - slot: (level as u64).into(), - session: 1, - }; - - let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(level + 1); - overseer_send(overseer, msg).await; - - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - parent_hash = hash; - } + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, _, + )) => { + assert_eq!(assignment.tranche(), 0); + } + ); + expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await; - parent_hash - }; + let expected_y = [50, 51, 52, 53]; - // No-op on non-originator - - // Add blocks until aggression L2 is triggered. - { - let mut parent_hash = chain_head; - for level in 0..aggression_l2_threshold - aggression_l1_threshold { - let number = aggression_l1_threshold + level + 1 + 1; // first block had number 1 - let hash = BlakeTwo256::hash_of(&(parent_hash, number)); - let meta = BlockApprovalMeta { - hash, - parent_hash, - number, - candidates: vec![], - slot: (level as u64).into(), - session: 1, - }; - - let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate( - aggression_l1_threshold + level + 1, + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + )) + )) => { + assert_eq!(sent_peers.len(), expected_y.len() + 4); + for &i in &expected_y { + assert!( + sent_peers.contains(&peers[i].0), + "Message not sent to expected peer {}", + i, + ); + } + assert_eq!(sent_assignments, assignments); + } ); - overseer_send(overseer, msg).await; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; + }; + + let mut number = 1; + for _ in 0..10 { + // Add blocks until resend is done. + { + let mut parent_hash = hash; + for level in 0..2 { + number = number + 1; + let hash = BlakeTwo256::hash_of(&(parent_hash, number)); + let meta = BlockApprovalMeta { + hash, + parent_hash, + number, + candidates: vec![], + slot: (level as u64).into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + + let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(2); + overseer_send(overseer, msg).await; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + parent_hash = hash; + } + } - parent_hash = hash; + let mut expected_y = vec![50, 51, 52, 53]; + + // Expect messages sent only to topology peers, one by one. + for _ in 0..expected_y.len() { + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + sent_peers, + Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( + protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + )) + )) => { + assert_eq!(sent_peers.len(), 1); + let expected_pos = expected_y.iter() + .position(|&i| &peers[i].0 == &sent_peers[0]) + .unwrap(); + + expected_y.remove(expected_pos); + assert_eq!(sent_assignments, assignments); + } + ); + } } - } - // XY dimension - previously sent. - let unsent_indices = [0, 10, 20, 30, 50, 51, 52, 53] - .iter() - .cloned() - .filter(|i| !prev_sent_indices.contains(&i)) - .collect::>(); + assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); + virtual_overseer + }, + ); +} + +/// Tests that peers correctly receive versioned messages. +#[test] +fn import_versioned_approval() { + let peers = make_peers_and_authority_ids(15); + let peer_a = peers.get(0).unwrap().0; + let peer_b = peers.get(1).unwrap().0; + let peer_c = peers.get(2).unwrap().0; + + let parent_hash = Hash::repeat_byte(0xFF); + let hash = Hash::repeat_byte(0xAA); + let state = state_without_reputation_delay(); + let candidate_hash = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB)); + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(SystemClock {}), + state, + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // All peers are aware of relay parent. + setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V2).await; + setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await; + setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V2).await; + + // Set up a gossip topology, where a, b, c and d are topology neighbors to the node + // under testing. + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), + ) + .await; + + let mut keystore = LocalKeystore::in_memory(); + let session = dummy_session_info_valid(1, &mut keystore, 1); + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 1, + candidates: vec![(candidate_hash, 0.into(), 0.into()); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // import an assignment related to `hash` locally + let validator_index = ValidatorIndex(0); + let candidate_index = 0u32; + let cert = fake_assignment_cert(hash, validator_index); + overseer_send( + overseer, + ApprovalDistributionMessage::DistributeAssignment( + cert.into(), + candidate_index.into(), + ), + ) + .await; - for _ in 0..unsent_indices.len() { assert_matches!( overseer_recv(overseer).await, AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, + peers, Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + protocol_v1::ApprovalDistributionMessage::Assignments(assignments) )) )) => { - // Sends to all expected peers. - assert_eq!(sent_peers.len(), 1); - assert_eq!(sent_assignments, assignments); - - assert!(unsent_indices.iter() - .any(|i| &peers[*i].0 == &sent_peers[0])); + assert_eq!(peers, vec![peer_b]); + assert_eq!(assignments.len(), 1); } ); - } - - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); -} - -// Tests that messages propagate to the unshared dimension. -#[test] -fn resends_messages_periodically() { - let parent_hash = Hash::repeat_byte(0xFF); - let hash = Hash::repeat_byte(0xAA); - - let peers = make_peers_and_authority_ids(100); - let mut state = state_without_reputation_delay(); - state.aggression_config.l1_threshold = None; - state.aggression_config.l2_threshold = None; - state.aggression_config.resend_unfinalized_period = Some(2); - let _ = test_harness(state, |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - - // Connect all peers. - for (peer, _) in &peers { - setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await; - } - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - // Set up a gossip topology. - setup_gossip_topology( - overseer, - make_gossip_topology( - 1, - &peers_with_optional_peer_id, - &[0, 10, 20, 30], - &[50, 51, 52, 53], - 1, - ), - ) - .await; - - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; - - // import an assignment and approval locally. - let cert = fake_assignment_cert(hash, validator_index); - let assignments = vec![(cert.clone(), candidate_index)]; + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V2(protocol_v2::ValidationProtocol::ApprovalDistribution( + protocol_v2::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 2); + assert!(peers.contains(&peer_a)); + assert!(peers.contains(&peer_c)); - { - let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + assert_eq!(assignments.len(), 1); + } + ); - // Issuer of the message is important, not the peer we receive from. - // 99 deliberately chosen because it's not in X or Y. - send_message_from_peer(overseer, &peers[99].0, msg).await; + // send the an approval from peer_a + let approval = IndirectSignedApprovalVote { + block_hash: hash, + candidate_index, + validator: validator_index, + signature: signature_for( + &keystore, + &session, + vec![candidate_hash], + validator_index, + ), + }; + let msg = protocol_v2::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); + send_message_from_peer_v2(overseer, &peer_a, msg).await; + provide_session(overseer, session).await; assert_matches!( overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment( - _, - _, - tx, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportApproval( + vote, _, )) => { - tx.send(AssignmentCheckResult::Accepted).unwrap(); + assert_eq!(Into::::into(vote), approval.into()); } ); - expect_reputation_change(overseer, &peers[99].0, BENEFIT_VALID_MESSAGE_FIRST).await; - let expected_y = [50, 51, 52, 53]; + expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await; + // Peers b and c receive versioned approval messages. assert_matches!( overseer_recv(overseer).await, AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, + peers, Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) + protocol_v1::ApprovalDistributionMessage::Approvals(approvals) )) )) => { - assert_eq!(sent_peers.len(), expected_y.len() + 4); - for &i in &expected_y { - assert!( - sent_peers.contains(&peers[i].0), - "Message not sent to expected peer {}", - i, - ); - } - assert_eq!(sent_assignments, assignments); + assert_eq!(peers, vec![peer_b]); + assert_eq!(approvals.len(), 1); } ); - }; - - let mut number = 1; - for _ in 0..10 { - // Add blocks until resend is done. - { - let mut parent_hash = hash; - for level in 0..2 { - number = number + 1; - let hash = BlakeTwo256::hash_of(&(parent_hash, number)); - let meta = BlockApprovalMeta { - hash, - parent_hash, - number, - candidates: vec![], - slot: (level as u64).into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::ApprovalCheckingLagUpdate(2); - overseer_send(overseer, msg).await; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - parent_hash = hash; + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V2(protocol_v2::ValidationProtocol::ApprovalDistribution( + protocol_v2::ApprovalDistributionMessage::Approvals(approvals) + )) + )) => { + assert_eq!(peers, vec![peer_c]); + assert_eq!(approvals.len(), 1); } - } - - let mut expected_y = vec![50, 51, 52, 53]; - - // Expect messages sent only to topology peers, one by one. - for _ in 0..expected_y.len() { - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - sent_peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(sent_assignments) - )) - )) => { - assert_eq!(sent_peers.len(), 1); - let expected_pos = expected_y.iter() - .position(|&i| &peers[i].0 == &sent_peers[0]) - .unwrap(); - - expected_y.remove(expected_pos); - assert_eq!(sent_assignments, assignments); - } - ); - } - } - - assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent"); - virtual_overseer - }); -} - -/// Tests that peers correctly receive versioned messages. -#[test] -fn import_versioned_approval() { - let peers = make_peers_and_authority_ids(15); - let peer_a = peers.get(0).unwrap().0; - let peer_b = peers.get(1).unwrap().0; - let peer_c = peers.get(2).unwrap().0; + ); - let parent_hash = Hash::repeat_byte(0xFF); - let hash = Hash::repeat_byte(0xAA); - let state = state_without_reputation_delay(); - let _ = test_harness(state, |mut virtual_overseer| async move { - let overseer = &mut virtual_overseer; - // All peers are aware of relay parent. - setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V2).await; - setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await; - setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V2).await; - - // Set up a gossip topology, where a, b, c and d are topology neighbors to the node under - // testing. - let peers_with_optional_peer_id = peers - .iter() - .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) - .collect_vec(); - setup_gossip_topology( - overseer, - make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), - ) - .await; + // send an obviously invalid approval + let approval = IndirectSignedApprovalVote { + block_hash: hash, + // Invalid candidate index, should not pass sanitization. + candidate_index: 16777284, + validator: validator_index, + signature: dummy_signature(), + }; + let msg = protocol_v2::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); + send_message_from_peer_v2(overseer, &peer_a, msg).await; - // new block `hash_a` with 1 candidates - let meta = BlockApprovalMeta { - hash, - parent_hash, - number: 1, - candidates: vec![Default::default(); 1], - slot: 1.into(), - session: 1, - }; - let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); - overseer_send(overseer, msg).await; - - // import an assignment related to `hash` locally - let validator_index = ValidatorIndex(0); - let candidate_index = 0u32; - let cert = fake_assignment_cert(hash, validator_index); - overseer_send( - overseer, - ApprovalDistributionMessage::DistributeAssignment(cert.into(), candidate_index.into()), - ) - .await; + expect_reputation_change(overseer, &peer_a, COST_OVERSIZED_BITFIELD).await; - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers, vec![peer_b]); - assert_eq!(assignments.len(), 1); - } - ); - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V2(protocol_v2::ValidationProtocol::ApprovalDistribution( - protocol_v2::ApprovalDistributionMessage::Assignments(assignments) - )) - )) => { - assert_eq!(peers.len(), 2); - assert!(peers.contains(&peer_a)); - assert!(peers.contains(&peer_c)); - - assert_eq!(assignments.len(), 1); - } - ); - - // send the an approval from peer_a - let approval = IndirectSignedApprovalVote { - block_hash: hash, - candidate_index, - validator: validator_index, - signature: dummy_signature(), - }; - let msg = protocol_v2::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); - send_message_from_peer_v2(overseer, &peer_a, msg).await; - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportApproval( - vote, - tx, - )) => { - assert_eq!(vote, approval.into()); - tx.send(ApprovalCheckResult::Accepted).unwrap(); - } - ); - - expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await; - - // Peers b and c receive versioned approval messages. - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V1(protocol_v1::ValidationProtocol::ApprovalDistribution( - protocol_v1::ApprovalDistributionMessage::Approvals(approvals) - )) - )) => { - assert_eq!(peers, vec![peer_b]); - assert_eq!(approvals.len(), 1); - } - ); - - assert_matches!( - overseer_recv(overseer).await, - AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( - peers, - Versioned::V2(protocol_v2::ValidationProtocol::ApprovalDistribution( - protocol_v2::ApprovalDistributionMessage::Approvals(approvals) - )) - )) => { - assert_eq!(peers, vec![peer_c]); - assert_eq!(approvals.len(), 1); - } - ); - - // send an obviously invalid approval - let approval = IndirectSignedApprovalVote { - block_hash: hash, - // Invalid candidate index, should not pass sanitization. - candidate_index: 16777284, - validator: validator_index, - signature: dummy_signature(), - }; - let msg = protocol_v2::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); - send_message_from_peer_v2(overseer, &peer_a, msg).await; - - expect_reputation_change(overseer, &peer_a, COST_OVERSIZED_BITFIELD).await; - - // send an obviously invalid approval - let approval = IndirectSignedApprovalVoteV2 { - block_hash: hash, - // Invalid candidates len, should not pass sanitization. - candidate_indices: 16777284.into(), - validator: validator_index, - signature: dummy_signature(), - }; - let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); - send_message_from_peer_v3(overseer, &peer_a, msg).await; + // send an obviously invalid approval + let approval = IndirectSignedApprovalVoteV2 { + block_hash: hash, + // Invalid candidates len, should not pass sanitization. + candidate_indices: 16777284.into(), + validator: validator_index, + signature: dummy_signature(), + }; + let msg = protocol_v3::ApprovalDistributionMessage::Approvals(vec![approval.clone()]); + send_message_from_peer_v3(overseer, &peer_a, msg).await; - expect_reputation_change(overseer, &peer_a, COST_OVERSIZED_BITFIELD).await; + expect_reputation_change(overseer, &peer_a, COST_OVERSIZED_BITFIELD).await; - virtual_overseer - }); + virtual_overseer + }, + ); } fn batch_test_round(message_count: usize) { @@ -3667,11 +4128,26 @@ fn batch_test_round(message_count: usize) { let (mut context, mut virtual_overseer) = polkadot_node_subsystem_test_helpers::make_subsystem_context(pool.clone()); - let subsystem = ApprovalDistribution::new(Default::default()); + let subsystem = ApprovalDistribution::new_with_clock( + Default::default(), + Default::default(), + Box::new(SystemClock {}), + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + ); let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(12345); let mut sender = context.sender().clone(); - let subsystem = - subsystem.run_inner(context, &mut state, REPUTATION_CHANGE_TEST_INTERVAL, &mut rng); + let mut session_info_provider = RuntimeInfo::new_with_config(RuntimeInfoConfig { + keystore: None, + session_cache_lru_size: DISPUTE_WINDOW.get(), + }); + + let subsystem = subsystem.run_inner( + context, + &mut state, + REPUTATION_CHANGE_TEST_INTERVAL, + &mut rng, + &mut session_info_provider, + ); let test_fut = async move { let overseer = &mut virtual_overseer; @@ -3817,3 +4293,420 @@ fn const_ensure_size_not_zero() { crate::ensure_size_not_zero(super::MAX_ASSIGNMENT_BATCH_SIZE); crate::ensure_size_not_zero(super::MAX_APPROVAL_BATCH_SIZE); } + +struct DummyClock; +impl Clock for DummyClock { + fn tick_now(&self) -> polkadot_node_primitives::approval::time::Tick { + 0 + } + + fn wait( + &self, + _tick: polkadot_node_primitives::approval::time::Tick, + ) -> std::pin::Pin + Send + 'static>> { + todo!() + } +} + +/// Subsystem rejects assignments too far into the future. +#[test] +fn subsystem_rejects_assignment_in_future() { + let peers = make_peers_and_authority_ids(15); + let peer_a = peers.get(0).unwrap().0; + let parent_hash = Hash::repeat_byte(0xFF); + let hash = Hash::repeat_byte(0xAA); + + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(89) }), + Box::new(DummyClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // setup peers + setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await; + + // Set up a gossip topology, where a, b, c and d are topology neighbors to the node + // under testing. + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), + ) + .await; + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 2, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // send the assignment related to `hash` + let validator_index = ValidatorIndex(0); + let cert = fake_assignment_cert(hash, validator_index); + let assignments = vec![(cert.clone(), 0u32)]; + setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await; + + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer(overseer, &peer_a, msg).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; + + expect_reputation_change(overseer, &peer_a, COST_ASSIGNMENT_TOO_FAR_IN_THE_FUTURE) + .await; + + virtual_overseer + }, + ); +} + +/// Subsystem rejects bad vrf assignments. +#[test] +fn subsystem_rejects_bad_assignments() { + let peers = make_peers_and_authority_ids(15); + let peer_a = peers.get(0).unwrap().0; + let parent_hash = Hash::repeat_byte(0xFF); + let hash = Hash::repeat_byte(0xAA); + + let _ = test_harness( + Arc::new(MockAssignmentCriteria { + tranche: Err(InvalidAssignment(criteria::InvalidAssignmentReason::NullAssignment)), + }), + Box::new(DummyClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // setup peers + setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await; + + // Set up a gossip topology, where a, b, c and d are topology neighbors to the node + // under testing. + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), + ) + .await; + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 2, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // send the assignment related to `hash` + let validator_index = ValidatorIndex(0); + let cert = fake_assignment_cert(hash, validator_index); + let assignments = vec![(cert.clone(), 0u32)]; + setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await; + + let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer(overseer, &peer_a, msg).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; + + expect_reputation_change(overseer, &peer_a, COST_INVALID_MESSAGE).await; + + virtual_overseer + }, + ); +} + +/// Subsystem rejects assignments that have invalid claimed candidates. +#[test] +fn subsystem_rejects_wrong_claimed_assignments() { + let peers = make_peers_and_authority_ids(15); + let peer_a = peers.get(0).unwrap().0; + let parent_hash = Hash::repeat_byte(0xFF); + let hash = Hash::repeat_byte(0xAA); + + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(DummyClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // setup peers + setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await; + + // Set up a gossip topology, where a, b, c and d are topology neighbors to the node + // under testing. + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), + ) + .await; + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 2, + candidates: vec![Default::default(); 1], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // send the assignment related to `hash` + let validator_index = ValidatorIndex(0); + + // Claimed core 1 which does not have a candidate included on it, so the assignment + // should be rejected. + let cores = vec![1]; + let core_bitfield: CoreBitfield = cores + .iter() + .map(|index| CoreIndex(*index)) + .collect::>() + .try_into() + .unwrap(); + let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield); + let assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)> = + vec![(cert.clone(), cores.try_into().unwrap())]; + setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await; + + let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer_v3(overseer, &peer_a, msg).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; + + expect_reputation_change(overseer, &peer_a, COST_INVALID_MESSAGE).await; + + virtual_overseer + }, + ); +} + +/// Subsystem accepts tranche0 duplicate assignments, sometimes on validator Compact tranche0 +/// assignment and Delay tranche assignments land on the same candidate. The delay tranche0 can be +/// safely ignored and we don't need to gossip it however, the compact tranche0 assignment should be +/// gossiped, because other candidates are included in it, this test makes sure this invariant is +/// upheld, see https://github.com/paritytech/polkadot/pull/2160#discussion_r557628699, for +/// this edge case. +#[test] +fn subsystem_accepts_tranche0_duplicate_assignments() { + let peers = make_peers_and_authority_ids(15); + let peer_a = peers.get(0).unwrap().0; + let peer_b = peers.get(1).unwrap().0; + let parent_hash = Hash::repeat_byte(0xFF); + let hash = Hash::repeat_byte(0xAA); + let candidate_hash_first = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB)); + let candidate_hash_second = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xCC)); + let candidate_hash_third = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB)); + let candidate_hash_fourth = polkadot_primitives::CandidateHash(Hash::repeat_byte(0xBB)); + + let _ = test_harness( + Arc::new(MockAssignmentCriteria { tranche: Ok(0) }), + Box::new(DummyClock {}), + state_without_reputation_delay(), + |mut virtual_overseer| async move { + let overseer = &mut virtual_overseer; + // setup peers + setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V3).await; + setup_peer_with_view(overseer, &peer_b, view![], ValidationVersion::V3).await; + + // Set up a gossip topology, where a, b, c and d are topology neighbors to the node + // under testing. + let peers_with_optional_peer_id = peers + .iter() + .map(|(peer_id, authority)| (Some(*peer_id), authority.clone())) + .collect_vec(); + setup_gossip_topology( + overseer, + make_gossip_topology(1, &peers_with_optional_peer_id, &[0, 1], &[2, 4], 3), + ) + .await; + + // new block `hash_a` with 1 candidates + let meta = BlockApprovalMeta { + hash, + parent_hash, + number: 2, + candidates: vec![ + (candidate_hash_first, 0.into(), 0.into()), + (candidate_hash_second, 1.into(), 1.into()), + (candidate_hash_third, 2.into(), 2.into()), + (candidate_hash_fourth, 3.into(), 3.into()), + ], + slot: 1.into(), + session: 1, + vrf_story: RelayVRFStory(Default::default()), + }; + let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]); + overseer_send(overseer, msg).await; + + // send the assignment related to `hash` + let validator_index = ValidatorIndex(0); + + setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V3).await; + setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V3).await; + + // 1. Compact assignment with multiple candidates, coming after delay assignment which + // covered just one of the candidate is still imported and gossiped. + let candidate_indices: CandidateBitfield = + vec![1 as CandidateIndex].try_into().unwrap(); + let core_bitfield = vec![CoreIndex(1)].try_into().unwrap(); + let cert = fake_assignment_cert_delay(hash, validator_index, core_bitfield); + let assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)> = + vec![(cert.clone(), candidate_indices.clone())]; + let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer_v3(overseer, &peer_a, msg).await; + provide_session( + overseer, + dummy_session_info_valid(1, &mut LocalKeystore::in_memory(), 1), + ) + .await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, + _, + )) => { + assert_eq!(assignment.candidate_indices(), &candidate_indices); + assert_eq!(assignment.assignment(), &cert.into()); + assert_eq!(assignment.tranche(), 0); + } + ); + + expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(assignments.len(), 1); + } + ); + + let candidate_indices: CandidateBitfield = + vec![0 as CandidateIndex, 1 as CandidateIndex].try_into().unwrap(); + let core_bitfield = vec![CoreIndex(0), CoreIndex(1)].try_into().unwrap(); + + let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield); + + let assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)> = + vec![(cert.clone(), candidate_indices.clone())]; + let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer_v3(overseer, &peer_a, msg).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, + _, + )) => { + assert_eq!(assignment.candidate_indices(), &candidate_indices); + assert_eq!(assignment.assignment(), &cert.into()); + assert_eq!(assignment.tranche(), 0); + } + ); + + expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(assignments.len(), 1); + } + ); + + // 2. Delay assignment coming after compact assignment that already covered the + // candidate is not gossiped anymore. + let candidate_indices: CandidateBitfield = + vec![2 as CandidateIndex, 3 as CandidateIndex].try_into().unwrap(); + let core_bitfield = vec![CoreIndex(2), CoreIndex(3)].try_into().unwrap(); + let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield); + let assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)> = + vec![(cert.clone(), candidate_indices.clone())]; + let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer_v3(overseer, &peer_a, msg).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::ApprovalVoting(ApprovalVotingMessage::ImportAssignment( + assignment, + _, + )) => { + assert_eq!(assignment.candidate_indices(), &candidate_indices); + assert_eq!(assignment.assignment(), &cert.into()); + assert_eq!(assignment.tranche(), 0); + } + ); + + expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await; + + assert_matches!( + overseer_recv(overseer).await, + AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage( + peers, + Versioned::V3(protocol_v3::ValidationProtocol::ApprovalDistribution( + protocol_v3::ApprovalDistributionMessage::Assignments(assignments) + )) + )) => { + assert_eq!(peers.len(), 1); + assert_eq!(assignments.len(), 1); + } + ); + + let candidate_indices: CandidateBitfield = vec![3].try_into().unwrap(); + let core_bitfield = vec![CoreIndex(3)].try_into().unwrap(); + + let cert = fake_assignment_cert_delay(hash, validator_index, core_bitfield); + + let assignments: Vec<(IndirectAssignmentCertV2, CandidateBitfield)> = + vec![(cert.clone(), candidate_indices.clone())]; + let msg = protocol_v3::ApprovalDistributionMessage::Assignments(assignments.clone()); + send_message_from_peer_v3(overseer, &peer_a, msg).await; + + expect_reputation_change(overseer, &peer_a, COST_DUPLICATE_MESSAGE).await; + + virtual_overseer + }, + ); +} diff --git a/polkadot/node/overseer/src/lib.rs b/polkadot/node/overseer/src/lib.rs index baaff9c7c9f6..26a6a907e324 100644 --- a/polkadot/node/overseer/src/lib.rs +++ b/polkadot/node/overseer/src/lib.rs @@ -581,6 +581,7 @@ pub struct Overseer { #[subsystem(blocking, message_capacity: 64000, ApprovalDistributionMessage, sends: [ NetworkBridgeTxMessage, ApprovalVotingMessage, + RuntimeApiMessage, ], can_receive_priority_messages)] approval_distribution: ApprovalDistribution, diff --git a/polkadot/node/primitives/Cargo.toml b/polkadot/node/primitives/Cargo.toml index cd642bf16ff9..7185205f905b 100644 --- a/polkadot/node/primitives/Cargo.toml +++ b/polkadot/node/primitives/Cargo.toml @@ -12,11 +12,13 @@ workspace = true [dependencies] bounded-vec = { workspace = true } futures = { workspace = true } +futures-timer = { workspace = true } polkadot-primitives = { workspace = true, default-features = true } codec = { features = ["derive"], workspace = true } sp-core = { workspace = true, default-features = true } sp-application-crypto = { workspace = true, default-features = true } sp-consensus-babe = { workspace = true, default-features = true } +sp-consensus-slots = { workspace = true } sp-keystore = { workspace = true, default-features = true } sp-maybe-compressed-blob = { workspace = true, default-features = true } sp-runtime = { workspace = true, default-features = true } @@ -25,6 +27,7 @@ schnorrkel = { workspace = true, default-features = true } thiserror = { workspace = true } bitvec = { features = ["alloc"], workspace = true } serde = { features = ["derive"], workspace = true, default-features = true } +sc-keystore = { workspace = true } [target.'cfg(not(target_os = "unknown"))'.dependencies] zstd = { version = "0.12.4", default-features = false } diff --git a/polkadot/node/primitives/src/approval/criteria.rs b/polkadot/node/primitives/src/approval/criteria.rs new file mode 100644 index 000000000000..0a1a0ee2367f --- /dev/null +++ b/polkadot/node/primitives/src/approval/criteria.rs @@ -0,0 +1,177 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Assignment criteria VRF generation and checking interfaces. + +use crate::approval::{ + v1::{DelayTranche, RelayVRFStory}, + v2::{AssignmentCertV2, CoreBitfield}, +}; +use codec::{Decode, Encode}; +use polkadot_primitives::{ + AssignmentId, CandidateHash, CoreIndex, GroupIndex, IndexedVec, SessionInfo, ValidatorIndex, +}; +use sc_keystore::LocalKeystore; + +use std::collections::HashMap; + +/// Details pertaining to our assignment on a block. +#[derive(Debug, Clone, Encode, Decode, PartialEq)] +pub struct OurAssignment { + cert: AssignmentCertV2, + tranche: DelayTranche, + validator_index: ValidatorIndex, + // Whether the assignment has been triggered already. + triggered: bool, +} + +impl OurAssignment { + /// Create a new `OurAssignment`. + pub fn new( + cert: AssignmentCertV2, + tranche: DelayTranche, + validator_index: ValidatorIndex, + triggered: bool, + ) -> Self { + OurAssignment { cert, tranche, validator_index, triggered } + } + /// Returns a reference to the assignment cert. + pub fn cert(&self) -> &AssignmentCertV2 { + &self.cert + } + + /// Returns the assignment cert. + pub fn into_cert(self) -> AssignmentCertV2 { + self.cert + } + + /// Returns the delay tranche of the assignment. + pub fn tranche(&self) -> DelayTranche { + self.tranche + } + + /// Returns the validator index of the assignment. + pub fn validator_index(&self) -> ValidatorIndex { + self.validator_index + } + + /// Returns whether the assignment has been triggered. + pub fn triggered(&self) -> bool { + self.triggered + } + + /// Marks the assignment as triggered. + pub fn mark_triggered(&mut self) { + self.triggered = true; + } +} + +/// Information about the world assignments are being produced in. +#[derive(Clone, Debug)] +pub struct Config { + /// The assignment public keys for validators. + pub assignment_keys: Vec, + /// The groups of validators assigned to each core. + pub validator_groups: IndexedVec>, + /// The number of availability cores used by the protocol during this session. + pub n_cores: u32, + /// The zeroth delay tranche width. + pub zeroth_delay_tranche_width: u32, + /// The number of samples we do of `relay_vrf_modulo`. + pub relay_vrf_modulo_samples: u32, + /// The number of delay tranches in total. + pub n_delay_tranches: u32, +} + +impl<'a> From<&'a SessionInfo> for Config { + fn from(s: &'a SessionInfo) -> Self { + Config { + assignment_keys: s.assignment_keys.clone(), + validator_groups: s.validator_groups.clone(), + n_cores: s.n_cores, + zeroth_delay_tranche_width: s.zeroth_delay_tranche_width, + relay_vrf_modulo_samples: s.relay_vrf_modulo_samples, + n_delay_tranches: s.n_delay_tranches, + } + } +} + +/// A trait for producing and checking assignments. +/// +/// Approval voting subsystem implements a a real implemention +/// for it and tests use a mock implementation. +pub trait AssignmentCriteria { + /// Compute the assignments for the given relay VRF story. + fn compute_assignments( + &self, + keystore: &LocalKeystore, + relay_vrf_story: RelayVRFStory, + config: &Config, + leaving_cores: Vec<(CandidateHash, CoreIndex, GroupIndex)>, + enable_v2_assignments: bool, + ) -> HashMap; + + /// Check the assignment cert for the given relay VRF story and returns the delay tranche. + fn check_assignment_cert( + &self, + claimed_core_bitfield: CoreBitfield, + validator_index: ValidatorIndex, + config: &Config, + relay_vrf_story: RelayVRFStory, + assignment: &AssignmentCertV2, + // Backing groups for each "leaving core". + backing_groups: Vec, + ) -> Result; +} + +/// Assignment invalid. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InvalidAssignment(pub InvalidAssignmentReason); + +impl std::fmt::Display for InvalidAssignment { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Invalid Assignment: {:?}", self.0) + } +} + +impl std::error::Error for InvalidAssignment {} + +/// Failure conditions when checking an assignment cert. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InvalidAssignmentReason { + /// The validator index is out of bounds. + ValidatorIndexOutOfBounds, + /// Sample index is out of bounds. + SampleOutOfBounds, + /// Core index is out of bounds. + CoreIndexOutOfBounds, + /// Invalid assignment key. + InvalidAssignmentKey, + /// Node is in backing group. + IsInBackingGroup, + /// Modulo core index mismatch. + VRFModuloCoreIndexMismatch, + /// Modulo output mismatch. + VRFModuloOutputMismatch, + /// Delay core index mismatch. + VRFDelayCoreIndexMismatch, + /// Delay output mismatch. + VRFDelayOutputMismatch, + /// Invalid arguments + InvalidArguments, + /// Assignment vrf check resulted in 0 assigned cores. + NullAssignment, +} diff --git a/polkadot/node/primitives/src/approval.rs b/polkadot/node/primitives/src/approval/mod.rs similarity index 98% rename from polkadot/node/primitives/src/approval.rs rename to polkadot/node/primitives/src/approval/mod.rs index 66883b33367b..79f4cfa9e0be 100644 --- a/polkadot/node/primitives/src/approval.rs +++ b/polkadot/node/primitives/src/approval/mod.rs @@ -16,6 +16,12 @@ //! Types relevant for approval. +/// Criteria for assignment. +pub mod criteria; + +/// Time utilities for approval voting. +pub mod time; + /// A list of primitives introduced in v1. pub mod v1 { use sp_consensus_babe as babe_primitives; @@ -25,8 +31,8 @@ pub mod v1 { use codec::{Decode, Encode}; use polkadot_primitives::{ - BlockNumber, CandidateHash, CandidateIndex, CoreIndex, Hash, Header, SessionIndex, - ValidatorIndex, ValidatorSignature, + BlockNumber, CandidateHash, CandidateIndex, CoreIndex, GroupIndex, Hash, Header, + SessionIndex, ValidatorIndex, ValidatorSignature, }; use sp_application_crypto::ByteArray; @@ -128,11 +134,13 @@ pub mod v1 { pub parent_hash: Hash, /// The candidates included by the block. /// Note that these are not the same as the candidates that appear within the block body. - pub candidates: Vec, + pub candidates: Vec<(CandidateHash, CoreIndex, GroupIndex)>, /// The consensus slot of the block. pub slot: Slot, /// The session of the block. pub session: SessionIndex, + /// The vrf story. + pub vrf_story: RelayVRFStory, } /// Errors that can occur during the approvals protocol. diff --git a/polkadot/node/core/approval-voting/src/time.rs b/polkadot/node/primitives/src/approval/time.rs similarity index 95% rename from polkadot/node/core/approval-voting/src/time.rs rename to polkadot/node/primitives/src/approval/time.rs index 5c3e7e85a17a..465aae23c90e 100644 --- a/polkadot/node/core/approval-voting/src/time.rs +++ b/polkadot/node/primitives/src/approval/time.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -//! Time utilities for approval voting. +//! Time utilities for approval voting subsystems. use futures::{ future::BoxFuture, @@ -23,7 +23,7 @@ use futures::{ Stream, StreamExt, }; -use polkadot_node_primitives::approval::v1::DelayTranche; +use crate::approval::v1::DelayTranche; use sp_consensus_slots::Slot; use std::{ collections::HashSet, @@ -33,11 +33,15 @@ use std::{ }; use polkadot_primitives::{Hash, ValidatorIndex}; +/// The duration of a single tick in milliseconds. pub const TICK_DURATION_MILLIS: u64 = 500; /// A base unit of time, starting from the Unix epoch, split into half-second intervals. pub type Tick = u64; +/// How far in the future a tick can be accepted. +pub const TICK_TOO_FAR_IN_FUTURE: Tick = 20; // 10 seconds. + /// A clock which allows querying of the current tick as well as /// waiting for a tick to be reached. pub trait Clock { @@ -50,6 +54,7 @@ pub trait Clock { /// Extension methods for clocks. pub trait ClockExt { + /// Returns the current tranche. fn tranche_now(&self, slot_duration_millis: u64, base_slot: Slot) -> DelayTranche; } @@ -124,7 +129,7 @@ impl DelayedApprovalTimer { /// /// Guarantees that if a timer already exits for the give block hash, /// no additional timer is started. - pub(crate) fn maybe_arm_timer( + pub fn maybe_arm_timer( &mut self, wait_until: Tick, clock: &dyn Clock, @@ -173,7 +178,7 @@ mod tests { use futures_timer::Delay; use polkadot_primitives::{Hash, ValidatorIndex}; - use crate::time::{Clock, SystemClock}; + use crate::approval::time::{Clock, SystemClock}; use super::DelayedApprovalTimer; diff --git a/polkadot/node/service/src/overseer.rs b/polkadot/node/service/src/overseer.rs index add5d239364d..0b57ff6e395f 100644 --- a/polkadot/node/service/src/overseer.rs +++ b/polkadot/node/service/src/overseer.rs @@ -20,7 +20,7 @@ use polkadot_overseer::{DummySubsystem, InitializedOverseerBuilder, SubsystemErr use sp_core::traits::SpawnNamed; use polkadot_availability_distribution::IncomingRequestReceivers; -use polkadot_node_core_approval_voting::Config as ApprovalVotingConfig; +use polkadot_node_core_approval_voting::{Config as ApprovalVotingConfig, RealAssignmentCriteria}; use polkadot_node_core_av_store::Config as AvailabilityConfig; use polkadot_node_core_candidate_validation::Config as CandidateValidationConfig; use polkadot_node_core_chain_selection::Config as ChainSelectionConfig; @@ -309,7 +309,11 @@ where Metrics::register(registry)?, rand::rngs::StdRng::from_entropy(), )) - .approval_distribution(ApprovalDistributionSubsystem::new(Metrics::register(registry)?)) + .approval_distribution(ApprovalDistributionSubsystem::new( + Metrics::register(registry)?, + approval_voting_config.slot_duration_millis, + Arc::new(RealAssignmentCriteria {}), + )) .approval_voting(ApprovalVotingSubsystem::with_config( approval_voting_config, parachains_db.clone(), diff --git a/polkadot/node/subsystem-bench/src/lib/approval/helpers.rs b/polkadot/node/subsystem-bench/src/lib/approval/helpers.rs index ca58875c8139..4b2b91696824 100644 --- a/polkadot/node/subsystem-bench/src/lib/approval/helpers.rs +++ b/polkadot/node/subsystem-bench/src/lib/approval/helpers.rs @@ -16,11 +16,11 @@ use crate::configuration::TestAuthorities; use itertools::Itertools; -use polkadot_node_core_approval_voting::time::{Clock, SystemClock, Tick}; use polkadot_node_network_protocol::{ grid_topology::{SessionGridTopology, TopologyPeerInfo}, View, }; +use polkadot_node_primitives::approval::time::{Clock, SystemClock, Tick}; use polkadot_node_subsystem_types::messages::{ network_bridge_event::NewGossipTopology, ApprovalDistributionMessage, NetworkBridgeEvent, }; diff --git a/polkadot/node/subsystem-bench/src/lib/approval/message_generator.rs b/polkadot/node/subsystem-bench/src/lib/approval/message_generator.rs index 6d3e7dd92db1..da25a3bf3b79 100644 --- a/polkadot/node/subsystem-bench/src/lib/approval/message_generator.rs +++ b/polkadot/node/subsystem-bench/src/lib/approval/message_generator.rs @@ -28,16 +28,15 @@ use crate::{ use codec::Encode; use futures::SinkExt; use itertools::Itertools; -use polkadot_node_core_approval_voting::{ - criteria::{compute_assignments, Config}, - time::tranche_to_tick, -}; +use polkadot_node_core_approval_voting::criteria::{compute_assignments, Config}; + use polkadot_node_network_protocol::{ grid_topology::{GridNeighbors, RandomRouting, RequiredRouting, SessionGridTopology}, v3 as protocol_v3, }; use polkadot_node_primitives::approval::{ self, + time::tranche_to_tick, v2::{CoreBitfield, IndirectAssignmentCertV2, IndirectSignedApprovalVoteV2}, }; use polkadot_primitives::{ diff --git a/polkadot/node/subsystem-bench/src/lib/approval/mock_chain_selection.rs b/polkadot/node/subsystem-bench/src/lib/approval/mock_chain_selection.rs index 77ba80d4b2bb..709d56d52f0b 100644 --- a/polkadot/node/subsystem-bench/src/lib/approval/mock_chain_selection.rs +++ b/polkadot/node/subsystem-bench/src/lib/approval/mock_chain_selection.rs @@ -16,7 +16,7 @@ use crate::approval::{ApprovalTestState, PastSystemClock, LOG_TARGET, SLOT_DURATION_MILLIS}; use futures::FutureExt; -use polkadot_node_core_approval_voting::time::{slot_number_to_tick, Clock, TICK_DURATION_MILLIS}; +use polkadot_node_primitives::approval::time::{slot_number_to_tick, Clock, TICK_DURATION_MILLIS}; use polkadot_node_subsystem::{overseer, SpawnedSubsystem, SubsystemError}; use polkadot_node_subsystem_types::messages::ChainSelectionMessage; diff --git a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs index 404df2f8c653..f05d061f3fde 100644 --- a/polkadot/node/subsystem-bench/src/lib/approval/mod.rs +++ b/polkadot/node/subsystem-bench/src/lib/approval/mod.rs @@ -49,9 +49,13 @@ use itertools::Itertools; use orchestra::TimeoutExt; use overseer::{metrics::Metrics as OverseerMetrics, MetricsTrait}; use polkadot_approval_distribution::ApprovalDistribution; +use polkadot_node_primitives::approval::time::{ + slot_number_to_tick, tick_to_slot_number, Clock, ClockExt, SystemClock, +}; + use polkadot_node_core_approval_voting::{ - time::{slot_number_to_tick, tick_to_slot_number, Clock, ClockExt, SystemClock}, ApprovalVotingSubsystem, Config as ApprovalVotingConfig, Metrics as ApprovalVotingMetrics, + RealAssignmentCriteria, }; use polkadot_node_network_protocol::v3 as protocol_v3; use polkadot_node_primitives::approval::{self, v1::RelayVRFStory}; @@ -813,8 +817,12 @@ fn build_overseer( Box::new(system_clock.clone()), ); - let approval_distribution = - ApprovalDistribution::new(Metrics::register(Some(&dependencies.registry)).unwrap()); + let approval_distribution = ApprovalDistribution::new_with_clock( + Metrics::register(Some(&dependencies.registry)).unwrap(), + SLOT_DURATION_MILLIS, + Box::new(system_clock.clone()), + Arc::new(RealAssignmentCriteria {}), + ); let mock_chain_api = MockChainApi::new(state.build_chain_api_state()); let mock_chain_selection = MockChainSelection { state: state.clone(), clock: system_clock }; let mock_runtime_api = MockRuntimeApi::new( diff --git a/polkadot/node/subsystem-types/src/messages.rs b/polkadot/node/subsystem-types/src/messages.rs index d067ca468011..854a9da158be 100644 --- a/polkadot/node/subsystem-types/src/messages.rs +++ b/polkadot/node/subsystem-types/src/messages.rs @@ -33,7 +33,7 @@ use polkadot_node_network_protocol::{ }; use polkadot_node_primitives::{ approval::{ - v1::BlockApprovalMeta, + v1::{BlockApprovalMeta, DelayTranche}, v2::{CandidateBitfield, IndirectAssignmentCertV2, IndirectSignedApprovalVoteV2}, }, AvailableData, BabeEpoch, BlockWeight, CandidateVotes, CollationGenerationConfig, @@ -879,7 +879,7 @@ pub enum CollationGenerationMessage { SubmitCollation(SubmitCollationParams), } -/// The result type of [`ApprovalVotingMessage::CheckAndImportAssignment`] request. +/// The result type of [`ApprovalVotingMessage::ImportAssignment`] request. #[derive(Debug, Clone, PartialEq, Eq)] pub enum AssignmentCheckResult { /// The vote was accepted and should be propagated onwards. @@ -892,7 +892,7 @@ pub enum AssignmentCheckResult { Bad(AssignmentCheckError), } -/// The error result type of [`ApprovalVotingMessage::CheckAndImportAssignment`] request. +/// The error result type of [`ApprovalVotingMessage::ImportAssignment`] request. #[derive(Error, Debug, Clone, PartialEq, Eq)] #[allow(missing_docs)] pub enum AssignmentCheckError { @@ -912,7 +912,7 @@ pub enum AssignmentCheckError { InvalidBitfield(usize), } -/// The result type of [`ApprovalVotingMessage::CheckAndImportApproval`] request. +/// The result type of [`ApprovalVotingMessage::ImportApproval`] request. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ApprovalCheckResult { /// The vote was accepted and should be propagated onwards. @@ -921,7 +921,7 @@ pub enum ApprovalCheckResult { Bad(ApprovalCheckError), } -/// The error result type of [`ApprovalVotingMessage::CheckAndImportApproval`] request. +/// The error result type of [`ApprovalVotingMessage::ImportApproval`] request. #[derive(Error, Debug, Clone, PartialEq, Eq)] #[allow(missing_docs)] pub enum ApprovalCheckError { @@ -969,21 +969,68 @@ pub struct HighestApprovedAncestorBlock { pub descriptions: Vec, } +/// A checked indirect assignment, the crypto for the cert has been validated +/// and the `candidate_bitfield` is correctly claimed at `delay_tranche`. +#[derive(Debug)] +pub struct CheckedIndirectAssignment { + assignment: IndirectAssignmentCertV2, + candidate_indices: CandidateBitfield, + tranche: DelayTranche, +} + +impl CheckedIndirectAssignment { + /// Builds a checked assignment from an assignment that was checked to be valid for the + /// `claimed_candidate_indices` at the give tranche + pub fn from_checked( + assignment: IndirectAssignmentCertV2, + claimed_candidate_indices: CandidateBitfield, + tranche: DelayTranche, + ) -> Self { + Self { assignment, candidate_indices: claimed_candidate_indices, tranche } + } + + /// Returns the indirect assignment. + pub fn assignment(&self) -> &IndirectAssignmentCertV2 { + &self.assignment + } + + /// Returns the candidate bitfield claimed by the assignment. + pub fn candidate_indices(&self) -> &CandidateBitfield { + &self.candidate_indices + } + + /// Returns the tranche this assignment is claimed at. + pub fn tranche(&self) -> DelayTranche { + self.tranche + } +} + +/// A checked indirect signed approval vote. +/// +/// The crypto for the vote has been validated and the signature can be trusted as being valid and +/// to correspond to the `validator_index` inside the structure. +#[derive(Debug, derive_more::Deref, derive_more::Into)] +pub struct CheckedIndirectSignedApprovalVote(IndirectSignedApprovalVoteV2); + +impl CheckedIndirectSignedApprovalVote { + /// Builds a checked vote from a vote that was checked to be valid and correctly signed. + pub fn from_checked(vote: IndirectSignedApprovalVoteV2) -> Self { + Self(vote) + } +} + /// Message to the Approval Voting subsystem. #[derive(Debug)] pub enum ApprovalVotingMessage { - /// Check if the assignment is valid and can be accepted by our view of the protocol. - /// Should not be sent unless the block hash is known. - CheckAndImportAssignment( - IndirectAssignmentCertV2, - CandidateBitfield, - oneshot::Sender, - ), - /// Check if the approval vote is valid and can be accepted by our view of the - /// protocol. + /// Import an assignment into the approval-voting database. + /// + /// Should not be sent unless the block hash is known and the VRF assignment checks out. + ImportAssignment(CheckedIndirectAssignment, Option>), + /// Import an approval vote into approval-voting database /// - /// Should not be sent unless the block hash within the indirect vote is known. - CheckAndImportApproval(IndirectSignedApprovalVoteV2, oneshot::Sender), + /// Should not be sent unless the block hash within the indirect vote is known, vote is + /// correctly signed and we had a previous assignment for the candidate. + ImportApproval(CheckedIndirectSignedApprovalVote, Option>), /// Returns the highest possible ancestor hash of the provided block hash which is /// acceptable to vote on finality for. /// The `BlockNumber` provided is the number of the block's ancestor which is the diff --git a/polkadot/roadmap/implementers-guide/src/node/approval/approval-distribution.md b/polkadot/roadmap/implementers-guide/src/node/approval/approval-distribution.md index c987b7fe5bea..e09418c7d5ab 100644 --- a/polkadot/roadmap/implementers-guide/src/node/approval/approval-distribution.md +++ b/polkadot/roadmap/implementers-guide/src/node/approval/approval-distribution.md @@ -66,8 +66,8 @@ Input: * `OverseerSignal::BlockFinalized` Output: - * `ApprovalVotingMessage::CheckAndImportAssignment` - * `ApprovalVotingMessage::CheckAndImportApproval` + * `ApprovalVotingMessage::ImportAssignment` + * `ApprovalVotingMessage::ImportApproval` * `NetworkBridgeMessage::SendValidationMessage::ApprovalDistribution` ## Functionality @@ -253,8 +253,30 @@ The algorithm is the following: boost, add the fingerprint to the peer's knowledge only if it knows about the block and return. Note that we must do this after checking for out-of-view and if the peers knows about the block to avoid being spammed. If we did this check earlier, a peer could provide data out-of-view repeatedly and be rewarded for it. - * Dispatch `ApprovalVotingMessage::CheckAndImportAssignment(assignment)` and wait for the response. + * Check the assignment certificate is valid. + * If the cert kind is `RelayVRFModulo`, then the certificate is valid as long as `sample < + session_info.relay_vrf_samples` and the VRF is valid for the validator's key with the input + `block_entry.relay_vrf_story ++ sample.encode()` as described with + [the approvals protocol section](../../protocol-approval.md#assignment-criteria). We set + `core_index = vrf.make_bytes().to_u32() % session_info.n_cores`. If the `BlockEntry` causes + inclusion of a candidate at `core_index`, then this is a valid assignment for the candidate + at `core_index` and has delay tranche 0. Otherwise, it can be ignored. + * If the cert kind is `RelayVRFModuloCompact`, then the certificate is valid as long as the VRF + is valid for the validator's key with the input `block_entry.relay_vrf_story ++ relay_vrf_samples.encode()` + as described with [the approvals protocol section](../../protocol-approval.md#assignment-criteria). + We enforce that all `core_bitfield` indices are included in the set of the core indices sampled from the + VRF Output. The assignment is considered a valid tranche0 assignment for all claimed candidates if all + `core_bitfield` indices match the core indices where the claimed candidates were included at. + * If the cert kind is `RelayVRFDelay`, then we check if the VRF is valid for the validator's key with the + input `block_entry.relay_vrf_story ++ cert.core_index.encode()` as described in [the approvals protocol + section](../../protocol-approval.md#assignment-criteria). The cert can be ignored if the block did not + cause inclusion of a candidate on that core index. Otherwise, this is a valid assignment for the included + candidate. The delay tranche for the assignment is determined by reducing + `(vrf.make_bytes().to_u64() % (session_info.n_delay_tranches + session_info.zeroth_delay_tranche_width)).saturating_sub(session_info.zeroth_delay_tranche_width)`. + * We also check that the core index derived by the output is covered by the `VRFProof` by means of an auxiliary signature. + * If the delay tranche is too far in the future, return `AssignmentCheckResult::TooFarInFuture`. * If the result is `AssignmentCheckResult::Accepted` + * Dispatch `ApprovalVotingMessage::ImportAssignment(assignment)` to approval-voting to import the assignment. * If the vote was accepted but not duplicate, give the peer a positive reputation boost * add the fingerprint to both our and the peer's knowledge in the `BlockEntry`. Note that we only doing this after making sure we have the right fingerprint. @@ -293,10 +315,12 @@ Imports an approval signature referenced by block hash and candidate index: boost, add the fingerprint to the peer's knowledge only if it knows about the block and return. Note that we must do this after checking for out-of-view to avoid being spammed. If we did this check earlier, a peer could provide data out-of-view repeatedly and be rewarded for it. - * Dispatch `ApprovalVotingMessage::CheckAndImportApproval(approval)` and wait for the response. - * If the result is `VoteCheckResult::Accepted(())`: + * Construct a `SignedApprovalVote` using the candidates hashes and check against the validator's approval key, + based on the session info of the block. If invalid or no such validator, return `Err(InvalidVoteError)`. + * If the result of checking the signature is `Ok(CheckedIndirectSignedApprovalVote)`: + * Dispatch `ApprovalVotingMessage::ImportApproval(approval)` . * Give the peer a positive reputation boost and add the fingerprint to both our and the peer's knowledge. - * If the result is `VoteCheckResult::Bad`: + * If the result is `Err(InvalidVoteError)`: * Report the peer and return. * Load the candidate entry for the given candidate index. It should exist unless there is a logic error in the approval voting subsystem. diff --git a/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md b/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md index 9b4082c49e2f..40394412d81b 100644 --- a/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md +++ b/polkadot/roadmap/implementers-guide/src/node/approval/approval-voting.md @@ -39,8 +39,8 @@ been approved. ## Protocol Input: - * `ApprovalVotingMessage::CheckAndImportAssignment` - * `ApprovalVotingMessage::CheckAndImportApproval` + * `ApprovalVotingMessage::ImportAssignment` + * `ApprovalVotingMessage::ImportApproval` * `ApprovalVotingMessage::ApprovedAncestor` Output: @@ -266,39 +266,17 @@ On receiving an `OverseerSignal::ActiveLeavesUpdate(update)`: 0-tranche assignment, kick off approval work, and schedule the next delay. * Dispatch an `ApprovalDistributionMessage::NewBlocks` with the meta information filled out for each new block. -#### `ApprovalVotingMessage::CheckAndImportAssignment` +#### `ApprovalVotingMessage::ImportAssignment` -On receiving a `ApprovalVotingMessage::CheckAndImportAssignment` message, we check the assignment cert against the block -entry. The cert itself contains information necessary to determine the candidate that is being assigned-to. In detail: +On receiving a `ApprovalVotingMessage::ImportAssignment` message, we assume the assignment cert itself has already been +checked to be valid we proceed then to import the assignment inside the block entry. The cert itself contains +information necessary to determine the candidate that is being assigned-to. In detail: * Load the `BlockEntry` for the relay-parent referenced by the message. If there is none, return `AssignmentCheckResult::Bad`. * Fetch the `SessionInfo` for the session of the block * Determine the assignment key of the validator based on that. * Determine the claimed core index by looking up the candidate with given index in `block_entry.candidates`. Return `AssignmentCheckResult::Bad` if missing. - * Check the assignment cert - * If the cert kind is `RelayVRFModulo`, then the certificate is valid as long as `sample < - session_info.relay_vrf_samples` and the VRF is valid for the validator's key with the input - `block_entry.relay_vrf_story ++ sample.encode()` as described with - [the approvals protocol section](../../protocol-approval.md#assignment-criteria). We set - `core_index = vrf.make_bytes().to_u32() % session_info.n_cores`. If the `BlockEntry` causes - inclusion of a candidate at `core_index`, then this is a valid assignment for the candidate - at `core_index` and has delay tranche 0. Otherwise, it can be ignored. - * If the cert kind is `RelayVRFModuloCompact`, then the certificate is valid as long as the VRF - is valid for the validator's key with the input `block_entry.relay_vrf_story ++ relay_vrf_samples.encode()` - as described with [the approvals protocol section](../../protocol-approval.md#assignment-criteria). - We enforce that all `core_bitfield` indices are included in the set of the core indices sampled from the - VRF Output. The assignment is considered a valid tranche0 assignment for all claimed candidates if all - `core_bitfield` indices match the core indices where the claimed candidates were included at. - - * If the cert kind is `RelayVRFDelay`, then we check if the VRF is valid for the validator's key with the - input `block_entry.relay_vrf_story ++ cert.core_index.encode()` as described in [the approvals protocol - section](../../protocol-approval.md#assignment-criteria). The cert can be ignored if the block did not - cause inclusion of a candidate on that core index. Otherwise, this is a valid assignment for the included - candidate. The delay tranche for the assignment is determined by reducing - `(vrf.make_bytes().to_u64() % (session_info.n_delay_tranches + session_info.zeroth_delay_tranche_width)).saturating_sub(session_info.zeroth_delay_tranche_width)`. - * We also check that the core index derived by the output is covered by the `VRFProof` by means of an auxiliary signature. - * If the delay tranche is too far in the future, return `AssignmentCheckResult::TooFarInFuture`. * Import the assignment. * Load the candidate in question and access the `approval_entry` for the block hash the cert references. * Ignore if we already observe the validator as having been assigned. @@ -309,14 +287,12 @@ entry. The cert itself contains information necessary to determine the candidate * [Schedule a wakeup](#schedule-wakeup) for this block, candidate pair. * return the appropriate `AssignmentCheckResult` on the response channel. -#### `ApprovalVotingMessage::CheckAndImportApproval` +#### `ApprovalVotingMessage::ImportApproval` -On receiving a `CheckAndImportApproval(indirect_approval_vote, response_channel)` message: +On receiving a `ImportApproval(indirect_approval_vote, response_channel)` message: * Fetch the `BlockEntry` from the indirect approval vote's `block_hash`. If none, return `ApprovalCheckResult::Bad`. * Fetch all `CandidateEntry` from the indirect approval vote's `candidate_indices`. If the block did not trigger inclusion of enough candidates, return `ApprovalCheckResult::Bad`. - * Construct a `SignedApprovalVote` using the candidates hashes and check against the validator's approval key, - based on the session info of the block. If invalid or no such validator, return `ApprovalCheckResult::Bad`. * Send `ApprovalCheckResult::Accepted` * [Import the checked approval vote](#import-checked-approval) for all candidates diff --git a/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md b/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md index c82d89d2d879..317f339ddd4e 100644 --- a/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md +++ b/polkadot/roadmap/implementers-guide/src/types/overseer-protocol.md @@ -111,21 +111,15 @@ pub enum ApprovalCheckError { } enum ApprovalVotingMessage { - /// Check if the assignment is valid and can be accepted by our view of the protocol. - /// Should not be sent unless the block hash is known. - CheckAndImportAssignment( - IndirectAssignmentCert, - CandidateIndex, // The index of the candidate included in the block. - ResponseChannel, - ), - /// Check if the approval vote is valid and can be accepted by our view of the - /// protocol. - /// - /// Should not be sent unless the block hash within the indirect vote is known. - CheckAndImportApproval( - IndirectSignedApprovalVote, - ResponseChannel, - ), + /// Import an assignment into the approval-voting database. + /// + /// Should not be sent unless the block hash is known and the VRF assignment checks out. + ImportAssignment(CheckedIndirectAssignment, Option>), + /// Import an approval vote into approval-voting database + /// + /// Should not be sent unless the block hash within the indirect vote is known, vote is + /// correctly signed and we had a previous assignment for the candidate. + ImportApproval(CheckedIndirectSignedApprovalVote, Option>), /// Returns the highest possible ancestor hash of the provided block hash which is /// acceptable to vote on finality for. Along with that, return the lists of candidate hashes /// which appear in every block from the (non-inclusive) base number up to (inclusive) the specified diff --git a/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs index fa94ee5f1caa..3108068686f9 100644 --- a/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs +++ b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/adapter.rs @@ -136,7 +136,7 @@ where give.clone() })?; - (credit_out, Some(credit_change)) + (credit_out, if credit_change.peek() > 0 { Some(credit_change) } else { None }) }; // We create an `AssetsInHolding` instance by putting in the resulting asset diff --git a/polkadot/xcm/xcm-executor/src/config.rs b/polkadot/xcm/xcm-executor/src/config.rs index 63b113bc250f..5bcbbd3466e8 100644 --- a/polkadot/xcm/xcm-executor/src/config.rs +++ b/polkadot/xcm/xcm-executor/src/config.rs @@ -33,6 +33,10 @@ pub trait Config { type RuntimeCall: Parameter + Dispatchable + GetDispatchInfo; /// How to send an onward XCM message. + /// + /// The sender is tasked with returning the assets it needs to pay for delivery fees. + /// Only one asset should be returned as delivery fees, any other will be ignored by + /// the executor. type XcmSender: SendXcm; /// How to withdraw and deposit an asset. @@ -74,6 +78,9 @@ pub trait Config { type AssetLocker: AssetLock; /// Handler for exchanging assets. + /// + /// This is used in the executor to swap the asset wanted for fees with the asset needed for + /// delivery fees. type AssetExchanger: AssetExchange; /// The handler for when there is an instruction to claim assets. diff --git a/polkadot/xcm/xcm-executor/src/lib.rs b/polkadot/xcm/xcm-executor/src/lib.rs index 74561e931e7e..a8110ca3d19f 100644 --- a/polkadot/xcm/xcm-executor/src/lib.rs +++ b/polkadot/xcm/xcm-executor/src/lib.rs @@ -83,6 +83,9 @@ pub struct XcmExecutor { appendix_weight: Weight, transact_status: MaybeErrorCode, fees_mode: FeesMode, + /// Asset provided in last `BuyExecution` instruction (if any) in current XCM program. Same + /// asset type will be used for paying any potential delivery fees incurred by the program. + asset_used_for_fees: Option, _config: PhantomData, } @@ -269,7 +272,7 @@ impl ExecuteXcm for XcmExecutor XcmExecutor { appendix_weight: Weight::zero(), transact_status: Default::default(), fees_mode: FeesMode { jit_withdraw: false }, + asset_used_for_fees: None, _config: PhantomData, } } @@ -469,31 +473,112 @@ impl XcmExecutor { Ok(()) } - fn take_fee(&mut self, fee: Assets, reason: FeeReason) -> XcmResult { + fn take_fee(&mut self, fees: Assets, reason: FeeReason) -> XcmResult { if Config::FeeManager::is_waived(self.origin_ref(), reason.clone()) { return Ok(()) } tracing::trace!( target: "xcm::fees", - ?fee, + ?fees, origin_ref = ?self.origin_ref(), fees_mode = ?self.fees_mode, ?reason, "Taking fees", ); - let paid = if self.fees_mode.jit_withdraw { + // We only ever use the first asset from `fees`. + let asset_needed_for_fees = match fees.get(0) { + Some(fee) => fee, + None => return Ok(()), // No delivery fees need to be paid. + }; + // If `BuyExecution` was called, we use that asset for delivery fees as well. + let asset_to_pay_for_fees = + self.calculate_asset_for_delivery_fees(asset_needed_for_fees.clone()); + tracing::trace!(target: "xcm::fees", ?asset_to_pay_for_fees); + // We withdraw or take from holding the asset the user wants to use for fee payment. + let withdrawn_fee_asset = if self.fees_mode.jit_withdraw { let origin = self.origin_ref().ok_or(XcmError::BadOrigin)?; - for asset in fee.inner() { - Config::AssetTransactor::withdraw_asset(&asset, origin, Some(&self.context))?; - } - fee + Config::AssetTransactor::withdraw_asset( + &asset_to_pay_for_fees, + origin, + Some(&self.context), + )?; + tracing::trace!(target: "xcm::fees", ?asset_needed_for_fees); + asset_to_pay_for_fees.clone().into() + } else { + let assets_taken_from_holding_to_pay_delivery_fees = self + .holding + .try_take(asset_to_pay_for_fees.clone().into()) + .map_err(|_| XcmError::NotHoldingFees)?; + tracing::trace!(target: "xcm::fees", ?assets_taken_from_holding_to_pay_delivery_fees); + let mut iter = assets_taken_from_holding_to_pay_delivery_fees.fungible_assets_iter(); + let asset = iter.next().ok_or(XcmError::NotHoldingFees)?; + asset.into() + }; + // We perform the swap, if needed, to pay fees. + let paid = if asset_to_pay_for_fees.id != asset_needed_for_fees.id { + let swapped_asset: Assets = Config::AssetExchanger::exchange_asset( + self.origin_ref(), + withdrawn_fee_asset, + &asset_needed_for_fees.clone().into(), + false, + ) + .map_err(|given_assets| { + tracing::error!( + target: "xcm::fees", + ?given_assets, + "Swap was deemed necessary but couldn't be done", + ); + XcmError::FeesNotMet + })? + .into(); + swapped_asset } else { - self.holding.try_take(fee.into()).map_err(|_| XcmError::NotHoldingFees)?.into() + // If the asset wanted to pay for fees is the one that was needed, + // we don't need to do any swap. + // We just use the assets withdrawn or taken from holding. + withdrawn_fee_asset.into() }; Config::FeeManager::handle_fee(paid, Some(&self.context), reason); Ok(()) } + /// Calculates the amount of `self.asset_used_for_fees` required to swap for + /// `asset_needed_for_fees`. + /// + /// The calculation is done by `Config::AssetExchanger`. + /// If `self.asset_used_for_fees` is not set, it will just return `asset_needed_for_fees`. + fn calculate_asset_for_delivery_fees(&self, asset_needed_for_fees: Asset) -> Asset { + if let Some(asset_wanted_for_fees) = &self.asset_used_for_fees { + if *asset_wanted_for_fees != asset_needed_for_fees.id { + match Config::AssetExchanger::quote_exchange_price( + &(asset_wanted_for_fees.clone(), Fungible(0)).into(), + &asset_needed_for_fees.clone().into(), + false, // Minimal. + ) { + Some(necessary_assets) => + // We only use the first asset for fees. + // If this is not enough to swap for the fee asset then it will error later down + // the line. + necessary_assets.get(0).unwrap_or(&asset_needed_for_fees.clone()).clone(), + // If we can't convert, then we return the original asset. + // It will error later in any case. + None => { + tracing::trace!( + target: "xcm::calculate_asset_for_delivery_fees", + ?asset_wanted_for_fees, + "Could not convert fees", + ); + asset_needed_for_fees.clone() + }, + } + } else { + asset_needed_for_fees + } + } else { + asset_needed_for_fees + } + } + /// Calculates what `local_querier` would be from the perspective of `destination`. fn to_querier( local_querier: Option, @@ -878,11 +963,23 @@ impl XcmExecutor { message_to_weigh.extend(xcm.0.clone().into_iter()); let (_, fee) = validate_send::(dest.clone(), Xcm(message_to_weigh))?; - // set aside fee to be charged by XcmSender - let transport_fee = self.holding.saturating_take(fee.into()); - - // now take assets to deposit (excluding transport_fee) + let maybe_delivery_fee = fee.get(0).map(|asset_needed_for_fees| { + tracing::trace!( + target: "xcm::DepositReserveAsset", + "Asset provided to pay for fees {:?}, asset required for delivery fees: {:?}", + self.asset_used_for_fees, asset_needed_for_fees, + ); + let asset_to_pay_for_fees = + self.calculate_asset_for_delivery_fees(asset_needed_for_fees.clone()); + // set aside fee to be charged by XcmSender + let delivery_fee = + self.holding.saturating_take(asset_to_pay_for_fees.into()); + tracing::trace!(target: "xcm::DepositReserveAsset", ?delivery_fee); + delivery_fee + }); + // now take assets to deposit (after having taken delivery fees) let deposited = self.holding.saturating_take(assets); + tracing::trace!(target: "xcm::DepositReserveAsset", ?deposited, "Assets except delivery fee"); self.deposit_assets_with_retry(&deposited, &dest)?; // Note that we pass `None` as `maybe_failed_bin` and drop any assets which // cannot be reanchored because we have already called `deposit_asset` on all @@ -890,8 +987,10 @@ impl XcmExecutor { let assets = Self::reanchored(deposited, &dest, None); let mut message = vec![ReserveAssetDeposited(assets), ClearOrigin]; message.extend(xcm.0.into_iter()); - // put back transport_fee in holding register to be charged by XcmSender - self.holding.subsume_assets(transport_fee); + // put back delivery_fee in holding register to be charged by XcmSender + if let Some(delivery_fee) = maybe_delivery_fee { + self.holding.subsume_assets(delivery_fee); + } self.send(dest, Xcm(message), FeeReason::DepositReserveAsset)?; Ok(()) }); @@ -969,6 +1068,10 @@ impl XcmExecutor { // should be executed. let Some(weight) = Option::::from(weight_limit) else { return Ok(()) }; let old_holding = self.holding.clone(); + // Save the asset being used for execution fees, so we later know what should be + // used for delivery fees. + self.asset_used_for_fees = Some(fees.id.clone()); + tracing::trace!(target: "xcm::executor::BuyExecution", asset_used_for_fees = ?self.asset_used_for_fees); // pay for `weight` using up to `fees` of the holding register. let max_fee = self.holding.try_take(fees.into()).map_err(|_| XcmError::NotHoldingFees)?; diff --git a/prdoc/pr_4928.prdoc b/prdoc/pr_4928.prdoc new file mode 100644 index 000000000000..9935652dc511 --- /dev/null +++ b/prdoc/pr_4928.prdoc @@ -0,0 +1,28 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: Move assignment VRF check and vote signature in approval-distribution + +doc: + - audience: Node Dev + description: | + This PR moves the assignment VRF check and approval vote signature from approval-voting into approval-distribution. + This optimization creates a better pipelining for processing new messages, because in this way approval-distribution + does not have to wait after approval-voting anymore and it will just notify it when it received a valid message that + is ready to be imported. + +crates: + - name: polkadot-node-subsystem-types + bump: major + - name: polkadot-approval-distribution + bump: major + - name: polkadot-node-core-approval-voting + bump: major + - name: polkadot-node-primitives + bump: major + - name: polkadot-service + bump: major + - name: polkadot-subsystem-bench + bump: major + - name: polkadot-overseer + bump: patch \ No newline at end of file diff --git a/prdoc/pr_5131.prdoc b/prdoc/pr_5131.prdoc new file mode 100644 index 000000000000..db1003ab4033 --- /dev/null +++ b/prdoc/pr_5131.prdoc @@ -0,0 +1,42 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: Swap for paying delivery fees in different assets + +doc: + - audience: Runtime User + description: | + If the `AssetExchanger` is configured on a runtime, the XCM executor is now able to swap assets + to pay for delivery fees. + This was already possible for execution fees via the `SwapFirstAssetTrader`. + A runtime where this will be possible is Asset Hub. + That means reserve asset transfers from Parachain A to Parachain B passing through Asset Hub no + longer need to have any DOT to pay for fees on AssetHub. + They can have any asset in a pool with DOT on Asset Hub, for example USDT or USDC. + - audience: Runtime Dev + description: | + Using the `AssetExchanger` XCM config item, the executor now swaps fees to use for delivery fees, + if possible. + If you want your runtime to support this, you need to configure this new item. + Thankfully, `xcm-builder` now has a new adapter for this, which lets you use `pallet-asset-conversion` + or any type that implements the `SwapCredit` and `QuotePrice` traits. + It's called `SingleAssetExchangeAdapter`, you can read more about it in its rust docs. + This item is already configured in Asset Hub. + + IMPORTANT: The executor now only takes the first asset for delivery fees. If you have configured a custom router + that returns more than one asset for delivery fees, then only the first one will be taken into account. + This is most likely not what you want. + +crates: + - name: staging-xcm-executor + bump: minor + - name: asset-hub-westend-runtime + bump: minor + - name: asset-hub-rococo-runtime + bump: minor + - name: staging-xcm-builder + bump: patch + - name: assets-common + bump: patch + - name: penpal-runtime + bump: minor