diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebff33603da..f4deef87145 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -617,12 +617,15 @@ jobs: QUICKCHECK_TESTS: 2 with: # Run all tests with --all-features, which will run the `e2e-tests` feature if present. + # todo @cmichi: We ignore `contract-xcm` until we can run a relaychain + parachain + # setup with `ink-node`. This is required for testing XCM. command: | cat ./all-contracts | \ grep integration-tests | \ scripts/for_all_contracts_exec.sh \ --ignore internal/static-buffer \ --ignore internal/mapping \ + --ignore public/contract-xcm \ --partition ${{ matrix.partition }}/4 -- \ cargo contract test --all-features --manifest-path {} diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b18543920..3e7067b5277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `#[ink::contract_ref]` attribute - [#2648](https://github.com/use-ink/ink/pull/2648) - Add `ink_revive_types` (and remove `pallet-revive` dependency from `ink_e2e`) - [#2657](https://github.com/use-ink/ink/pull/2657) - non-allocating Solidity ABI encoder - [#2655](https://github.com/use-ink/ink/pull/2655) +- Implement XCM precompile, stabilize XCM API - [#2687](https://github.com/use-ink/ink/pull/2687) ### Changed - Marks the `pallet-revive` host function `account_id` stable - [#2578](https://github.com/use-ink/ink/pull/2578) diff --git a/Cargo.lock b/Cargo.lock index cb199eaf65a..e7f29d9f992 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3618,6 +3618,8 @@ dependencies = [ "parity-scale-codec", "polkavm-derive 0.26.0", "scale-info", + "sp-io 42.0.0", + "sp-runtime-interface 31.0.0", "staging-xcm", "trybuild", ] diff --git a/crates/e2e/src/subxt_client.rs b/crates/e2e/src/subxt_client.rs index 5bc4f171496..5f21e5201ef 100644 --- a/crates/e2e/src/subxt_client.rs +++ b/crates/e2e/src/subxt_client.rs @@ -410,7 +410,7 @@ where let best_block = self.api.best_block().await; - let account = self + let account_info = self .api .client .storage() @@ -425,7 +425,7 @@ where panic!("unable to decode account info: {err:?}"); }); - let account_data = get_composite_field_value(&account, "data")?; + let account_data = get_composite_field_value(&account_info, "data")?; let balance = get_composite_field_value(account_data, "free")?; let balance = balance.as_u128().ok_or_else(|| { Error::Balance(format!("{balance:?} should convert to u128")) diff --git a/crates/e2e/src/xts.rs b/crates/e2e/src/xts.rs index 5a75b9b91c1..be0a6051f37 100644 --- a/crates/e2e/src/xts.rs +++ b/crates/e2e/src/xts.rs @@ -79,10 +79,10 @@ use subxt::{ pub struct Weight { #[codec(compact)] /// The weight of computational time used based on some reference hardware. - ref_time: u64, + pub ref_time: u64, #[codec(compact)] /// The weight of storage space used by proof of validity. - proof_size: u64, + pub proof_size: u64, } impl From for Weight { diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs index 8490199e7dd..3a09f4727a8 100644 --- a/crates/env/src/api.rs +++ b/crates/env/src/api.rs @@ -14,6 +14,8 @@ //! The public raw interface towards the host engine. +#[cfg(feature = "xcm")] +use ink_primitives::Weight; use ink_primitives::{ Address, CodeHashErr, @@ -874,27 +876,49 @@ where ::on_instance(|instance| instance.set_code_hash(code_hash)) } +/// Estimates the [`Weight`] required to execute a given XCM message. +/// +/// This is done by invoking [the XCM precompile](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/precompiles/struct.XcmPrecompile.html). +/// For more details consult the [precompile interface](https://github.com/paritytech/polkadot-sdk/blob/master/polkadot/xcm/pallet-xcm/precompiles/src/interface/IXcm.sol). +/// +/// # Errors +/// +/// - If the message cannot be properly decoded in the XCM precompile. +/// - If the XCM execution fails because of the runtime's XCM configuration. +/// +/// # Panics +/// +/// Panics in the off-chain environment. +#[cfg(feature = "xcm")] +pub fn xcm_weigh(msg: &xcm::VersionedXcm) -> Result +where + Call: scale::Encode, +{ + ::on_instance(|instance| { + TypedEnvBackend::xcm_weigh(instance, msg) + }) +} + /// Execute an XCM message locally, using the contract's address as the origin. /// -/// For more details consult the -/// [host function documentation](https://paritytech.github.io/polkadot-sdk/master/pallet_contracts/api_doc/trait.Current.html#tymethod.xcm_execute). +/// This is done by invoking [the XCM precompile](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/precompiles/struct.XcmPrecompile.html). +/// For more details consult the [precompile interface](https://github.com/paritytech/polkadot-sdk/blob/master/polkadot/xcm/pallet-xcm/precompiles/src/interface/IXcm.sol). /// /// # Errors /// -/// - If the message cannot be properly decoded on the `pallet-revive` side. +/// - If the message cannot be properly decoded in the XCM precompile. /// - If the XCM execution fails because of the runtime's XCM configuration. /// /// # Panics /// /// Panics in the off-chain environment. -#[cfg(all(feature = "xcm", feature = "unstable-hostfn"))] -pub fn xcm_execute(msg: &xcm::VersionedXcm) -> Result<()> +#[cfg(feature = "xcm")] +pub fn xcm_execute(msg: &xcm::VersionedXcm, weight: Weight) -> Result<()> where - E: Environment, Call: scale::Encode, { ::on_instance(|instance| { - TypedEnvBackend::xcm_execute::(instance, msg) + TypedEnvBackend::xcm_execute(instance, msg, weight) }) } @@ -903,27 +927,26 @@ where /// The `msg` argument has to be SCALE encoded, it needs to be decodable to a valid /// instance of the `RuntimeCall` enum. /// -/// For more details consult -/// [host function documentation](https://paritytech.github.io/polkadot-sdk/master/pallet_contracts/api_doc/trait.Current.html#tymethod.xcm_send). +/// This is done by invoking [the XCM precompile](https://paritytech.github.io/polkadot-sdk/master/pallet_xcm/precompiles/struct.XcmPrecompile.html). +/// For more details consult the [precompile interface](https://github.com/paritytech/polkadot-sdk/blob/master/polkadot/xcm/pallet-xcm/precompiles/src/interface/IXcm.sol). /// /// # Errors /// -/// - If the message cannot be properly decoded on the `pallet-revive` side. +/// - If the message cannot be properly decoded in the XCM precompile. /// /// # Panics /// /// Panics in the off-chain environment. -#[cfg(all(feature = "xcm", feature = "unstable-hostfn"))] -pub fn xcm_send( +#[cfg(feature = "xcm")] +pub fn xcm_send( dest: &xcm::VersionedLocation, msg: &xcm::VersionedXcm, -) -> Result +) -> Result<()> where - E: Environment, Call: scale::Encode, { ::on_instance(|instance| { - TypedEnvBackend::xcm_send::(instance, dest, msg) + TypedEnvBackend::xcm_send(instance, dest, msg) }) } diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index 3c02d58efb2..717943c0b9d 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(feature = "xcm")] +use ink_primitives::Weight; use ink_primitives::{ Address, CodeHashErr, @@ -415,15 +417,28 @@ pub trait TypedEnvBackend: EnvBackend { /// For more details visit: [`own_code_hash`][`crate::own_code_hash`] fn own_code_hash(&mut self) -> H256; + /// Estimates the [`Weight`] required to execute a given XCM message. + /// + /// # Note + /// + /// For more details visit: [`xcm`][`crate::xcm_weigh`]. + #[cfg(feature = "xcm")] + fn xcm_weigh(&mut self, msg: &xcm::VersionedXcm) -> Result + where + Call: scale::Encode; + /// Execute an XCM message locally, using the contract's address as the origin. /// /// # Note /// /// For more details visit: [`xcm`][`crate::xcm_execute`]. - #[cfg(all(feature = "xcm", feature = "unstable-hostfn"))] - fn xcm_execute(&mut self, msg: &xcm::VersionedXcm) -> Result<()> + #[cfg(feature = "xcm")] + fn xcm_execute( + &mut self, + msg: &xcm::VersionedXcm, + weight: Weight, + ) -> Result<()> where - E: Environment, Call: scale::Encode; /// Send an XCM message, using the contract's address as the origin. @@ -431,13 +446,12 @@ pub trait TypedEnvBackend: EnvBackend { /// # Note /// /// For more details visit: [`xcm`][`crate::xcm_send`]. - #[cfg(all(feature = "xcm", feature = "unstable-hostfn"))] - fn xcm_send( + #[cfg(feature = "xcm")] + fn xcm_send( &mut self, dest: &xcm::VersionedLocation, msg: &xcm::VersionedXcm, - ) -> Result + ) -> Result<()> where - E: Environment, Call: scale::Encode; } diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index c7b3cfcc2ae..ac92e776a9c 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -13,6 +13,8 @@ // limitations under the License. use ink_engine::ext::Engine; +#[cfg(feature = "xcm")] +use ink_primitives::Weight; use ink_primitives::{ Address, CodeHashErr, @@ -799,23 +801,26 @@ impl TypedEnvBackend for EnvInstance { .expect("own code hash not found") } - #[cfg(all(feature = "xcm", feature = "unstable-hostfn"))] - fn xcm_execute(&mut self, _msg: &xcm::VersionedXcm) -> Result<()> - where - E: Environment, - { + #[cfg(feature = "xcm")] + fn xcm_weigh(&mut self, _msg: &xcm::VersionedXcm) -> Result { unimplemented!("off-chain environment does not support `xcm_execute`") } - #[cfg(all(feature = "xcm", feature = "unstable-hostfn"))] - fn xcm_send( + #[cfg(feature = "xcm")] + fn xcm_execute( + &mut self, + _msg: &xcm::VersionedXcm, + _weight: Weight, + ) -> Result<()> { + unimplemented!("off-chain environment does not support `xcm_execute`") + } + + #[cfg(feature = "xcm")] + fn xcm_send( &mut self, _dest: &xcm::VersionedLocation, _msg: &xcm::VersionedXcm, - ) -> Result - where - E: Environment, - { + ) -> Result<()> { unimplemented!("off-chain environment does not support `xcm_send`") } } diff --git a/crates/env/src/engine/on_chain/pallet_revive.rs b/crates/env/src/engine/on_chain/pallet_revive.rs index a61d1e83879..b67ffa3765b 100644 --- a/crates/env/src/engine/on_chain/pallet_revive.rs +++ b/crates/env/src/engine/on_chain/pallet_revive.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#[cfg(feature = "xcm")] +use ink_primitives::Weight; use ink_primitives::{ Address, CodeHashErr, @@ -36,7 +38,7 @@ use pallet_revive_uapi::{ ReturnFlags, StorageFlags, }; -#[cfg(all(feature = "xcm", feature = "unstable-hostfn"))] +#[cfg(feature = "xcm")] use xcm::VersionedXcm; use crate::{ @@ -108,7 +110,9 @@ impl CryptoHash for Blake2x128 { let sel = const { solidity_selector("hashBlake128(bytes)") }; buffer[..4].copy_from_slice(&sel[..4]); - let n = solidity_encode_bytes(input, 32, &mut buffer[4..]); + // we pass `offset = 32`, as the `bytes` payload starts there (right after the + // offset word), we pass `offset_pos = 0`, as the callee takes only one argument. + let n = solidity_encode_bytes(input, 32, 0, &mut buffer[4..]); const ADDR: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000900"); @@ -170,6 +174,10 @@ fn encode_bool(value: bool, out: &mut [u8]) { const STORAGE_PRECOMPILE_ADDR: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000901"); +#[cfg(feature = "xcm")] +const XCM_PRECOMPILE_ADDR: [u8; 20] = + hex_literal::hex!("00000000000000000000000000000000000A0000"); + /// Four bytes are required to encode a Solidity selector; const SOL_ENCODED_SELECTOR_LEN: usize = 4; @@ -195,13 +203,21 @@ const SOL_BYTES_ENCODING_OVERHEAD: usize = 64; /// /// Returns the number of bytes written. /// +/// # Arguments +/// +/// - `input`: the bytes to encode. +/// - `offset`: the position in `out` where the data segment of `input` starts. The data +/// segment is composed of [length word (32 bytes)] [input (padded to 32)]`. +/// - `offset_pos`: the position in `out` where to write the `offset` word to. +/// - `out`: the output buffer. +/// /// # Developer Note /// /// The returned layout will be /// -/// `[offset (32 bytes)] [len (32 bytes)] [data (padded to 32)]` +/// `... [offset (32 bytes)] ... [len (32 bytes)] [data (padded to 32)] ...` /// -/// The `out` byte array need to be able to hold +/// The `out` byte array needs to be able to hold /// (in the worst case) 95 bytes more than `input.len()`. /// /// This is because we write the following to `out`: @@ -209,33 +225,54 @@ const SOL_BYTES_ENCODING_OVERHEAD: usize = 64; /// * The length word → always 32 bytes. /// * The input itself → exactly `input.len()` bytes. /// * We pad the input to a multiple of 32 → between 0 and 31 extra bytes. -fn solidity_encode_bytes(input: &[u8], offset: u32, out: &mut [u8]) -> usize { - let len = input.len(); - let padded_len = solidity_padded_len(len); +fn solidity_encode_bytes( + input: &[u8], + offset: u32, + offset_pos: usize, + out: &mut [u8], +) -> usize { + let input_len = input.len(); + let padded_len = solidity_padded_len(input_len); // out_len = 32 + padded_len // = 32 + ceil(input_len / 32) * 32 assert!(out.len() >= padded_len + SOL_BYTES_ENCODING_OVERHEAD); // Encode offset as a 32-byte big-endian word - out[28..32].copy_from_slice(&offset.to_be_bytes()[..4]); - out[..28].copy_from_slice(&[0u8; 28]); // make sure the first bytes are zeroed + out[offset_pos + 28..offset_pos + 32].copy_from_slice(&offset.to_be_bytes()[..4]); + out[offset_pos..offset_pos + 28].copy_from_slice(&[0u8; 28]); // make sure the first bytes are zeroed // Encode length as a 32-byte big-endian word let mut len_word = [0u8; 32]; // We are at most on a 64-bit architecture, hence we can safely assume `len < 2^64`. - let len_bytes = (len as u64).to_be_bytes(); + let len_bytes = (input_len as u64).to_be_bytes(); len_word[24..32].copy_from_slice(&len_bytes); - out[32..64].copy_from_slice(&len_word); - // Write data after `offset` and `len` word - out[64..64 + len].copy_from_slice(input); + // we take `offset: u32` as a parameter of `solidity_encode_bytes`, + // as we need to extract the big endian bytes above. + let offset = offset as usize; + + // The offset is a 32 byte word + out[offset..offset + 32].copy_from_slice(&len_word); + + // Number of bytes required to encode the length of the `bytes` array + // (i.e. the "length word"). + const BYTES_LEN_WORD: usize = 32; + + // Write the `input` at `offset_pos`, after the 32 byte `len` word + out[offset + BYTES_LEN_WORD..offset + BYTES_LEN_WORD + input_len] + .copy_from_slice(input); 64 + padded_len } /// Returns the Solidity word padded length for the given input length (i.e. next multiple /// of 32 for the given number). +/// +/// # Developer Note +// The implementation does not use `.div_ceil()`, as that function is more complex +// and would use up more stack space. +#[allow(clippy::manual_div_ceil)] #[inline(always)] const fn solidity_padded_len(len: usize) -> usize { ((len + 31) / 32) * 32 @@ -261,7 +298,9 @@ impl CryptoHash for Blake2x256 { let sel = const { solidity_selector("hashBlake256(bytes)") }; buffer[..4].copy_from_slice(&sel[..4]); - let n = solidity_encode_bytes(input, 32, &mut buffer[4..]); + // we pass `offset = 32`, as the `bytes` payload starts there (right after the + // offset word) + let n = solidity_encode_bytes(input, 32, 0, &mut buffer[4..]); const ADDR: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000900"); @@ -473,7 +512,7 @@ impl EnvInstance { fn call_bool_precompile(selector: [u8; 4], output: &mut [u8]) -> bool { debug_assert_eq!(output.len(), 32); const ADDR: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000900"); - let _ = ext::call( + ext::call( CallFlags::empty(), &ADDR, u64::MAX, // `ref_time` to devote for execution. `u64::MAX` = all @@ -489,7 +528,7 @@ fn call_bool_precompile(selector: [u8; 4], output: &mut [u8]) -> bool { return true; } debug_assert_eq!(&output[..32], [0u8; 32]); - return false; + false } /// Calls a function on the `pallet-revive` `Storage` pre-compile "contract". @@ -499,7 +538,9 @@ fn call_bool_precompile(selector: [u8; 4], output: &mut [u8]) -> bool { /// This function assumes that the called pre-compiles all have this function /// signature for the arguments: /// -/// function containsStorage(uint32 flags, bool isFixedKey, bytes memory key) +/// ```no_compile +/// function containsStorage(uint32 flags, bool isFixedKey, bytes memory key) +/// ``` /// /// The function makes heavy use of operating on byte slices and the positions /// in the slice are calculated based on the size of these three arguments. @@ -539,9 +580,8 @@ fn call_storage_precompile( (SOL_ENCODED_FLAGS_LEN + SOL_ENCODED_IS_FIXED_KEY_LEN + SOL_BYTES_OFFSET_WORD_LEN) as u32, // encode the `bytes` starting at the appropriate position in the slice - &mut input_buf[SOL_ENCODED_SELECTOR_LEN - + SOL_ENCODED_FLAGS_LEN - + SOL_ENCODED_IS_FIXED_KEY_LEN..], + 64, + &mut input_buf[SOL_ENCODED_SELECTOR_LEN..], ); // todo @cmichi check if we might better return `None` in this situation. perhaps a @@ -589,8 +629,8 @@ fn decode_bytes(input: &[u8], out: &mut [u8]) -> usize { buf[..].copy_from_slice(&input[28..32]); debug_assert_eq!( { - let offset = u32::from_be_bytes(buf) as usize; - offset + // offset + u32::from_be_bytes(buf) as usize }, 64 ); @@ -643,8 +683,10 @@ impl EnvBackend for EnvInstance { /// Calls the following function on the `pallet-revive` `Storage` pre-compile: /// - /// function takeStorage(uint32 flags, bool isFixedKey, bytes memory key) - /// external returns (bytes memory) + /// ```no_compile + /// function takeStorage(uint32 flags, bool isFixedKey, bytes memory key) + /// external returns (bytes memory) + /// ``` fn take_contract_storage(&mut self, key: &K) -> Result> where K: scale::Encode, @@ -665,7 +707,7 @@ impl EnvBackend for EnvInstance { let output = &mut buffer.take_rest(); let sel = const { solidity_selector("takeStorage(uint32,bool,bytes)") }; - let _ = call_storage_precompile(&mut &mut buf[..], sel, key, output) + call_storage_precompile(&mut &mut buf[..], sel, key, output) .expect("failed calling Storage pre-compile (take)"); debug_assert!( @@ -699,8 +741,10 @@ impl EnvBackend for EnvInstance { /// Calls the following function on the `pallet-revive` `Storage` pre-compile: /// - /// function containsStorage(uint32 flags, bool isFixedKey, bytes memory key) - /// external returns (bool containedKey, uint valueLen) + /// ```no_compile + /// function containsStorage(uint32 flags, bool isFixedKey, bytes memory key) + /// external returns (bool containedKey, uint valueLen) + /// ``` fn contains_contract_storage(&mut self, key: &K) -> Option where K: scale::Encode, @@ -718,7 +762,7 @@ impl EnvBackend for EnvInstance { ); let output = buffer.take(64); let sel = const { solidity_selector("containsStorage(uint32,bool,bytes)") }; - call_storage_precompile(&mut &mut buf[..], sel, key, &mut &mut output[..]) + call_storage_precompile(&mut &mut buf[..], sel, key, &mut output[..]) .expect("failed calling Storage pre-compile (contains)"); // Check the returned `containedKey` boolean value @@ -737,8 +781,10 @@ impl EnvBackend for EnvInstance { /// Calls the following function on the `pallet-revive` `Storage` pre-compile: /// - /// function clearStorage(uint32 flags, bool isFixedKey, bytes memory key) - /// external returns (bool containedKey, uint valueLen); + /// ```no_compile + /// function clearStorage(uint32 flags, bool isFixedKey, bytes memory key) + /// external returns (bool containedKey, uint valueLen); + /// ``` fn clear_contract_storage(&mut self, key: &K) -> Option where K: scale::Encode, @@ -757,7 +803,7 @@ impl EnvBackend for EnvInstance { let output = buffer.take(64); let sel = const { solidity_selector("clearStorage(uint32,bool,bytes)") }; - let _ = call_storage_precompile(&mut &mut buf[..], sel, key, &mut output[..]) + call_storage_precompile(&mut &mut buf[..], sel, key, &mut output[..]) .expect("failed calling Storage pre-compile (clear)"); // Check the returned `containedKey` boolean value @@ -908,7 +954,7 @@ impl TypedEnvBackend for EnvInstance { let mut scope = self.scoped_buffer(); let h160: &mut [u8; 20] = scope.take(20).try_into().unwrap(); ext::address(h160); - h160.clone() + *h160 }; self.to_account_id::(h160.into()) } @@ -972,7 +1018,7 @@ impl TypedEnvBackend for EnvInstance { const ADDR: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000900"); - let _ = ext::call( + ext::call( CallFlags::empty(), &ADDR, u64::MAX, // `ref_time` to devote for execution. `u64::MAX` = all @@ -1279,52 +1325,125 @@ impl TypedEnvBackend for EnvInstance { H256::from_slice(output) } - #[cfg(all(feature = "xcm", feature = "unstable-hostfn"))] - fn xcm_execute(&mut self, _msg: &VersionedXcm) -> Result<()> + #[cfg(feature = "xcm")] + fn xcm_weigh(&mut self, msg: &VersionedXcm) -> Result where - E: Environment, Call: scale::Encode, { - panic!( - "todo Native ink! XCM functions are not supported yet, you have to call the pre-compile contracts for XCM directly until then." - ); - /* let mut scope = self.scoped_buffer(); + let enc_msg = scope.take_encoded(msg); + let buffer = scope.take_rest(); + + let mut output_buffer = [0u8; 64]; + + let sel = const { solidity_selector("weighMessage(bytes)") }; + buffer[..4].copy_from_slice(&sel[..4]); + + let n = solidity_encode_bytes(enc_msg, 32, 0, &mut buffer[4..]); + + let call_result = ext::call( + CallFlags::empty(), + &XCM_PRECOMPILE_ADDR, + u64::MAX, // `ref_time` to devote for execution. `u64::MAX` = all + u64::MAX, // `proof_size` to devote for execution. `u64::MAX` = all + &[u8::MAX; 32], // No deposit limit. + &U256::zero().to_little_endian(), // Value transferred to the contract. + &buffer[..4 + n], + Some(&mut &mut output_buffer[..]), + ); + call_result.expect("call host function failed"); + + let mut ref_time_bytes = [0u8; 8]; + ref_time_bytes.copy_from_slice(&output_buffer[24..32]); + let mut proof_size_bytes = [0u8; 8]; + proof_size_bytes.copy_from_slice(&output_buffer[56..64]); + + let weight: Weight = Weight::from_parts( + u64::from_be_bytes(ref_time_bytes), + u64::from_be_bytes(proof_size_bytes), + ); + Ok(weight) + } + #[cfg(feature = "xcm")] + fn xcm_execute( + &mut self, + msg: &VersionedXcm, + weight: Weight, + ) -> Result<()> + where + Call: scale::Encode, + { + let mut scope = self.scoped_buffer(); let enc_msg = scope.take_encoded(msg); + let buffer = scope.take_rest(); - #[allow(deprecated)] - ext::xcm_execute(enc_msg).map_err(Into::into) - */ + let sel = const { solidity_selector("execute(bytes,(uint64,uint64))") }; + buffer[..4].copy_from_slice(&sel[..4]); + + // 96 because 64 for `Weight` and 32 for `bytes` offset + let n = solidity_encode_bytes(enc_msg, 96, 0, &mut buffer[4..]); + + let mut weight_bytes = [0u8; 64]; + weight_bytes[24..32].copy_from_slice(&weight.ref_time().to_be_bytes()); + weight_bytes[56..64].copy_from_slice(&weight.proof_size().to_be_bytes()); + // put the `Weight` after the `bytes` offset word + buffer[4 + 32..4 + 32 + 64].copy_from_slice(&weight_bytes[..]); + + let _call_result = ext::call( + CallFlags::empty(), + &XCM_PRECOMPILE_ADDR, + u64::MAX, // `ref_time` to devote for execution. `u64::MAX` = all + u64::MAX, // `proof_size` to devote for execution. `u64::MAX` = all + &[u8::MAX; 32], // No deposit limit. + &U256::zero().to_little_endian(), // Value transferred to the contract. + &buffer[..4 + n + 64], + None, + )?; + Ok(()) } - #[cfg(all(feature = "xcm", feature = "unstable-hostfn"))] - // todo - fn xcm_send( + #[cfg(feature = "xcm")] + fn xcm_send( &mut self, - _dest: &xcm::VersionedLocation, - _msg: &VersionedXcm, - ) -> Result + dest: &xcm::VersionedLocation, + msg: &VersionedXcm, + ) -> Result<()> where - E: Environment, Call: scale::Encode, { - panic!( - "todo Native ink! XCM functions are not supported yet, you have to call the pre-compile contracts for XCM directly until then." - ); - /* let mut scope = self.scoped_buffer(); - let output = scope.take(32); - scope.append_encoded(dest); - let enc_dest = scope.take_appended(); + let enc_dest = scope.take_encoded(dest); + let enc_msg = scope.take_encoded(msg); - scope.append_encoded(msg); - let enc_msg = scope.take_appended(); - #[allow(deprecated)] - ext::xcm_send(enc_dest, enc_msg, output.try_into().unwrap())?; - let hash: xcm::v4::XcmHash = scale::Decode::decode(&mut &output[..])?; - Ok(hash) - */ + let buffer = scope.take_rest(); + + let sel = const { solidity_selector("send(bytes,bytes)") }; + buffer[..4].copy_from_slice(&sel[..4]); + + let n_dest = solidity_encode_bytes(enc_dest, 64, 0, &mut buffer[4..]); + let n_msg = solidity_encode_bytes( + enc_msg, + 32 /* first `bytes` offset word */ + + 32 /* second `bytes` offset word */ + + (n_dest - 32usize) as u32, /* we have to subtract 32, because that's the + * length of the `offset` word */ + 32, // the offset word is written right after the offset word for `dest` + &mut buffer[4..], + ); + + let call_result = ext::call( + CallFlags::empty(), + &XCM_PRECOMPILE_ADDR, + u64::MAX, // `ref_time` to devote for execution. `u64::MAX` = all + u64::MAX, // `proof_size` to devote for execution. `u64::MAX` = all + &[u8::MAX; 32], // No deposit limit. + &U256::zero().to_little_endian(), // Value transferred to the contract. + &buffer[..4 + n_dest + n_msg], + None, + ); + call_result.expect("calling host function failed"); + Ok(()) } } diff --git a/crates/env/src/lib.rs b/crates/env/src/lib.rs index 0ae7820c413..9253ebe891b 100644 --- a/crates/env/src/lib.rs +++ b/crates/env/src/lib.rs @@ -54,13 +54,13 @@ pub const BUFFER_SIZE: usize = 16384; #[cfg(target_arch = "riscv64")] #[panic_handler] -fn panic(info: &core::panic::PanicInfo) -> ! { +fn panic(_info: &core::panic::PanicInfo) -> ! { // In case the contract is build in debug-mode, we return the // panic message as a payload by triggering a contract revert. #[cfg(any(feature = "ink-debug", feature = "std"))] self::return_value( ReturnFlags::REVERT, - &ink_prelude::format!("{}", info.message()).as_bytes(), + &ink_prelude::format!("{}", _info.message()).as_bytes(), ); // If contract is compiled with `cargo contract --release`, it will diff --git a/crates/ink/Cargo.toml b/crates/ink/Cargo.toml index 2299a97c7da..7e41b244c28 100644 --- a/crates/ink/Cargo.toml +++ b/crates/ink/Cargo.toml @@ -33,6 +33,10 @@ linkme = { workspace = true, optional = true } polkavm-derive = { workspace = true } xcm = { workspace = true, optional = true } +# required for overwriting child dependency features of `pallet-xcm` +sp-runtime-interface = { version = "31.0.0", features = ["disable_target_static_assertions"], default-features = false, optional = true } +sp-io = { version = "42.0.0", default-features = false, features = ["disable_panic_handler", "disable_oom", "disable_allocator"], optional = true } + # Hotfix for https://github.com/fizyk20/generic-array/issues/158. # Can be removed once there is a new release of https://github.com/RustCrypto, # from which we use some hashing crates (`sha3`, `sha2`, etc.). @@ -73,7 +77,7 @@ no-panic-handler = ["ink_env/no-panic-handler"] unstable-hostfn = ["ink_env/unstable-hostfn"] # Enable xcm utilities. -xcm = ["dep:xcm", "ink_env/xcm"] +xcm = ["dep:xcm", "ink_env/xcm", "dep:sp-runtime-interface", "dep:sp-io"] # For the ui tests, which use this `Cargo.toml` [lints.rust.unexpected_cfgs] diff --git a/crates/ink/src/env_access.rs b/crates/ink/src/env_access.rs index 4f990d84865..c18574ffcf1 100644 --- a/crates/ink/src/env_access.rs +++ b/crates/ink/src/env_access.rs @@ -35,6 +35,8 @@ use ink_env::{ HashOutput, }, }; +#[cfg(feature = "xcm")] +use ink_primitives::Weight; use ink_primitives::{ Address, CodeHashErr, @@ -1248,20 +1250,29 @@ where ink_env::set_code_hash::(code_hash) } - #[cfg(all(feature = "xcm", feature = "unstable-hostfn"))] + #[cfg(feature = "xcm")] + pub fn xcm_weigh( + self, + msg: &xcm::VersionedXcm, + ) -> Result { + ink_env::xcm_weigh(msg) + } + + #[cfg(feature = "xcm")] pub fn xcm_execute( self, msg: &xcm::VersionedXcm, + weight: Weight, ) -> Result<()> { - ink_env::xcm_execute::(msg) + ink_env::xcm_execute(msg, weight) } - #[cfg(all(feature = "xcm", feature = "unstable-hostfn"))] + #[cfg(feature = "xcm")] pub fn xcm_send( self, dest: &xcm::VersionedLocation, msg: &xcm::VersionedXcm, - ) -> Result { - ink_env::xcm_send::(dest, msg) + ) -> Result<()> { + ink_env::xcm_send(dest, msg) } } diff --git a/crates/ink/tests/ui/contract/fail/event/event-too-many-topics.rs b/crates/ink/tests/ui/contract/fail/event/event-too-many-topics.rs index 8866b59c711..b1b4e109c05 100644 --- a/crates/ink/tests/ui/contract/fail/event/event-too-many-topics.rs +++ b/crates/ink/tests/ui/contract/fail/event/event-too-many-topics.rs @@ -30,4 +30,3 @@ mod contract { } fn main() {} - diff --git a/crates/ink/tests/ui/event/fail/conficting_attributes.rs b/crates/ink/tests/ui/event/fail/conflicting_attributes.rs similarity index 100% rename from crates/ink/tests/ui/event/fail/conficting_attributes.rs rename to crates/ink/tests/ui/event/fail/conflicting_attributes.rs diff --git a/crates/ink/tests/ui/event/fail/conflicting_attributes.stderr b/crates/ink/tests/ui/event/fail/conflicting_attributes.stderr index 0cceeb9369e..341f1bfc40b 100644 --- a/crates/ink/tests/ui/event/fail/conflicting_attributes.stderr +++ b/crates/ink/tests/ui/event/fail/conflicting_attributes.stderr @@ -1,5 +1,5 @@ error: cannot specify `signature_topic` with `anonymous` in ink! event item configuration argument - --> tests/ui/event/fail/conficting_attributes.rs:3:7 + --> tests/ui/event/fail/conflicting_attributes.rs:3:7 | 3 | #[ink(signature_topic = "1111111111111111111111111111111111111111111111111111111111111111")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/ink/tests/ui/event/fail/conficting_attributes_inline.rs b/crates/ink/tests/ui/event/fail/conflicting_attributes_inline.rs similarity index 100% rename from crates/ink/tests/ui/event/fail/conficting_attributes_inline.rs rename to crates/ink/tests/ui/event/fail/conflicting_attributes_inline.rs diff --git a/crates/ink/tests/ui/event/fail/conflicting_attributes_inline.stderr b/crates/ink/tests/ui/event/fail/conflicting_attributes_inline.stderr index 4bf3e57b3f4..ad713b48747 100644 --- a/crates/ink/tests/ui/event/fail/conflicting_attributes_inline.stderr +++ b/crates/ink/tests/ui/event/fail/conflicting_attributes_inline.stderr @@ -1,5 +1,5 @@ error: cannot specify `signature_topic` with `anonymous` in ink! event item configuration argument - --> tests/ui/event/fail/conficting_attributes_inline.rs:2:18 + --> tests/ui/event/fail/conflicting_attributes_inline.rs:2:18 | 2 | #[ink(anonymous, signature_topic = "1111111111111111111111111111111111111111111111111111111111111111")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/integration-tests/internal/misc-hostfns/lib.rs b/integration-tests/internal/misc-hostfns/lib.rs index 9ecbf560e46..eb94943ed9c 100755 --- a/integration-tests/internal/misc-hostfns/lib.rs +++ b/integration-tests/internal/misc-hostfns/lib.rs @@ -93,11 +93,7 @@ mod misc_hostfns { ) -> E2EResult<()> { // given let contract = client - .instantiate( - "misc_hostfns", - &ink_e2e::alice(), - &mut MiscHostfnsRef::new(), - ) + .instantiate("misc_hostfns", &ink_e2e::bob(), &mut MiscHostfnsRef::new()) .submit() .await .expect("instantiate failed"); @@ -105,7 +101,7 @@ mod misc_hostfns { // then let _call_res = client - .call(&ink_e2e::alice(), &call_builder.is_contract()) + .call(&ink_e2e::bob(), &call_builder.is_contract()) .submit() .await .unwrap_or_else(|err| { diff --git a/integration-tests/public/contract-xcm/Cargo.toml b/integration-tests/public/contract-xcm/Cargo.toml index be238833e0a..025f78e0cf5 100644 --- a/integration-tests/public/contract-xcm/Cargo.toml +++ b/integration-tests/public/contract-xcm/Cargo.toml @@ -6,13 +6,10 @@ edition = "2024" publish = false [dependencies] -ink = { path = "../../../crates/ink", default-features = false, features = ["unstable-hostfn"] } -frame-support = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } -pallet-balances = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } +ink = { path = "../../../crates/ink", default-features = false, features = ["xcm"] } [dev-dependencies] ink_e2e = { path = "../../../crates/e2e" } -ink_sandbox = { path = "../../../crates/sandbox" } [lib] path = "lib.rs" @@ -21,8 +18,6 @@ path = "lib.rs" default = ["std"] std = [ "ink/std", - "pallet-balances/std", - "frame-support/std", ] ink-as-dependency = [] e2e-tests = [] diff --git a/integration-tests/public/contract-xcm/lib.rs b/integration-tests/public/contract-xcm/lib.rs new file mode 100644 index 00000000000..959f7fed906 --- /dev/null +++ b/integration-tests/public/contract-xcm/lib.rs @@ -0,0 +1,247 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod contract_xcm { + use ink::xcm::prelude::*; + + /// A contract demonstrating usage of the XCM API. + #[ink(storage)] + #[derive(Default)] + pub struct ContractXcm; + + #[derive(Debug, PartialEq, Eq)] + #[ink::scale_derive(Encode, Decode, TypeInfo)] + pub enum RuntimeError { + XcmExecuteFailed, + XcmSendFailed, + } + + impl ContractXcm { + /// The constructor is `payable`, so that during instantiation it can be given + /// some tokens that will be further transferred when transferring funds through + /// XCM. + #[ink(constructor, payable)] + pub fn new() -> Self { + Default::default() + } + + /// Tries to transfer `value` from the contract's balance to `receiver`. + /// + /// Fails if: + /// - called in the off-chain environment + /// - the chain is not configured to support XCM + /// - the XCM program executed failed (e.g contract doesn't have enough balance) + #[ink(message)] + pub fn transfer_through_xcm( + &mut self, + receiver: AccountId, + value: Balance, + ) -> Result<(), RuntimeError> { + let asset: Asset = (Parent, value).into(); + let beneficiary = AccountId32 { + network: None, + id: *receiver.as_ref(), + }; + + let message: ink::xcm::v5::Xcm<()> = Xcm::builder() + .withdraw_asset(asset.clone()) + .buy_execution(asset.clone(), Unlimited) + .deposit_asset(asset, beneficiary) + .build(); + let msg = VersionedXcm::V5(message); + + let weight = self.env().xcm_weigh(&msg).expect("weight should work"); + + self.env() + .xcm_execute(&msg, weight) + .map_err(|_| RuntimeError::XcmExecuteFailed) + } + + /// Transfer some funds on the relay chain via XCM from the contract's derivative + /// account to the caller's account. + /// + /// Fails if: + /// - called in the off-chain environment + /// - the chain is not configured to support XCM + /// - the XCM program executed failed (e.g. contract doesn't have enough balance) + #[ink(message)] + pub fn send_funds( + &mut self, + value: Balance, + fee: Balance, + ) -> Result<(), RuntimeError> { + let destination: ink::xcm::v5::Location = ink::xcm::v5::Parent.into(); + let asset: Asset = (Here, value).into(); + let caller_account_id = self.env().to_account_id(self.env().caller()); + let beneficiary = AccountId32 { + network: None, + id: caller_account_id.0, + }; + + let message: Xcm<()> = Xcm::builder() + .withdraw_asset(asset.clone()) + .buy_execution((Here, fee), WeightLimit::Unlimited) + .deposit_asset(asset, beneficiary) + .build(); + + self.env() + .xcm_send( + &VersionedLocation::V5(destination), + &VersionedXcm::V5(message), + ) + .map_err(|_| RuntimeError::XcmSendFailed) + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + use super::*; + use ink::primitives::AccountId; + use ink_e2e::{ + ChainBackend, + ContractsBackend, + }; + + type E2EResult = Result>; + + #[ink_e2e::test] + async fn xcm_execute_works( + mut client: Client, + ) -> E2EResult<()> { + // given + let mut constructor = ContractXcmRef::new(); + let contract = client + .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) + .value(100_000_000_000) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let receiver = AccountId::from(ink_e2e::bob().public_key().0); + + let contract_balance_before = client + .free_balance(contract.account_id) + .await + .expect("Failed to get account balance"); + let receiver_balance_before = client + .free_balance(receiver) + .await + .expect("Failed to get account balance"); + + // when + let amount = 100_000_000; + let transfer_message = call_builder.transfer_through_xcm(receiver, amount); + let call_res = client + .call(&ink_e2e::alice(), &transfer_message) + .submit() + .await + .expect("call failed"); + assert!(call_res.return_value().is_ok()); + + // then + let contract_balance_after = client + .free_balance(contract.account_id) + .await + .expect("Failed to get account balance"); + let receiver_balance_after = client + .free_balance(receiver) + .await + .expect("Failed to get account balance"); + + assert_eq!(contract_balance_after, contract_balance_before - amount); + assert_eq!(receiver_balance_after, receiver_balance_before + amount); + + Ok(()) + } + + #[ink_e2e::test] + async fn xcm_execute_failure_detection_works( + mut client: Client, + ) -> E2EResult<()> { + // todo @cmichi: This sleep is necessary until we have our `ink-node` + // support a parachain/relaychain setup. For the moment we use the + // Rococo runtime for testing the examples locally. That runtime + // only has Alice and Bob endowed. Due to the nature of the tests + // we have to use Alice for sending the transactions. If the tests + // run at the same time, we'll get an error because the nonce + // of Alice is the same for all transactions. + std::thread::sleep(std::time::Duration::from_secs(10)); + + // given + let mut constructor = ContractXcmRef::new(); + let contract = client + .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + // when + let receiver = AccountId::from(ink_e2e::bob().public_key().0); + let amount = u128::MAX; + let transfer_message = call_builder.transfer_through_xcm(receiver, amount); + + // then + let call_res = client + .call(&ink_e2e::alice(), &transfer_message) + .submit() + .await; + assert!(call_res.is_err()); + + let expected = "revert: XCM execute failed: message may be invalid or execution constraints not satisfied"; + assert!(format!("{:?}", call_res).contains(expected)); + + Ok(()) + } + + #[ink_e2e::test] + async fn xcm_send_works(mut client: Client) -> E2EResult<()> { + // todo @cmichi: This sleep is necessary until we have our `ink-node` + // support a parachain/relaychain setup. For the moment we use the + // Rococo runtime for testing the examples locally. That runtime + // only has Alice and Bob endowed. Due to the nature of the tests + // we have to use Alice for sending the transactions. If the tests + // run at the same time, we'll get an error because the nonce + // of Alice is the same for all transactions. + std::thread::sleep(std::time::Duration::from_secs(30)); + + // given + let mut constructor = ContractXcmRef::new(); + let contract = client + .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) + .value(100_000_000_000) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let contract_balance_before = client + .free_balance(contract.account_id) + .await + .expect("Failed to get account balance"); + + // when + let amount = 100_000_000; + let transfer_message = call_builder.send_funds(amount, amount / 2); + let call_res = client + .call(&ink_e2e::alice(), &transfer_message) + .submit() + .await + .expect("call failed"); + assert!(call_res.return_value().is_ok()); + + // then + let contract_balance_after = client + .free_balance(contract.account_id) + .await + .expect("Failed to get account balance"); + + assert!( + contract_balance_after <= contract_balance_before - amount - (amount / 2) + ); + + Ok(()) + } + } +}