Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

runtime-sdk/modules/evm: Drop signed queries in new contracts #1481

Merged
merged 2 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions runtime-sdk/modules/evm/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,15 @@
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,

Check warning on line 559 in runtime-sdk/modules/evm/src/backend.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/backend.rs#L559

Added line #L559 was not covered by tests
},
);
}

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

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 @@
if !Cfg::CONFIDENTIAL {
return Ok((call, callformat::Metadata::Empty));
}

if let Ok(types::SignedCallDataPack {
data,
leash,
Expand All @@ -615,13 +617,22 @@
));
}

// Determine the caller based on per-contract features.
let caller = if state::get_metadata(&call.address)
.has_features(FeatureMask::QUERIES_NO_CALLER_ZEROIZE)

Check warning on line 622 in runtime-sdk/modules/evm/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/lib.rs#L622

Added line #L622 was not covered by tests
{
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 @@
#[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 @@
Self::set_params(genesis.parameters);
}

#[migration(from = 1)]
fn migrate_v1_to_v2() {

Check warning on line 681 in runtime-sdk/modules/evm/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/lib.rs#L681

Added line #L681 was not covered by tests
// 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
Original file line number Diff line number Diff line change
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 @@
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,

Check warning on line 119 in runtime-sdk/modules/evm/src/mock.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/mock.rs#L119

Added line #L119 was not covered by tests
transaction::Call {
format: transaction::CallFormat::EncryptedX25519DeoxysII,
method: "".into(),
body: cbor::Value::from(data),
..Default::default()
},
&client_keypair,

Check warning on line 126 in runtime-sdk/modules/evm/src/mock.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/mock.rs#L126

Added line #L126 was not covered by tests
)
.unwrap(),

Check warning on line 128 in runtime-sdk/modules/evm/src/mock.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/mock.rs#L128

Added line #L128 was not covered by tests
);
}

let mut result: Vec<u8> = self.query(
ctx,

Check warning on line 133 in runtime-sdk/modules/evm/src/mock.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/mock.rs#L133

Added line #L133 was not covered by tests
"evm.SimulateCall",
types::SimulateCallQuery {
gas_price: 0.into(),
gas_limit: opts.gas_limit,
caller: opts.caller.unwrap_or_else(|| self.address()),
address,

Check warning on line 139 in runtime-sdk/modules/evm/src/mock.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/mock.rs#L139

Added line #L139 was not covered by tests
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");

Check warning on line 148 in runtime-sdk/modules/evm/src/mock.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/mock.rs#L148

Added line #L148 was not covered by tests
let call_result = callformat::decode_result(
ctx,

Check warning on line 150 in runtime-sdk/modules/evm/src/mock.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/mock.rs#L150

Added line #L150 was not covered by tests
transaction::CallFormat::EncryptedX25519DeoxysII,
call_result,
&client_keypair,

Check warning on line 153 in runtime-sdk/modules/evm/src/mock.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/mock.rs#L153

Added line #L153 was not covered by tests
)
.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}"),

Check warning on line 166 in runtime-sdk/modules/evm/src/mock.rs

View check run for this annotation

Codecov / codecov/patch

runtime-sdk/modules/evm/src/mock.rs#L161-L166

Added lines #L161 - L166 were not covered by tests
};
}

Ok(result)
}
}

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

/// 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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading
Loading