Skip to content

Commit

Permalink
Feat: Exit and XCC promises are sequential (#868)
Browse files Browse the repository at this point in the history
## Description

This PR is addressing a [usability issue with
XCC](aurora-is-near/aurora-contracts-sdk#13 (comment))
brought up by @karim-en .

It is somewhat common that XCC requires tokens to be bridged from the
user's Aurora address to their XCC account on Near. Naturally, this
bridging needs to happen before the XCC promise resolves so that the
tokens are available for it to use. However, due to the async nature of
Near we could not guarantee that the bridging would happen before the
XCC call.

In this PR we change how the Engine produces promises so that they are
sequential instead of concurrent (i.e. all the promises produced by the
EVM are connected by the `then` combinator, in the order they were
produced during the EVM execution). This means a contract which calls
the exit precompile and then later calls XCC will have the promises
happen in that same order, as the developer intended.

## Performance / NEAR gas cost considerations

N/A

## Testing

Existing XCC tests
  • Loading branch information
birchmd authored Nov 20, 2023
1 parent a746806 commit 896005e
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 120 deletions.
244 changes: 134 additions & 110 deletions engine-sdk/src/near_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,118 @@ impl Runtime {
);
}
}

#[allow(clippy::too_many_lines)]
unsafe fn append_batch_actions(id: u64, args: &PromiseBatchAction) {
for action in &args.actions {
match action {
PromiseAction::CreateAccount => {
exports::promise_batch_action_create_account(id);
}
PromiseAction::Transfer { amount } => {
let amount = amount.as_u128();
let amount_addr = core::ptr::addr_of!(amount);
exports::promise_batch_action_transfer(id, amount_addr as _);
}
PromiseAction::DeployContract { code } => {
let code = code.as_slice();
exports::promise_batch_action_deploy_contract(
id,
code.len() as _,
code.as_ptr() as _,
);
}
PromiseAction::FunctionCall {
name,
gas,
attached_yocto,
args,
} => {
let method_name = name.as_bytes();
let arguments = args.as_slice();
let amount = attached_yocto.as_u128();
let amount_addr = core::ptr::addr_of!(amount);
exports::promise_batch_action_function_call(
id,
method_name.len() as _,
method_name.as_ptr() as _,
arguments.len() as _,
arguments.as_ptr() as _,
amount_addr as _,
gas.as_u64(),
);
}
PromiseAction::Stake { amount, public_key } => {
feature_gated!("all-promise-actions", {
let amount = amount.as_u128();
let amount_addr = core::ptr::addr_of!(amount);
let pk: RawPublicKey = public_key.into();
let pk_bytes = pk.as_bytes();
exports::promise_batch_action_stake(
id,
amount_addr as _,
pk_bytes.len() as _,
pk_bytes.as_ptr() as _,
);
});
}
PromiseAction::AddFullAccessKey { public_key, nonce } => {
let pk: RawPublicKey = public_key.into();
let pk_bytes = pk.as_bytes();
exports::promise_batch_action_add_key_with_full_access(
id,
pk_bytes.len() as _,
pk_bytes.as_ptr() as _,
*nonce,
);
}
PromiseAction::AddFunctionCallKey {
public_key,
nonce,
allowance,
receiver_id,
function_names,
} => {
let pk: RawPublicKey = public_key.into();
let pk_bytes = pk.as_bytes();
let allowance = allowance.as_u128();
let allowance_addr = core::ptr::addr_of!(allowance);
let receiver_id = receiver_id.as_bytes();
let function_names = function_names.as_bytes();
exports::promise_batch_action_add_key_with_function_call(
id,
pk_bytes.len() as _,
pk_bytes.as_ptr() as _,
*nonce,
allowance_addr as _,
receiver_id.len() as _,
receiver_id.as_ptr() as _,
function_names.len() as _,
function_names.as_ptr() as _,
);
}
PromiseAction::DeleteKey { public_key } => {
let pk: RawPublicKey = public_key.into();
let pk_bytes = pk.as_bytes();
exports::promise_batch_action_delete_key(
id,
pk_bytes.len() as _,
pk_bytes.as_ptr() as _,
);
}
PromiseAction::DeleteAccount { beneficiary_id } => {
feature_gated!("all-promise-actions", {
let beneficiary_id = beneficiary_id.as_bytes();
exports::promise_batch_action_delete_key(
id,
beneficiary_id.len() as _,
beneficiary_id.as_ptr() as _,
);
});
}
}
}
}
}

impl StorageIntermediate for RegisterIndex {
Expand Down Expand Up @@ -350,120 +462,28 @@ impl crate::promise::PromiseHandler for Runtime {
PromiseId::new(id)
}

#[allow(clippy::too_many_lines)]
unsafe fn promise_create_batch(&mut self, args: &PromiseBatchAction) -> PromiseId {
let account_id = args.target_account_id.as_bytes();

let id = { exports::promise_batch_create(account_id.len() as _, account_id.as_ptr() as _) };

for action in &args.actions {
match action {
PromiseAction::CreateAccount => {
exports::promise_batch_action_create_account(id);
}
PromiseAction::Transfer { amount } => {
let amount = amount.as_u128();
let amount_addr = core::ptr::addr_of!(amount);
exports::promise_batch_action_transfer(id, amount_addr as _);
}
PromiseAction::DeployContract { code } => {
let code = code.as_slice();
exports::promise_batch_action_deploy_contract(
id,
code.len() as _,
code.as_ptr() as _,
);
}
PromiseAction::FunctionCall {
name,
gas,
attached_yocto,
args,
} => {
let method_name = name.as_bytes();
let arguments = args.as_slice();
let amount = attached_yocto.as_u128();
let amount_addr = core::ptr::addr_of!(amount);
exports::promise_batch_action_function_call(
id,
method_name.len() as _,
method_name.as_ptr() as _,
arguments.len() as _,
arguments.as_ptr() as _,
amount_addr as _,
gas.as_u64(),
);
}
PromiseAction::Stake { amount, public_key } => {
feature_gated!("all-promise-actions", {
let amount = amount.as_u128();
let amount_addr = core::ptr::addr_of!(amount);
let pk: RawPublicKey = public_key.into();
let pk_bytes = pk.as_bytes();
exports::promise_batch_action_stake(
id,
amount_addr as _,
pk_bytes.len() as _,
pk_bytes.as_ptr() as _,
);
});
}
PromiseAction::AddFullAccessKey { public_key, nonce } => {
let pk: RawPublicKey = public_key.into();
let pk_bytes = pk.as_bytes();
exports::promise_batch_action_add_key_with_full_access(
id,
pk_bytes.len() as _,
pk_bytes.as_ptr() as _,
*nonce,
);
}
PromiseAction::AddFunctionCallKey {
public_key,
nonce,
allowance,
receiver_id,
function_names,
} => {
let pk: RawPublicKey = public_key.into();
let pk_bytes = pk.as_bytes();
let allowance = allowance.as_u128();
let allowance_addr = core::ptr::addr_of!(allowance);
let receiver_id = receiver_id.as_bytes();
let function_names = function_names.as_bytes();
exports::promise_batch_action_add_key_with_function_call(
id,
pk_bytes.len() as _,
pk_bytes.as_ptr() as _,
*nonce,
allowance_addr as _,
receiver_id.len() as _,
receiver_id.as_ptr() as _,
function_names.len() as _,
function_names.as_ptr() as _,
);
}
PromiseAction::DeleteKey { public_key } => {
let pk: RawPublicKey = public_key.into();
let pk_bytes = pk.as_bytes();
exports::promise_batch_action_delete_key(
id,
pk_bytes.len() as _,
pk_bytes.as_ptr() as _,
);
}
PromiseAction::DeleteAccount { beneficiary_id } => {
feature_gated!("all-promise-actions", {
let beneficiary_id = beneficiary_id.as_bytes();
exports::promise_batch_action_delete_key(
id,
beneficiary_id.len() as _,
beneficiary_id.as_ptr() as _,
);
});
}
}
}
Self::append_batch_actions(id, args);

PromiseId::new(id)
}

unsafe fn promise_attach_batch_callback(
&mut self,
base: PromiseId,
args: &PromiseBatchAction,
) -> PromiseId {
let account_id = args.target_account_id.as_bytes();

let id = {
exports::promise_batch_then(base.raw(), account_id.len() as _, account_id.as_ptr() as _)
};

Self::append_batch_actions(id, args);

PromiseId::new(id)
}
Expand Down Expand Up @@ -658,7 +678,11 @@ pub(crate) mod exports {
) -> u64;
pub(crate) fn promise_and(promise_idx_ptr: u64, promise_idx_count: u64) -> u64;
pub(crate) fn promise_batch_create(account_id_len: u64, account_id_ptr: u64) -> u64;
fn promise_batch_then(promise_index: u64, account_id_len: u64, account_id_ptr: u64) -> u64;
pub(crate) fn promise_batch_then(
promise_index: u64,
account_id_len: u64,
account_id_ptr: u64,
) -> u64;
// #######################
// # Promise API actions #
// #######################
Expand Down
18 changes: 18 additions & 0 deletions engine-sdk/src/promise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ pub trait PromiseHandler {
/// code or adding/removing access keys.
unsafe fn promise_create_batch(&mut self, args: &PromiseBatchAction) -> PromiseId;

/// # Safety
/// See note on `promise_create_call`. Promise batches in particular must be used very
/// carefully because they can take destructive actions such as deploying new contract
/// code or adding/removing access keys.
unsafe fn promise_attach_batch_callback(
&mut self,
base: PromiseId,
args: &PromiseBatchAction,
) -> PromiseId;

fn promise_return(&mut self, promise: PromiseId);

/// # Safety
Expand Down Expand Up @@ -132,6 +142,14 @@ impl PromiseHandler for Noop {
PromiseId::new(0)
}

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

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

fn read_only(&self) -> Self::ReadOnly {
Expand Down
8 changes: 8 additions & 0 deletions engine-standalone-storage/src/promise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ impl<'a> PromiseHandler for NoScheduler<'a> {
PromiseId::new(0)
}

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

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

fn read_only(&self) -> Self::ReadOnly {
Expand Down
33 changes: 33 additions & 0 deletions engine-test-doubles/src/promise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ impl PromiseTracker {
self.internal_index += 1;
id
}

fn remove_as_near_promise(&mut self, id: u64) -> Option<NearPromise> {
let result = match self.scheduled_promises.remove(&id)? {
PromiseArgs::Batch(x) => NearPromise::Simple(SimpleNearPromise::Batch(x)),
PromiseArgs::Create(x) => NearPromise::Simple(SimpleNearPromise::Create(x)),
PromiseArgs::Recursive(x) => x,
PromiseArgs::Callback { base, callback } => {
let base_promise = self.remove_as_near_promise(base.raw())?;
NearPromise::Then {
base: Box::new(base_promise),
callback: SimpleNearPromise::Create(callback),
}
}
};
Some(result)
}
}

impl PromiseHandler for PromiseTracker {
Expand Down Expand Up @@ -91,6 +107,23 @@ impl PromiseHandler for PromiseTracker {
PromiseId::new(id)
}

unsafe fn promise_attach_batch_callback(
&mut self,
base: PromiseId,
args: &PromiseBatchAction,
) -> PromiseId {
let id = self.take_id();
let base_promise = self
.remove_as_near_promise(base.raw())
.expect("Base promise id must be known");
let new_promise = PromiseArgs::Recursive(NearPromise::Then {
base: Box::new(base_promise),
callback: SimpleNearPromise::Batch(args.clone()),
});
self.scheduled_promises.insert(id, new_promise);
PromiseId::new(id)
}

fn promise_return(&mut self, promise: PromiseId) {
self.returned_promise = Some(promise);
}
Expand Down
Loading

0 comments on commit 896005e

Please sign in to comment.