Skip to content

Commit

Permalink
feat: ibc testing framework (#165)
Browse files Browse the repository at this point in the history
* ibc testing framework

* fix test

* refactor
  • Loading branch information
beer-1 authored Nov 15, 2024
1 parent 76d7ca8 commit 7ceb720
Show file tree
Hide file tree
Showing 29 changed files with 2,772 additions and 15 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ test-storage:
test-e2e:
cargo test -p e2e-move-tests --features testing

test-unit:
cargo test stdlib_move_unit_tests --features testing -p e2e-move-tests

build: precompile build-rust build-go

build-rust: build-rust-release
Expand Down
3 changes: 3 additions & 0 deletions crates/e2e-move-tests/src/tests/move_unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use move_cli::base::test::{run_move_unit_tests_with_factory, UnitTestResult};
use move_core_types::effects::ChangeSet;
use move_unit_test::UnitTestingConfig;
use move_vm_runtime::native_extensions::NativeContextExtensions;
use move_model::metadata::{CompilerVersion, LanguageVersion};

use once_cell::sync::Lazy;
use std::path::PathBuf;
Expand Down Expand Up @@ -69,6 +70,8 @@ fn run_tests_for_pkg(path_to_pkg: impl Into<String>) {
.compiler_config
.known_attributes
.clone_from(metadata::get_all_attribute_names());
build_config.compiler_config.compiler_version = Some(CompilerVersion::V2_1);
build_config.compiler_config.language_version = Some(LanguageVersion::V2_1);

let res = run_move_unit_tests_with_factory(
&pkg_path,
Expand Down
35 changes: 31 additions & 4 deletions crates/natives/src/cosmos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ fn native_requested_messages(
ty_args: Vec<Type>,
_arguments: VecDeque<Value>,
) -> SafeNativeResult<SmallVec<[Value; 1]>> {
let gas_params = &context.native_gas_params.initia_stdlib;
context.charge(gas_params.cosmos_stargate_base)?;

debug_assert!(ty_args.is_empty());
debug_assert!(_arguments.is_empty());

Expand All @@ -100,7 +97,37 @@ fn native_requested_messages(
.into_iter()
.map(|m| Value::struct_(Struct::pack(vec![Value::vector_u8(m.data)])));

Ok(smallvec![Value::vector_for_testing_only(messages)])
let options = cosmos_context
.messages
.borrow()
.clone()
.into_iter()
.map(|m| {
let (callback_id, callback_fid) = if let Some(callback) = m.callback {
(
callback.id,
format!(
"{}::{}::{}",
callback.module_address.to_standard_string(),
callback.module_name,
callback.function_name
)
.into_bytes(),
)
} else {
(0, vec![])
};
Value::struct_(Struct::pack(vec![
Value::bool(m.allow_failure),
Value::u64(callback_id),
Value::vector_u8(callback_fid),
]))
});

Ok(smallvec![
Value::vector_for_testing_only(messages),
Value::vector_for_testing_only(options)
])
}

/***************************************************************************************************
Expand Down
22 changes: 22 additions & 0 deletions crates/natives/src/ibctesting.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use crate::dispatchable_fungible_asset::native_dispatch;
use crate::interface::{RawSafeNative, SafeNativeBuilder};
use move_vm_runtime::native_functions::NativeFunction;

/***************************************************************************************************
* module
*
**************************************************************************************************/
pub fn make_all(
builder: &SafeNativeBuilder,
) -> impl Iterator<Item = (String, NativeFunction)> + '_ {
let mut natives = vec![];

natives.extend([
("dispatchable_ibc_ack", native_dispatch as RawSafeNative),
("dispatchable_ibc_timeout", native_dispatch),
("dispatchable_callback", native_dispatch),
("dispatchable_on_receive", native_dispatch),
]);

builder.make_named_natives(natives)
}
6 changes: 6 additions & 0 deletions crates/natives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ pub mod table;
pub mod transaction_context;
pub mod type_info;

#[cfg(feature = "testing")]
pub mod ibctesting;

use initia_move_gas::{MiscGasParameters, NativeGasParameters};
use interface::SafeNativeBuilder;
use move_core_types::account_address::AccountAddress;
Expand Down Expand Up @@ -83,6 +86,9 @@ pub fn initia_move_natives(
);
add_natives_from_module!("biguint", biguint::make_all(builder));

#[cfg(feature = "testing")]
add_natives_from_module!("ibctesting", ibctesting::make_all(builder));

make_table_from_iter(initia_std_addr, natives)
}

Expand Down
Binary file modified precompile/binaries/minlib/cosmos.mv
Binary file not shown.
Binary file modified precompile/binaries/minlib/json.mv
Binary file not shown.
Binary file modified precompile/binaries/stdlib/cosmos.mv
Binary file not shown.
Binary file modified precompile/binaries/stdlib/json.mv
Binary file not shown.
1 change: 1 addition & 0 deletions precompile/modules/initia_stdlib/Move.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ MoveNursery = { local = "../move_nursery" }
std = "0x1"
initia_std = "0x1"
relayer = "0x3d18d54532fc42e567090852db6eb21fa528f952"
cafe = "0xcafe"
2 changes: 1 addition & 1 deletion precompile/modules/initia_stdlib/sources/address.move
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ module initia_std::address {
assert!(addr == from_sdk(addr_sdk), 0)
}

// string <> address
// hex string <> address
native public fun to_string(addr: address): String;
native public fun from_string(addr_str: String): address;

Expand Down
55 changes: 53 additions & 2 deletions precompile/modules/initia_stdlib/sources/cosmos.move
Original file line number Diff line number Diff line change
Expand Up @@ -414,17 +414,30 @@ module initia_std::cosmos {
)
}

//
// Native Functions
//

native fun stargate_internal(
sender: address, data: vector<u8>, option: Options
);

#[test_only]
native public fun requested_messages(): vector<String>;
native public fun requested_messages(): (vector<String>, vector<Options>);

#[test_only]
public fun was_message_requested(msg: &String): bool {
was_message_requested_with_options(msg, &disallow_failure())
}

#[test_only]
public fun was_message_requested_with_options(
msg: &String, opt: &Options
): bool {
use std::vector;
vector::contains(&requested_messages(), msg)
let (messages, opts) = requested_messages();
let (found, idx) = vector::index_of(&messages, msg);
found && vector::borrow(&opts, idx) == opt
}

// ================================================== Options =================================================
Expand Down Expand Up @@ -493,6 +506,11 @@ module initia_std::cosmos {
}
}

/// Unpack options for external use
public fun unpack_options(opt: Options): (bool, u64, String) {
(opt.allow_failure, opt.callback_id, string::utf8(opt.callback_fid))
}

//=========================================== Tests ===========================================

#[test(sender = @0xcafe)]
Expand All @@ -518,4 +536,37 @@ module initia_std::cosmos {

assert!(was_message_requested(&msg), 1);
}

#[test(sender = @0xcafe)]
public fun test_stargate_with_options(sender: &signer) {
use std::string::{utf8, bytes};

let voter = utf8(b"voter");
let proposal_id = 1;
let option = 1;
let metadata = utf8(b"metadata");
let msg =
json::marshal_to_string(
&VoteRequest {
_type_: utf8(b"/cosmos.gov.v1.MsgVote"),
proposal_id,
voter: voter,
option,
metadata: metadata
}
);

stargate_with_options(
sender,
*bytes(&msg),
allow_failure_with_callback(1, utf8(b"0x1::test::test_fn"))
);

assert!(
was_message_requested_with_options(
&msg, &allow_failure_with_callback(1, utf8(b"0x1::test::test_fn"))
),
1
);
}
}
19 changes: 19 additions & 0 deletions precompile/modules/initia_stdlib/sources/function_info.move
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,23 @@ module initia_std::function_info {
// Test only dependencies so we can invoke those friend functions.
#[test_only]
friend initia_std::function_info_tests;

#[test_only]
public fun load_module_from_function_for_testing(f: &FunctionInfo) {
load_module_from_function(f)
}

#[test_only]
public fun new_function_info_for_testing(
module_address: address, module_name: String, function_name: String
): FunctionInfo {
new_function_info_from_address(module_address, module_name, function_name)
}

#[test_only]
public fun check_dispatch_type_compatibility_for_testing(
framework_function: &FunctionInfo, dispatch_target: &FunctionInfo
): bool {
check_dispatch_type_compatibility(framework_function, dispatch_target)
}
}
53 changes: 53 additions & 0 deletions precompile/modules/initia_stdlib/sources/ibctesting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# IBC Testing

This package provides convenient unit-test tools for IBC operations.

## How to Use

There are three steps in IBC testing:

### `execute_cosmos_messages()`

This function checks pending Cosmos messages and verifies if a message can be executed.

- If a message is executable, it transfers the requested token from the sender to the `@std` address.
- Otherwise, it performs no operation.

If a message is registered with options at `cosmos::stargate_with_options`, it will check and execute the given callback function if option contains callback. There are four cases for these options excluding callback options:

| Success | Option (allow_failure) | Abort | Revert |
|---------|-------------------------|-------|--------|
| Failed | true | false | true |
| Failed | false | true | true |
| True | true | false | false |
| True | false | false | false |

Refer to [ibc_transfer_tests.move](../../tests/ibc_transfer_tests.move) and [ibc_transfer_tests_helpers.move#95](../../tests/ibc_transfer_tests_helpers.move#95) for details.

### `relay_packets()`

If there is at least one executable IBC transfer from the previous step, it will simulate IBC packet relaying by depositing the counterparty token to the recipient and executing the `on_receive` dispatch function.

The `on_receive` function should have the following signature:

```rust
public fun on_receive(recipient: &signer, msg_opt: &Option<ibctesting::MoveMessage>): bool
```

In the `on_receive` function, you can verify the passed message and ensure the counterparty token is correctly received. Refer to [ibc_transfer_tests_helpers.move#115](../../tests/ibc_transfer_tests_helpers.move#115) for details.

Based on the response of `on_receive`, the IBC packet relaying actions decide whether to execute acknowledgment with success or failure. Refer to [ibc_transfer_tests.move#222](../../tests/ibc_transfer_tests.move#222) for details.

You can also use `block::set_block_info` to simulate timeout cases. Refer to [ibc_transfer_tests.move#264](../../tests/ibc_transfer_tests.move#264) for details.

### `relay_acks_timeouts()`

Initia provides an async callback feature to allow a dApp developer to receive IBC packet success notifications. For more details, see [Initia IBC Hooks](https://github.com/initia-labs/initia/tree/main/x/ibc-hooks/move-hooks).

If a message contains a memo field like `{"move": {"message": {}, "async_callback": {"id": "100", "module_address": "0xabc", "module_name": "testing"}}}`, it will execute `0xabc::testing::ibc_ack` or `0xabc::testing::ibc_timeout` according to the result of IBC packet relaying.

## Avoid Re-entrancy in Unit Tests

The IBC testing package is built with the dispatch function of Aptos Move, which does not allow re-entrancy. When writing testing scripts, ensure that you do not call `ibctesting` or test modules from callback functions (`on_callback`, `on_receive`, `ibc_ack`, `ibc_timeout`).

To avoid re-entrancy issues, create a new test module, as demonstrated in [`ibc_transfer_tests`](../../tests/ibc_transfer_tests.move).
Loading

0 comments on commit 7ceb720

Please sign in to comment.