Skip to content

Commit

Permalink
yield/resume: init integration tests (near#10746)
Browse files Browse the repository at this point in the history
This PR adds a couple of basic integration tests for yield execution:

- tests behavior in the case `promise_yield_resume` is called with an
unexpected data id
- tests behavior in the case that `promise_yield_create` and
`promise_yield_resume` are called within the same transaction

Why do we start with these edge cases? As it stands the testing
infrastructure in
[runtime_user.rs](https://github.com/near/nearcore/blob/master/integration-tests/src/user/runtime_user.rs#L344)
expects all receipts generated by a transaction to be resolved
immediately once the transaction is executed. Calling yield without
resume in any transaction breaks that assumption, so adding such tests
will be a bit more involved.

I had to drop the placeholder gas costs to get these tests to pass.

I will follow up with a PR which tweaks RuntimeUser and adds tests for
the main cases:

- yield in one transaction, followed by resume in a later one
- yield without resume, leading to a timeout
  • Loading branch information
saketh-are authored Mar 12, 2024
1 parent 836dbb7 commit d9409a0
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 9 deletions.
4 changes: 2 additions & 2 deletions core/parameters/res/runtime_configs/139.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
yield_resume: { old: false, new: true }
# FIXME(yield_resume): These fees are placeholders.
wasm_yield_create_base: { old: 300_000_000_000_000, new: 50_000_000_000_000 }
wasm_yield_create_base: { old: 300_000_000_000_000, new: 10_000_000_000_000 }
wasm_yield_create_byte: { old: 300_000_000_000_000, new: 10_000_000 }
wasm_yield_resume_base: { old: 300_000_000_000_000, new: 50_000_000_000_000 }
wasm_yield_resume_base: { old: 300_000_000_000_000, new: 10_000_000_000_000 }
wasm_yield_resume_byte: { old: 300_000_000_000_000, new: 100_000_000 }
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,10 @@ expression: config_view
"alt_bn128_g1_sum_element": 5000000000,
"alt_bn128_pairing_check_base": 9686000000000,
"alt_bn128_pairing_check_element": 5102000000000,
"yield_create_base": 50000000000000,
"yield_create_base": 10000000000000,
"yield_create_byte": 10000000,
"yield_resume_base": 50000000000000,
"yield_resume_byte": 50000000000000
"yield_resume_base": 10000000000000,
"yield_resume_byte": 10000000000000
},
"grow_mem_cost": 1,
"regular_op_cost": 822756,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,10 @@ expression: config_view
"alt_bn128_g1_sum_element": 5000000000,
"alt_bn128_pairing_check_base": 9686000000000,
"alt_bn128_pairing_check_element": 5102000000000,
"yield_create_base": 50000000000000,
"yield_create_base": 10000000000000,
"yield_create_byte": 10000000,
"yield_resume_base": 50000000000000,
"yield_resume_byte": 50000000000000
"yield_resume_base": 10000000000000,
"yield_resume_byte": 10000000000000
},
"grow_mem_cost": 1,
"regular_op_cost": 822756,
Expand Down
2 changes: 2 additions & 0 deletions integration-tests/src/tests/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ mod deployment;
mod sanity_checks;
mod state_viewer;
mod test_evil_contracts;
#[cfg(feature = "nightly")]
mod test_yield_resume;
2 changes: 1 addition & 1 deletion integration-tests/src/tests/runtime/test_evil_contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ fn test_evil_deep_trie() {
/// well be necessary to adjust the `expected_max_depth` to at most that limit.
#[test]
fn test_self_delay() {
let node = setup_test_contract(near_test_contracts::nightly_rs_contract());
let node = setup_test_contract(near_test_contracts::rs_contract());
let res = node
.user()
.function_call(
Expand Down
88 changes: 88 additions & 0 deletions integration-tests/src/tests/runtime/test_yield_resume.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use crate::node::{Node, RuntimeNode};
use near_primitives::views::FinalExecutionStatus;

/// Initial balance used in tests.
pub const TESTING_INIT_BALANCE: u128 = 1_000_000_000 * NEAR_BASE;

/// One NEAR, divisible by 10^24.
pub const NEAR_BASE: u128 = 1_000_000_000_000_000_000_000_000;

/// Max prepaid amount of gas.
const MAX_GAS: u64 = 300_000_000_000_000;

fn setup_test_contract(wasm_binary: &[u8]) -> RuntimeNode {
let node = RuntimeNode::new(&"alice.near".parse().unwrap());
let account_id = node.account_id().unwrap();
let node_user = node.user();
let transaction_result = node_user
.create_account(
account_id,
"test_contract".parse().unwrap(),
node.signer().public_key(),
TESTING_INIT_BALANCE / 2,
)
.unwrap();
assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new()));
assert_eq!(transaction_result.receipts_outcome.len(), 2);

let transaction_result =
node_user.deploy_contract("test_contract".parse().unwrap(), wasm_binary.to_vec()).unwrap();
assert_eq!(transaction_result.status, FinalExecutionStatus::SuccessValue(Vec::new()));
assert_eq!(transaction_result.receipts_outcome.len(), 1);

node
}

#[test]
fn create_and_resume_in_one_call() {
let node = setup_test_contract(near_test_contracts::nightly_rs_contract());

let yield_payload = vec![23u8; 16];

let res = node
.user()
.function_call(
"alice.near".parse().unwrap(),
"test_contract".parse().unwrap(),
"call_yield_create_and_resume",
yield_payload,
MAX_GAS,
0,
)
.unwrap();

// the yield callback is expected to execute successfully,
// returning twice the value of the first byte of the payload
assert_eq!(
res.status,
FinalExecutionStatus::SuccessValue(vec![46u8]),
"{res:?} unexpected result; expected 46",
);
}

#[test]
fn resume_without_yield() {
let node = setup_test_contract(near_test_contracts::nightly_rs_contract());

// payload followed by data id
let args: Vec<u8> = vec![42u8; 12].into_iter().chain(vec![23u8; 32].into_iter()).collect();

let res = node
.user()
.function_call(
"alice.near".parse().unwrap(),
"test_contract".parse().unwrap(),
"call_yield_resume",
args,
MAX_GAS,
0,
)
.unwrap();

// expect the execution to suceed, but return 'false'
assert_eq!(
res.status,
FinalExecutionStatus::SuccessValue(vec![0u8]),
"{res:?} unexpected result; expected 0",
);
}
113 changes: 113 additions & 0 deletions runtime/near-test-contracts/test-contract-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,26 @@ extern "C" {
beneficiary_id_len: u64,
beneficiary_id_ptr: u64,
);
// ########################
// # Promise Yield/Resume #
// ########################
#[cfg(feature = "nightly")]
fn promise_yield_create(
method_name_len: u64,
method_name_ptr: u64,
arguments_len: u64,
arguments_ptr: u64,
gas: u64,
gas_weight: u64,
register_id: u64,
) -> u64;
#[cfg(feature = "nightly")]
fn promise_yield_resume(
data_id_len: u64,
data_id_ptr: u64,
payload_len: u64,
payload_ptr: u64,
) -> u32;
// #######################
// # Promise API results #
// #######################
Expand Down Expand Up @@ -819,6 +839,99 @@ fn call_promise() {
}
}

// Function which expects to receive exactly one promise result,
// the contents of which should match the function's input.
#[no_mangle]
unsafe fn check_promise_result() {
input(0);
let expected_result_len = register_len(0) as usize;
let expected = vec![0u8; expected_result_len];
read_register(0, expected.as_ptr() as u64);

assert_eq!(promise_results_count(), 1);
match promise_result(0, 0) {
1 => {
let mut result = vec![0; register_len(0) as usize];
read_register(0, result.as_ptr() as *const u64 as u64);
assert_eq!(expected, result);

// Return the first byte of the payload, doubled
result[0] *= 2;
value_return(1u64, result.as_ptr() as u64);
}
2 => {
assert_eq!(expected_result_len, 0);
}
_ => unreachable!(),
}
}

/// Call promise_yield_resume.
/// Input is the byte array with `data_id` represented by last 32 bytes and `payload`
/// represented by the first `register_len(0) - 32` bytes.
#[cfg(feature = "nightly")]
#[no_mangle]
pub unsafe fn call_yield_resume() {
input(0);
let data_len = register_len(0) as usize;
let data = vec![0u8; data_len];
read_register(0, data.as_ptr() as u64);

let data_id = &data[data_len - 32..];
let payload = &data[0..data_len - 32];

let success = promise_yield_resume(
data_id.len() as u64,
data_id.as_ptr() as u64,
payload.len() as u64,
payload.as_ptr() as u64,
);

let result = vec![success as u8];
value_return(result.len() as u64, result.as_ptr() as u64);
}

/// Call promise_yield_create and promise_yield_resume within the same function.
#[cfg(feature = "nightly")]
#[no_mangle]
pub unsafe fn call_yield_create_and_resume() {
input(0);
let payload = vec![0u8; register_len(0) as usize];
read_register(0, payload.as_ptr() as u64);

// Create a promise yield with callback `check_promise_result`,
// passing the expected payload as an argument to the function.
let method_name = "check_promise_result";
let gas_fixed = 0;
let gas_weight = 1;
let data_id_register = 0;
let promise_index = promise_yield_create(
method_name.len() as u64,
method_name.as_ptr() as u64,
payload.len() as u64,
payload.as_ptr() as u64,
gas_fixed,
gas_weight,
data_id_register,
);

let data_id = vec![0u8; register_len(0) as usize];
read_register(data_id_register, data_id.as_ptr() as u64);

// Resolve the promise yield with the expected payload
let success = promise_yield_resume(
data_id.len() as u64,
data_id.as_ptr() as u64,
payload.len() as u64,
payload.as_ptr() as u64,
);
assert_eq!(success, 1u32);

// This function's return value will resolve to the value returned by the
// `check_promise_result` callback
promise_return(promise_index);
}

#[cfg(feature = "latest_protocol")]
#[no_mangle]
fn attach_unspent_gas_but_burn_all_gas() {
Expand Down

0 comments on commit d9409a0

Please sign in to comment.