diff --git a/Cargo.lock b/Cargo.lock
index 71cb808d83d4c..9019acfcbb239 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7353,7 +7353,6 @@ dependencies = [
  "sp-core",
  "sp-externalities",
  "sp-io",
- "sp-maybe-compressed-blob",
  "sp-panic-handler",
  "sp-runtime",
  "sp-runtime-interface",
@@ -7381,6 +7380,7 @@ dependencies = [
  "pwasm-utils 0.14.0",
  "sp-allocator",
  "sp-core",
+ "sp-maybe-compressed-blob",
  "sp-serializer",
  "sp-wasm-interface",
  "thiserror",
@@ -8883,6 +8883,7 @@ dependencies = [
  "sp-core",
  "sp-externalities",
  "sp-keystore",
+ "sp-maybe-compressed-blob",
  "sp-runtime-interface",
  "sp-state-machine",
  "sp-std",
@@ -9063,6 +9064,7 @@ name = "sp-runtime-interface-test"
 version = "2.0.0"
 dependencies = [
  "sc-executor",
+ "sc-executor-common",
  "sp-core",
  "sp-io",
  "sp-runtime",
@@ -9291,6 +9293,19 @@ dependencies = [
  "serde",
  "sp-runtime",
  "sp-std",
+ "sp-version-proc-macro",
+]
+
+[[package]]
+name = "sp-version-proc-macro"
+version = "3.0.0"
+dependencies = [
+ "parity-scale-codec",
+ "proc-macro-crate 1.0.0",
+ "proc-macro2",
+ "quote",
+ "sp-version",
+ "syn",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 1b35c7181d17d..9d7017be1d0de 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -182,6 +182,7 @@ members = [
 	"primitives/trie",
 	"primitives/utils",
 	"primitives/version",
+	"primitives/version/proc-macro",
 	"primitives/wasm-interface",
 	"test-utils/client",
 	"test-utils/derive",
diff --git a/bin/node-template/runtime/src/lib.rs b/bin/node-template/runtime/src/lib.rs
index 178918266a7fc..b928f8d3410ef 100644
--- a/bin/node-template/runtime/src/lib.rs
+++ b/bin/node-template/runtime/src/lib.rs
@@ -94,6 +94,7 @@ pub mod opaque {
 
 // To learn more about runtime versioning and what each of the following value means:
 //   https://substrate.dev/docs/en/knowledgebase/runtime/upgrades#runtime-versioning
+#[sp_version::runtime_version]
 pub const VERSION: RuntimeVersion = RuntimeVersion {
 	spec_name: create_runtime_str!("node-template"),
 	impl_name: create_runtime_str!("node-template"),
diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs
index 52eb5e42bd4da..a20437c25659b 100644
--- a/bin/node/runtime/src/lib.rs
+++ b/bin/node/runtime/src/lib.rs
@@ -105,6 +105,7 @@ pub fn wasm_binary_unwrap() -> &'static [u8] {
 }
 
 /// Runtime version.
+#[sp_version::runtime_version]
 pub const VERSION: RuntimeVersion = RuntimeVersion {
 	spec_name: create_runtime_str!("node"),
 	impl_name: create_runtime_str!("substrate-node"),
diff --git a/client/executor/Cargo.toml b/client/executor/Cargo.toml
index e9f0fa14d8e7e..f678029d06743 100644
--- a/client/executor/Cargo.toml
+++ b/client/executor/Cargo.toml
@@ -30,7 +30,6 @@ sp-api = { version = "3.0.0", path = "../../primitives/api" }
 sp-wasm-interface = { version = "3.0.0", path = "../../primitives/wasm-interface" }
 sp-runtime-interface = { version = "3.0.0", path = "../../primitives/runtime-interface" }
 sp-externalities = { version = "0.9.0", path = "../../primitives/externalities" }
-sp-maybe-compressed-blob = { version = "3.0.0", path = "../../primitives/maybe-compressed-blob" }
 sc-executor-common = { version = "0.9.0", path = "common" }
 sc-executor-wasmi = { version = "0.9.0", path = "wasmi" }
 sc-executor-wasmtime = { version = "0.9.0", path = "wasmtime", optional = true }
diff --git a/client/executor/common/Cargo.toml b/client/executor/common/Cargo.toml
index 95c090686e83b..9f9ec989431fb 100644
--- a/client/executor/common/Cargo.toml
+++ b/client/executor/common/Cargo.toml
@@ -22,6 +22,7 @@ wasmi = "0.6.2"
 sp-core = { version = "3.0.0", path = "../../../primitives/core" }
 sp-allocator = { version = "3.0.0", path = "../../../primitives/allocator" }
 sp-wasm-interface = { version = "3.0.0", path = "../../../primitives/wasm-interface" }
+sp-maybe-compressed-blob = { version = "3.0.0", path = "../../../primitives/maybe-compressed-blob" }
 sp-serializer = { version = "3.0.0", path = "../../../primitives/serializer" }
 thiserror = "1.0.21"
 
diff --git a/client/executor/common/src/runtime_blob/runtime_blob.rs b/client/executor/common/src/runtime_blob/runtime_blob.rs
index d90a48fde0c81..6541f9f5d966d 100644
--- a/client/executor/common/src/runtime_blob/runtime_blob.rs
+++ b/client/executor/common/src/runtime_blob/runtime_blob.rs
@@ -27,6 +27,17 @@ pub struct RuntimeBlob {
 }
 
 impl RuntimeBlob {
+	/// Create `RuntimeBlob` from the given wasm code. Will attempt to decompress the code before
+	/// deserializing it.
+	///
+	/// See [`sp_maybe_compressed_blob`] for details about decompression.
+	pub fn uncompress_if_needed(wasm_code: &[u8]) -> Result<Self, WasmError> {
+		use sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT;
+		let wasm_code = sp_maybe_compressed_blob::decompress(wasm_code, CODE_BLOB_BOMB_LIMIT)
+			.map_err(|e| WasmError::Other(format!("Decompression error: {:?}", e)))?;
+		Self::new(&wasm_code)
+	}
+
 	/// Create `RuntimeBlob` from the given wasm code.
 	///
 	/// Returns `Err` if the wasm code cannot be deserialized.
@@ -85,9 +96,23 @@ impl RuntimeBlob {
 		})
 	}
 
+	/// Scans the wasm blob for the first section with the name that matches the given. Returns the
+	/// contents of the custom section if found or `None` otherwise.
+	pub fn custom_section_contents(&self, section_name: &str) -> Option<&[u8]> {
+		self.raw_module
+			.custom_sections()
+			.find(|cs| cs.name() == section_name)
+			.map(|cs| cs.payload())
+		}
+
 	/// Consumes this runtime blob and serializes it.
 	pub fn serialize(self) -> Vec<u8> {
 		serialize(self.raw_module)
 			.expect("serializing into a vec should succeed; qed")
 	}
+
+	/// Destructure this structure into the underlying parity-wasm Module.
+	pub fn into_inner(self) -> RawModule {
+		self.raw_module
+	}
 }
diff --git a/client/executor/src/integration_tests/mod.rs b/client/executor/src/integration_tests/mod.rs
index d08f830f40dae..fb39429dfdb24 100644
--- a/client/executor/src/integration_tests/mod.rs
+++ b/client/executor/src/integration_tests/mod.rs
@@ -17,18 +17,20 @@
 // along with this program. If not, see <https://www.gnu.org/licenses/>.
 mod sandbox;
 
+use std::sync::Arc;
 use codec::{Encode, Decode};
 use hex_literal::hex;
 use sp_core::{
 	blake2_128, blake2_256, ed25519, sr25519, map, Pair,
 	offchain::{OffchainWorkerExt, OffchainDbExt, testing},
-	traits::{Externalities, CallInWasm},
+	traits::Externalities,
 };
 use sc_runtime_test::wasm_binary_unwrap;
 use sp_state_machine::TestExternalities as CoreTestExternalities;
 use sp_trie::{TrieConfiguration, trie_types::Layout};
 use sp_wasm_interface::HostFunctions as _;
 use sp_runtime::traits::BlakeTwo256;
+use sc_executor_common::{wasm_runtime::WasmModule, runtime_blob::RuntimeBlob};
 use tracing_subscriber::layer::SubscriberExt;
 
 use crate::WasmExecutionMethod;
@@ -77,13 +79,12 @@ fn call_in_wasm<E: Externalities>(
 		8,
 		None,
 	);
-	executor.call_in_wasm(
-		&wasm_binary_unwrap()[..],
-		None,
+	executor.uncached_call(
+		RuntimeBlob::uncompress_if_needed(&wasm_binary_unwrap()[..]).unwrap(),
+		ext,
+		true,
 		function,
 		call_data,
-		ext,
-		sp_core::traits::MissingHostFunctions::Allow,
 	)
 }
 
@@ -541,28 +542,37 @@ fn should_trap_when_heap_exhausted(wasm_method: WasmExecutionMethod) {
 		None,
 	);
 
-	let err = executor.call_in_wasm(
-		&wasm_binary_unwrap()[..],
-		None,
-		"test_exhaust_heap",
-		&[0],
-		&mut ext.ext(),
-		sp_core::traits::MissingHostFunctions::Allow,
-	).unwrap_err();
+	let err = executor
+		.uncached_call(
+			RuntimeBlob::uncompress_if_needed(&wasm_binary_unwrap()[..]).unwrap(),
+			&mut ext.ext(),
+			true,
+			"test_exhaust_heap",
+			&[0],
+		)
+		.unwrap_err();
 
 	assert!(err.contains("Allocator ran out of space"));
 }
 
-test_wasm_execution!(returns_mutable_static);
-fn returns_mutable_static(wasm_method: WasmExecutionMethod) {
-	let runtime = crate::wasm_runtime::create_wasm_runtime_with_code(
+fn mk_test_runtime(wasm_method: WasmExecutionMethod, pages: u64) -> Arc<dyn WasmModule> {
+	let blob = RuntimeBlob::uncompress_if_needed(&wasm_binary_unwrap()[..])
+		.expect("failed to create a runtime blob out of test runtime");
+
+	crate::wasm_runtime::create_wasm_runtime_with_code(
 		wasm_method,
-		1024,
-		&wasm_binary_unwrap()[..],
+		pages,
+		blob,
 		HostFunctions::host_functions(),
 		true,
 		None,
-	).expect("Creates runtime");
+	)
+	.expect("failed to instantiate wasm runtime")
+}
+
+test_wasm_execution!(returns_mutable_static);
+fn returns_mutable_static(wasm_method: WasmExecutionMethod) {
+	let runtime = mk_test_runtime(wasm_method, 1024);
 
 	let instance = runtime.new_instance().unwrap();
 	let res = instance.call_export("returns_mutable_static", &[0]).unwrap();
@@ -589,14 +599,7 @@ fn restoration_of_globals(wasm_method: WasmExecutionMethod) {
 	// to our allocator algorithm there are inefficiencies.
 	const REQUIRED_MEMORY_PAGES: u64 = 32;
 
-	let runtime = crate::wasm_runtime::create_wasm_runtime_with_code(
-		wasm_method,
-		REQUIRED_MEMORY_PAGES,
-		&wasm_binary_unwrap()[..],
-		HostFunctions::host_functions(),
-		true,
-		None,
-	).expect("Creates runtime");
+	let runtime = mk_test_runtime(wasm_method, REQUIRED_MEMORY_PAGES);
 	let instance = runtime.new_instance().unwrap();
 
 	// On the first invocation we allocate approx. 768KB (75%) of stack and then trap.
@@ -610,14 +613,7 @@ fn restoration_of_globals(wasm_method: WasmExecutionMethod) {
 
 test_wasm_execution!(interpreted_only heap_is_reset_between_calls);
 fn heap_is_reset_between_calls(wasm_method: WasmExecutionMethod) {
-	let runtime = crate::wasm_runtime::create_wasm_runtime_with_code(
-		wasm_method,
-		1024,
-		&wasm_binary_unwrap()[..],
-		HostFunctions::host_functions(),
-		true,
-		None,
-	).expect("Creates runtime");
+	let runtime = mk_test_runtime(wasm_method, 1024);
 	let instance = runtime.new_instance().unwrap();
 
 	let heap_base = instance.get_global_const("__heap_base")
@@ -642,27 +638,27 @@ fn parallel_execution(wasm_method: WasmExecutionMethod) {
 		8,
 		None,
 	));
-	let code_hash = blake2_256(wasm_binary_unwrap()).to_vec();
-	let threads: Vec<_> = (0..8).map(|_|
-		{
+	let threads: Vec<_> = (0..8)
+		.map(|_| {
 			let executor = executor.clone();
-			let code_hash = code_hash.clone();
 			std::thread::spawn(move || {
 				let mut ext = TestExternalities::default();
 				let mut ext = ext.ext();
 				assert_eq!(
-					executor.call_in_wasm(
-						&wasm_binary_unwrap()[..],
-						Some(code_hash.clone()),
-						"test_twox_128",
-						&[0],
-						&mut ext,
-						sp_core::traits::MissingHostFunctions::Allow,
-					).unwrap(),
+					executor
+						.uncached_call(
+							RuntimeBlob::uncompress_if_needed(&wasm_binary_unwrap()[..]).unwrap(),
+							&mut ext,
+							true,
+							"test_twox_128",
+							&[0],
+						)
+						.unwrap(),
 					hex!("99e9d85137db46ef4bbea33613baafd5").to_vec().encode(),
 				);
 			})
-		}).collect();
+		})
+		.collect();
 
 	for t in threads.into_iter() {
 		t.join().unwrap();
@@ -671,9 +667,7 @@ fn parallel_execution(wasm_method: WasmExecutionMethod) {
 
 test_wasm_execution!(wasm_tracing_should_work);
 fn wasm_tracing_should_work(wasm_method: WasmExecutionMethod) {
-
-	use std::sync::{Arc, Mutex};
-
+	use std::sync::Mutex;
 	use sc_tracing::{SpanDatum, TraceEvent};
 
 	struct TestTraceHandler(Arc<Mutex<Vec<SpanDatum>>>);
@@ -779,6 +773,5 @@ fn panic_in_spawned_instance_panics_on_joining_its_result(wasm_method: WasmExecu
 		&mut ext,
 	).unwrap_err();
 
-	dbg!(&error_result);
 	assert!(format!("{}", error_result).contains("Spawned task"));
 }
diff --git a/client/executor/src/lib.rs b/client/executor/src/lib.rs
index c30015a86b20e..c0cbf9c94dafd 100644
--- a/client/executor/src/lib.rs
+++ b/client/executor/src/lib.rs
@@ -38,14 +38,17 @@ mod wasm_runtime;
 mod integration_tests;
 
 pub use wasmi;
-pub use native_executor::{with_externalities_safe, NativeExecutor, WasmExecutor, NativeExecutionDispatch};
+pub use native_executor::{
+	with_externalities_safe, NativeExecutor, WasmExecutor, NativeExecutionDispatch,
+};
 pub use sp_version::{RuntimeVersion, NativeVersion};
 pub use codec::Codec;
 #[doc(hidden)]
-pub use sp_core::traits::{Externalities, CallInWasm};
+pub use sp_core::traits::{Externalities};
 #[doc(hidden)]
 pub use sp_wasm_interface;
 pub use wasm_runtime::WasmExecutionMethod;
+pub use wasm_runtime::read_embedded_version;
 
 pub use sc_executor_common::{error, sandbox};
 
@@ -68,7 +71,7 @@ mod tests {
 	use sc_runtime_test::wasm_binary_unwrap;
 	use sp_io::TestExternalities;
 	use sp_wasm_interface::HostFunctions;
-	use sp_core::traits::CallInWasm;
+	use sc_executor_common::runtime_blob::RuntimeBlob;
 
 	#[test]
 	fn call_in_interpreted_wasm_works() {
@@ -82,14 +85,15 @@ mod tests {
 			8,
 			None,
 		);
-		let res = executor.call_in_wasm(
-			&wasm_binary_unwrap()[..],
-			None,
-			"test_empty_return",
-			&[],
-			&mut ext,
-			sp_core::traits::MissingHostFunctions::Allow,
-		).unwrap();
+		let res = executor
+			.uncached_call(
+				RuntimeBlob::uncompress_if_needed(&wasm_binary_unwrap()[..]).unwrap(),
+				&mut ext,
+				true,
+				"test_empty_return",
+				&[],
+			)
+			.unwrap();
 		assert_eq!(res, vec![0u8; 0]);
 	}
 }
diff --git a/client/executor/src/native_executor.rs b/client/executor/src/native_executor.rs
index 6df651e1b776c..760e0c066beec 100644
--- a/client/executor/src/native_executor.rs
+++ b/client/executor/src/native_executor.rs
@@ -33,14 +33,14 @@ use sp_version::{NativeVersion, RuntimeVersion};
 use codec::{Decode, Encode};
 use sp_core::{
 	NativeOrEncoded,
-	traits::{
-		CodeExecutor, Externalities, RuntimeCode, MissingHostFunctions,
-		RuntimeSpawnExt, RuntimeSpawn,
-	},
+	traits::{CodeExecutor, Externalities, RuntimeCode, RuntimeSpawnExt, RuntimeSpawn},
 };
 use log::trace;
 use sp_wasm_interface::{HostFunctions, Function};
-use sc_executor_common::wasm_runtime::{WasmInstance, WasmModule, InvokeMethod};
+use sc_executor_common::{
+	wasm_runtime::{WasmInstance, WasmModule, InvokeMethod},
+	runtime_blob::RuntimeBlob,
+};
 use sp_externalities::ExternalitiesExt as _;
 use sp_tasks::new_async_externalities;
 
@@ -188,64 +188,81 @@ impl WasmExecutor {
 			Err(e) => Err(e),
 		}
 	}
+
+	/// Perform a call into the given runtime.
+	///
+	/// The runtime is passed as a [`RuntimeBlob`]. The runtime will be isntantiated with the
+	/// parameters this `WasmExecutor` was initialized with.
+	///
+	/// In case of problems with during creation of the runtime or instantation, a `Err` is returned.
+	/// that describes the message.
+	#[doc(hidden)] // We use this function for tests across multiple crates.
+	pub fn uncached_call(
+		&self,
+		runtime_blob: RuntimeBlob,
+		ext: &mut dyn Externalities,
+		allow_missing_host_functions: bool,
+		export_name: &str,
+		call_data: &[u8],
+	) -> std::result::Result<Vec<u8>, String> {
+		let module = crate::wasm_runtime::create_wasm_runtime_with_code(
+			self.method,
+			self.default_heap_pages,
+			runtime_blob,
+			self.host_functions.to_vec(),
+			allow_missing_host_functions,
+			self.cache_path.as_deref(),
+		)
+		.map_err(|e| format!("Failed to create module: {:?}", e))?;
+
+		let instance = module
+			.new_instance()
+			.map_err(|e| format!("Failed to create instance: {:?}", e))?;
+
+		let instance = AssertUnwindSafe(instance);
+		let mut ext = AssertUnwindSafe(ext);
+		let module = AssertUnwindSafe(module);
+
+		with_externalities_safe(&mut **ext, move || {
+			preregister_builtin_ext(module.clone());
+			instance.call_export(export_name, call_data)
+		})
+		.and_then(|r| r)
+		.map_err(|e| e.to_string())
+	}
 }
 
-impl sp_core::traits::CallInWasm for WasmExecutor {
-	fn call_in_wasm(
+impl sp_core::traits::ReadRuntimeVersion for WasmExecutor {
+	fn read_runtime_version(
 		&self,
 		wasm_code: &[u8],
-		code_hash: Option<Vec<u8>>,
-		method: &str,
-		call_data: &[u8],
 		ext: &mut dyn Externalities,
-		missing_host_functions: MissingHostFunctions,
 	) -> std::result::Result<Vec<u8>, String> {
-		let allow_missing_host_functions = missing_host_functions.allowed();
-
-		if let Some(hash) = code_hash {
-			let code = RuntimeCode {
-				code_fetcher: &sp_core::traits::WrappedRuntimeCode(wasm_code.into()),
-				hash,
-				heap_pages: None,
-			};
+		let runtime_blob = RuntimeBlob::uncompress_if_needed(&wasm_code)
+			.map_err(|e| format!("Failed to create runtime blob: {:?}", e))?;
+
+		if let Some(version) = crate::wasm_runtime::read_embedded_version(&runtime_blob)
+			.map_err(|e| format!("Failed to read the static section: {:?}", e))
+			.map(|v| v.map(|v| v.encode()))?
+		{
+			return Ok(version);
+		}
 
-			self.with_instance(&code, ext, allow_missing_host_functions, |module, instance, _, mut ext| {
-				with_externalities_safe(
-					&mut **ext,
-					move || {
-						RuntimeInstanceSpawn::register_on_externalities(module.clone());
-						instance.call_export(method, call_data)
-					}
-				)
-			}).map_err(|e| e.to_string())
-		} else {
-			let module = crate::wasm_runtime::create_wasm_runtime_with_code(
-				self.method,
-				self.default_heap_pages,
-				&wasm_code,
-				self.host_functions.to_vec(),
-				allow_missing_host_functions,
-				self.cache_path.as_deref(),
-			)
-				.map_err(|e| format!("Failed to create module: {:?}", e))?;
-
-			let instance = module.new_instance()
-				.map_err(|e| format!("Failed to create instance: {:?}", e))?;
-
-			let instance = AssertUnwindSafe(instance);
-			let mut ext = AssertUnwindSafe(ext);
-			let module = AssertUnwindSafe(module);
+		// If the blob didn't have embedded runtime version section, we fallback to the legacy
+		// way of fetching the verison: i.e. instantiating the given instance and calling
+		// `Core_version` on it.
 
-			with_externalities_safe(
-				&mut **ext,
-				move || {
-					RuntimeInstanceSpawn::register_on_externalities(module.clone());
-					instance.call_export(method, call_data)
-				}
-			)
-			.and_then(|r| r)
-			.map_err(|e| e.to_string())
-		}
+		self.uncached_call(
+			runtime_blob,
+			ext,
+			// If a runtime upgrade introduces new host functions that are not provided by
+			// the node, we should not fail at instantiation. Otherwise nodes that are
+			// updated could run this successfully and it could lead to a storage root
+			// mismatch when importing this block.
+			true,
+			"Core_version",
+			&[],
+		)
 	}
 }
 
@@ -436,29 +453,25 @@ impl RuntimeInstanceSpawn {
 		ext.extension::<sp_core::traits::TaskExecutorExt>()
 			.map(move |task_ext| Self::new(module, task_ext.clone()))
 	}
+}
 
-	/// Register new `RuntimeSpawnExt` on current externalities.
-	///
-	/// This extensions will spawn instances from provided `module`.
-	pub fn register_on_externalities(module: Arc<dyn WasmModule>) {
-		sp_externalities::with_externalities(
-			move |mut ext| {
-				if let Some(runtime_spawn) =
-					Self::with_externalities_and_module(module.clone(), ext)
-				{
-					if let Err(e) = ext.register_extension(
-						RuntimeSpawnExt(Box::new(runtime_spawn))
-					) {
-						trace!(
-							target: "executor",
-							"Failed to register `RuntimeSpawnExt` instance on externalities: {:?}",
-							e,
-						)
-					}
-				}
+/// Pre-registers the built-in extensions to the currently effective externalities.
+///
+/// Meant to be called each time before calling into the runtime.
+fn preregister_builtin_ext(module: Arc<dyn WasmModule>) {
+	sp_externalities::with_externalities(move |mut ext| {
+		if let Some(runtime_spawn) =
+			RuntimeInstanceSpawn::with_externalities_and_module(module, ext)
+		{
+			if let Err(e) = ext.register_extension(RuntimeSpawnExt(Box::new(runtime_spawn))) {
+				trace!(
+					target: "executor",
+					"Failed to register `RuntimeSpawnExt` instance on externalities: {:?}",
+					e,
+				)
 			}
-		);
-	}
+		}
+	});
 }
 
 impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeExecutor<D> {
@@ -506,7 +519,7 @@ impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeExecutor<D> {
 						with_externalities_safe(
 							&mut **ext,
 							move || {
-								RuntimeInstanceSpawn::register_on_externalities(module.clone());
+								preregister_builtin_ext(module.clone());
 								instance.call_export(method, data).map(NativeOrEncoded::Encoded)
 							}
 						)
@@ -557,17 +570,13 @@ impl<D: NativeExecutionDispatch> Clone for NativeExecutor<D> {
 	}
 }
 
-impl<D: NativeExecutionDispatch> sp_core::traits::CallInWasm for NativeExecutor<D> {
-	fn call_in_wasm(
+impl<D: NativeExecutionDispatch> sp_core::traits::ReadRuntimeVersion for NativeExecutor<D> {
+	fn read_runtime_version(
 		&self,
-		wasm_blob: &[u8],
-		code_hash: Option<Vec<u8>>,
-		method: &str,
-		call_data: &[u8],
+		wasm_code: &[u8],
 		ext: &mut dyn Externalities,
-		missing_host_functions: MissingHostFunctions,
 	) -> std::result::Result<Vec<u8>, String> {
-		self.wasm.call_in_wasm(wasm_blob, code_hash, method, call_data, ext, missing_host_functions)
+		self.wasm.read_runtime_version(wasm_code, ext)
 	}
 }
 
diff --git a/client/executor/src/wasm_runtime.rs b/client/executor/src/wasm_runtime.rs
index 53968a645c994..23e88f9440907 100644
--- a/client/executor/src/wasm_runtime.rs
+++ b/client/executor/src/wasm_runtime.rs
@@ -29,7 +29,10 @@ use sp_core::traits::{Externalities, RuntimeCode, FetchRuntimeCode};
 use sp_version::RuntimeVersion;
 use std::panic::AssertUnwindSafe;
 use std::path::{Path, PathBuf};
-use sc_executor_common::wasm_runtime::{WasmModule, WasmInstance};
+use sc_executor_common::{
+	wasm_runtime::{WasmModule, WasmInstance},
+	runtime_blob::RuntimeBlob,
+};
 
 use sp_wasm_interface::Function;
 
@@ -235,6 +238,9 @@ impl RuntimeCache {
 			None =>  {
 				let code = runtime_code.fetch_runtime_code().ok_or(WasmError::CodeNotFound)?;
 
+				#[cfg(not(target_os = "unknown"))]
+				let time = std::time::Instant::now();
+
 				let result = create_versioned_wasm_runtime(
 					&code,
 					code_hash.clone(),
@@ -246,9 +252,22 @@ impl RuntimeCache {
 					self.max_runtime_instances,
 					self.cache_path.as_deref(),
 				);
-				if let Err(ref err) = result {
-					log::warn!(target: "wasm-runtime", "Cannot create a runtime: {:?}", err);
+
+				match result {
+					Ok(ref result) => {
+						#[cfg(not(target_os = "unknown"))]
+						log::debug!(
+							target: "wasm-runtime",
+							"Prepared new runtime version {:?} in {} ms.",
+							result.version,
+							time.elapsed().as_millis(),
+						);
+					}
+					Err(ref err) => {
+						log::warn!(target: "wasm-runtime", "Cannot create a runtime: {:?}", err);
+					}
 				}
+
 				Arc::new(result?)
 			}
 		};
@@ -278,16 +297,11 @@ impl RuntimeCache {
 pub fn create_wasm_runtime_with_code(
 	wasm_method: WasmExecutionMethod,
 	heap_pages: u64,
-	code: &[u8],
+	blob: RuntimeBlob,
 	host_functions: Vec<&'static dyn Function>,
 	allow_missing_func_imports: bool,
 	cache_path: Option<&Path>,
 ) -> Result<Arc<dyn WasmModule>, WasmError> {
-	use sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT;
-
-	let code = sp_maybe_compressed_blob::decompress(code, CODE_BLOB_BOMB_LIMIT)
-		.map_err(|e| WasmError::Other(format!("Decompression error: {:?}", e)))?;
-
 	match wasm_method {
 		WasmExecutionMethod::Interpreted => {
 			// Wasmi doesn't have any need in a cache directory.
@@ -297,7 +311,7 @@ pub fn create_wasm_runtime_with_code(
 			drop(cache_path);
 
 			sc_executor_wasmi::create_runtime(
-				&code,
+				blob,
 				heap_pages,
 				host_functions,
 				allow_missing_func_imports,
@@ -306,7 +320,6 @@ pub fn create_wasm_runtime_with_code(
 		}
 		#[cfg(feature = "wasmtime")]
 		WasmExecutionMethod::Compiled => {
-			let blob = sc_executor_common::runtime_blob::RuntimeBlob::new(&code)?;
 			sc_executor_wasmtime::create_runtime(
 				sc_executor_wasmtime::CodeSupplyMode::Verbatim { blob },
 				sc_executor_wasmtime::Config {
@@ -343,6 +356,55 @@ fn decode_version(version: &[u8]) -> Result<RuntimeVersion, WasmError> {
 	}
 }
 
+fn decode_runtime_apis(apis: &[u8]) -> Result<Vec<([u8; 8], u32)>, WasmError> {
+	use std::convert::TryFrom;
+	use sp_api::RUNTIME_API_INFO_SIZE;
+
+	apis.chunks(RUNTIME_API_INFO_SIZE)
+		.map(|chunk| {
+			// `chunk` can be less than `RUNTIME_API_INFO_SIZE` if the total length of `apis` doesn't
+			// completely divide by `RUNTIME_API_INFO_SIZE`.
+			<[u8; RUNTIME_API_INFO_SIZE]>::try_from(chunk)
+				.map(sp_api::deserialize_runtime_api_info)
+				.map_err(|_| {
+					WasmError::Other(format!(
+						"a clipped runtime api info declaration"
+					))
+				})
+		})
+		.collect::<Result<Vec<_>, WasmError>>()
+}
+
+/// Take the runtime blob and scan it for the custom wasm sections containing the version information
+/// and construct the `RuntimeVersion` from them.
+///
+/// If there are no such sections, it returns `None`. If there is an error during decoding those
+/// sections, `Err` will be returned.
+pub fn read_embedded_version(
+	blob: &RuntimeBlob,
+) -> Result<Option<RuntimeVersion>, WasmError> {
+	if let Some(version_section) = blob.custom_section_contents("runtime_version") {
+		// We do not use `decode_version` here because the runtime_version section is not supposed
+		// to ever contain a legacy version. Apart from that `decode_version` relies on presence
+		// of a special API in the `apis` field to treat the input as a non-legacy version. However
+		// the structure found in the `runtime_version` always contain an empty `apis` field. Therefore
+		// the version read will be mistakingly treated as an legacy one.
+		let mut decoded_version = sp_api::RuntimeVersion::decode(&mut &version_section[..])
+			.map_err(|_|
+				WasmError::Instantiation("failed to decode verison section".into())
+			)?;
+
+		// Don't stop on this and check if there is a special section that encodes all runtime APIs.
+		if let Some(apis_section) = blob.custom_section_contents("runtime_apis") {
+			decoded_version.apis = decode_runtime_apis(apis_section)?.into();
+		}
+
+		Ok(Some(decoded_version))
+	} else {
+		Ok(None)
+	}
+}
+
 fn create_versioned_wasm_runtime(
 	code: &[u8],
 	code_hash: Vec<u8>,
@@ -354,41 +416,44 @@ fn create_versioned_wasm_runtime(
 	max_instances: usize,
 	cache_path: Option<&Path>,
 ) -> Result<VersionedRuntime, WasmError> {
-	#[cfg(not(target_os = "unknown"))]
-	let time = std::time::Instant::now();
+	// The incoming code may be actually compressed. We decompress it here and then work with
+	// the uncompressed code from now on.
+	let blob = sc_executor_common::runtime_blob::RuntimeBlob::uncompress_if_needed(&code)?;
+
+	// Use the runtime blob to scan if there is any metadata embedded into the wasm binary pertaining
+	// to runtime version. We do it before consuming the runtime blob for creating the runtime.
+	let mut version: Option<_> = read_embedded_version(&blob)?;
+
 	let runtime = create_wasm_runtime_with_code(
 		wasm_method,
 		heap_pages,
-		&code,
+		blob,
 		host_functions,
 		allow_missing_func_imports,
 		cache_path,
 	)?;
 
-	// Call to determine runtime version.
-	let version_result = {
-		// `ext` is already implicitly handled as unwind safe, as we store it in a global variable.
-		let mut ext = AssertUnwindSafe(ext);
-
-		// The following unwind safety assertion is OK because if the method call panics, the
-		// runtime will be dropped.
-		let runtime = AssertUnwindSafe(runtime.as_ref());
-		crate::native_executor::with_externalities_safe(
-			&mut **ext,
-			move || runtime.new_instance()?.call("Core_version".into(), &[])
-		).map_err(|_| WasmError::Instantiation("panic in call to get runtime version".into()))?
-	};
-	let version = match version_result {
-		Ok(version) => Some(decode_version(&version)?),
-		Err(_) => None,
-	};
-	#[cfg(not(target_os = "unknown"))]
-	log::debug!(
-		target: "wasm-runtime",
-		"Prepared new runtime version {:?} in {} ms.",
-		version,
-		time.elapsed().as_millis(),
-	);
+	// If the runtime blob doesn't embed the runtime version then use the legacy version query
+	// mechanism: call the runtime.
+	if version.is_none() {
+		// Call to determine runtime version.
+		let version_result = {
+			// `ext` is already implicitly handled as unwind safe, as we store it in a global variable.
+			let mut ext = AssertUnwindSafe(ext);
+
+			// The following unwind safety assertion is OK because if the method call panics, the
+			// runtime will be dropped.
+			let runtime = AssertUnwindSafe(runtime.as_ref());
+			crate::native_executor::with_externalities_safe(
+				&mut **ext,
+				move || runtime.new_instance()?.call("Core_version".into(), &[])
+			).map_err(|_| WasmError::Instantiation("panic in call to get runtime version".into()))?
+		};
+
+		if let Ok(version_buf) = version_result {
+			version = Some(decode_version(&version_buf)?)
+		}
+	}
 
 	let mut instances = Vec::with_capacity(max_instances);
 	instances.resize_with(max_instances, || Mutex::new(None));
diff --git a/client/executor/wasmi/src/lib.rs b/client/executor/wasmi/src/lib.rs
index 0163e07e654bf..953c5e5178a61 100644
--- a/client/executor/wasmi/src/lib.rs
+++ b/client/executor/wasmi/src/lib.rs
@@ -641,18 +641,18 @@ impl WasmModule for WasmiRuntime {
 /// Create a new `WasmiRuntime` given the code. This function loads the module and
 /// stores it in the instance.
 pub fn create_runtime(
-	code: &[u8],
+	blob: RuntimeBlob,
 	heap_pages: u64,
 	host_functions: Vec<&'static dyn Function>,
 	allow_missing_func_imports: bool,
 ) -> Result<WasmiRuntime, WasmError> {
-	let module = Module::from_buffer(&code).map_err(|_| WasmError::InvalidModule)?;
+	let data_segments_snapshot = DataSegmentsSnapshot::take(&blob)
+		.map_err(|e| WasmError::Other(e.to_string()))?;
 
-	// Extract the data segments from the wasm code.
-	//
-	// A return of this error actually indicates that there is a problem in logic, since
-	// we just loaded and validated the `module` above.
-	let (data_segments_snapshot, global_vals_snapshot) = {
+	let module = Module::from_parity_wasm_module(blob.into_inner())
+		.map_err(|_| WasmError::InvalidModule)?;
+
+	let global_vals_snapshot = {
 		let (instance, _, _) = instantiate_module(
 			heap_pages as usize,
 			&module,
@@ -660,12 +660,7 @@ pub fn create_runtime(
 			allow_missing_func_imports,
 		)
 		.map_err(|e| WasmError::Instantiation(e.to_string()))?;
-
-		let data_segments_snapshot = DataSegmentsSnapshot::take(&RuntimeBlob::new(code)?)
-			.map_err(|e| WasmError::Other(e.to_string()))?;
-		let global_vals_snapshot = GlobalValsSnapshot::take(&instance);
-
-		(data_segments_snapshot, global_vals_snapshot)
+		GlobalValsSnapshot::take(&instance)
 	};
 
 	Ok(WasmiRuntime {
diff --git a/frame/system/src/tests.rs b/frame/system/src/tests.rs
index 25f67d7a1a499..df28e2c118c21 100644
--- a/frame/system/src/tests.rs
+++ b/frame/system/src/tests.rs
@@ -420,17 +420,13 @@ fn prunes_block_hash_mappings() {
 
 #[test]
 fn set_code_checks_works() {
-	struct CallInWasm(Vec<u8>);
+	struct ReadRuntimeVersion(Vec<u8>);
 
-	impl sp_core::traits::CallInWasm for CallInWasm {
-		fn call_in_wasm(
+	impl sp_core::traits::ReadRuntimeVersion for ReadRuntimeVersion {
+		fn read_runtime_version(
 			&self,
-			_: &[u8],
-			_: Option<Vec<u8>>,
-			_: &str,
-			_: &[u8],
-			_: &mut dyn sp_externalities::Externalities,
-			_: sp_core::traits::MissingHostFunctions,
+			_wasm_code: &[u8],
+			_ext: &mut dyn sp_externalities::Externalities,
 		) -> Result<Vec<u8>, String> {
 			Ok(self.0.clone())
 		}
@@ -452,10 +448,10 @@ fn set_code_checks_works() {
 			impl_version,
 			..Default::default()
 		};
-		let call_in_wasm = CallInWasm(version.encode());
+		let read_runtime_version = ReadRuntimeVersion(version.encode());
 
 		let mut ext = new_test_ext();
-		ext.register_extension(sp_core::traits::CallInWasmExt::new(call_in_wasm));
+		ext.register_extension(sp_core::traits::ReadRuntimeVersionExt::new(read_runtime_version));
 		ext.execute_with(|| {
 			let res = System::set_code(
 				RawOrigin::Root.into(),
@@ -471,7 +467,7 @@ fn set_code_checks_works() {
 fn set_code_with_real_wasm_blob() {
 	let executor = substrate_test_runtime_client::new_native_executor();
 	let mut ext = new_test_ext();
-	ext.register_extension(sp_core::traits::CallInWasmExt::new(executor));
+	ext.register_extension(sp_core::traits::ReadRuntimeVersionExt::new(executor));
 	ext.execute_with(|| {
 		System::set_block_number(1);
 		System::set_code(
@@ -494,7 +490,7 @@ fn set_code_with_real_wasm_blob() {
 fn runtime_upgraded_with_set_storage() {
 	let executor = substrate_test_runtime_client::new_native_executor();
 	let mut ext = new_test_ext();
-	ext.register_extension(sp_core::traits::CallInWasmExt::new(executor));
+	ext.register_extension(sp_core::traits::ReadRuntimeVersionExt::new(executor));
 	ext.execute_with(|| {
 		System::set_storage(
 			RawOrigin::Root.into(),
diff --git a/primitives/api/proc-macro/src/impl_runtime_apis.rs b/primitives/api/proc-macro/src/impl_runtime_apis.rs
index 85ba0788105d7..e918724c0f5b2 100644
--- a/primitives/api/proc-macro/src/impl_runtime_apis.rs
+++ b/primitives/api/proc-macro/src/impl_runtime_apis.rs
@@ -633,8 +633,11 @@ fn generate_api_impl_for_runtime_api(impls: &[ItemImpl]) -> Result<TokenStream>
 /// runtime apis.
 fn generate_runtime_api_versions(impls: &[ItemImpl]) -> Result<TokenStream> {
 	let mut result = Vec::with_capacity(impls.len());
+	let mut sections = Vec::with_capacity(impls.len());
 	let mut processed_traits = HashSet::new();
 
+	let c = generate_crate_access(HIDDEN_INCLUDES_ID);
+
 	for impl_ in impls {
 		let mut path = extend_with_runtime_decl_path(
 			extract_impl_trait(&impl_, RequireQualifiedTraitPath::Yes)?.clone(),
@@ -667,12 +670,21 @@ fn generate_runtime_api_versions(impls: &[ItemImpl]) -> Result<TokenStream> {
 			#( #attrs )*
 			(#id, #version)
 		));
-	}
 
-	let c = generate_crate_access(HIDDEN_INCLUDES_ID);
+		sections.push(quote!(
+			#( #attrs )*
+			const _: () = {
+				// All sections with the same name are going to be merged by concatenation.
+				#[link_section = "runtime_apis"]
+				static SECTION_CONTENTS: [u8; 12] = #c::serialize_runtime_api_info(#id, #version);
+			};
+		));
+	}
 
 	Ok(quote!(
 		const RUNTIME_API_VERSIONS: #c::ApisVec = #c::create_apis_vec!([ #( #result ),* ]);
+
+		#( #sections )*
 	))
 }
 
diff --git a/primitives/api/src/lib.rs b/primitives/api/src/lib.rs
index 155bb899a2ed5..97342377a76c8 100644
--- a/primitives/api/src/lib.rs
+++ b/primitives/api/src/lib.rs
@@ -613,6 +613,49 @@ pub trait RuntimeApiInfo {
 	const VERSION: u32;
 }
 
+/// The number of bytes required to encode a [`RuntimeApiInfo`].
+///
+/// 8 bytes for `ID` and 4 bytes for a version.
+pub const RUNTIME_API_INFO_SIZE: usize = 12;
+
+/// Crude and simple way to serialize the `RuntimeApiInfo` into a bunch of bytes.
+pub const fn serialize_runtime_api_info(id: [u8; 8], version: u32) -> [u8; RUNTIME_API_INFO_SIZE] {
+	let version = version.to_le_bytes();
+
+	let mut r = [0; RUNTIME_API_INFO_SIZE];
+	r[0] = id[0];
+	r[1] = id[1];
+	r[2] = id[2];
+	r[3] = id[3];
+	r[4] = id[4];
+	r[5] = id[5];
+	r[6] = id[6];
+	r[7] = id[7];
+
+	r[8] = version[0];
+	r[9] = version[1];
+	r[10] = version[2];
+	r[11] = version[3];
+	r
+}
+
+/// Deserialize the runtime API info serialized by [`serialize_runtime_api_info`].
+pub fn deserialize_runtime_api_info(bytes: [u8; RUNTIME_API_INFO_SIZE]) -> ([u8; 8], u32) {
+	use sp_std::convert::TryInto;
+
+	let id: [u8; 8] = bytes[0..8]
+		.try_into()
+		.expect("the source slice size is equal to the dest array length; qed");
+
+	let version = u32::from_le_bytes(
+		bytes[8..12]
+			.try_into()
+			.expect("the source slice size is equal to the array length; qed"),
+	);
+
+	(id, version)
+}
+
 #[derive(codec::Encode, codec::Decode)]
 pub struct OldRuntimeVersion {
 	pub spec_name: RuntimeString,
diff --git a/primitives/core/src/traits.rs b/primitives/core/src/traits.rs
index 90f8060f9a565..948830cf5ca68 100644
--- a/primitives/core/src/traits.rs
+++ b/primitives/core/src/traits.rs
@@ -26,7 +26,7 @@ use std::{
 pub use sp_externalities::{Externalities, ExternalitiesExt};
 
 /// Code execution engine.
-pub trait CodeExecutor: Sized + Send + Sync + CallInWasm + Clone + 'static {
+pub trait CodeExecutor: Sized + Send + Sync + ReadRuntimeVersion + Clone + 'static {
 	/// Externalities error type.
 	type Error: Display + Debug + Send + Sync + 'static;
 
@@ -123,53 +123,42 @@ impl std::fmt::Display for CodeNotFound {
 	}
 }
 
-/// `Allow` or `Disallow` missing host functions when instantiating a WASM blob.
-#[derive(Clone, Copy, Debug)]
-pub enum MissingHostFunctions {
-	/// Any missing host function will be replaced by a stub that returns an error when
-	/// being called.
-	Allow,
-	/// Any missing host function will result in an error while instantiating the WASM blob,
-	Disallow,
-}
-
-impl MissingHostFunctions {
-	/// Are missing host functions allowed?
-	pub fn allowed(self) -> bool {
-		matches!(self, Self::Allow)
-	}
-}
-
-/// Something that can call a method in a WASM blob.
-pub trait CallInWasm: Send + Sync {
-	/// Call the given `method` in the given `wasm_blob` using `call_data` (SCALE encoded arguments)
-	/// to decode the arguments for the method.
+/// A trait that allows reading version information from the binary.
+pub trait ReadRuntimeVersion: Send + Sync {
+	/// Reads the runtime version information from the given wasm code.
 	///
-	/// Returns the SCALE encoded return value of the method.
+	/// The version information may be embedded into the wasm binary itself. If it is not present,
+	/// then this function may fallback to the legacy way of reading the version.
 	///
-	/// # Note
+	/// The legacy mechanism involves instantiating the passed wasm runtime and calling `Core_version`
+	/// on it. This is a very expensive operation.
 	///
-	/// If `code_hash` is `Some(_)` the `wasm_code` module and instance will be cached internally,
-	/// otherwise it is thrown away after the call.
-	fn call_in_wasm(
+	/// `ext` is only needed in case the calling into runtime happens. Otherwise it is ignored.
+	///
+	/// Compressed wasm blobs are supported and will be decompressed if needed. If uncompression fails,
+	/// the error is returned.
+	///
+	/// # Errors
+	///
+	/// If the version information present in binary, but is corrupted - returns an error.
+	///
+	/// Otherwise, if there is no version information present, and calling into the runtime takes
+	/// place, then an error would be returned if `Core_version` is not provided.
+	fn read_runtime_version(
 		&self,
 		wasm_code: &[u8],
-		code_hash: Option<Vec<u8>>,
-		method: &str,
-		call_data: &[u8],
 		ext: &mut dyn Externalities,
-		missing_host_functions: MissingHostFunctions,
 	) -> Result<Vec<u8>, String>;
 }
 
 sp_externalities::decl_extension! {
-	/// The call-in-wasm extension to register/retrieve from the externalities.
-	pub struct CallInWasmExt(Box<dyn CallInWasm>);
+	/// An extension that provides functionality to read version information from a given wasm blob.
+	pub struct ReadRuntimeVersionExt(Box<dyn ReadRuntimeVersion>);
 }
 
-impl CallInWasmExt {
-	/// Creates a new instance of `Self`.
-	pub fn new<T: CallInWasm + 'static>(inner: T) -> Self {
+impl ReadRuntimeVersionExt {
+	/// Creates a new instance of the extension given a version determinator instance.
+	pub fn new<T: ReadRuntimeVersion + 'static>(inner: T) -> Self {
 		Self(Box::new(inner))
 	}
 }
diff --git a/primitives/io/Cargo.toml b/primitives/io/Cargo.toml
index cbbda1807cc2f..e63fcb909573a 100644
--- a/primitives/io/Cargo.toml
+++ b/primitives/io/Cargo.toml
@@ -24,6 +24,7 @@ libsecp256k1 = { version = "0.3.4", optional = true }
 sp-state-machine = { version = "0.9.0", optional = true, path = "../state-machine" }
 sp-wasm-interface = { version = "3.0.0", path = "../wasm-interface", default-features = false }
 sp-runtime-interface = { version = "3.0.0", default-features = false, path = "../runtime-interface" }
+sp-maybe-compressed-blob = { version = "3.0.0", optional = true, path = "../maybe-compressed-blob" }
 sp-trie = { version = "3.0.0", optional = true, path = "../trie" }
 sp-externalities = { version = "0.9.0", optional = true, path = "../externalities" }
 sp-tracing = { version = "3.0.0", default-features = false, path = "../tracing" }
@@ -47,6 +48,7 @@ std = [
 	"sp-runtime-interface/std",
 	"sp-externalities",
 	"sp-wasm-interface/std",
+	"sp-maybe-compressed-blob",
 	"sp-tracing/std",
 	"tracing/std",
 	"tracing-core/std",
diff --git a/primitives/io/src/lib.rs b/primitives/io/src/lib.rs
index 35daaa3989907..72695f2156b67 100644
--- a/primitives/io/src/lib.rs
+++ b/primitives/io/src/lib.rs
@@ -38,7 +38,7 @@ use tracing;
 #[cfg(feature = "std")]
 use sp_core::{
 	crypto::Pair,
-	traits::{CallInWasmExt, TaskExecutorExt, RuntimeSpawnExt},
+	traits::{TaskExecutorExt, RuntimeSpawnExt},
 	offchain::{OffchainDbExt, OffchainWorkerExt, TransactionPoolExt},
 	hexdisplay::HexDisplay,
 	storage::ChildInfo,
@@ -70,6 +70,8 @@ mod batch_verifier;
 #[cfg(feature = "std")]
 use batch_verifier::BatchVerifier;
 
+const LOG_TARGET: &str = "runtime::io";
+
 /// Error verifying ECDSA signature
 #[derive(Encode, Decode)]
 pub enum EcdsaVerifyError {
@@ -432,6 +434,9 @@ pub trait Trie {
 /// Interface that provides miscellaneous functions for communicating between the runtime and the node.
 #[runtime_interface]
 pub trait Misc {
+	// NOTE: We use the target 'runtime' for messages produced by general printing functions, instead
+	// of LOG_TARGET.
+
 	/// Print a number.
 	fn print_num(val: u64) {
 		log::debug!(target: "runtime", "{}", val);
@@ -456,28 +461,34 @@ pub trait Misc {
 	///
 	/// # Performance
 	///
-	/// Calling this function is very expensive and should only be done very occasionally.
-	/// For getting the runtime version, it requires instantiating the wasm blob and calling a
-	/// function in this blob.
+	/// This function may be very expensive to call depending on the wasm binary. It may be
+	/// relatively cheap if the wasm binary contains version information. In that case, uncompression
+	/// of the wasm blob is the dominating factor.
+	///
+	/// If the wasm binary does not have the version information attached, then a legacy mechanism
+	/// may be involved. This means that a runtime call will be performed to query the version.
+	///
+	/// Calling into the runtime may be incredible expensive and should be approached with care.
 	fn runtime_version(&mut self, wasm: &[u8]) -> Option<Vec<u8>> {
-		// Create some dummy externalities, `Core_version` should not write data anyway.
+		use sp_core::traits::ReadRuntimeVersionExt;
+
 		let mut ext = sp_state_machine::BasicExternalities::default();
 
-		self.extension::<CallInWasmExt>()
-			.expect("No `CallInWasmExt` associated for the current context!")
-			.call_in_wasm(
-				wasm,
-				None,
-				"Core_version",
-				&[],
-				&mut ext,
-				// If a runtime upgrade introduces new host functions that are not provided by
-				// the node, we should not fail at instantiation. Otherwise nodes that are
-				// updated could run this successfully and it could lead to a storage root
-				// mismatch when importing this block.
-				sp_core::traits::MissingHostFunctions::Allow,
-			)
-			.ok()
+		match self
+			.extension::<ReadRuntimeVersionExt>()
+			.expect("No `ReadRuntimeVersionExt` associated for the current context!")
+			.read_runtime_version(wasm, &mut ext)
+		{
+			Ok(v) => Some(v),
+			Err(err) => {
+				log::debug!(
+					target: LOG_TARGET,
+					"cannot read version from the given runtime: {}",
+					err,
+				);
+				None
+			}
+		}
 	}
 }
 
diff --git a/primitives/runtime-interface/test/Cargo.toml b/primitives/runtime-interface/test/Cargo.toml
index 8b6c9cbe5df05..fb9b3c4b71edf 100644
--- a/primitives/runtime-interface/test/Cargo.toml
+++ b/primitives/runtime-interface/test/Cargo.toml
@@ -14,6 +14,7 @@ targets = ["x86_64-unknown-linux-gnu"]
 [dependencies]
 sp-runtime-interface = { version = "3.0.0", path = "../" }
 sc-executor = { version = "0.9.0", path = "../../../client/executor" }
+sc-executor-common = { version = "0.9.0", path = "../../../client/executor/common" }
 sp-runtime-interface-test-wasm = { version = "2.0.0", path = "../test-wasm" }
 sp-runtime-interface-test-wasm-deprecated = { version = "2.0.0", path = "../test-wasm-deprecated" }
 sp-state-machine = { version = "0.9.0", path = "../../state-machine" }
diff --git a/primitives/runtime-interface/test/src/lib.rs b/primitives/runtime-interface/test/src/lib.rs
index 4426997663485..a021a93939a10 100644
--- a/primitives/runtime-interface/test/src/lib.rs
+++ b/primitives/runtime-interface/test/src/lib.rs
@@ -24,7 +24,7 @@ use sp_runtime_interface_test_wasm::{wasm_binary_unwrap, test_api::HostFunctions
 use sp_runtime_interface_test_wasm_deprecated::wasm_binary_unwrap as wasm_binary_deprecated_unwrap;
 
 use sp_wasm_interface::HostFunctions as HostFunctionsT;
-use sc_executor::CallInWasm;
+use sc_executor_common::runtime_blob::RuntimeBlob;
 
 use std::{collections::HashSet, sync::{Arc, Mutex}};
 
@@ -46,14 +46,15 @@ fn call_wasm_method_with_result<HF: HostFunctionsT>(
 		8,
 		None,
 	);
-	executor.call_in_wasm(
-		binary,
-		None,
-		method,
-		&[],
-		&mut ext_ext,
-		sp_core::traits::MissingHostFunctions::Disallow,
-	).map_err(|e| format!("Failed to execute `{}`: {}", method, e))?;
+	executor
+		.uncached_call(
+			RuntimeBlob::uncompress_if_needed(binary).expect("Failed to parse binary"),
+			&mut ext_ext,
+			false,
+			method,
+			&[],
+		)
+		.map_err(|e| format!("Failed to execute `{}`: {}", method, e))?;
 	Ok(ext)
 }
 
diff --git a/primitives/state-machine/src/lib.rs b/primitives/state-machine/src/lib.rs
index a6f1fb1f0e788..479184b4b9905 100644
--- a/primitives/state-machine/src/lib.rs
+++ b/primitives/state-machine/src/lib.rs
@@ -178,7 +178,7 @@ mod execution {
 	use codec::{Decode, Encode, Codec};
 	use sp_core::{
 		storage::ChildInfo, NativeOrEncoded, NeverNativeValue, hexdisplay::HexDisplay,
-		traits::{CodeExecutor, CallInWasmExt, RuntimeCode, SpawnNamed},
+		traits::{CodeExecutor, ReadRuntimeVersionExt, RuntimeCode, SpawnNamed},
 	};
 	use sp_externalities::Extensions;
 
@@ -339,7 +339,7 @@ mod execution {
 			runtime_code: &'a RuntimeCode,
 			spawn_handle: impl SpawnNamed + Send + 'static,
 		) -> Self {
-			extensions.register(CallInWasmExt::new(exec.clone()));
+			extensions.register(ReadRuntimeVersionExt::new(exec.clone()));
 			extensions.register(sp_core::traits::TaskExecutorExt::new(spawn_handle));
 
 			Self {
@@ -943,15 +943,11 @@ mod tests {
 		}
 	}
 
-	impl sp_core::traits::CallInWasm for DummyCodeExecutor {
-		fn call_in_wasm(
+	impl sp_core::traits::ReadRuntimeVersion for DummyCodeExecutor {
+		fn read_runtime_version(
 			&self,
 			_: &[u8],
-			_: Option<Vec<u8>>,
-			_: &str,
-			_: &[u8],
 			_: &mut dyn Externalities,
-			_: sp_core::traits::MissingHostFunctions,
 		) -> std::result::Result<Vec<u8>, String> {
 			unimplemented!("Not required in tests.")
 		}
diff --git a/primitives/version/Cargo.toml b/primitives/version/Cargo.toml
index bfb9a742c8689..b50da9e9eacf9 100644
--- a/primitives/version/Cargo.toml
+++ b/primitives/version/Cargo.toml
@@ -20,6 +20,7 @@ serde = { version = "1.0.101", optional = true, features = ["derive"] }
 codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] }
 sp-std = { version = "3.0.0", default-features = false, path = "../std" }
 sp-runtime = { version = "3.0.0", default-features = false, path = "../runtime" }
+sp-version-proc-macro = { version = "3.0.0", default-features = false, path = "proc-macro" }
 
 [features]
 default = ["std"]
diff --git a/primitives/version/proc-macro/Cargo.toml b/primitives/version/proc-macro/Cargo.toml
new file mode 100644
index 0000000000000..ea3144090c70e
--- /dev/null
+++ b/primitives/version/proc-macro/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "sp-version-proc-macro"
+version = "3.0.0"
+authors = ["Parity Technologies <admin@parity.io>"]
+edition = "2018"
+license = "Apache-2.0"
+homepage = "https://substrate.dev"
+repository = "https://github.com/paritytech/substrate/"
+description = "Macro for defining a runtime version."
+documentation = "https://docs.rs/sp-api-proc-macro"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[lib]
+proc-macro = true
+
+[dependencies]
+quote = "1.0.3"
+syn = { version = "1.0.58", features = ["full", "fold", "extra-traits", "visit"] }
+proc-macro2 = "1.0.6"
+proc-macro-crate = "1.0.0"
+codec = { package = "parity-scale-codec", version = "2.0.0", features = [ "derive" ] }
+
+[dev-dependencies]
+sp-version = { version = "3.0.0", path = ".." }
diff --git a/primitives/version/proc-macro/src/decl_runtime_version.rs b/primitives/version/proc-macro/src/decl_runtime_version.rs
new file mode 100644
index 0000000000000..6df0b71b202c4
--- /dev/null
+++ b/primitives/version/proc-macro/src/decl_runtime_version.rs
@@ -0,0 +1,279 @@
+// This file is part of Substrate.
+
+// Copyright (C) 2021 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use codec::Encode;
+use syn::{
+	Expr, ExprLit, FieldValue, ItemConst, Lit,
+	parse::{Result, Error},
+	parse_macro_input,
+	spanned::Spanned as _,
+};
+use quote::quote;
+use proc_macro2::{TokenStream, Span};
+
+/// This macro accepts a `const` item that has a struct initializer expression of `RuntimeVersion`-like type.
+/// The macro will pass through this declaration and append an item declaration that will
+/// lead to emitting a wasm custom section with the contents of `RuntimeVersion`.
+pub fn decl_runtime_version_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+	let item = parse_macro_input!(input as ItemConst);
+	decl_runtime_version_impl_inner(item)
+		.unwrap_or_else(|e| e.to_compile_error())
+		.into()
+}
+
+fn decl_runtime_version_impl_inner(item: ItemConst) -> Result<TokenStream> {
+	let runtime_version = ParseRuntimeVersion::parse_expr(&*item.expr)?.build(item.expr.span())?;
+	let link_section =
+		generate_emit_link_section_decl(&runtime_version.encode(), "runtime_version");
+
+	Ok(quote! {
+		#item
+		#link_section
+	})
+}
+
+/// This is a duplicate of `sp_version::RuntimeVersion`. We cannot unfortunately use the original
+/// declaration, because if we directly depend on `sp_version` from this proc-macro cargo will
+/// enable `std` feature even for `no_std` wasm runtime builds.
+///
+/// One difference from the original definition is the `apis` field. Since we don't actually parse
+/// `apis` from this macro it will always be emitteed as empty. An empty vector can be encoded as
+/// a zero-byte, thus `u8` is sufficient here.
+#[derive(Encode)]
+struct RuntimeVersion {
+	spec_name: String,
+	impl_name: String,
+	authoring_version: u32,
+	spec_version: u32,
+	impl_version: u32,
+	apis: u8,
+	transaction_version: u32,
+}
+
+#[derive(Default, Debug)]
+struct ParseRuntimeVersion {
+	spec_name: Option<String>,
+	impl_name: Option<String>,
+	authoring_version: Option<u32>,
+	spec_version: Option<u32>,
+	impl_version: Option<u32>,
+	transaction_version: Option<u32>,
+}
+
+impl ParseRuntimeVersion {
+	fn parse_expr(init_expr: &Expr) -> Result<ParseRuntimeVersion> {
+		let init_expr = match init_expr {
+			Expr::Struct(ref e) => e,
+			_ => {
+				return Err(Error::new(
+					init_expr.span(),
+					"expected a struct initializer expression",
+				));
+			}
+		};
+
+		let mut parsed = ParseRuntimeVersion::default();
+		for field_value in init_expr.fields.iter() {
+			parsed.parse_field_value(field_value)?;
+		}
+		Ok(parsed)
+	}
+
+	fn parse_field_value(&mut self, field_value: &FieldValue) -> Result<()> {
+		let field_name = match field_value.member {
+			syn::Member::Named(ref ident) => ident,
+			syn::Member::Unnamed(_) => {
+				return Err(Error::new(
+					field_value.span(),
+					"only named members must be used",
+				));
+			}
+		};
+
+		fn parse_once<T>(
+			value: &mut Option<T>,
+			field: &FieldValue,
+			parser: impl FnOnce(&Expr) -> Result<T>,
+		) -> Result<()> {
+			if value.is_some() {
+				return Err(Error::new(
+					field.span(),
+					"field is already initialized before",
+				));
+			} else {
+				*value = Some(parser(&field.expr)?);
+				Ok(())
+			}
+		}
+
+		if field_name == "spec_name" {
+			parse_once(&mut self.spec_name, field_value, Self::parse_str_literal)?;
+		} else if field_name == "impl_name" {
+			parse_once(&mut self.impl_name, field_value, Self::parse_str_literal)?;
+		} else if field_name == "authoring_version" {
+			parse_once(
+				&mut self.authoring_version,
+				field_value,
+				Self::parse_num_literal,
+			)?;
+		} else if field_name == "spec_version" {
+			parse_once(&mut self.spec_version, field_value, Self::parse_num_literal)?;
+		} else if field_name == "impl_version" {
+			parse_once(&mut self.impl_version, field_value, Self::parse_num_literal)?;
+		} else if field_name == "transaction_version" {
+			parse_once(
+				&mut self.transaction_version,
+				field_value,
+				Self::parse_num_literal,
+			)?;
+		} else if field_name == "apis" {
+			// Intentionally ignored
+			//
+			// The definition will pass through for the declaration, however, it won't get into
+			// the "runtime_version" custom section. `impl_runtime_apis` is responsible for generating
+			// a custom section with the supported runtime apis descriptor.
+		} else {
+			return Err(Error::new(field_name.span(), "unknown field"));
+		}
+
+		Ok(())
+	}
+
+	fn parse_num_literal(expr: &Expr) -> Result<u32> {
+		let lit = match *expr {
+			Expr::Lit(ExprLit {
+				lit: Lit::Int(ref lit),
+				..
+			}) => lit,
+			_ => {
+				return Err(Error::new(
+					expr.span(),
+					"only numeric literals (e.g. `10`) are supported here",
+				));
+			}
+		};
+		lit.base10_parse::<u32>()
+	}
+
+	fn parse_str_literal(expr: &Expr) -> Result<String> {
+		let mac = match *expr {
+			Expr::Macro(syn::ExprMacro { ref mac, .. }) => mac,
+			_ => {
+				return Err(Error::new(
+					expr.span(),
+					"a macro expression is expected here",
+				));
+			}
+		};
+
+		let lit: ExprLit = mac.parse_body().map_err(|e| {
+			Error::new(
+				e.span(),
+				format!(
+					"a single literal argument is expected, but parsing is failed: {}",
+					e
+				),
+			)
+		})?;
+
+		match lit.lit {
+			Lit::Str(ref lit) => Ok(lit.value()),
+			_ => Err(Error::new(
+				lit.span(),
+				"only string literals are supported here",
+			)),
+		}
+	}
+
+	fn build(self, span: Span) -> Result<RuntimeVersion> {
+		macro_rules! required {
+			($e:expr) => {
+				$e.ok_or_else(||
+					{
+						Error::new(
+							span,
+							format!("required field '{}' is missing", stringify!($e)),
+						)
+					}
+				)?
+			};
+		}
+
+		let Self {
+			spec_name,
+			impl_name,
+			authoring_version,
+			spec_version,
+			impl_version,
+			transaction_version,
+		} = self;
+
+		Ok(RuntimeVersion {
+			spec_name: required!(spec_name),
+			impl_name: required!(impl_name),
+			authoring_version: required!(authoring_version),
+			spec_version: required!(spec_version),
+			impl_version: required!(impl_version),
+			transaction_version: required!(transaction_version),
+			apis: 0,
+		})
+	}
+}
+
+fn generate_emit_link_section_decl(contents: &[u8], section_name: &str) -> TokenStream {
+	let len = contents.len();
+	quote! {
+		const _: () = {
+			#[link_section = #section_name]
+			static SECTION_CONTENTS: [u8; #len] = [#(#contents),*];
+		};
+	}
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use codec::DecodeAll;
+	use std::borrow::Cow;
+
+	#[test]
+	fn version_can_be_deserialized() {
+		let version_bytes = RuntimeVersion {
+			spec_name: "hello".to_string(),
+			impl_name: "world".to_string(),
+			authoring_version: 10,
+			spec_version: 265,
+			impl_version: 1,
+			apis: 0,
+			transaction_version: 2,
+		}
+		.encode();
+
+		assert_eq!(
+			sp_version::RuntimeVersion::decode_all(&mut &version_bytes[..]).unwrap(),
+			sp_version::RuntimeVersion {
+				spec_name: "hello".into(),
+				impl_name: "world".into(),
+				authoring_version: 10,
+				spec_version: 265,
+				impl_version: 1,
+				apis: Cow::Owned(vec![]),
+				transaction_version: 2,
+			},
+		);
+	}
+}
diff --git a/primitives/version/proc-macro/src/lib.rs b/primitives/version/proc-macro/src/lib.rs
new file mode 100644
index 0000000000000..9a6d4d60bbf9f
--- /dev/null
+++ b/primitives/version/proc-macro/src/lib.rs
@@ -0,0 +1,32 @@
+// This file is part of Substrate.
+
+// Copyright (C) 2021 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! A proc-macro that generates a custom wasm section from a given RuntimeVersion declaration
+//!
+//! This macro is re-exported from the `sp_version::runtime_version` and intended to be used from
+//! there. Documentation can also be found there.
+
+#![recursion_limit = "512"]
+
+use proc_macro::TokenStream;
+
+mod decl_runtime_version;
+
+#[proc_macro_attribute]
+pub fn runtime_version(_: TokenStream, input: TokenStream) -> TokenStream {
+	decl_runtime_version::decl_runtime_version_impl(input)
+}
diff --git a/primitives/version/src/lib.rs b/primitives/version/src/lib.rs
index 24a1b85ed0c38..603989f5d2f61 100644
--- a/primitives/version/src/lib.rs
+++ b/primitives/version/src/lib.rs
@@ -35,6 +35,51 @@ pub use sp_std;
 #[cfg(feature = "std")]
 use sp_runtime::{traits::Block as BlockT, generic::BlockId};
 
+/// An attribute that accepts a version declaration of a runtime and generates a custom wasm section
+/// with the equivalent contents.
+///
+/// The custom section allows to read the version of the runtime without having to execute any code.
+/// Instead, the generated custom section can be relatively easily parsed from the wasm binary. The
+/// identifier of the custom section is "runtime_version".
+///
+/// A shortcoming of this macro is that it is unable to embed information regarding supported APIs.
+/// This is supported by the `construct_runtime!` macro.
+///
+/// This macro accepts a const item like the following:
+///
+/// ```rust
+/// use sp_version::{create_runtime_str, RuntimeVersion};
+///
+/// #[sp_version::runtime_version]
+/// pub const VERSION: RuntimeVersion = RuntimeVersion {
+/// 	spec_name: create_runtime_str!("test"),
+/// 	impl_name: create_runtime_str!("test"),
+/// 	authoring_version: 10,
+/// 	spec_version: 265,
+/// 	impl_version: 1,
+/// 	apis: RUNTIME_API_VERSIONS,
+/// 	transaction_version: 2,
+/// };
+///
+/// # const RUNTIME_API_VERSIONS: sp_version::ApisVec = sp_version::create_apis_vec!([]);
+/// ```
+///
+/// It will pass it through and add code required for emitting a custom section. The information that
+/// will go into the custom section is parsed from the item declaration. Due to that, the macro is
+/// somewhat rigid in terms of the code it accepts. There are the following considerations:
+///
+/// - The `spec_name` and `impl_name` must be set by a macro-like expression. The name of the macro
+///   doesn't matter though.
+///
+/// - `authoring_version`, `spec_version`, `impl_version` and `transaction_version` must be set
+///   by a literal. Literal must be an integer. No other expressions are allowed there. In particular,
+///   you can't supply a constant variable.
+///
+/// - `apis` doesn't have any specific constraints. This is because this information doesn't get into
+///   the custom section and is not parsed.
+///
+pub use sp_version_proc_macro::runtime_version;
+
 /// The identity of a particular API interface that the runtime might provide.
 pub type ApiId = [u8; 8];
 
diff --git a/test-utils/runtime/src/lib.rs b/test-utils/runtime/src/lib.rs
index 4afb313eef35d..7ee1072a7b83e 100644
--- a/test-utils/runtime/src/lib.rs
+++ b/test-utils/runtime/src/lib.rs
@@ -92,6 +92,7 @@ pub fn wasm_binary_logging_disabled_unwrap() -> &'static [u8] {
 }
 
 /// Test runtime version.
+#[sp_version::runtime_version]
 pub const VERSION: RuntimeVersion = RuntimeVersion {
 	spec_name: create_runtime_str!("test"),
 	impl_name: create_runtime_str!("parity-test"),