Skip to content

Commit

Permalink
Feat(engine): Get promise results precompile (#575)
Browse files Browse the repository at this point in the history
  • Loading branch information
birchmd authored and joshuajbouw committed Aug 18, 2022
1 parent 24c4833 commit c06d680
Show file tree
Hide file tree
Showing 23 changed files with 542 additions and 70 deletions.
31 changes: 21 additions & 10 deletions engine-precompiles/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod modexp;
pub mod native;
mod prelude;
pub mod prepaid_gas;
pub mod promise_result;
pub mod random;
pub mod secp256k1;
mod utils;
Expand All @@ -30,10 +31,12 @@ use crate::random::RandomSeed;
use crate::secp256k1::ECRecover;
use aurora_engine_sdk::env::Env;
use aurora_engine_sdk::io::IO;
use aurora_engine_sdk::promise::ReadOnlyPromiseHandler;
use aurora_engine_types::{account_id::AccountId, types::Address, vec, BTreeMap, Box};
use evm::backend::Log;
use evm::executor::{self, stack::PrecompileHandle};
use evm::{Context, ExitError, ExitSucceed};
use promise_result::PromiseResult;

#[derive(Debug, Default)]
pub struct PrecompileOutput {
Expand Down Expand Up @@ -94,17 +97,20 @@ impl HardFork for Istanbul {}

impl HardFork for Berlin {}

pub struct Precompiles<'a, I, E> {
pub struct Precompiles<'a, I, E, H> {
pub generic_precompiles: prelude::BTreeMap<Address, Box<dyn Precompile>>,
// Cannot be part of the generic precompiles because the `dyn` type-erasure messes with
// with the lifetime requirements on the type parameter `I`.
pub near_exit: ExitToNear<I>,
pub ethereum_exit: ExitToEthereum<I>,
pub predecessor_account_id: PredecessorAccount<'a, E>,
pub prepaid_gas: PrepaidGas<'a, E>,
pub promise_results: PromiseResult<H>,
}

impl<'a, I: IO + Copy, E: Env> executor::stack::PrecompileSet for Precompiles<'a, I, E> {
impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> executor::stack::PrecompileSet
for Precompiles<'a, I, E, H>
{
fn execute(
&self,
handle: &mut impl PrecompileHandle,
Expand Down Expand Up @@ -139,16 +145,17 @@ impl<'a, I: IO + Copy, E: Env> executor::stack::PrecompileSet for Precompiles<'a
}
}

pub struct PrecompileConstructorContext<'a, I, E> {
pub struct PrecompileConstructorContext<'a, I, E, H> {
pub current_account_id: AccountId,
pub random_seed: H256,
pub io: I,
pub env: &'a E,
pub promise_handler: H,
}

impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> {
impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, H> {
#[allow(dead_code)]
pub fn new_homestead(ctx: PrecompileConstructorContext<'a, I, E>) -> Self {
pub fn new_homestead(ctx: PrecompileConstructorContext<'a, I, E, H>) -> Self {
let addresses = vec![
ECRecover::ADDRESS,
SHA256::ADDRESS,
Expand All @@ -168,7 +175,7 @@ impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> {
}

#[allow(dead_code)]
pub fn new_byzantium(ctx: PrecompileConstructorContext<'a, I, E>) -> Self {
pub fn new_byzantium(ctx: PrecompileConstructorContext<'a, I, E, H>) -> Self {
let addresses = vec![
ECRecover::ADDRESS,
SHA256::ADDRESS,
Expand Down Expand Up @@ -198,7 +205,7 @@ impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> {
Self::with_generic_precompiles(map, ctx)
}

pub fn new_istanbul(ctx: PrecompileConstructorContext<'a, I, E>) -> Self {
pub fn new_istanbul(ctx: PrecompileConstructorContext<'a, I, E, H>) -> Self {
let addresses = vec![
ECRecover::ADDRESS,
SHA256::ADDRESS,
Expand Down Expand Up @@ -230,7 +237,7 @@ impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> {
Self::with_generic_precompiles(map, ctx)
}

pub fn new_berlin(ctx: PrecompileConstructorContext<'a, I, E>) -> Self {
pub fn new_berlin(ctx: PrecompileConstructorContext<'a, I, E, H>) -> Self {
let addresses = vec![
ECRecover::ADDRESS,
SHA256::ADDRESS,
Expand Down Expand Up @@ -262,26 +269,28 @@ impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> {
Self::with_generic_precompiles(map, ctx)
}

pub fn new_london(ctx: PrecompileConstructorContext<'a, I, E>) -> Self {
pub fn new_london(ctx: PrecompileConstructorContext<'a, I, E, H>) -> Self {
// no precompile changes in London HF
Self::new_berlin(ctx)
}

fn with_generic_precompiles(
generic_precompiles: BTreeMap<Address, Box<dyn Precompile>>,
ctx: PrecompileConstructorContext<'a, I, E>,
ctx: PrecompileConstructorContext<'a, I, E, H>,
) -> Self {
let near_exit = ExitToNear::new(ctx.current_account_id.clone(), ctx.io);
let ethereum_exit = ExitToEthereum::new(ctx.current_account_id, ctx.io);
let predecessor_account_id = PredecessorAccount::new(ctx.env);
let prepaid_gas = PrepaidGas::new(ctx.env);
let promise_results = PromiseResult::new(ctx.promise_handler);

Self {
generic_precompiles,
near_exit,
ethereum_exit,
predecessor_account_id,
prepaid_gas,
promise_results,
}
}

Expand All @@ -298,6 +307,8 @@ impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> {
return Some(f(&self.predecessor_account_id));
} else if address == prepaid_gas::ADDRESS {
return Some(f(&self.prepaid_gas));
} else if address == promise_result::ADDRESS {
return Some(f(&self.promise_results));
}
self.generic_precompiles
.get(&address)
Expand Down
2 changes: 1 addition & 1 deletion engine-precompiles/src/prepaid_gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use aurora_engine_sdk::env::Env;
use aurora_engine_types::{vec, U256};
use evm::{Context, ExitError};

/// predecessor_account_id precompile address
/// prepaid_gas precompile address
///
/// Address: `0x536822d27de53629ef1f84c60555689e9488609f`
/// This address is computed as: `&keccak("prepaidGas")[12..]`
Expand Down
90 changes: 90 additions & 0 deletions engine-precompiles/src/promise_result.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use super::{EvmPrecompileResult, Precompile};
use crate::prelude::types::{Address, EthGas};
use crate::PrecompileOutput;
use aurora_engine_sdk::promise::ReadOnlyPromiseHandler;
use aurora_engine_types::{Cow, Vec};
use borsh::BorshSerialize;
use evm::{Context, ExitError};

/// get_promise_results precompile address
///
/// Address: `0x0a3540f79be10ef14890e87c1a0040a68cc6af71`
/// This address is computed as: `&keccak("getPromiseResults")[12..]`
pub const ADDRESS: Address = crate::make_address(0x0a3540f7, 0x9be10ef14890e87c1a0040a68cc6af71);

pub mod costs {
use crate::prelude::types::EthGas;

/// This cost is always charged for calling this precompile.
pub const PROMISE_RESULT_BASE_COST: EthGas = EthGas::new(125);
/// This is the cost per byte of promise result data.
pub const PROMISE_RESULT_BYTE_COST: EthGas = EthGas::new(1);
}

pub struct PromiseResult<H> {
handler: H,
}

impl<H> PromiseResult<H> {
pub fn new(handler: H) -> Self {
Self { handler }
}
}

impl<H: ReadOnlyPromiseHandler> Precompile for PromiseResult<H> {
fn required_gas(_input: &[u8]) -> Result<EthGas, ExitError> {
// Only gives the cost we can know without reading any promise data.
// This allows failing fast in the case the base cost cannot even be covered.
Ok(costs::PROMISE_RESULT_BASE_COST)
}

fn run(
&self,
input: &[u8],
target_gas: Option<EthGas>,
_context: &Context,
_is_static: bool,
) -> EvmPrecompileResult {
let mut cost = Self::required_gas(input)?;
let check_cost = |cost: EthGas| -> Result<(), ExitError> {
if let Some(target_gas) = target_gas {
if cost > target_gas {
return Err(ExitError::OutOfGas);
}
}
Ok(())
};
check_cost(cost)?;

let num_promises = self.handler.ro_promise_results_count();
let n_usize = usize::try_from(num_promises).map_err(crate::utils::err_usize_conv)?;
let mut results = Vec::with_capacity(n_usize);
for i in 0..num_promises {
if let Some(result) = self.handler.ro_promise_result(i) {
let n_bytes = u64::try_from(result.size()).map_err(crate::utils::err_usize_conv)?;
cost += n_bytes * costs::PROMISE_RESULT_BYTE_COST;
check_cost(cost)?;
results.push(result);
}
}

let bytes = results
.try_to_vec()
.map_err(|_| ExitError::Other(Cow::Borrowed("ERR_PROMISE_RESULT_SERIALIZATION")))?;
Ok(PrecompileOutput::without_logs(cost, bytes))
}
}

#[cfg(test)]
mod tests {
use crate::prelude::sdk::types::near_account_to_evm_address;
use crate::promise_result;

#[test]
fn test_get_promise_results_precompile_id() {
assert_eq!(
promise_result::ADDRESS,
near_account_to_evm_address("getPromiseResults".as_bytes())
);
}
}
6 changes: 6 additions & 0 deletions engine-sdk/src/near_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ impl crate::env::Env for Runtime {
}

impl crate::promise::PromiseHandler for Runtime {
type ReadOnly = Self;

fn promise_results_count(&self) -> u64 {
unsafe { exports::promise_results_count() }
}
Expand Down Expand Up @@ -379,6 +381,10 @@ impl crate::promise::PromiseHandler for Runtime {
exports::promise_return(promise.raw());
}
}

fn read_only(&self) -> Self::ReadOnly {
Self
}
}

/// Some host functions are not usable in NEAR view calls.
Expand Down
57 changes: 57 additions & 0 deletions engine-sdk/src/promise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ impl PromiseId {
}

pub trait PromiseHandler {
type ReadOnly: ReadOnlyPromiseHandler;

fn promise_results_count(&self) -> u64;
fn promise_result(&self, index: u64) -> Option<PromiseResult>;

Expand All @@ -33,4 +35,59 @@ pub trait PromiseHandler {
let base = self.promise_create_call(&args.base);
self.promise_attach_callback(base, &args.callback)
}

fn read_only(&self) -> Self::ReadOnly;
}

pub trait ReadOnlyPromiseHandler {
fn ro_promise_results_count(&self) -> u64;
fn ro_promise_result(&self, index: u64) -> Option<PromiseResult>;
}

impl<T: PromiseHandler> ReadOnlyPromiseHandler for T {
fn ro_promise_results_count(&self) -> u64 {
self.promise_results_count()
}

fn ro_promise_result(&self, index: u64) -> Option<PromiseResult> {
self.promise_result(index)
}
}

/// A promise handler which does nothing. Should only be used when promises can be safely ignored.
#[derive(Debug, Copy, Clone)]
pub struct Noop;

impl PromiseHandler for Noop {
type ReadOnly = Self;

fn promise_results_count(&self) -> u64 {
0
}

fn promise_result(&self, _index: u64) -> Option<PromiseResult> {
None
}

fn promise_create_call(&mut self, _args: &PromiseCreateArgs) -> PromiseId {
PromiseId::new(0)
}

fn promise_attach_callback(
&mut self,
_base: PromiseId,
_callback: &PromiseCreateArgs,
) -> PromiseId {
PromiseId::new(0)
}

fn promise_create_batch(&mut self, _args: &PromiseBatchAction) -> PromiseId {
PromiseId::new(0)
}

fn promise_return(&mut self, _promise: PromiseId) {}

fn read_only(&self) -> Self::ReadOnly {
Self
}
}
31 changes: 25 additions & 6 deletions engine-standalone-storage/src/promise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,31 @@ use aurora_engine_sdk::promise::{PromiseHandler, PromiseId};
use aurora_engine_types::parameters::{PromiseBatchAction, PromiseCreateArgs};
use aurora_engine_types::types::PromiseResult;

/// A promise handler which does nothing. Should only be used when promises can be safely ignored.
pub struct Noop;
/// Implements `PromiseHandler` so that it can be used in the standalone engine implementation of
/// methods like `call`, however since the standalone engine cannot schedule promises in a
/// meaningful way, the mutable implementations are no-ops. Functionally, this is only an implementation
/// of `ReadOnlyPromiseHandler`, which is needed for the standalone engine to properly serve the
/// EVM precompile that gives back information on the results of promises (possibly scheduled using
/// the cross-contract calls feature).
#[derive(Debug, Clone, Copy)]
pub struct NoScheduler<'a> {
pub promise_data: &'a [Option<Vec<u8>>],
}

impl<'a> PromiseHandler for NoScheduler<'a> {
type ReadOnly = Self;

impl PromiseHandler for Noop {
fn promise_results_count(&self) -> u64 {
0
u64::try_from(self.promise_data.len()).unwrap_or_default()
}

fn promise_result(&self, _index: u64) -> Option<PromiseResult> {
None
fn promise_result(&self, index: u64) -> Option<PromiseResult> {
let i = usize::try_from(index).ok()?;
let result = match self.promise_data.get(i)? {
Some(bytes) => PromiseResult::Successful(bytes.clone()),
None => PromiseResult::Failed,
};
Some(result)
}

fn promise_create_call(&mut self, _args: &PromiseCreateArgs) -> PromiseId {
Expand All @@ -31,4 +46,8 @@ impl PromiseHandler for Noop {
}

fn promise_return(&mut self, _promise: PromiseId) {}

fn read_only(&self) -> Self::ReadOnly {
*self
}
}
5 changes: 4 additions & 1 deletion engine-standalone-storage/src/relayer_db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ where
random_seed: H256::zero(),
prepaid_gas: DEFAULT_PREPAID_GAS,
};
let mut handler = crate::promise::Noop;
// We use the Noop handler here since the relayer DB does not contain any promise information.
let mut handler = aurora_engine_sdk::promise::Noop;

while let Some(row) = rows.next()? {
let near_tx_hash = row.near_hash;
Expand Down Expand Up @@ -150,6 +151,7 @@ where
caller: env.predecessor_account_id(),
attached_near: 0,
transaction: crate::sync::types::TransactionKind::Submit(tx),
promise_data: Vec::new(),
};
storage.set_transaction_included(tx_hash, &tx_msg, &diff)?;
}
Expand Down Expand Up @@ -256,6 +258,7 @@ mod test {
caller: "aurora".parse().unwrap(),
attached_near: 0,
transaction: TransactionKind::Unknown,
promise_data: Vec::new(),
},
&diff,
)
Expand Down
Loading

0 comments on commit c06d680

Please sign in to comment.