From 3231d1f864de853e18d4dc4650c0baf822563e64 Mon Sep 17 00:00:00 2001 From: Diego Date: Thu, 2 Jan 2025 12:50:20 -0300 Subject: [PATCH] Add xcm upm mock endpoint and tests (#792) * Accept tokens from other parachains * Remove waived locations * xcm tests constants for dancelight * Configure dancelight xcm mocknets * Dancelight xcm test working * toml maid * Extract Parse and its impl for location * Rust fmt * Toml fmt * Extract NativeAssetReserve from xcm config to commons * Add xcm upm mock endpoint * Xcm mock working * Remove unnecesary changes * Add xcm upm mock endpoint * Xcm mock working * Remove unnecesary changes * Remove unnecesary logs * Revert moonwall config changes * Prettify * add test for receiving a ump message * Fix tests * Improve xcm ump dancelight test * Clippy is not complaining anymore :D * Fix typo * Fix merge * Fix merge issues * Fix merge issues * Fix merge issues * Fix merge issues * Missing fixes * Format * Improve the way we handle upward messages * Remove unnecesary clone * Improve dev_rpc_data type --------- Co-authored-by: Agusrodri --- node/src/service.rs | 1 - .../node/tanssi-relay-service/src/dev_rpcs.rs | 43 +++++++++ .../tanssi-relay-service/src/dev_service.rs | 55 +++++++---- solo-chains/runtime/dancelight/src/lib.rs | 12 +++ .../xcm/test-xcm-send-upward.ts | 96 +++++++++++++++++++ test/util/xcm.ts | 20 +++- .../interfaces/augment-api-runtime.ts | 15 ++- 7 files changed, 221 insertions(+), 21 deletions(-) create mode 100644 test/suites/dev-tanssi-relay/xcm/test-xcm-send-upward.ts diff --git a/node/src/service.rs b/node/src/service.rs index d0d5e90b6..75aa0cfec 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -901,7 +901,6 @@ pub fn start_dev_node( if parachain_config.role.is_authority() { let client = node_builder.client.clone(); let (downward_xcm_sender, downward_xcm_receiver) = flume::bounded::>(100); - let (hrmp_xcm_sender, hrmp_xcm_receiver) = flume::bounded::<(ParaId, Vec)>(100); // Create channels for mocked parachain candidates. let (mock_randomness_sender, mock_randomness_receiver) = diff --git a/solo-chains/node/tanssi-relay-service/src/dev_rpcs.rs b/solo-chains/node/tanssi-relay-service/src/dev_rpcs.rs index 264670ca8..937c1f036 100644 --- a/solo-chains/node/tanssi-relay-service/src/dev_rpcs.rs +++ b/solo-chains/node/tanssi-relay-service/src/dev_rpcs.rs @@ -26,6 +26,7 @@ use jsonrpsee::{ ErrorObjectOwned, }, }; +use xcm::latest::prelude::*; /// This RPC interface is used to provide methods in dev mode only #[rpc(server)] @@ -38,10 +39,15 @@ pub trait DevApi { /// Indicate the mock parachain candidate insertion to be disabled #[method(name = "mock_disableParaInherentCandidate")] async fn disable_para_inherent_candidate(&self) -> RpcResult<()>; + + #[method(name = "xcm_injectUpwardMessage")] + async fn inject_upward_message(&self, message: Vec) -> RpcResult<()>; } +#[derive(Clone)] pub struct DevRpc { pub mock_para_inherent_channel: flume::Sender>, + pub upward_message_channel: flume::Sender>, } #[jsonrpsee::core::async_trait] @@ -69,6 +75,43 @@ impl DevApiServer for DevRpc { Ok(()) } + + async fn inject_upward_message(&self, msg: Vec) -> RpcResult<()> { + let upward_message_channel = self.upward_message_channel.clone(); + // If no message is supplied, inject a default one. + let msg = if msg.is_empty() { + // Note: Sovereign account of the origin parachain must be funded before injecting the message. + xcm::VersionedXcm::<()>::V4(Xcm(vec![ + WithdrawAsset((Here, 10000000000000u128).into()), + BuyExecution { + fees: (Here, 10000000000000u128).into(), + weight_limit: Unlimited, + }, + DepositAsset { + assets: AllCounted(1).into(), + beneficiary: Location::new( + 0, + [AccountKey20 { + network: None, + key: hex_literal::hex!("f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac"), + }], + ), + }, + ])) + .encode() + } else { + msg + }; + + // Push the message to the shared channel where it will be queued up + // to be injected in to an upcoming block. + upward_message_channel + .send_async(msg) + .await + .map_err(|err| internal_err(err.to_string()))?; + + Ok(()) + } } // This bit cribbed from frontier. diff --git a/solo-chains/node/tanssi-relay-service/src/dev_service.rs b/solo-chains/node/tanssi-relay-service/src/dev_service.rs index 9638e61e8..403e4020e 100644 --- a/solo-chains/node/tanssi-relay-service/src/dev_service.rs +++ b/solo-chains/node/tanssi-relay-service/src/dev_service.rs @@ -43,6 +43,7 @@ use { polkadot_core_primitives::{AccountId, Balance, Block, Hash, Nonce}, polkadot_node_core_parachains_inherent::Error as InherentError, polkadot_overseer::Handle, + polkadot_parachain_primitives::primitives::UpwardMessages, polkadot_primitives::{ runtime_api::ParachainHost, BackedCandidate, CandidateCommitments, CandidateDescriptor, CollatorPair, CommittedCandidateReceipt, CompactStatement, EncodeAs, @@ -107,8 +108,8 @@ struct DevDeps { pub pool: Arc

, /// Manual seal command sink pub command_sink: Option>>, - /// Channels for dev rpcs - pub dev_rpc_data: Option>>, + /// Dev rpcs + pub dev_rpc: Option, } fn create_dev_rpc_extension( @@ -116,7 +117,7 @@ fn create_dev_rpc_extension( client, pool, command_sink: maybe_command_sink, - dev_rpc_data: maybe_dev_rpc_data, + dev_rpc: maybe_dev_rpc, }: DevDeps, ) -> Result> where @@ -145,13 +146,8 @@ where io.merge(ManualSeal::new(command_sink).into_rpc())?; } - if let Some(mock_para_inherent_channel) = maybe_dev_rpc_data { - io.merge( - DevRpc { - mock_para_inherent_channel, - } - .into_rpc(), - )?; + if let Some(dev_rpc_data) = maybe_dev_rpc { + io.merge(dev_rpc_data.into_rpc())?; } Ok(io) @@ -226,17 +222,25 @@ struct MockParachainsInherentDataProvider + ProvideRunti pub client: Arc, pub parent: Hash, pub keystore: KeystorePtr, + pub upward_messages_receiver: flume::Receiver>, } impl + ProvideRuntimeApi> MockParachainsInherentDataProvider where C::Api: ParachainHost, + C: AuxStore, { - pub fn new(client: Arc, parent: Hash, keystore: KeystorePtr) -> Self { + pub fn new( + client: Arc, + parent: Hash, + keystore: KeystorePtr, + upward_messages_receiver: flume::Receiver>, + ) -> Self { MockParachainsInherentDataProvider { client, parent, keystore, + upward_messages_receiver, } } @@ -244,6 +248,7 @@ where client: Arc, parent: Hash, keystore: KeystorePtr, + upward_messages_receiver: flume::Receiver>, ) -> Result { let parent_header = match client.header(parent) { Ok(Some(h)) => h, @@ -388,6 +393,12 @@ where &validation_code_hash, ); let collator_signature = collator_pair.sign(&payload); + + let upward_messages = UpwardMessages::try_from( + upward_messages_receiver.drain().collect::>(), + ) + .expect("create upward messages from raw messages"); + // generate a candidate with most of the values mocked let candidate = CommittedCandidateReceipt:: { descriptor: CandidateDescriptor:: { @@ -402,7 +413,7 @@ where validation_code_hash, }, commitments: CandidateCommitments:: { - upward_messages: Default::default(), + upward_messages, horizontal_messages: Default::default(), new_validation_code: None, head_data: parachain_mocked_header.clone().encode().into(), @@ -481,6 +492,7 @@ where self.client.clone(), self.parent, self.keystore.clone(), + self.upward_messages_receiver.clone(), ) .await .map_err(|e| sp_inherents::Error::Application(Box::new(e)))? @@ -594,6 +606,8 @@ fn new_full< let (downward_mock_para_inherent_sender, downward_mock_para_inherent_receiver) = flume::bounded::>(100); + let (upward_mock_sender, upward_mock_receiver) = flume::bounded::>(100); + let (network, system_rpc_tx, tx_handler_controller, network_starter, sync_service) = service::build_network(service::BuildNetworkParams { config: &config, @@ -705,12 +719,15 @@ fn new_full< let client_clone = client_clone.clone(); let keystore = keystore_clone.clone(); let downward_mock_para_inherent_receiver = downward_mock_para_inherent_receiver.clone(); + let upward_mock_receiver = upward_mock_receiver.clone(); async move { let downward_mock_para_inherent_receiver = downward_mock_para_inherent_receiver.clone(); // here we only take the last one let para_inherent_decider_messages: Vec> = downward_mock_para_inherent_receiver.drain().collect(); + let upward_messages_receiver = upward_mock_receiver.clone(); + // If there is a value to be updated, we update it if let Some(value) = para_inherent_decider_messages.last() { client_clone @@ -719,13 +736,13 @@ fn new_full< &[], ) .expect("Should be able to write to aux storage; qed"); - } let parachain = MockParachainsInherentDataProvider::new( client_clone.clone(), parent, - keystore + keystore, + upward_messages_receiver, ); let timestamp = get_next_timestamp(client_clone, slot_duration); @@ -744,9 +761,11 @@ fn new_full< ); } - // We dont need the flume receiver if we are not a validator - let dev_rpc_data = if role.clone().is_authority() { - Some(downward_mock_para_inherent_sender) + let dev_rpc = if role.clone().is_authority() { + Some(DevRpc { + mock_para_inherent_channel: downward_mock_para_inherent_sender, + upward_message_channel: upward_mock_sender, + }) } else { None }; @@ -761,7 +780,7 @@ fn new_full< client: client.clone(), pool: transaction_pool.clone(), command_sink: command_sink.clone(), - dev_rpc_data: dev_rpc_data.clone(), + dev_rpc: dev_rpc.clone(), }; create_dev_rpc_extension(deps).map_err(Into::into) diff --git a/solo-chains/runtime/dancelight/src/lib.rs b/solo-chains/runtime/dancelight/src/lib.rs index bf87423ed..7e5aea0f9 100644 --- a/solo-chains/runtime/dancelight/src/lib.rs +++ b/solo-chains/runtime/dancelight/src/lib.rs @@ -2265,6 +2265,18 @@ sp_api::impl_runtime_apis! { } } + impl xcm_runtime_apis::conversions::LocationToAccountApi for Runtime { + fn convert_location(location: VersionedLocation) -> Result< + AccountId, + xcm_runtime_apis::conversions::Error + > { + xcm_runtime_apis::conversions::LocationToAccountHelper::< + AccountId, + xcm_config::LocationConverter, + >::convert_location(location) + } + } + impl sp_api::Metadata for Runtime { fn metadata() -> OpaqueMetadata { OpaqueMetadata::new(Runtime::metadata().into()) diff --git a/test/suites/dev-tanssi-relay/xcm/test-xcm-send-upward.ts b/test/suites/dev-tanssi-relay/xcm/test-xcm-send-upward.ts new file mode 100644 index 000000000..9128cf6be --- /dev/null +++ b/test/suites/dev-tanssi-relay/xcm/test-xcm-send-upward.ts @@ -0,0 +1,96 @@ +import { beforeAll, customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; +import { generateKeyringPair, KeyringPair } from "@moonwall/util"; +import { ApiPromise, Keyring } from "@polkadot/api"; +import { u8aToHex } from "@polkadot/util"; +import { jumpToSession } from "util/block"; +import { injectUmpMessageAndSeal, RawXcmMessage, XcmFragment } from "../../../util/xcm"; + +describeSuite({ + id: "DTR1003", + title: "XCM - Succeeds sending XCM", + foundationMethods: "dev", + testCases: ({ context, it }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + let random: KeyringPair; + let transferredBalance; + + beforeAll(async function () { + polkadotJs = context.polkadotJs(); + alice = new Keyring({ type: "sr25519" }).addFromUri("//Alice", { + name: "Alice default", + }); + + random = generateKeyringPair("sr25519"); + + transferredBalance = 100_000_000_000_000_000n; + + const location = { + V3: { + parents: 0, + interior: { X1: { Parachain: 2000 } }, + }, + }; + + const locationToAccountResult = await polkadotJs.call.locationToAccountApi.convertLocation(location); + expect(locationToAccountResult.isOk); + + const convertedAddress = locationToAccountResult.asOk.toJSON(); + + let aliceNonce = (await polkadotJs.query.system.account(alice.address)).nonce.toNumber(); + + // Send some tokens to the sovereign account of para 2000 + const txSigned = polkadotJs.tx.balances.transferAllowDeath(convertedAddress, transferredBalance); + await context.createBlock(await txSigned.signAsync(alice, { nonce: aliceNonce++ }), { + allowFailures: false, + }); + + const balanceSigned = (await polkadotJs.query.system.account(convertedAddress)).data.free.toBigInt(); + expect(balanceSigned).to.eq(transferredBalance); + }); + + it({ + id: "T01", + title: "Should succeed receiving tokens", + test: async function () { + const balanceRandomBefore = ( + await polkadotJs.query.system.account(random.address) + ).data.free.toBigInt(); + expect(balanceRandomBefore).to.eq(0n); + + const xcmMessage = new XcmFragment({ + assets: [ + { + multilocation: { + parents: 0, + interior: { Here: null }, + }, + fungible: transferredBalance / 10n, + }, + ], + beneficiary: u8aToHex(random.addressRaw), + }) + .withdraw_asset() + .buy_execution() + .deposit_asset_v3() + .as_v3(); + + // Enable para inherent to process xcm message + await customDevRpcRequest("mock_enableParaInherentCandidate", []); + + // Send ump message + await injectUmpMessageAndSeal(context, { + type: "XcmVersionedXcm", + payload: xcmMessage, + } as RawXcmMessage); + + // Wait until message is processed + await jumpToSession(context, 3); + await context.createBlock(); + + const balanceRandomAfter = (await polkadotJs.query.system.account(random.address)).data.free.toBigInt(); + expect(Number(balanceRandomAfter)).to.be.greaterThan(0); + }, + }); + }, +}); diff --git a/test/util/xcm.ts b/test/util/xcm.ts index 2f49fe47c..593cd82d7 100644 --- a/test/util/xcm.ts +++ b/test/util/xcm.ts @@ -1,4 +1,5 @@ import { DevModeContext, customDevRpcRequest, expect } from "@moonwall/cli"; +import { ApiPromise } from "@polkadot/api"; import { XcmpMessageFormat } from "@polkadot/types/interfaces"; import { CumulusPalletParachainSystemRelayStateSnapshotMessagingStateSnapshot, @@ -7,7 +8,6 @@ import { } from "@polkadot/types/lookup"; import { BN, hexToU8a, stringToU8a, u8aToHex } from "@polkadot/util"; import { xxhashAsU8a } from "@polkadot/util-crypto"; -import { ApiPromise } from "@polkadot/api"; // Creates and returns the tx that overrides the paraHRMP existence // This needs to be inserted at every block in which you are willing to test @@ -192,6 +192,12 @@ export function buildDmpMessage(context: DevModeContext, message: RawXcmMessage) return [...receivedMessage.toU8a()]; } +export function buildUmpMessage(context: DevModeContext, message: RawXcmMessage): number[] { + const receivedMessage: XcmVersionedXcm = context.polkadotJs().createType("XcmVersionedXcm", message.payload) as any; + + return [...receivedMessage.toU8a()]; +} + export async function injectHrmpMessage(context: DevModeContext, paraId: number, message?: RawXcmMessage) { const totalMessage = message != null ? buildXcmpMessage(context, message) : []; // Send RPC call to inject XCM message @@ -204,6 +210,12 @@ export async function injectDmpMessage(context: DevModeContext, message?: RawXcm await customDevRpcRequest("xcm_injectDownwardMessage", [totalMessage]); } +export async function injectUmpMessage(context: DevModeContext, message?: RawXcmMessage) { + const totalMessage = message != null ? buildUmpMessage(context, message) : []; + // Send RPC call to inject XCM message + await customDevRpcRequest("xcm_injectUpwardMessage", [totalMessage]); +} + // Weight a particular message using the xcm utils precompile export async function weightMessage(context: DevModeContext, message: XcmVersionedXcm) { return (await context.readPrecompile!({ @@ -225,6 +237,12 @@ export async function injectDmpMessageAndSeal(context: DevModeContext, message?: await context.createBlock(); } +export async function injectUmpMessageAndSeal(context: DevModeContext, message?: RawXcmMessage) { + await injectUmpMessage(context, message); + // Create a block in which the XCM will be executed + await context.createBlock(); +} + interface Junction { Parachain?: number; AccountId32?: { diff --git a/typescript-api/src/dancelight/interfaces/augment-api-runtime.ts b/typescript-api/src/dancelight/interfaces/augment-api-runtime.ts index 460286f05..63f8de4ae 100644 --- a/typescript-api/src/dancelight/interfaces/augment-api-runtime.ts +++ b/typescript-api/src/dancelight/interfaces/augment-api-runtime.ts @@ -74,7 +74,8 @@ import type { RuntimeVersion } from "@polkadot/types/interfaces/state"; import type { ApplyExtrinsicResult } from "@polkadot/types/interfaces/system"; import type { TransactionSource, TransactionValidity } from "@polkadot/types/interfaces/txqueue"; import type { XcmPaymentApiError } from "@polkadot/types/interfaces/xcmPaymentApi"; -import type { XcmVersionedAssetId, XcmVersionedXcm } from "@polkadot/types/lookup"; +import type { Error } from "@polkadot/types/interfaces/xcmRuntimeApi"; +import type { XcmVersionedAssetId, XcmVersionedLocation, XcmVersionedXcm } from "@polkadot/types/lookup"; import type { IExtrinsic, Observable } from "@polkadot/types/types"; export type __AugmentedCall = AugmentedCall; @@ -230,6 +231,18 @@ declare module "@polkadot/api-base/types/calls" { /** Generic call */ [key: string]: DecoratedCallBase; }; + /** 0x9ffb505aa738d69c/1 */ + locationToAccountApi: { + /** Converts `Location` to `AccountId` */ + convertLocation: AugmentedCall< + ApiType, + ( + location: XcmVersionedLocation | { V2: any } | { V3: any } | { V4: any } | string | Uint8Array + ) => Observable> + >; + /** Generic call */ + [key: string]: DecoratedCallBase; + }; /** 0x37e397fc7c91f5e4/2 */ metadata: { /** Returns the metadata of a runtime */