Skip to content

Commit

Permalink
Merge pull request #1481 from oasisprotocol/kostko/feature/evm-drop-s…
Browse files Browse the repository at this point in the history
…igned-queries

runtime-sdk/modules/evm: Drop signed queries in new contracts
  • Loading branch information
kostko committed Sep 18, 2023
2 parents b178b66 + 75870b3 commit 998adda
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 7 deletions.
9 changes: 9 additions & 0 deletions runtime-sdk/modules/evm/src/backend.rs
Expand Up @@ -550,6 +550,15 @@ impl<'ctx, 'backend, 'config, C: TxContext, Cfg: Config> StackState<'config>
let mut store = state::codes(store);
store.insert(address, code);
});

// Set metadata for newly created contracts.
state::set_metadata(
&address.into(),
types::ContractMetadata {
// For all new contracts we don't zeroize the caller in queries.
features: types::FeatureMask::QUERIES_NO_CALLER_ZEROIZE,
},
);
}

fn transfer(&mut self, transfer: Transfer) -> Result<(), ExitError> {
Expand Down
21 changes: 19 additions & 2 deletions runtime-sdk/modules/evm/src/lib.rs
Expand Up @@ -39,8 +39,9 @@ use oasis_runtime_sdk::{
},
};

use types::{H160, H256, U256};
use types::{FeatureMask, H160, H256, U256};

#[cfg(any(test, feature = "test"))]
pub mod mock;
#[cfg(test)]
mod test;
Expand Down Expand Up @@ -596,6 +597,7 @@ impl<Cfg: Config> Module<Cfg> {
if !Cfg::CONFIDENTIAL {
return Ok((call, callformat::Metadata::Empty));
}

if let Ok(types::SignedCallDataPack {
data,
leash,
Expand All @@ -615,13 +617,22 @@ impl<Cfg: Config> Module<Cfg> {
));
}

// Determine the caller based on per-contract features.
let caller = if state::get_metadata(&call.address)
.has_features(FeatureMask::QUERIES_NO_CALLER_ZEROIZE)
{
call.caller
} else {
Default::default() // Zeroize caller.
};

// The call is not signed, but it must be encoded as an oasis-sdk call.
let tx_call_format = transaction::CallFormat::Plain; // Queries cannot be encrypted.
let (data, tx_metadata) = Self::decode_call_data(ctx, call.data, tx_call_format, 0, true)?
.expect("processing always proceeds");
Ok((
types::SimulateCallQuery {
caller: Default::default(), // The sender cannot be spoofed.
caller,
data,
..call
},
Expand Down Expand Up @@ -654,6 +665,7 @@ impl<Cfg: Config> Module<Cfg> {
#[sdk_derive(Module)]
impl<Cfg: Config> Module<Cfg> {
const NAME: &'static str = MODULE_NAME;
const VERSION: u32 = 2;
type Error = Error;
type Event = Event;
type Parameters = Parameters;
Expand All @@ -665,6 +677,11 @@ impl<Cfg: Config> Module<Cfg> {
Self::set_params(genesis.parameters);
}

#[migration(from = 1)]
fn migrate_v1_to_v2() {
// No state migration is needed for v2.
}

#[handler(call = "evm.Create")]
fn tx_create<C: TxContext>(ctx: &mut C, body: types::Create) -> Result<Vec<u8>, Error> {
Self::create(ctx, body.value, body.init_code)
Expand Down
129 changes: 127 additions & 2 deletions runtime-sdk/modules/evm/src/mock.rs
Expand Up @@ -2,13 +2,20 @@
use uint::hex::FromHex;

use oasis_runtime_sdk::{
callformat,
core::common::crypto::mrae::deoxysii,
dispatcher,
error::RuntimeError,
module,
testing::mock::{CallOptions, Signer},
types::address::SignatureAddressSpec,
types::{address::SignatureAddressSpec, transaction},
BatchContext,
};

use crate::types::{self, H160};
use crate::{
derive_caller,
types::{self, H160},
};

/// A mock EVM signer for use during tests.
pub struct EvmSigner(Signer);
Expand Down Expand Up @@ -64,6 +71,104 @@ impl EvmSigner {
opts,
)
}

/// Ethereum address for this signer.
pub fn address(&self) -> H160 {
derive_caller::from_sigspec(self.sigspec()).expect("caller should be evm-compatible")
}

/// Dispatch a query to the given EVM contract method.
pub fn query_evm<C>(
&self,
ctx: &mut C,
address: H160,
name: &str,
param_types: &[ethabi::ParamType],
params: &[ethabi::Token],
) -> Result<Vec<u8>, RuntimeError>
where
C: BatchContext,
{
self.query_evm_opts(ctx, address, name, param_types, params, Default::default())
}

/// Dispatch a query to the given EVM contract method.
pub fn query_evm_opts<C>(
&self,
ctx: &mut C,
address: H160,
name: &str,
param_types: &[ethabi::ParamType],
params: &[ethabi::Token],
opts: QueryOptions,
) -> Result<Vec<u8>, RuntimeError>
where
C: BatchContext,
{
let mut data = [
ethabi::short_signature(name, param_types).to_vec(),
ethabi::encode(params),
]
.concat();

// Handle optional encryption.
let client_keypair = deoxysii::generate_key_pair();
if opts.encrypt {
data = cbor::to_vec(
callformat::encode_call(
ctx,
transaction::Call {
format: transaction::CallFormat::EncryptedX25519DeoxysII,
method: "".into(),
body: cbor::Value::from(data),
..Default::default()
},
&client_keypair,
)
.unwrap(),
);
}

let mut result: Vec<u8> = self.query(
ctx,
"evm.SimulateCall",
types::SimulateCallQuery {
gas_price: 0.into(),
gas_limit: opts.gas_limit,
caller: opts.caller.unwrap_or_else(|| self.address()),
address,
value: 0.into(),
data,
},
)?;

// Handle optional decryption.
if opts.encrypt {
let call_result: transaction::CallResult =
cbor::from_slice(&result).expect("result from EVM should be properly encoded");
let call_result = callformat::decode_result(
ctx,
transaction::CallFormat::EncryptedX25519DeoxysII,
call_result,
&client_keypair,
)
.expect("callformat decoding should succeed");

result = match call_result {
module::CallResult::Ok(v) => {
cbor::from_value(v).expect("result from EVM should be correct")
}
module::CallResult::Failed {
module,
code,
message,
} => return Err(RuntimeError::new(&module, code, &message)),
module::CallResult::Aborted(e) => panic!("aborted with error: {e}"),
};
}

Ok(result)
}
}

impl std::ops::Deref for EvmSigner {
Expand All @@ -80,6 +185,26 @@ impl std::ops::DerefMut for EvmSigner {
}
}

/// Options for making queries.
pub struct QueryOptions {
/// Whether the call should be encrypted.
pub encrypt: bool,
/// Gas limit.
pub gas_limit: u64,
/// Use specified caller instead of signer.
pub caller: Option<H160>,
}

impl Default for QueryOptions {
fn default() -> Self {
Self {
encrypt: false,
gas_limit: 10_000_000,
caller: None,
}
}
}

/// Load contract bytecode from a hex-encoded string.
pub fn load_contract_bytecode(raw: &str) -> Vec<u8> {
Vec::from_hex(raw.split_whitespace().collect::<String>())
Expand Down
25 changes: 24 additions & 1 deletion runtime-sdk/modules/evm/src/state.rs
Expand Up @@ -3,7 +3,10 @@ use oasis_runtime_sdk::{
storage::{ConfidentialStore, CurrentStore, HashedStore, PrefixStore, Store, TypedStore},
};

use crate::{types::H160, Config};
use crate::{
types::{ContractMetadata, H160},
Config,
};

/// Prefix for Ethereum account code in our storage (maps H160 -> Vec<u8>).
pub const CODES: &[u8] = &[0x01];
Expand All @@ -14,6 +17,8 @@ pub const STORAGES: &[u8] = &[0x02];
pub const BLOCK_HASHES: &[u8] = &[0x03];
/// Prefix for Ethereum account storage in our confidential storage (maps H160||H256 -> H256).
pub const CONFIDENTIAL_STORAGES: &[u8] = &[0x04];
/// Prefix for contract metadata (maps H160 -> ContractMetadata).
pub const METADATA: &[u8] = &[0x05];

/// Confidential store key pair ID domain separation context base.
pub const CONFIDENTIAL_STORE_KEY_PAIR_ID_CONTEXT_BASE: &[u8] = b"oasis-runtime-sdk/evm: state";
Expand Down Expand Up @@ -118,3 +123,21 @@ pub fn block_hashes<'a, S: Store + 'a>(state: S) -> TypedStore<impl Store + 'a>
let store = PrefixStore::new(state, &crate::MODULE_NAME);
TypedStore::new(PrefixStore::new(store, &BLOCK_HASHES))
}

/// Set contract metadata.
pub fn set_metadata(address: &H160, metadata: ContractMetadata) {
CurrentStore::with(|store| {
let store = PrefixStore::new(store, &crate::MODULE_NAME);
let mut store = TypedStore::new(PrefixStore::new(store, &METADATA));
store.insert(address, metadata);
})
}

/// Get contract metadata.
pub fn get_metadata(address: &H160) -> ContractMetadata {
CurrentStore::with(|store| {
let store = PrefixStore::new(store, &crate::MODULE_NAME);
let store = TypedStore::new(PrefixStore::new(store, &METADATA));
store.get(address).unwrap_or_default()
})
}
88 changes: 87 additions & 1 deletion runtime-sdk/modules/evm/src/test.rs
Expand Up @@ -27,7 +27,8 @@ use oasis_runtime_sdk::{

use crate::{
derive_caller,
mock::{decode_reverted, decode_reverted_raw, load_contract_bytecode, EvmSigner},
mock::{decode_reverted, decode_reverted_raw, load_contract_bytecode, EvmSigner, QueryOptions},
state,
types::{self, H160},
Config, Genesis, Module as EVMModule,
};
Expand Down Expand Up @@ -792,6 +793,91 @@ fn test_c10l_evm_runtime() {
do_test_evm_runtime::<ConfidentialEVMConfig>();
}

#[test]
fn test_c10l_queries() {
let mut mock = mock::Mock::default();
let mut ctx = mock.create_ctx_for_runtime::<EVMRuntime<ConfidentialEVMConfig>>(
context::Mode::ExecuteTx,
true,
);
let mut signer = EvmSigner::new(0, keys::dave::sigspec());

EVMRuntime::<ConfidentialEVMConfig>::migrate(&mut ctx);

static QUERY_CONTRACT_CODE_HEX: &str =
include_str!("../../../../tests/e2e/contracts/query/query.hex");

// Create contract.
let dispatch_result = signer.call(
&mut ctx,
"evm.Create",
types::Create {
value: 0.into(),
init_code: load_contract_bytecode(QUERY_CONTRACT_CODE_HEX),
},
);
let result = dispatch_result.result.unwrap();
let result: Vec<u8> = cbor::from_value(result).unwrap();
let contract_address = H160::from_slice(&result);

let mut ctx = mock
.create_ctx_for_runtime::<EVMRuntime<ConfidentialEVMConfig>>(context::Mode::CheckTx, true);

// Call the `test` method on the contract via a query.
let result = signer
.query_evm(&mut ctx, contract_address, "test", &[], &[])
.expect("query should succeed");

let mut result =
ethabi::decode(&[ParamType::Address], &result).expect("output should be correct");

let test = result.pop().unwrap().into_address().unwrap();
assert_eq!(
test,
signer.address().into(),
"msg.signer should be correct (non-zeroized)"
);

// Test call with confidential envelope.
let result = signer
.query_evm_opts(
&mut ctx,
contract_address,
"test",
&[],
&[],
QueryOptions {
encrypt: true,
..Default::default()
},
)
.expect("query should succeed");

let mut result =
ethabi::decode(&[ParamType::Address], &result).expect("output should be correct");

let test = result.pop().unwrap().into_address().unwrap();
assert_eq!(
test,
signer.address().into(),
"msg.signer should be correct (non-zeroized)"
);

// Reset the contract metadata to remove the QUERIES_NO_CALLER_ZEROIZE feature.
state::set_metadata(&contract_address, Default::default());

// Call the `test` method again on the contract via a query.
let result = signer
.query_evm(&mut ctx, contract_address, "test", &[], &[])
.expect("query should succeed");

let mut result =
ethabi::decode(&[ParamType::Address], &result).expect("output should be correct");

let test = result.pop().unwrap().into_address().unwrap();
assert_eq!(test, Default::default(), "msg.signer should be zeroized");
}

#[test]
fn test_fee_refunds() {
let mut mock = mock::Mock::default();
Expand Down

0 comments on commit 998adda

Please sign in to comment.