diff --git a/Cargo.lock b/Cargo.lock index 74cfb38931e..81c698a7b72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3615,6 +3615,8 @@ dependencies = [ "borsh", "bs58", "byteorder", + "hex-literal", + "libsecp256k1", "near-primitives-core", "near-runtime-utils", "near-vm-errors", diff --git a/chain/jsonrpc/res/rpc_errors_schema.json b/chain/jsonrpc/res/rpc_errors_schema.json index e5ac6cec66e..c7dc48937b3 100644 --- a/chain/jsonrpc/res/rpc_errors_schema.json +++ b/chain/jsonrpc/res/rpc_errors_schema.json @@ -76,6 +76,11 @@ "method_name": "" } }, + "InvalidECDSASignature": { + "name": "InvalidECDSASignature", + "subtypes": [], + "props": {} + }, "Deserialization": { "name": "Deserialization", "subtypes": [], diff --git a/runtime/near-vm-errors/src/lib.rs b/runtime/near-vm-errors/src/lib.rs index fc5d30c47e2..af0317f7d86 100644 --- a/runtime/near-vm-errors/src/lib.rs +++ b/runtime/near-vm-errors/src/lib.rs @@ -186,6 +186,8 @@ pub enum HostError { ContractSizeExceeded { size: u64, limit: u64 }, /// The host function was deprecated. Deprecated { method_name: String }, + /// Invalid ECDSA signature. + InvalidECDSASignature, /// Deserialization error for alt_bn128 functions #[cfg(feature = "protocol_feature_alt_bn128")] AltBn128DeserializationError { msg: String }, @@ -473,6 +475,7 @@ impl std::fmt::Display for HostError { ReturnedValueLengthExceeded { length, limit } => write!(f, "The length of a returned value {} exceeds the limit {}", length, limit), ContractSizeExceeded { size, limit } => write!(f, "The size of a contract code in DeployContract action {} exceeds the limit {}", size, limit), Deprecated {method_name}=> write!(f, "Attempted to call deprecated host function {}", method_name), + InvalidECDSASignature => write!(f, "Invalid ECDSA signature"), #[cfg(feature = "protocol_feature_alt_bn128")] AltBn128DeserializationError { msg } => write!(f, "AltBn128 deserialization error: {}", msg), #[cfg(feature = "protocol_feature_alt_bn128")] diff --git a/runtime/near-vm-logic/Cargo.toml b/runtime/near-vm-logic/Cargo.toml index 7059e528738..731cbc3166a 100644 --- a/runtime/near-vm-logic/Cargo.toml +++ b/runtime/near-vm-logic/Cargo.toml @@ -18,6 +18,7 @@ blake2 = "0.9.1" borsh = "0.8.1" bs58 = "0.4" byteorder = "1.2" +libsecp256k1 = "0.3.5" ripemd160 = "0.9.0" serde = { version = "1", features = ["derive"] } sha2 = ">=0.8,<0.10" @@ -30,6 +31,7 @@ near-runtime-utils = { path = "../near-runtime-utils", version = "3.0.0" } bn = { package = "zeropool-bn", version = "0.5.9", features = [], optional = true } [dev-dependencies] +hex-literal = "0.2" serde_json = {version= "1", features= ["preserve_order"]} [features] diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index c66b49e814f..6ecd5c79b13 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -1001,10 +1001,46 @@ impl<'a> VMLogic<'a> { /// # Cost /// /// TODO - pub fn ecrecover(&mut self, hash_ptr: u64, v: u32, r_ptr: u64, s_ptr: u64, register_id: u64) -> Result<()> { + pub fn ecrecover( + &mut self, + hash_ptr: u64, + v: u32, + r_ptr: u64, + s_ptr: u64, + register_id: u64, + ) -> Result<()> { self.gas_counter.pay_base(ecrecover_base)?; + if v != 27 && v != 28 { + return Err(HostError::InvalidECDSASignature.into()); + } + let hash = self.memory_get_vec(hash_ptr, 32)?; + let v = (v & 0xFF) as u8; + let r = self.memory_get_vec(r_ptr, 32)?; + let s = self.memory_get_vec(s_ptr, 32)?; + + let hash = secp256k1::Message::parse_slice(hash.as_slice()).unwrap(); + + let mut signature = [0u8; 64]; + signature[0..32].copy_from_slice(r.as_slice()); + signature[32..64].copy_from_slice(s.as_slice()); + let signature = secp256k1::Signature::parse(&signature); + + let recovery_id = match secp256k1::RecoveryId::parse_rpc(v) { + Err(_) => return Err(HostError::InvalidECDSASignature.into()), + Ok(rid) => rid, + }; + + let public_key = match secp256k1::recover(&hash, &signature, &recovery_id) { + Err(_) => return Err(HostError::InvalidECDSASignature.into()), + Ok(pk) => pk, + }; + + use sha3::Digest; + let result = sha3::Keccak256::digest(&public_key.serialize()[1..]); + let mut address = [0u8; 20]; + address.copy_from_slice(&result[12..]); - Ok(()) // TODO + self.internal_write_register(register_id, address.to_vec()) } /// Called by gas metering injected into Wasm. Counts both towards `burnt_gas` and `used_gas`. diff --git a/runtime/near-vm-logic/tests/test_miscs.rs b/runtime/near-vm-logic/tests/test_miscs.rs index 2856dff20fd..faf3bdc7f2e 100644 --- a/runtime/near-vm-logic/tests/test_miscs.rs +++ b/runtime/near-vm-logic/tests/test_miscs.rs @@ -1,5 +1,6 @@ use fixtures::get_context; use helpers::*; +use hex_literal::hex; use near_vm_errors::HostError; use near_vm_logic::ExtCosts; use vm_logic_builder::VMLogicBuilder; @@ -534,8 +535,8 @@ fn test_keccak512() { fn test_ripemd160() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); - let data = b"tesdsst"; + let data = b"tesdsst"; logic.ripemd160(data.len() as _, data.as_ptr() as _, 0).unwrap(); let res = &vec![0u8; 20]; logic.read_register(0, res.as_ptr() as _).expect("OK"); @@ -563,8 +564,8 @@ fn test_ripemd160() { fn test_blake2b() { let mut logic_builder = VMLogicBuilder::default(); let mut logic = logic_builder.build(get_context(vec![], false)); - let data = b"tesdsst"; + let data = b"tesdsst"; logic.blake2b(data.len() as _, data.as_ptr() as _, 0).unwrap(); let res = &vec![0u8; 64]; logic.read_register(0, res.as_ptr() as _).expect("OK"); @@ -595,7 +596,34 @@ fn test_blake2b() { #[test] fn test_ecrecover() { - // TODO + let mut logic_builder = VMLogicBuilder::default(); + let mut logic = logic_builder.build(get_context(vec![], false)); + + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/cryptography/ECDSA.test.js + use sha3::Digest; + let hash = sha3::Keccak256::digest(b"OpenZeppelin"); + let signature = hex!("5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be8921b"); + let signer = hex!("2cc1166f6212628A0deEf2B33BEFB2187D35b86c"); + + let (r, s, v) = (&signature[0..32], &signature[32..64], signature[64] as u32); + logic.ecrecover(hash.as_ptr() as _, v, r.as_ptr() as _, s.as_ptr() as _, 0).unwrap(); + + let result = &vec![0u8; 20]; + logic.read_register(0, result.as_ptr() as _).expect("OK"); + + assert_eq!(result.to_vec(), signer); + assert_costs(map! { + ExtCosts::base: 1, + ExtCosts::read_memory_base: 3, + ExtCosts::read_memory_byte: 96, + ExtCosts::write_memory_base: 1, + ExtCosts::write_memory_byte: 20, + ExtCosts::read_register_base: 1, + ExtCosts::read_register_byte: 20, + ExtCosts::write_register_base: 1, + ExtCosts::write_register_byte: 20, + ExtCosts::ecrecover_base: 1, + }); } #[test] diff --git a/runtime/runtime-params-estimator/test-contract/src/lib.rs b/runtime/runtime-params-estimator/test-contract/src/lib.rs index 35fb520ade7..220d1449b41 100644 --- a/runtime/runtime-params-estimator/test-contract/src/lib.rs +++ b/runtime/runtime-params-estimator/test-contract/src/lib.rs @@ -480,8 +480,11 @@ pub unsafe fn blake2b_10kib_10k() { // Compute ecrecover 10k times. #[no_mangle] pub unsafe fn ecrecover_10k() { + let hash = [0u8; 32]; + let signature = [0u8; 65]; + let (r, s, v) = (&signature[0..32], &signature[32..64], 27); for _ in 0..10_000 { - // TODO + ecrecover(hash.as_ptr() as _, v, r.as_ptr() as _, s.as_ptr() as _, 0); } }