diff --git a/full-node/src/consensus_service.rs b/full-node/src/consensus_service.rs index eb247a149c..84fbbd0108 100644 --- a/full-node/src/consensus_service.rs +++ b/full-node/src/consensus_service.rs @@ -3503,6 +3503,9 @@ pub async fn runtime_call( runtime_call::RuntimeCall::SignatureVerification(sig) => { call = sig.verify_and_resume(); } + runtime_call::RuntimeCall::EcdsaPublicKeyRecover(sig) => { + call = sig.verify_and_resume(); + } runtime_call::RuntimeCall::Offchain(_) => { // Offchain storage calls are forbidden. return Err(RuntimeCallError::ForbiddenHostFunction); diff --git a/full-node/src/json_rpc_service/requests_handler.rs b/full-node/src/json_rpc_service/requests_handler.rs index ffb8ab154c..a1e626bd61 100644 --- a/full-node/src/json_rpc_service/requests_handler.rs +++ b/full-node/src/json_rpc_service/requests_handler.rs @@ -473,6 +473,9 @@ pub fn spawn_requests_handler(config: Config) { executor::runtime_call::RuntimeCall::SignatureVerification(req) => { call = req.verify_and_resume(); } + executor::runtime_call::RuntimeCall::EcdsaPublicKeyRecover(req) => { + call = req.verify_and_resume(); + } executor::runtime_call::RuntimeCall::Offchain(_) => { request.fail(service::ErrorResponse::InternalError); break; diff --git a/lib/src/chain/chain_information/build.rs b/lib/src/chain/chain_information/build.rs index 27b92d9824..9a49e74739 100644 --- a/lib/src/chain/chain_information/build.rs +++ b/lib/src/chain/chain_information/build.rs @@ -837,6 +837,9 @@ impl ChainInformationBuild { runtime_call::RuntimeCall::SignatureVerification(sig) => { call = sig.verify_and_resume(); } + runtime_call::RuntimeCall::EcdsaPublicKeyRecover(sig) => { + call = sig.verify_and_resume(); + } runtime_call::RuntimeCall::OffchainStorageSet(req) => { // Do nothing. call = req.resume(); diff --git a/lib/src/executor/host.rs b/lib/src/executor/host.rs index 549ada631c..2cbf12e80e 100644 --- a/lib/src/executor/host.rs +++ b/lib/src/executor/host.rs @@ -670,6 +670,10 @@ pub enum HostVm { /// Need to verify whether a signature is valid. #[from] SignatureVerification(SignatureVerification), + /// Need to verify whether an ECDSA signature is valid and provide the corresponding + /// public key. + #[from] + EcdsaPublicKeyRecover(EcdsaPublicKeyRecover), /// Need to call `Core_version` on the given Wasm code and return the raw output (i.e. /// still SCALE-encoded), or an error if the call has failed. #[from] @@ -718,6 +722,7 @@ impl HostVm { HostVm::OffchainRandomSeed(inner) => inner.inner.into_prototype(), HostVm::OffchainSubmitTransaction(inner) => inner.inner.into_prototype(), HostVm::SignatureVerification(inner) => inner.inner.into_prototype(), + HostVm::EcdsaPublicKeyRecover(inner) => inner.inner.into_prototype(), HostVm::CallRuntimeVersion(inner) => inner.inner.into_prototype(), HostVm::StartStorageTransaction(inner) => inner.inner.into_prototype(), HostVm::EndStorageTransaction { resume, .. } => resume.inner.into_prototype(), @@ -1631,104 +1636,15 @@ impl ReadyToRun { } HostFunction::ext_crypto_secp256k1_ecdsa_recover_version_1 - | HostFunction::ext_crypto_secp256k1_ecdsa_recover_version_2 => { - let sig = expect_pointer_constant_size!(0, 65); - let msg = expect_pointer_constant_size!(1, 32); - let is_v2 = matches!( - host_fn, - HostFunction::ext_crypto_secp256k1_ecdsa_recover_version_2 - ); - - let result = { - let rs = if is_v2 { - libsecp256k1::Signature::parse_standard_slice(&sig[0..64]) - } else { - libsecp256k1::Signature::parse_overflowing_slice(&sig[0..64]) - }; - - if let Ok(rs) = rs { - let v = libsecp256k1::RecoveryId::parse(if sig[64] > 26 { - sig[64] - 27 - } else { - sig[64] - }); - - if let Ok(v) = v { - let pubkey = libsecp256k1::recover( - &libsecp256k1::Message::parse_slice(&msg) - .unwrap_or_else(|_| unreachable!()), - &rs, - &v, - ); - - if let Ok(pubkey) = pubkey { - let mut res = Vec::with_capacity(65); - res.push(0); - res.extend_from_slice(&pubkey.serialize()[1..65]); - res - } else { - vec![1, 2] - } - } else { - vec![1, 1] - } - } else { - vec![1, 0] - } - }; - - self.inner - .alloc_write_and_return_pointer_size(host_fn.name(), iter::once(&result)) - } - HostFunction::ext_crypto_secp256k1_ecdsa_recover_compressed_version_1 + | HostFunction::ext_crypto_secp256k1_ecdsa_recover_version_2 + | HostFunction::ext_crypto_secp256k1_ecdsa_recover_compressed_version_1 | HostFunction::ext_crypto_secp256k1_ecdsa_recover_compressed_version_2 => { - let sig = expect_pointer_constant_size!(0, 65); - let msg = expect_pointer_constant_size!(1, 32); - let is_v2 = matches!( + HostVm::EcdsaPublicKeyRecover(EcdsaPublicKeyRecover { + signature_ptr: expect_pointer_constant_size_raw!(0, 65), + message_ptr: expect_pointer_constant_size_raw!(1, 32), + inner: self.inner, host_fn, - HostFunction::ext_crypto_secp256k1_ecdsa_recover_compressed_version_2 - ); - - let result = { - let rs = if is_v2 { - libsecp256k1::Signature::parse_standard_slice(&sig[0..64]) - } else { - libsecp256k1::Signature::parse_overflowing_slice(&sig[0..64]) - }; - - if let Ok(rs) = rs { - let v = libsecp256k1::RecoveryId::parse(if sig[64] > 26 { - sig[64] - 27 - } else { - sig[64] - }); - - if let Ok(v) = v { - let pubkey = libsecp256k1::recover( - &libsecp256k1::Message::parse_slice(&msg) - .unwrap_or_else(|_| unreachable!()), - &rs, - &v, - ); - - if let Ok(pubkey) = pubkey { - let mut res = Vec::with_capacity(34); - res.push(0); - res.extend_from_slice(&pubkey.serialize_compressed()); - res - } else { - vec![1, 2] - } - } else { - vec![1, 1] - } - } else { - vec![1, 0] - } - }; - - self.inner - .alloc_write_and_return_pointer_size(host_fn.name(), iter::once(&result)) + }) } HostFunction::ext_crypto_start_batch_verify_version_1 => { if self.inner.signatures_batch_verification.is_some() { @@ -3171,6 +3087,157 @@ impl fmt::Debug for SignatureVerification { } } +/// Must verify whether a signature is correct and return the corresponding public key. +pub struct EcdsaPublicKeyRecover { + inner: Box, + /// Pointer to the signature and public key tag. Always 65 bytes. Guaranteed to be in range. + signature_ptr: u32, + /// Pointer to the message. Always 32 bytes. Guaranteed to be in range. + message_ptr: u32, + /// Which host function this is. + host_fn: HostFunction, +} + +impl EcdsaPublicKeyRecover { + /// Returns the message that the signature is expected to sign. + /// + /// Always 32 bytes long. + pub fn message(&'_ self) -> impl AsRef<[u8]> + '_ { + self.inner + .vm + .read_memory(self.message_ptr, 32) + .unwrap_or_else(|_| unreachable!()) + } + + /// Returns the signature and public key tag. + /// + /// Always 65 bytes long. + /// + /// > **Note**: Be aware that this signature is untrusted input and might not be part of the + /// > set of valid signatures. + pub fn signature(&'_ self) -> impl AsRef<[u8]> + '_ { + self.inner + .vm + .read_memory(self.signature_ptr, 65) + .unwrap_or_else(|_| unreachable!()) + } + + /// Returns how the function would normally proceed. + pub fn normal_outcome(&self) -> Result<[u8; 65], EcdsaPublicKeyRecoverError> { + self.normal_outcome_inner().map(|pk| pk.serialize()) + } + + fn normal_outcome_inner(&self) -> Result { + let sig = self.signature(); + let sig = sig.as_ref(); + let msg = self.message(); + let msg = msg.as_ref(); + + let is_v2 = matches!( + self.host_fn, + HostFunction::ext_crypto_secp256k1_ecdsa_recover_version_2 + | HostFunction::ext_crypto_secp256k1_ecdsa_recover_compressed_version_2 + ); + + let rs = if is_v2 { + libsecp256k1::Signature::parse_standard_slice(&sig[0..64]) + } else { + libsecp256k1::Signature::parse_overflowing_slice(&sig[0..64]) + }; + + let Ok(rs) = rs else { + return Err(EcdsaPublicKeyRecoverError::IncorrectRSValue); + }; + + let Ok(v) = + libsecp256k1::RecoveryId::parse(if sig[64] > 26 { sig[64] - 27 } else { sig[64] }) + else { + return Err(EcdsaPublicKeyRecoverError::IncorrectVValue); + }; + + let Ok(pubkey) = libsecp256k1::recover( + &libsecp256k1::Message::parse_slice(&msg).unwrap_or_else(|_| unreachable!()), + &rs, + &v, + ) else { + return Err(EcdsaPublicKeyRecoverError::InvalidSignature); + }; + + Ok(pubkey) + } + + /// Verify the signature and resume execution. + /// + /// This is equivalent to calling `resume(normal_outcome())`. + pub fn verify_and_resume(self) -> HostVm { + let outcome = self.normal_outcome_inner(); + self.resume_inner(outcome) + } + + /// Resume the execution, indicating the public key. + /// + /// > **Note**: You are strongly encouraged to call + /// > [`EcdsaPublicKeyRecover::verify_and_resume`]. This function is meant to be + /// > used only in debugging situations. + /// + /// # Panic + /// + /// Panics if the public key isn't a valid ECDSA public key. + /// + pub fn resume(self, result: Result<&[u8; 65], EcdsaPublicKeyRecoverError>) -> HostVm { + let result = result.map(|pk| libsecp256k1::PublicKey::parse(pk).unwrap()); + self.resume_inner(result) + } + + fn resume_inner( + self, + result: Result, + ) -> HostVm { + let is_compressed_variant = matches!( + self.host_fn, + HostFunction::ext_crypto_secp256k1_ecdsa_recover_compressed_version_1 + | HostFunction::ext_crypto_secp256k1_ecdsa_recover_compressed_version_2 + ); + + let result = match result { + Ok(pubkey) => { + let mut res = Vec::with_capacity(65); + res.push(0); + if !is_compressed_variant { + res.extend_from_slice(&pubkey.serialize()[1..65]); + } else { + res.extend_from_slice(&pubkey.serialize_compressed()); + } + res + } + Err(EcdsaPublicKeyRecoverError::IncorrectRSValue) => vec![1, 0], + Err(EcdsaPublicKeyRecoverError::IncorrectVValue) => vec![1, 1], + Err(EcdsaPublicKeyRecoverError::InvalidSignature) => vec![1, 2], + }; + + self.inner + .alloc_write_and_return_pointer_size(self.host_fn.name(), iter::once(&result)) + } +} + +impl fmt::Debug for EcdsaPublicKeyRecover { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("SignatureVerification") + .field("message", &self.message().as_ref()) + .field("signature", &self.signature().as_ref()) + .finish() + } +} + +/// Error that can be returned to the virtual machine in case of error in the ECDSA signature +/// verification. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum EcdsaPublicKeyRecoverError { + IncorrectRSValue, + IncorrectVValue, + InvalidSignature, +} + /// Must provide the runtime version obtained by calling the `Core_version` entry point of a Wasm /// blob. pub struct CallRuntimeVersion { diff --git a/lib/src/executor/runtime_call.rs b/lib/src/executor/runtime_call.rs index 8e888fcbe8..025d2678e4 100644 --- a/lib/src/executor/runtime_call.rs +++ b/lib/src/executor/runtime_call.rs @@ -52,7 +52,8 @@ use alloc::{ use core::{fmt, iter, ops}; pub use host::{ - Error as ErrorDetail, LogEmitInfo, LogEmitInfoHex, LogEmitInfoStr, StorageProofSizeBehavior, + EcdsaPublicKeyRecoverError, Error as ErrorDetail, LogEmitInfo, LogEmitInfoHex, LogEmitInfoStr, + StorageProofSizeBehavior, }; pub use trie::{Nibble, TrieEntryVersion}; @@ -513,6 +514,9 @@ pub enum RuntimeCall { NextKey(NextKey), /// Verifying whether a signature is correct is required in order to continue. SignatureVerification(SignatureVerification), + /// Verifying whether a signature is correct and returning the corresponding public key is + /// required in order to continue. + EcdsaPublicKeyRecover(EcdsaPublicKeyRecover), /// Runtime would like to emit some log. LogEmit(LogEmit), /// Setting an offchain storage value is required in order to continue. @@ -534,6 +538,7 @@ impl RuntimeCall { RuntimeCall::ClosestDescendantMerkleValue(inner) => inner.inner.vm.into_prototype(), RuntimeCall::NextKey(inner) => inner.inner.vm.into_prototype(), RuntimeCall::SignatureVerification(inner) => inner.inner.vm.into_prototype(), + RuntimeCall::EcdsaPublicKeyRecover(inner) => inner.inner.vm.into_prototype(), RuntimeCall::LogEmit(inner) => inner.inner.vm.into_prototype(), RuntimeCall::OffchainStorageSet(inner) => inner.inner.vm.into_prototype(), RuntimeCall::Offchain(inner) => inner.into_prototype(), @@ -1033,6 +1038,76 @@ impl SignatureVerification { } } +/// Verifying whether a signature is correct is required in order to continue. +#[must_use] +pub struct EcdsaPublicKeyRecover { + inner: Inner, +} + +impl EcdsaPublicKeyRecover { + /// Returns the message that the signature is expected to sign. + /// + /// Always 32 bytes long. + pub fn message(&'_ self) -> impl AsRef<[u8]> + '_ { + match self.inner.vm { + host::HostVm::EcdsaPublicKeyRecover(ref sig) => sig.message(), + _ => unreachable!(), + } + } + + /// Returns the signature and public key tag. + /// + /// Always 65 bytes long. + /// + /// > **Note**: Be aware that this signature is untrusted input and might not be part of the + /// > set of valid signatures. + pub fn signature(&'_ self) -> impl AsRef<[u8]> + '_ { + match self.inner.vm { + host::HostVm::EcdsaPublicKeyRecover(ref sig) => sig.signature(), + _ => unreachable!(), + } + } + + /// Returns how the function would normally proceed. + pub fn normal_outcome(&self) -> Result<[u8; 65], EcdsaPublicKeyRecoverError> { + match self.inner.vm { + host::HostVm::EcdsaPublicKeyRecover(ref sig) => sig.normal_outcome(), + _ => unreachable!(), + } + } + + /// Verify the signature and resume execution. + /// + /// This is equivalent to calling `resume(normal_outcome())`. + pub fn verify_and_resume(mut self) -> RuntimeCall { + match self.inner.vm { + host::HostVm::EcdsaPublicKeyRecover(sig) => self.inner.vm = sig.verify_and_resume(), + _ => unreachable!(), + } + + self.inner.run() + } + + /// Resume the execution, indicating the public key. + /// + /// > **Note**: You are strongly encouraged to call + /// > [`EcdsaPublicKeyRecover::verify_and_resume`]. This function is meant to be + /// > used only in debugging situations. + /// + /// # Panic + /// + /// Panics if the public key isn't a valid ECDSA public key. + /// + pub fn resume(mut self, result: Result<&[u8; 65], EcdsaPublicKeyRecoverError>) -> RuntimeCall { + match self.inner.vm { + host::HostVm::EcdsaPublicKeyRecover(sig) => self.inner.vm = sig.resume(result), + _ => unreachable!(), + } + + self.inner.run() + } +} + /// Loading an offchain storage value is required in order to continue. #[must_use] pub struct OffchainStorageGet { @@ -1784,6 +1859,12 @@ impl Inner { inner: self, }); } + host::HostVm::EcdsaPublicKeyRecover(req) => { + self.vm = req.into(); + return RuntimeCall::EcdsaPublicKeyRecover(EcdsaPublicKeyRecover { + inner: self, + }); + } host::HostVm::CallRuntimeVersion(req) => { // TODO: make the user execute this ; see https://github.com/paritytech/smoldot/issues/144 diff --git a/lib/src/executor/runtime_call/tests.rs b/lib/src/executor/runtime_call/tests.rs index 1f180626de..90026dc2be 100644 --- a/lib/src/executor/runtime_call/tests.rs +++ b/lib/src/executor/runtime_call/tests.rs @@ -116,6 +116,7 @@ fn execute_blocks() { panic!("Error during test #{}: {:?}", test_num, err) } RuntimeCall::SignatureVerification(sig) => execution = sig.verify_and_resume(), + RuntimeCall::EcdsaPublicKeyRecover(sig) => execution = sig.verify_and_resume(), RuntimeCall::ClosestDescendantMerkleValue(req) => execution = req.resume_unknown(), RuntimeCall::StorageGet(get) => { let value = storage diff --git a/lib/src/transactions/validate/tests.rs b/lib/src/transactions/validate/tests.rs index 0d9434df30..66117d6397 100644 --- a/lib/src/transactions/validate/tests.rs +++ b/lib/src/transactions/validate/tests.rs @@ -95,6 +95,9 @@ fn validate_from_proof() { runtime_call::RuntimeCall::SignatureVerification(r) => { validation_in_progress = r.verify_and_resume() } + runtime_call::RuntimeCall::EcdsaPublicKeyRecover(r) => { + validation_in_progress = r.verify_and_resume() + } runtime_call::RuntimeCall::LogEmit(r) => validation_in_progress = r.resume(), runtime_call::RuntimeCall::OffchainStorageSet(r) => validation_in_progress = r.resume(), runtime_call::RuntimeCall::Offchain(_) => panic!(), diff --git a/light-base/src/runtime_service.rs b/light-base/src/runtime_service.rs index 272d48d21d..aca52cd747 100644 --- a/light-base/src/runtime_service.rs +++ b/light-base/src/runtime_service.rs @@ -3153,6 +3153,13 @@ async fn runtime_call_single_attempt( platform.now() - runtime_call_duration_before; continue; } + executor::runtime_call::RuntimeCall::EcdsaPublicKeyRecover(r) => { + let runtime_call_duration_before = platform.now(); + call = r.verify_and_resume(); + timing.virtual_machine_call_duration += + platform.now() - runtime_call_duration_before; + continue; + } executor::runtime_call::RuntimeCall::LogEmit(r) => { // Logs are ignored. let runtime_call_duration_before = platform.now();