diff --git a/.gitlab/pipeline/zombienet/polkadot.yml b/.gitlab/pipeline/zombienet/polkadot.yml
index e722239d890c79d7eadbd0c678a06f5f0b3e0b79..14a235bcda8604dfee771d49ffb22163510df73b 100644
--- a/.gitlab/pipeline/zombienet/polkadot.yml
+++ b/.gitlab/pipeline/zombienet/polkadot.yml
@@ -63,6 +63,8 @@
     LOCAL_SDK_TEST: "/builds/parity/mirrors/polkadot-sdk/polkadot/zombienet-sdk-tests"
     FF_DISABLE_UMASK_FOR_DOCKER_EXECUTOR: 1
     RUN_IN_CONTAINER: "1"
+    # don't retry sdk tests
+    NEXTEST_RETRIES: 0
   artifacts:
     name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}"
     when: always
@@ -190,6 +192,7 @@ zombienet-polkadot-elastic-scaling-0001-basic-3cores-6s-blocks:
       --local-dir="${LOCAL_DIR}/elastic_scaling"
       --test="0002-elastic-scaling-doesnt-break-parachains.zndsl"
 
+
 .zombienet-polkadot-functional-0012-spam-statement-distribution-requests:
   extends:
     - .zombienet-polkadot-common
@@ -397,3 +400,19 @@ zombienet-polkadot-malus-0001-dispute-valid:
     - unset NEXTEST_FAILURE_OUTPUT
     - unset NEXTEST_SUCCESS_OUTPUT
     - cargo nextest run --archive-file ./artifacts/polkadot-zombienet-tests.tar.zst --no-capture -- smoke::coretime_revenue::coretime_revenue_test
+
+zombienet-polkadot-elastic-scaling-slot-based-3cores:
+  extends:
+    - .zombienet-polkadot-common
+  needs:
+    - job: build-polkadot-zombienet-tests
+      artifacts: true
+  before_script:
+    - !reference [ ".zombienet-polkadot-common", "before_script" ]
+    - export POLKADOT_IMAGE="${ZOMBIENET_INTEGRATION_TEST_IMAGE}"
+    - export CUMULUS_IMAGE="docker.io/paritypr/test-parachain:${PIPELINE_IMAGE_TAG}"
+  script:
+    # we want to use `--no-capture` in zombienet tests.
+    - unset NEXTEST_FAILURE_OUTPUT
+    - unset NEXTEST_SUCCESS_OUTPUT
+    - cargo nextest run --archive-file ./artifacts/polkadot-zombienet-tests.tar.zst --no-capture -- elastic_scaling::slot_based_3cores::slot_based_3cores_test
diff --git a/Cargo.lock b/Cargo.lock
index c6438fdffa3fc209b71ce486ad050a00cd18305c..41c149c11d41f1fe850242c862529feba981a670 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -32164,9 +32164,9 @@ dependencies = [
 
 [[package]]
 name = "zombienet-configuration"
-version = "0.2.16"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ad4fc5b0f1aa54de6bf2d6771c449b41cad47e1cf30559af0a71452686b47ab"
+checksum = "d716b3ff8112d98ced15f53b0c72454f8cde533fe2b68bb04379228961efbd80"
 dependencies = [
  "anyhow",
  "lazy_static",
@@ -32184,9 +32184,9 @@ dependencies = [
 
 [[package]]
 name = "zombienet-orchestrator"
-version = "0.2.16"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4a7dd25842ded75c7f4dc4f38f05fef567bd0b37fd3057c223d4ee34d8fa817"
+checksum = "4098a7d33b729b59e32c41a87aa4d484bd1b8771a059bbd4edfb4d430b3b2d74"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -32217,9 +32217,9 @@ dependencies = [
 
 [[package]]
 name = "zombienet-prom-metrics-parser"
-version = "0.2.16"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a63e0c6024dd19b0f8b28afa94f78c211e5c163350ecda4a48084532d74d7cfe"
+checksum = "961e30be45b34f6ebeabf29ee2f47b0cd191ea62e40c064752572207509a6f5c"
 dependencies = [
  "pest",
  "pest_derive",
@@ -32228,9 +32228,9 @@ dependencies = [
 
 [[package]]
 name = "zombienet-provider"
-version = "0.2.16"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d87c29390a342d0f4f62b6796861fb82e0e56c49929a272b689e8dbf24eaab9"
+checksum = "ab0f7f01780b7c99a6c40539d195d979f234305f32808d547438b50829d44262"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -32259,14 +32259,15 @@ dependencies = [
 
 [[package]]
 name = "zombienet-sdk"
-version = "0.2.16"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "829e5111182caf00ba57cd63656cf0bde6ce6add7f6a9747d15821c202a3f27e"
+checksum = "99a3c5f2d657235b3ab7dc384677e63cde21983029e99106766ecd49e9f8d7f3"
 dependencies = [
  "async-trait",
  "futures",
  "lazy_static",
  "subxt",
+ "subxt-signer",
  "tokio",
  "zombienet-configuration",
  "zombienet-orchestrator",
@@ -32276,9 +32277,9 @@ dependencies = [
 
 [[package]]
 name = "zombienet-support"
-version = "0.2.16"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99568384a1d9645458ab9de377b3517cb543a1ece5aba905aeb58d269139df4e"
+checksum = "296f887ea88e07edd771f8e1d0dec5297a58b422f4b884a6292a21ebe03277cb"
 dependencies = [
  "anyhow",
  "async-trait",
diff --git a/Cargo.toml b/Cargo.toml
index 56ae70f8f993de5a7489f87451d78c4b6e365d1b..64a11a340d10d31d68129b0684007dc1954e6d05 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1389,7 +1389,7 @@ xcm-procedural = { path = "polkadot/xcm/procedural", default-features = false }
 xcm-runtime-apis = { path = "polkadot/xcm/xcm-runtime-apis", default-features = false }
 xcm-simulator = { path = "polkadot/xcm/xcm-simulator", default-features = false }
 zeroize = { version = "1.7.0", default-features = false }
-zombienet-sdk = { version = "0.2.16" }
+zombienet-sdk = { version = "0.2.19" }
 zstd = { version = "0.12.4", default-features = false }
 
 [profile.release]
diff --git a/cumulus/parachains/runtimes/testing/rococo-parachain/Cargo.toml b/cumulus/parachains/runtimes/testing/rococo-parachain/Cargo.toml
index 035d0ac94be6adf6715e8e872996967dc9d71536..2ddb3364fc09945174784558d825369c4877f620 100644
--- a/cumulus/parachains/runtimes/testing/rococo-parachain/Cargo.toml
+++ b/cumulus/parachains/runtimes/testing/rococo-parachain/Cargo.toml
@@ -51,7 +51,7 @@ polkadot-runtime-common = { workspace = true }
 # Cumulus
 cumulus-pallet-aura-ext = { workspace = true }
 pallet-message-queue = { workspace = true }
-cumulus-pallet-parachain-system = { workspace = true, features = ["experimental-ump-signals"] }
+cumulus-pallet-parachain-system = { workspace = true }
 cumulus-pallet-xcm = { workspace = true }
 cumulus-pallet-xcmp-queue = { workspace = true }
 cumulus-ping = { workspace = true }
diff --git a/cumulus/test/runtime/Cargo.toml b/cumulus/test/runtime/Cargo.toml
index 8117e6e6970961c6ce4ebc32a1f62462b1d6426d..b80170af3e83de7f886f438d56b71ace187c71b8 100644
--- a/cumulus/test/runtime/Cargo.toml
+++ b/cumulus/test/runtime/Cargo.toml
@@ -96,3 +96,4 @@ std = [
 ]
 increment-spec-version = []
 elastic-scaling = []
+experimental-ump-signals = ["cumulus-pallet-parachain-system/experimental-ump-signals"]
diff --git a/cumulus/test/runtime/build.rs b/cumulus/test/runtime/build.rs
index 7a7fe8ffaa82e6fe2ab861dddaea12c44442fd06..43e60c1074a047ecaeda05838178bac87f857899 100644
--- a/cumulus/test/runtime/build.rs
+++ b/cumulus/test/runtime/build.rs
@@ -29,6 +29,14 @@ fn main() {
 		.with_current_project()
 		.enable_feature("elastic-scaling")
 		.import_memory()
+		.set_file_name("wasm_binary_elastic_scaling_mvp.rs")
+		.build();
+
+	WasmBuilder::new()
+		.with_current_project()
+		.enable_feature("elastic-scaling")
+		.enable_feature("experimental-ump-signals")
+		.import_memory()
 		.set_file_name("wasm_binary_elastic_scaling.rs")
 		.build();
 }
diff --git a/cumulus/test/runtime/src/lib.rs b/cumulus/test/runtime/src/lib.rs
index b1649c410581a0f725e76312a535965db5e3cb6e..4abc10276af1852f8fcd272ae3ea3486355dab93 100644
--- a/cumulus/test/runtime/src/lib.rs
+++ b/cumulus/test/runtime/src/lib.rs
@@ -27,6 +27,11 @@ pub mod wasm_spec_version_incremented {
 	include!(concat!(env!("OUT_DIR"), "/wasm_binary_spec_version_incremented.rs"));
 }
 
+pub mod elastic_scaling_mvp {
+	#[cfg(feature = "std")]
+	include!(concat!(env!("OUT_DIR"), "/wasm_binary_elastic_scaling_mvp.rs"));
+}
+
 pub mod elastic_scaling {
 	#[cfg(feature = "std")]
 	include!(concat!(env!("OUT_DIR"), "/wasm_binary_elastic_scaling.rs"));
diff --git a/cumulus/test/service/src/chain_spec.rs b/cumulus/test/service/src/chain_spec.rs
index 3d4e4dca5f8df864a05858ca894ac60ae62ad795..5ebcc14592d74d2585fbc8f6e54f41dd1b0b700d 100644
--- a/cumulus/test/service/src/chain_spec.rs
+++ b/cumulus/test/service/src/chain_spec.rs
@@ -116,3 +116,13 @@ pub fn get_elastic_scaling_chain_spec(id: Option<ParaId>) -> ChainSpec {
 			.expect("WASM binary was not built, please build it!"),
 	)
 }
+
+/// Get the chain spec for a specific parachain ID.
+pub fn get_elastic_scaling_mvp_chain_spec(id: Option<ParaId>) -> ChainSpec {
+	get_chain_spec_with_extra_endowed(
+		id,
+		Default::default(),
+		cumulus_test_runtime::elastic_scaling_mvp::WASM_BINARY
+			.expect("WASM binary was not built, please build it!"),
+	)
+}
diff --git a/cumulus/test/service/src/cli.rs b/cumulus/test/service/src/cli.rs
index 220b0449f3392727228f99b3d10f2ccbc2165693..e019089e70fe8e8b7b12543d78459a0fed539ff7 100644
--- a/cumulus/test/service/src/cli.rs
+++ b/cumulus/test/service/src/cli.rs
@@ -262,10 +262,16 @@ impl SubstrateCli for TestCollatorCli {
 				tracing::info!("Using default test service chain spec.");
 				Box::new(cumulus_test_service::get_chain_spec(Some(ParaId::from(2000)))) as Box<_>
 			},
+			"elastic-scaling-mvp" => {
+				tracing::info!("Using elastic-scaling mvp chain spec.");
+				Box::new(cumulus_test_service::get_elastic_scaling_mvp_chain_spec(Some(
+					ParaId::from(2100),
+				))) as Box<_>
+			},
 			"elastic-scaling" => {
 				tracing::info!("Using elastic-scaling chain spec.");
 				Box::new(cumulus_test_service::get_elastic_scaling_chain_spec(Some(ParaId::from(
-					2100,
+					2200,
 				)))) as Box<_>
 			},
 			path => {
diff --git a/polkadot/runtime/rococo/src/genesis_config_presets.rs b/polkadot/runtime/rococo/src/genesis_config_presets.rs
index bdbf6f37d92c221df09fc8d7c98f333089e68a6f..a96a509b0e4da5b6439bb61849722db76f0bc881 100644
--- a/polkadot/runtime/rococo/src/genesis_config_presets.rs
+++ b/polkadot/runtime/rococo/src/genesis_config_presets.rs
@@ -129,7 +129,9 @@ fn default_parachains_host_configuration(
 			allowed_ancestry_len: 2,
 		},
 		node_features: bitvec::vec::BitVec::from_element(
-			1u8 << (FeatureIndex::ElasticScalingMVP as usize),
+			1u8 << (FeatureIndex::ElasticScalingMVP as usize) |
+				1u8 << (FeatureIndex::EnableAssignmentsV2 as usize) |
+				1u8 << (FeatureIndex::CandidateReceiptV2 as usize),
 		),
 		scheduler_params: SchedulerParams {
 			lookahead: 2,
diff --git a/polkadot/runtime/westend/src/genesis_config_presets.rs b/polkadot/runtime/westend/src/genesis_config_presets.rs
index b8f7710089e044b15ec97645ad95223181f0dd46..ea5aff554e8c5945383695cec1ad0b73b5fb2621 100644
--- a/polkadot/runtime/westend/src/genesis_config_presets.rs
+++ b/polkadot/runtime/westend/src/genesis_config_presets.rs
@@ -133,7 +133,8 @@ fn default_parachains_host_configuration(
 		},
 		node_features: bitvec::vec::BitVec::from_element(
 			1u8 << (FeatureIndex::ElasticScalingMVP as usize) |
-				1u8 << (FeatureIndex::EnableAssignmentsV2 as usize),
+				1u8 << (FeatureIndex::EnableAssignmentsV2 as usize) |
+				1u8 << (FeatureIndex::CandidateReceiptV2 as usize),
 		),
 		scheduler_params: SchedulerParams {
 			lookahead: 2,
diff --git a/polkadot/zombienet-sdk-tests/build.rs b/polkadot/zombienet-sdk-tests/build.rs
index 240d86386af2ee831f887d83a2ff9ced16c9e175..f7a62a53a8ac5f72a351b02113bffd6fafb3aced 100644
--- a/polkadot/zombienet-sdk-tests/build.rs
+++ b/polkadot/zombienet-sdk-tests/build.rs
@@ -25,39 +25,47 @@ fn make_env_key(k: &str) -> String {
 	replace_dashes(&k.to_ascii_uppercase())
 }
 
+fn wasm_sub_path(chain: &str) -> String {
+	let (package, runtime_name) =
+		if let Some(cumulus_test_runtime) = chain.strip_prefix("cumulus-test-runtime-") {
+			(
+				"cumulus-test-runtime".to_string(),
+				format!("wasm_binary_{}.rs", replace_dashes(cumulus_test_runtime)),
+			)
+		} else {
+			(format!("{chain}-runtime"), replace_dashes(&format!("{chain}-runtime")))
+		};
+
+	format!("{}/{}.wasm", package, runtime_name)
+}
+
 fn find_wasm(chain: &str) -> Option<PathBuf> {
 	const PROFILES: [&str; 2] = ["release", "testnet"];
 	let manifest_path = env::var("CARGO_WORKSPACE_ROOT_DIR").unwrap();
 	let manifest_path = manifest_path.strip_suffix('/').unwrap();
 	debug_output!("manifest_path is  : {}", manifest_path);
-	let package = format!("{chain}-runtime");
+
+	let sub_path = wasm_sub_path(chain);
+
 	let profile = PROFILES.into_iter().find(|p| {
-		let full_path = format!(
-			"{}/target/{}/wbuild/{}/{}.wasm",
-			manifest_path,
-			p,
-			&package,
-			replace_dashes(&package)
-		);
+		let full_path = format!("{}/target/{}/wbuild/{}", manifest_path, p, sub_path);
 		debug_output!("checking wasm at : {}", full_path);
 		matches!(path::PathBuf::from(&full_path).try_exists(), Ok(true))
 	});
 
 	debug_output!("profile is : {:?}", profile);
 	profile.map(|profile| {
-		PathBuf::from(&format!(
-			"{}/target/{}/wbuild/{}/{}.wasm",
-			manifest_path,
-			profile,
-			&package,
-			replace_dashes(&package)
-		))
+		PathBuf::from(&format!("{}/target/{}/wbuild/{}", manifest_path, profile, sub_path))
 	})
 }
 
 // based on https://gist.github.com/s0me0ne-unkn0wn/bbd83fe32ce10327086adbf13e750eec
 fn build_wasm(chain: &str) -> PathBuf {
-	let package = format!("{chain}-runtime");
+	let package = if chain.starts_with("cumulus-test-runtime-") {
+		String::from("cumulus-test-runtime")
+	} else {
+		format!("{chain}-runtime")
+	};
 
 	let cargo = env::var("CARGO").unwrap();
 	let target = env::var("TARGET").unwrap();
@@ -81,11 +89,7 @@ fn build_wasm(chain: &str) -> PathBuf {
 		.status()
 		.unwrap();
 
-	let wasm_path = &format!(
-		"{target_dir}/{target}/release/wbuild/{}/{}.wasm",
-		&package,
-		replace_dashes(&package)
-	);
+	let wasm_path = &format!("{target_dir}/{target}/release/wbuild/{}", wasm_sub_path(chain));
 	PathBuf::from(wasm_path)
 }
 
@@ -128,6 +132,10 @@ fn main() {
 	const METADATA_DIR: &str = "metadata-files";
 	const CHAINS: [&str; 2] = ["rococo", "coretime-rococo"];
 
+	// Add some cumulus test runtimes if needed. Formatted like
+	// "cumulus-test-runtime-elastic-scaling".
+	const CUMULUS_TEST_RUNTIMES: [&str; 0] = [];
+
 	let metadata_path = format!("{manifest_path}/{METADATA_DIR}");
 
 	for chain in CHAINS {
@@ -145,6 +153,21 @@ fn main() {
 		};
 	}
 
+	for chain in CUMULUS_TEST_RUNTIMES {
+		let full_path = format!("{metadata_path}/{chain}-local.scale");
+		let output_path = path::PathBuf::from(&full_path);
+
+		match output_path.try_exists() {
+			Ok(true) => {
+				debug_output!("got: {}", full_path);
+			},
+			_ => {
+				debug_output!("needs: {}", full_path);
+				fetch_metadata_file(chain, &output_path);
+			},
+		};
+	}
+
 	substrate_build_script_utils::generate_cargo_keys();
 	substrate_build_script_utils::rerun_if_git_head_changed();
 	println!("cargo:rerun-if-changed={}", metadata_path);
diff --git a/polkadot/zombienet-sdk-tests/tests/elastic_scaling/helpers.rs b/polkadot/zombienet-sdk-tests/tests/elastic_scaling/helpers.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7d4ad4a1dd8b1a05738e5dd9a9ef06f55ad26be3
--- /dev/null
+++ b/polkadot/zombienet-sdk-tests/tests/elastic_scaling/helpers.rs
@@ -0,0 +1,60 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+use super::rococo;
+use std::{collections::HashMap, ops::Range};
+use subxt::{OnlineClient, PolkadotConfig};
+
+// Helper function for asserting the throughput of parachains (total number of backed candidates in
+// a window of relay chain blocks), after the first session change.
+pub async fn assert_para_throughput(
+	relay_client: &OnlineClient<PolkadotConfig>,
+	stop_at: u32,
+	expected_candidate_ranges: HashMap<u32, Range<u32>>,
+) -> Result<(), anyhow::Error> {
+	let mut blocks_sub = relay_client.blocks().subscribe_finalized().await?;
+	let mut candidate_count: HashMap<u32, u32> = HashMap::new();
+	let mut current_block_count = 0;
+	let mut had_first_session_change = false;
+
+	while let Some(block) = blocks_sub.next().await {
+		let block = block?;
+		log::debug!("Finalized relay chain block {}", block.number());
+		let events = block.events().await?;
+		let is_session_change = events.has::<rococo::session::events::NewSession>()?;
+
+		if !had_first_session_change && is_session_change {
+			had_first_session_change = true;
+		}
+
+		if had_first_session_change && !is_session_change {
+			current_block_count += 1;
+
+			for event in events.find::<rococo::para_inclusion::events::CandidateBacked>() {
+				*(candidate_count.entry(event?.0.descriptor.para_id.0).or_default()) += 1;
+			}
+		}
+
+		if current_block_count == stop_at {
+			break;
+		}
+	}
+
+	log::info!(
+		"Reached {} finalized relay chain blocks that contain backed candidates. The per-parachain distribution is: {:#?}",
+		stop_at,
+		candidate_count
+	);
+
+	for (para_id, expected_candidate_range) in expected_candidate_ranges {
+		let actual = candidate_count
+			.get(&para_id)
+			.expect("ParaId did not have any backed candidates");
+		assert!(
+			expected_candidate_range.contains(actual),
+			"Candidate count {actual} not within range {expected_candidate_range:?}"
+		);
+	}
+
+	Ok(())
+}
diff --git a/polkadot/zombienet-sdk-tests/tests/elastic_scaling/mod.rs b/polkadot/zombienet-sdk-tests/tests/elastic_scaling/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..bb296a419df1f2387d7ff44d4f71769eaadd0467
--- /dev/null
+++ b/polkadot/zombienet-sdk-tests/tests/elastic_scaling/mod.rs
@@ -0,0 +1,8 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+#[subxt::subxt(runtime_metadata_path = "metadata-files/rococo-local.scale")]
+pub mod rococo {}
+
+mod helpers;
+mod slot_based_3cores;
diff --git a/polkadot/zombienet-sdk-tests/tests/elastic_scaling/slot_based_3cores.rs b/polkadot/zombienet-sdk-tests/tests/elastic_scaling/slot_based_3cores.rs
new file mode 100644
index 0000000000000000000000000000000000000000..41ec1250ecc4479f6d0cca4ff61c49b5bc33b901
--- /dev/null
+++ b/polkadot/zombienet-sdk-tests/tests/elastic_scaling/slot_based_3cores.rs
@@ -0,0 +1,166 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Test that parachains that use a single slot-based collator with elastic scaling MVP and with
+// elastic scaling with RFC103 can achieve full throughput of 3 candidates per block.
+
+use anyhow::anyhow;
+
+use super::{
+	helpers::assert_para_throughput,
+	rococo,
+	rococo::runtime_types::{
+		pallet_broker::coretime_interface::CoreAssignment,
+		polkadot_runtime_parachains::assigner_coretime::PartsOf57600,
+	},
+};
+use serde_json::json;
+use subxt::{OnlineClient, PolkadotConfig};
+use subxt_signer::sr25519::dev;
+use zombienet_sdk::NetworkConfigBuilder;
+
+#[tokio::test(flavor = "multi_thread")]
+async fn slot_based_3cores_test() -> Result<(), anyhow::Error> {
+	let _ = env_logger::try_init_from_env(
+		env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
+	);
+
+	let images = zombienet_sdk::environment::get_images_from_env();
+
+	let config = NetworkConfigBuilder::new()
+		.with_relaychain(|r| {
+			let r = r
+				.with_chain("rococo-local")
+				.with_default_command("polkadot")
+				.with_default_image(images.polkadot.as_str())
+				.with_default_args(vec![("-lparachain=debug").into()])
+				.with_genesis_overrides(json!({
+					"configuration": {
+						"config": {
+							"scheduler_params": {
+								// Num cores is 4, because 2 extra will be added automatically when registering the paras.
+								"num_cores": 4,
+								"max_validators_per_core": 2
+							},
+							"async_backing_params": {
+								"max_candidate_depth": 6,
+								"allowed_ancestry_len": 2
+							}
+						}
+					}
+				}))
+				// Have to set a `with_node` outside of the loop below, so that `r` has the right
+				// type.
+				.with_node(|node| node.with_name("validator-0"));
+
+			(1..12)
+				.fold(r, |acc, i| acc.with_node(|node| node.with_name(&format!("validator-{i}"))))
+		})
+		.with_parachain(|p| {
+			// Para 2100 uses the old elastic scaling mvp, which doesn't send the new UMP signal
+			// commitment for selecting the core index.
+			p.with_id(2100)
+				.with_default_command("test-parachain")
+				.with_default_image(images.cumulus.as_str())
+				.with_chain("elastic-scaling-mvp")
+				.with_default_args(vec![("--experimental-use-slot-based").into()])
+				.with_default_args(vec![
+					("--experimental-use-slot-based").into(),
+					("-lparachain=debug,aura=debug").into(),
+				])
+				.with_collator(|n| n.with_name("collator-elastic-mvp"))
+		})
+		.with_parachain(|p| {
+			// Para 2200 uses the new RFC103-enabled collator which sends the UMP signal commitment
+			// for selecting the core index
+			p.with_id(2200)
+				.with_default_command("test-parachain")
+				.with_default_image(images.cumulus.as_str())
+				.with_chain("elastic-scaling")
+				.with_default_args(vec![
+					("--experimental-use-slot-based").into(),
+					("-lparachain=debug,aura=debug").into(),
+				])
+				.with_collator(|n| n.with_name("collator-elastic"))
+		})
+		.build()
+		.map_err(|e| {
+			let errs = e.into_iter().map(|e| e.to_string()).collect::<Vec<_>>().join(" ");
+			anyhow!("config errs: {errs}")
+		})?;
+
+	let spawn_fn = zombienet_sdk::environment::get_spawn_fn();
+	let network = spawn_fn(config).await?;
+
+	let relay_node = network.get_node("validator-0")?;
+
+	let relay_client: OnlineClient<PolkadotConfig> = relay_node.wait_client().await?;
+	let alice = dev::alice();
+
+	// Assign two extra cores to each parachain.
+	relay_client
+		.tx()
+		.sign_and_submit_then_watch_default(
+			&rococo::tx()
+				.sudo()
+				.sudo(rococo::runtime_types::rococo_runtime::RuntimeCall::Utility(
+					rococo::runtime_types::pallet_utility::pallet::Call::batch {
+						calls: vec![
+							rococo::runtime_types::rococo_runtime::RuntimeCall::Coretime(
+								rococo::runtime_types::polkadot_runtime_parachains::coretime::pallet::Call::assign_core {
+									core: 0,
+									begin: 0,
+									assignment: vec![(CoreAssignment::Task(2100), PartsOf57600(57600))],
+									end_hint: None
+								}
+							),
+							rococo::runtime_types::rococo_runtime::RuntimeCall::Coretime(
+								rococo::runtime_types::polkadot_runtime_parachains::coretime::pallet::Call::assign_core {
+									core: 1,
+									begin: 0,
+									assignment: vec![(CoreAssignment::Task(2100), PartsOf57600(57600))],
+									end_hint: None
+								}
+							),
+							rococo::runtime_types::rococo_runtime::RuntimeCall::Coretime(
+								rococo::runtime_types::polkadot_runtime_parachains::coretime::pallet::Call::assign_core {
+									core: 2,
+									begin: 0,
+									assignment: vec![(CoreAssignment::Task(2200), PartsOf57600(57600))],
+									end_hint: None
+								}
+							),
+							rococo::runtime_types::rococo_runtime::RuntimeCall::Coretime(
+								rococo::runtime_types::polkadot_runtime_parachains::coretime::pallet::Call::assign_core {
+									core: 3,
+									begin: 0,
+									assignment: vec![(CoreAssignment::Task(2200), PartsOf57600(57600))],
+									end_hint: None
+								}
+							)
+						],
+					},
+				)),
+			&alice,
+		)
+		.await?
+		.wait_for_finalized_success()
+		.await?;
+
+	log::info!("2 more cores assigned to each parachain");
+
+	// Expect a backed candidate count of at least 39 for each parachain in 15 relay chain blocks
+	// (2.6 candidates per para per relay chain block).
+	// Note that only blocks after the first session change and blocks that don't contain a session
+	// change will be counted.
+	assert_para_throughput(
+		&relay_client,
+		15,
+		[(2100, 39..46), (2200, 39..46)].into_iter().collect(),
+	)
+	.await?;
+
+	log::info!("Test finished successfully");
+
+	Ok(())
+}
diff --git a/polkadot/zombienet-sdk-tests/tests/lib.rs b/polkadot/zombienet-sdk-tests/tests/lib.rs
index 74cdc0765600f52688c0bf255763c60c822c798a..977e0f90b1c94ad333fa02baa23b10a4132bde69 100644
--- a/polkadot/zombienet-sdk-tests/tests/lib.rs
+++ b/polkadot/zombienet-sdk-tests/tests/lib.rs
@@ -1,4 +1,7 @@
 // Copyright (C) Parity Technologies (UK) Ltd.
 // SPDX-License-Identifier: Apache-2.0
 
+#[cfg(feature = "zombie-metadata")]
+mod elastic_scaling;
+#[cfg(feature = "zombie-metadata")]
 mod smoke;
diff --git a/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs b/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs
index 7880dc782d05d0550c4b178c14362c897b1bcb08..2da2436a1111221fff1aeb2fb57b09798ea37880 100644
--- a/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs
+++ b/polkadot/zombienet-sdk-tests/tests/smoke/coretime_revenue.rs
@@ -180,7 +180,7 @@ where
 
 #[tokio::test(flavor = "multi_thread")]
 async fn coretime_revenue_test() -> Result<(), anyhow::Error> {
-	env_logger::init_from_env(
+	let _ = env_logger::try_init_from_env(
 		env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
 	);
 
@@ -499,7 +499,7 @@ async fn coretime_revenue_test() -> Result<(), anyhow::Error> {
 
 	assert_total_issuance(relay_client.clone(), para_client.clone(), total_issuance).await;
 
-	log::info!("Test finished successfuly");
+	log::info!("Test finished successfully");
 
 	Ok(())
 }
diff --git a/polkadot/zombienet-sdk-tests/tests/smoke/mod.rs b/polkadot/zombienet-sdk-tests/tests/smoke/mod.rs
index a3fe153826740dbd5d1f92e641fce21fc31cebeb..072a9d54ecdad96b2cc7790ea0cfc63ae1fdb96c 100644
--- a/polkadot/zombienet-sdk-tests/tests/smoke/mod.rs
+++ b/polkadot/zombienet-sdk-tests/tests/smoke/mod.rs
@@ -1,5 +1,4 @@
 // Copyright (C) Parity Technologies (UK) Ltd.
 // SPDX-License-Identifier: Apache-2.0
 
-#[cfg(feature = "zombie-metadata")]
 mod coretime_revenue;
diff --git a/prdoc/pr_6452.prdoc b/prdoc/pr_6452.prdoc
new file mode 100644
index 0000000000000000000000000000000000000000..f2cb69875e958d356586aa3b9adb27a5efbcd5c5
--- /dev/null
+++ b/prdoc/pr_6452.prdoc
@@ -0,0 +1,16 @@
+title: "elastic scaling RFC 103 end-to-end tests"
+
+doc:
+  - audience: [Node Dev, Runtime Dev]
+    description: |
+      Adds end-to-end zombienet-sdk tests for elastic scaling using the RFC103 implementation.
+      Only notable user-facing change is that the default chain configurations of westend and rococo
+      now enable by default the CandidateReceiptV2 node feature.
+
+crates:
+  - name: westend-runtime
+    bump: patch
+  - name: rococo-runtime
+    bump: patch
+  - name: rococo-parachain-runtime
+    bump: patch