Skip to content

Commit

Permalink
Fix approximation of meta transactions in EVM
Browse files Browse the repository at this point in the history
  • Loading branch information
ilblackdragon committed Sep 21, 2020
1 parent d21cb9c commit c2131d0
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 3 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions core/crypto/src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ impl std::fmt::Debug for Secp256K1PublicKey {
}
}

impl From<Secp256K1PublicKey> for [u8; 64] {
fn from(pubkey: Secp256K1PublicKey) -> Self {
pubkey.0
}
}

impl PartialEq for Secp256K1PublicKey {
fn eq(&self, other: &Self) -> bool {
self.0[..] == other.0[..]
Expand Down Expand Up @@ -479,6 +485,12 @@ impl Debug for Secp256K1Signature {
}
}

impl From<Secp256K1Signature> for [u8; 65] {
fn from(sig: Secp256K1Signature) -> [u8; 65] {
sig.0
}
}

/// Signature container supporting different curves.
#[derive(Clone, PartialEq, Eq)]
pub enum Signature {
Expand Down
2 changes: 2 additions & 0 deletions runtime/near-evm-runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ ethabi-contract = "8.0.0"
ethabi-derive = "8.0.0"
lazy_static = "1.4"
lazy-static-include = "2.2.2"

near-crypto = { path = "../../core/crypto" }
55 changes: 53 additions & 2 deletions runtime/near-evm-runner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ extern crate enum_primitive_derive;
use borsh::{BorshDeserialize, BorshSerialize};
use ethereum_types::{Address, H160, U256};
use evm::CreateContractAddress;
use keccak_hash::keccak;

use near_runtime_fees::RuntimeFeesConfig;
use near_vm_errors::{EvmError, FunctionCallError, VMError};
Expand All @@ -15,6 +16,7 @@ use crate::evm_state::{EvmAccount, EvmState, StateStore};
use crate::types::{
AddressArg, GetStorageAtArgs, Result, TransferArgs, ViewCallArgs, WithdrawArgs,
};
use crate::utils::ecrecover_address;

mod builtins;
mod evm_state;
Expand All @@ -25,6 +27,7 @@ pub mod utils;

pub struct EvmContext<'a> {
ext: &'a mut dyn External,
signer_id: AccountId,
predecessor_id: AccountId,
current_amount: Balance,
attached_deposit: Balance,
Expand Down Expand Up @@ -115,6 +118,7 @@ impl<'a> EvmContext<'a> {
config: &'a VMConfig,
fees_config: &'a RuntimeFeesConfig,
current_amount: Balance,
signer_id: AccountId,
predecessor_id: AccountId,
attached_deposit: Balance,
storage_usage: StorageUsage,
Expand All @@ -128,6 +132,7 @@ impl<'a> EvmContext<'a> {
};
Self {
ext,
signer_id,
predecessor_id,
current_amount,
attached_deposit,
Expand Down Expand Up @@ -163,14 +168,44 @@ impl<'a> EvmContext<'a> {
)
}

/// Make an EVM transaction. Calls `contract_address` with RLP encoded `input`. Execution
/// continues until all EVM messages have been processed. We expect this to behave identically
/// to an Ethereum transaction, however there may be some edge cases.
pub fn call_function(&mut self, args: Vec<u8>) -> Result<Vec<u8>> {
if args.len() <= 20 {
return Err(VMLogicError::EvmError(EvmError::ArgumentParseError));
}
let contract_address = Address::from_slice(&args[..20]);
let input = &args[20..];
let origin = utils::near_account_id_to_evm_address(&self.signer_id);
let sender = utils::near_account_id_to_evm_address(&self.predecessor_id);
self.add_balance(&sender, U256::from(self.attached_deposit))?;
let value =
if self.attached_deposit == 0 { None } else { Some(U256::from(self.attached_deposit)) };
interpreter::call(self, &origin, &sender, value, 0, &contract_address, &input, true)
.map(|rd| rd.to_vec())
}

/// Make an EVM call via a meta transaction pattern.
/// Specifically, providing signature and NEAREvm message that determines which contract and arguments to be called.
/// Format
/// 0..95: signature: v - 32 bytes, s - 32 bytes, r - 32 bytes
/// 96..115: contract_id: address for contract to call
/// 116..: RLP encoded arguments.
pub fn meta_call_function(&mut self, args: Vec<u8>) -> Result<Vec<u8>> {
if args.len() <= 116 {
return Err(VMLogicError::EvmError(EvmError::ArgumentParseError));
}
let mut signature: [u8; 96] = [0; 96];
signature.copy_from_slice(&args[..96]);
let args = &args[96..];
let sender = ecrecover_address(&keccak(args).0, &signature)?;
if sender == Address::zero() {
return Err(VMLogicError::EvmError(EvmError::InvalidEcRecoverSignature));
}
let contract_address = Address::from_slice(&args[..20]);
let input = &args[20..];
self.add_balance(&sender, U256::from(self.attached_deposit))?;
let value =
if self.attached_deposit == 0 { None } else { Some(U256::from(self.attached_deposit)) };
interpreter::call(self, &sender, &sender, value, 0, &contract_address, &input, true)
Expand Down Expand Up @@ -312,6 +347,7 @@ pub fn run_evm(
ext: &mut dyn External,
config: &VMConfig,
fees_config: &RuntimeFeesConfig,
signer_id: &AccountId,
predecessor_id: &AccountId,
amount: Balance,
attached_deposit: Balance,
Expand All @@ -328,6 +364,7 @@ pub fn run_evm(
// This is total amount of all $NEAR inside this EVM.
// Should already validate that will not overflow external to this call.
amount.checked_add(attached_deposit).unwrap_or(amount),
signer_id.clone(),
predecessor_id.clone(),
attached_deposit,
storage_usage,
Expand All @@ -338,11 +375,14 @@ pub fn run_evm(
// Change the state methods.
"deploy_code" => context.deploy_code(args).map(|address| utils::address_to_vec(&address)),
"call_function" => context.call_function(args),
"call" => context.call_function(args),
"meta_call" => context.meta_call_function(args),
"deposit" => context.deposit(args).map(|balance| utils::u256_to_arr(&balance).to_vec()),
"withdraw" => context.withdraw(args).map(|_| vec![]),
"transfer" => context.transfer(args).map(|_| vec![]),
// View methods.
"view_call_function" => context.view_call_function(args),
"view_function_call" => context.view_call_function(args),
"view" => context.view_call_function(args),
"get_code" => context.get_code(args),
"get_storage_at" => context.get_storage_at(args),
"get_nonce" => context.get_nonce(args).map(|nonce| utils::u256_to_arr(&nonce).to_vec()),
Expand Down Expand Up @@ -391,7 +431,18 @@ mod tests {
fees_config: &'a RuntimeFeesConfig,
account_id: &str,
) -> EvmContext<'a> {
EvmContext::new(external, vm_config, fees_config, 0, account_id.to_string(), 0, 0, 0, false)
EvmContext::new(
external,
vm_config,
fees_config,
0,
account_id.to_string(),
account_id.to_string(),
0,
0,
0,
false,
)
}

#[test]
Expand Down
36 changes: 36 additions & 0 deletions runtime/near-evm-runner/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ use keccak_hash::keccak;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use vm::CreateContractAddress;

use near_vm_errors::{EvmError, VMLogicError};

use crate::types;

pub fn safe_next_address(addr: &[u8; 20]) -> [u8; 20] {
let mut expanded_addr = [0u8; 32];
expanded_addr[12..].copy_from_slice(addr);
Expand Down Expand Up @@ -177,3 +181,35 @@ pub fn format_log(topics: Vec<H256>, data: &[u8]) -> std::result::Result<Vec<u8>
result.write(data)?;
Ok(result)
}

/// Given signature and data, validates that signature is valid for given data and returns ecrecover address.
pub fn ecrecover_address(hash: &[u8; 32], signature: &[u8; 96]) -> types::Result<Address> {
use sha3::Digest;

let hash = secp256k1::Message::parse(&H256::from_slice(hash).0);
let v = &signature[..32];
let r = &signature[32..64];
let s = &signature[64..96];

let bit = match v[31] {
27..=30 => v[31] - 27,
_ => {
// ??
return Ok(Address::zero());
}
};

let mut sig = [0u8; 64];
sig[..32].copy_from_slice(&r);
sig[32..].copy_from_slice(&s);
let s = secp256k1::Signature::parse(&sig);

if let Ok(rec_id) = secp256k1::RecoveryId::parse(bit) {
if let Ok(p) = secp256k1::recover(&hash, &s, &rec_id) {
// recover returns the 65-byte key, but addresses come from the raw 64-byte key
let r = sha3::Keccak256::digest(&p.serialize()[1..]);
return Ok(address_from_arr(&r[12..]));
}
}
Err(VMLogicError::EvmError(EvmError::InvalidEcRecoverSignature))
}
20 changes: 19 additions & 1 deletion runtime/near-evm-runner/tests/standard_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use borsh::BorshSerialize;
use ethabi_contract::use_contract;
use ethereum_types::{Address, H256, U256};

use near_crypto::{InMemorySigner, KeyType};
use near_evm_runner::types::{TransferArgs, WithdrawArgs};
use near_evm_runner::utils::{
address_from_arr, address_to_vec, encode_call_function_args, encode_view_call_function_args,
Expand All @@ -15,7 +16,9 @@ use near_vm_errors::{EvmError, VMLogicError};
use near_vm_logic::mocks::mock_external::MockedExternal;
use near_vm_logic::VMConfig;

use crate::utils::{accounts, create_context, setup};
use crate::utils::{
accounts, create_context, encode_meta_call_function_args, public_key_to_address, setup,
};

mod utils;

Expand Down Expand Up @@ -239,3 +242,18 @@ fn test_solidity_accurate_storage_on_selfdestruct() {
let raw = context.call_function(encode_call_function_args(factory_addr, input)).unwrap();
assert!(create2factory::functions::test_double_deploy::decode_output(&raw).unwrap());
}

#[test]
fn test_meta_call() {
let (mut fake_external, test_addr, vm_config, fees_config) = setup_and_deploy_test();
let signer = InMemorySigner::from_random("doesnt".to_string(), KeyType::SECP256K1);
let mut context =
create_context(&mut fake_external, &vm_config, &fees_config, accounts(1), 100);
let (input, _) = soltest::functions::return_some_funds::call();
let _ = context
.meta_call_function(encode_meta_call_function_args(&signer, test_addr, input))
.unwrap();
let signer_addr = public_key_to_address(signer.public_key);
assert_eq!(context.get_balance(test_addr.0.to_vec()).unwrap(), U256::from(150));
assert_eq!(context.get_balance(signer_addr.0.to_vec()).unwrap(), U256::from(50));
}
43 changes: 43 additions & 0 deletions runtime/near-evm-runner/tests/utils.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use ethereum_types::Address;
use keccak_hash::keccak_256;
use near_crypto::{PublicKey, Signature, Signer};
use near_evm_runner::utils::encode_call_function_args;
use near_evm_runner::EvmContext;
use near_runtime_fees::RuntimeFeesConfig;
use near_vm_logic::mocks::mock_external::MockedExternal;
Expand Down Expand Up @@ -28,9 +32,48 @@ pub fn create_context<'a>(
fees_config,
1000,
account_id.to_string(),
account_id.to_string(),
attached_deposit,
0,
10u64.pow(14),
false,
)
}

fn hash(message: &[u8]) -> [u8; 32] {
let mut bytes = [0u8; 32];
keccak_256(message, &mut bytes);
bytes
}

pub fn public_key_to_address(public_key: PublicKey) -> Address {
match public_key {
PublicKey::ED25519(_) => panic!("Wrong PublicKey"),
PublicKey::SECP256K1(pubkey) => {
let pk: [u8; 64] = pubkey.into();
let bytes = hash(&pk);
let mut result = Address::zero();
result.as_bytes_mut().copy_from_slice(&bytes[12..]);
result
}
}
}

pub fn encode_meta_call_function_args(
signer: &dyn Signer,
address: Address,
input: Vec<u8>,
) -> Vec<u8> {
let call_args = encode_call_function_args(address, input);
let hash = hash(&call_args);
match signer.sign(&hash) {
Signature::ED25519(_) => panic!("Wrong Signer"),
Signature::SECP256K1(sig) => {
let sig: [u8; 65] = sig.into();
let mut vsr = vec![0u8; 96];
vsr[31] = sig[64] + 27;
vsr[32..].copy_from_slice(&sig[..64]);
[vsr, call_args].concat()
}
}
}
2 changes: 2 additions & 0 deletions runtime/near-vm-errors/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ pub enum EvmError {
IntegerOverflow,
/// Method not found.
MethodNotFound,
/// Invalid signature when recovering.
InvalidEcRecoverSignature,
}

#[derive(Debug, Clone, PartialEq, BorshDeserialize, BorshSerialize, Deserialize, Serialize)]
Expand Down
1 change: 1 addition & 0 deletions runtime/runtime/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub(crate) fn execute_function_call(
runtime_ext,
&config.wasm_config,
&config.transaction_costs,
&action_receipt.signer_id,
predecessor_id,
account.amount,
function_call.deposit,
Expand Down

0 comments on commit c2131d0

Please sign in to comment.