From aaf0f136950aed95a8fb9fc12917683bc656c01f Mon Sep 17 00:00:00 2001 From: Ermal Kaleci Date: Tue, 27 Dec 2022 21:58:27 +0100 Subject: [PATCH 1/6] xcm setup multichain --- README.md | 5 + e2e/__snapshots__/xcm.test.ts.snap | 672 ++++++++++++++++++ e2e/helper.ts | 11 +- e2e/xcm.test.ts | 102 +++ src/blockchain/head-state.ts | 8 +- src/blockchain/index.ts | 6 +- src/blockchain/inherent/index.ts | 9 +- .../inherent/parachain/validation-data.ts | 128 +++- src/blockchain/txpool.ts | 25 +- src/index.ts | 61 +- src/server.ts | 42 +- src/setup-with-server.ts | 8 +- src/setup.ts | 2 +- src/utils/index.ts | 11 +- src/utils/proof.ts | 19 +- src/xcm/index.ts | 37 + 16 files changed, 1073 insertions(+), 73 deletions(-) create mode 100644 e2e/__snapshots__/xcm.test.ts.snap create mode 100644 e2e/xcm.test.ts create mode 100644 src/xcm/index.ts diff --git a/README.md b/README.md index 655dd8bb..2306b4d6 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,8 @@ Make sure you have setup Rust environment (>= 1.64). - Run Kusama fork - Edit configs/kusama.yml if needed. (e.g. update the block number) - `yarn start dev --config=configs/kusama.yml` + +- Setup XCM multichain +```bash +yarn start xcm --relaychain=configs/kusama.yml --parachain=configs/acala.yml --parachain=configs/karura.yml +``` diff --git a/e2e/__snapshots__/xcm.test.ts.snap b/e2e/__snapshots__/xcm.test.ts.snap new file mode 100644 index 00000000..a9f5d569 --- /dev/null +++ b/e2e/__snapshots__/xcm.test.ts.snap @@ -0,0 +1,672 @@ +// Vitest Snapshot v1 + +exports[`XCM > Acala handles downward messages 1`] = ` +[ + { + "event": { + "data": { + "dispatchInfo": { + "class": "Mandatory", + "paysFee": "Yes", + "weight": "158,080,000", + }, + }, + "index": "0x0000", + "method": "ExtrinsicSuccess", + "section": "system", + }, + "phase": { + "ApplyExtrinsic": "0", + }, + "topics": [], + }, + { + "event": { + "data": { + "count": "1", + }, + "index": "0x1e04", + "method": "DownwardMessagesReceived", + "section": "parachainSystem", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "246,129,953,122", + "currencyId": { + "Token": "DOT", + }, + "who": "25EPNyxpdqMUeVj9WndwUYVKkfFkMLroo2fg9V1MA79PJ6iE", + }, + "index": "0x0b0a", + "method": "Deposited", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "account": "25EPNyxpdqMUeVj9WndwUYVKkfFkMLroo2fg9V1MA79PJ6iE", + }, + "index": "0x0003", + "method": "NewAccount", + "section": "system", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "246,129,953,122", + "currencyId": { + "Token": "DOT", + }, + "who": "25EPNyxpdqMUeVj9WndwUYVKkfFkMLroo2fg9V1MA79PJ6iE", + }, + "index": "0x0b00", + "method": "Endowed", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "2,771,999", + "currencyId": { + "Token": "DOT", + }, + "who": "23M5ttkmR6KcoTAAE6gcmibnKFtVaTP5yxnY8HF1BmrJ2A1i", + }, + "index": "0x0b0a", + "method": "Deposited", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "messageId": "0xa004205cd3559f06413e92aa4bd5a0f4a12eea722da2bf27c52447f69fa7b660", + "outcome": { + "Complete": "800,000,000", + }, + }, + "index": "0x3502", + "method": "ExecutedDownward", + "section": "dmpQueue", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "dmqHead": "0x43bc1b7d0a62cc18a3ac5218f8076cfce521669761c7cc68761a4094167ab974", + "weightUsed": "800,000,000", + }, + "index": "0x1e05", + "method": "DownwardMessagesProcessed", + "section": "parachainSystem", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "dispatchInfo": { + "class": "Mandatory", + "paysFee": "Yes", + "weight": "0", + }, + }, + "index": "0x0000", + "method": "ExtrinsicSuccess", + "section": "system", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, +] +`; + +exports[`XCM > Acala handles horizonal messages 1`] = ` +[ + { + "event": { + "data": { + "dispatchInfo": { + "class": "Mandatory", + "paysFee": "Yes", + "weight": "158,080,000", + }, + }, + "index": "0x0000", + "method": "ExtrinsicSuccess", + "section": "system", + }, + "phase": { + "ApplyExtrinsic": "0", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "9,111,469,011,754,434", + "currencyId": { + "Token": "AUSD", + }, + "who": "23UvQ3ZQXJ5LfTUSYkcRPkQX2FHgcKxGmqdxYJe9j5e3Lwsi", + }, + "index": "0x0b08", + "method": "Withdrawn", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "9,111,466,917,717,450", + "currencyId": { + "Token": "AUSD", + }, + "who": "2561WgnRM2Nbec2WseHxVdpeW19YYHh7RM5gRWQV7zwwwgXQ", + }, + "index": "0x0b0a", + "method": "Deposited", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "account": "2561WgnRM2Nbec2WseHxVdpeW19YYHh7RM5gRWQV7zwwwgXQ", + }, + "index": "0x0003", + "method": "NewAccount", + "section": "system", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "9,111,466,917,717,450", + "currencyId": { + "Token": "AUSD", + }, + "who": "2561WgnRM2Nbec2WseHxVdpeW19YYHh7RM5gRWQV7zwwwgXQ", + }, + "index": "0x0b00", + "method": "Endowed", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "2,094,036,984", + "currencyId": { + "Token": "AUSD", + }, + "who": "23M5ttkmR6KcoTAAE6gcmibnKFtVaTP5yxnY8HF1BmrJ2A1i", + }, + "index": "0x0b0a", + "method": "Deposited", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "messageHash": "0x09e1b5b4cd5b9ab2cb31449175dc5504e4314db308d38de8dfd8d6529e481b79", + "weight": "800,000,000", + }, + "index": "0x3200", + "method": "Success", + "section": "xcmpQueue", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "dispatchInfo": { + "class": "Mandatory", + "paysFee": "Yes", + "weight": "0", + }, + }, + "index": "0x0000", + "method": "ExtrinsicSuccess", + "section": "system", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, +] +`; + +exports[`XCM > Polkadot send downward messages to Acala 1`] = ` +[ + { + "event": { + "data": { + "dispatchInfo": { + "class": "Mandatory", + "paysFee": "Yes", + "weight": "218,918,000", + }, + }, + "index": "0x0000", + "method": "ExtrinsicSuccess", + "section": "system", + }, + "phase": { + "ApplyExtrinsic": "0", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "184,908,979", + "who": "146SvjUZXoMaemdeiecyxgALeYMm8ZWh1yrGo8RtpoPfe7WL", + }, + "index": "0x0508", + "method": "Withdraw", + "section": "balances", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "1,000,000,000,000", + "from": "146SvjUZXoMaemdeiecyxgALeYMm8ZWh1yrGo8RtpoPfe7WL", + "to": "13YMK2eYoAvStnzReuxBjMrAvPXmmdsURwZvc62PrdXimbNy", + }, + "index": "0x0502", + "method": "Transfer", + "section": "balances", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": [ + { + "Complete": "1,000,000,000", + }, + ], + "index": "0x6300", + "method": "Attempted", + "section": "xcmPallet", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "147,927,183", + "who": "13UVJyLnbVp9RBZYFwFGyDvVd1y27Tt8tkntv6Q7JVPhFsTB", + }, + "index": "0x0507", + "method": "Deposit", + "section": "balances", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "value": "147,927,183", + }, + "index": "0x1306", + "method": "Deposit", + "section": "treasury", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "36,981,796", + "who": "12HFymxpDmi4XXPHaEMp74CNpRhkqwG5qxnrgikkhon1XMrj", + }, + "index": "0x0507", + "method": "Deposit", + "section": "balances", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "actualFee": "184,908,979", + "tip": "0", + "who": "146SvjUZXoMaemdeiecyxgALeYMm8ZWh1yrGo8RtpoPfe7WL", + }, + "index": "0x2000", + "method": "TransactionFeePaid", + "section": "transactionPayment", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "dispatchInfo": { + "class": "Normal", + "paysFee": "Yes", + "weight": "1,185,212,000", + }, + }, + "index": "0x0000", + "method": "ExtrinsicSuccess", + "section": "system", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, +] +`; + +exports[`XCM > Polkadot send downward messages to Acala 2`] = ` +[ + { + "event": { + "data": { + "dispatchInfo": { + "class": "Mandatory", + "paysFee": "Yes", + "weight": "158,080,000", + }, + }, + "index": "0x0000", + "method": "ExtrinsicSuccess", + "section": "system", + }, + "phase": { + "ApplyExtrinsic": "0", + }, + "topics": [], + }, + { + "event": { + "data": { + "count": "2", + }, + "index": "0x1e04", + "method": "DownwardMessagesReceived", + "section": "parachainSystem", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "235,975,337,226", + "currencyId": { + "Token": "DOT", + }, + "who": "23e3iqWXzeCN5H3aJzDmqk2bz1mZ9pRG94PM1NJdfBaMj6ZR", + }, + "index": "0x0b0a", + "method": "Deposited", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "235,975,337,226", + "currencyId": { + "Token": "DOT", + }, + "who": "23e3iqWXzeCN5H3aJzDmqk2bz1mZ9pRG94PM1NJdfBaMj6ZR", + }, + "index": "0x0b00", + "method": "Endowed", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "2,771,999", + "currencyId": { + "Token": "DOT", + }, + "who": "23M5ttkmR6KcoTAAE6gcmibnKFtVaTP5yxnY8HF1BmrJ2A1i", + }, + "index": "0x0b0a", + "method": "Deposited", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "messageId": "0x8088b1e1a4d66511b52e8a8f6155e42241ea1818151625a924852c814d9aa265", + "outcome": { + "Complete": "800,000,000", + }, + }, + "index": "0x3502", + "method": "ExecutedDownward", + "section": "dmpQueue", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "999,997,228,001", + "currencyId": { + "Token": "DOT", + }, + "who": "23y3WetbNi6rDMgHmyRDjgpb7PnhgPotuPPawxruTMLYTLzG", + }, + "index": "0x0b0a", + "method": "Deposited", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "account": "23y3WetbNi6rDMgHmyRDjgpb7PnhgPotuPPawxruTMLYTLzG", + }, + "index": "0x0003", + "method": "NewAccount", + "section": "system", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "999,997,228,001", + "currencyId": { + "Token": "DOT", + }, + "who": "23y3WetbNi6rDMgHmyRDjgpb7PnhgPotuPPawxruTMLYTLzG", + }, + "index": "0x0b00", + "method": "Endowed", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "amount": "2,771,999", + "currencyId": { + "Token": "DOT", + }, + "who": "23M5ttkmR6KcoTAAE6gcmibnKFtVaTP5yxnY8HF1BmrJ2A1i", + }, + "index": "0x0b0a", + "method": "Deposited", + "section": "tokens", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "messageId": "0xc4dd612e0a23c9e0d3c677adbfb3bca299caa4c37c2085baa5d7a33736d19e80", + "outcome": { + "Complete": "800,000,000", + }, + }, + "index": "0x3502", + "method": "ExecutedDownward", + "section": "dmpQueue", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "dmqHead": "0xd6308f1fd114709106d34c2cecce1bdd2fd9fec73898e25ccd5c62f84a96f8bd", + "weightUsed": "1,600,000,000", + }, + "index": "0x1e05", + "method": "DownwardMessagesProcessed", + "section": "parachainSystem", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, + { + "event": { + "data": { + "dispatchInfo": { + "class": "Mandatory", + "paysFee": "Yes", + "weight": "0", + }, + }, + "index": "0x0000", + "method": "ExtrinsicSuccess", + "section": "system", + }, + "phase": { + "ApplyExtrinsic": "1", + }, + "topics": [], + }, +] +`; diff --git a/e2e/helper.ts b/e2e/helper.ts index 9a4b7aa3..fa9559a7 100644 --- a/e2e/helper.ts +++ b/e2e/helper.ts @@ -65,10 +65,9 @@ export const setupAll = async ({ allowUnresolvedImports, }) - const { port: listeningPortPromise, close } = createServer(handler({ chain })) - const listeningPort = await listeningPortPromise + const { port, close } = await createServer(handler({ chain })) - const ws = new WsProvider(`ws://localhost:${listeningPort}`) + const ws = new WsProvider(`ws://localhost:${port}`) const apiPromise = await ApiPromise.create({ provider: ws, signedExtensions: { @@ -132,6 +131,12 @@ export const expectHex = (codec: CodecOrArray | Promise) => { return expect(Promise.resolve(codec).then((x) => (Array.isArray(x) ? x.map((x) => x.toHex()) : x.toHex()))).resolves } +export const matchSnapshot = (codec: CodecOrArray | Promise) => { + return expect( + Promise.resolve(codec).then((x) => (Array.isArray(x) ? x.map((x) => x.toHuman()) : x.toHuman())) + ).resolves.toMatchSnapshot() +} + export const dev = { newBlock: (param?: { count?: number; to?: number }): Promise => { return ws.send('dev_newBlock', [param]) diff --git a/e2e/xcm.test.ts b/e2e/xcm.test.ts new file mode 100644 index 00000000..09f393d6 --- /dev/null +++ b/e2e/xcm.test.ts @@ -0,0 +1,102 @@ +import { afterAll, describe, it } from 'vitest' + +import { DownwardMessage, HorizontalMessage } from '../src/blockchain/txpool' +import { connectDownward } from '../src/xcm' +import { matchSnapshot, setupAll, testingPairs } from './helper' +import { setStorage } from '../src/utils/set-storage' + +const downwardMessages: DownwardMessage[] = [ + { + sentAt: 1, + msg: '0x0210010400010000078155a74e390a1300010000078155a74e39010300286bee0d01000400010100c0cbffafddbe39f71f0190c2369adfc59eaa4c81a308ebcad88cdd9c400ba57c', + }, +] + +const horizontalMessages: Record = { + 2004: [ + { + data: '0x000210000400000106080001000fc2ddd331d55e200a1300000106080001000fc2ddd331d55e20010700f2052a010d01000400010100ba686c8fa59178c699a698ea4d8e2c595394c2594bce4b6c2ca3a9bf3018e25d', + sentAt: 13509121, + }, + ], +} + +describe('XCM', async () => { + const ctxAcala = await setupAll({ + endpoint: 'wss://acala-rpc-1.aca-api.network', + blockHash: '0x663c25dc86521f4b7f74dcbc26224bb0fac40e316e6b0bcf6a51de373f37afac', + }) + + const ctxPolkadot = await setupAll({ + endpoint: 'wss://rpc.polkadot.io', + blockHash: '0x0a26b277b252fc61efcda02e44e95c73bf7ae21233bacb2d3bd7631212350d59', + }) + + afterAll(async () => { + await ctxAcala.teardownAll() + await ctxPolkadot.teardownAll() + }) + + it('Acala handles downward messages', async () => { + const { chain, api, teardown } = await ctxAcala.setup() + await chain.newBlock({ inherent: { downwardMessages } }) + await matchSnapshot(api.query.system.events()) + await teardown() + }) + + it('Acala handles horizonal messages', async () => { + const { chain, api, teardown } = await ctxAcala.setup() + await chain.newBlock({ inherent: { horizontalMessages } }) + await matchSnapshot(api.query.system.events()) + await teardown() + }) + + it('Polkadot send downward messages to Acala', async () => { + const polkadot = await ctxPolkadot.setup() + const acala = await ctxAcala.setup() + + await connectDownward(polkadot.chain, acala.chain) + + const { alice } = testingPairs() + + await setStorage(polkadot.chain, { + System: { + Account: [[[alice.address], { data: { free: 1000 * 1e10 } }]], + }, + }) + + await polkadot.api.tx.xcmPallet + .reserveTransferAssets( + { V0: { X1: { Parachain: 2000 } } }, + { + V0: { + X1: { + AccountId32: { + network: 'Any', + id: alice.addressRaw, + }, + }, + }, + }, + { + V0: [ + { + ConcreteFungible: { id: 'Null', amount: 100e10 }, + }, + ], + }, + 0 + ) + .signAndSend(alice) + + await polkadot.chain.newBlock() + await matchSnapshot(polkadot.api.query.system.events()) + + // wait for 10 secs for new block to built + await new Promise((resolve) => setTimeout(resolve, 10000)) + await matchSnapshot(acala.api.query.system.events()) + + await polkadot.teardown() + await acala.teardown() + }) +}) diff --git a/src/blockchain/head-state.ts b/src/blockchain/head-state.ts index aa560321..25e67836 100644 --- a/src/blockchain/head-state.ts +++ b/src/blockchain/head-state.ts @@ -3,11 +3,13 @@ import _ from 'lodash' import { Block } from './block' +type Callback = (block: Block, pairs: [string, string][]) => void | Promise + export const randomId = () => Math.random().toString(36).substring(2) export class HeadState { #headListeners: Record void> = {} - #storageListeners: Record void]> = {} + #storageListeners: Record = {} #oldValues: Record = {} #head: Block @@ -26,7 +28,7 @@ export class HeadState { delete this.#headListeners[id] } - async subscribeStorage(keys: string[], cb: (block: Block, pairs: [string, string][]) => void) { + async subscribeStorage(keys: string[], cb: Callback) { const id = randomId() this.#storageListeners[id] = [keys, cb] @@ -65,7 +67,7 @@ export class HeadState { for (const [keys, cb] of Object.values(this.#storageListeners)) { const changed = keys.filter((key) => diff[key]).map((key) => [key, diff[key]] as [string, string]) if (changed.length > 0) { - cb(head, changed) + await cb(head, changed) } } diff --git a/src/blockchain/index.ts b/src/blockchain/index.ts index 5dac1a09..ec5e1837 100644 --- a/src/blockchain/index.ts +++ b/src/blockchain/index.ts @@ -7,7 +7,7 @@ import type { TransactionValidity } from '@polkadot/types/interfaces/txqueue' import { Api } from '../api' import { Block } from './block' -import { BuildBlockMode, TxPool } from './txpool' +import { BuildBlockMode, BuildBlockParams, TxPool } from './txpool' import { HeadState } from './head-state' import { InherentProvider } from './inherent' import { ResponseError } from '../rpc/shared' @@ -156,8 +156,8 @@ export class Blockchain { throw new ResponseError(1, `Extrinsic is invalid: ${validity.asErr.toString()}`) } - async newBlock(): Promise { - await this.#txpool.buildBlock() + async newBlock(params?: BuildBlockParams): Promise { + await this.#txpool.buildBlock(params) return this.#head } } diff --git a/src/blockchain/inherent/index.ts b/src/blockchain/inherent/index.ts index fa9d420e..4ca18511 100644 --- a/src/blockchain/inherent/index.ts +++ b/src/blockchain/inherent/index.ts @@ -1,4 +1,5 @@ import { Block } from '../block' +import { BuildBlockParams } from '../txpool' import { GenericExtrinsic } from '@polkadot/types' import { HexString } from '@polkadot/util/types' import { getCurrentTimestamp, getSlotDuration } from '../../utils/time-travel' @@ -6,7 +7,7 @@ import { getCurrentTimestamp, getSlotDuration } from '../../utils/time-travel' export { SetValidationData } from './parachain/validation-data' export interface CreateInherents { - createInherents(parent: Block): Promise + createInherents(parent: Block, params?: BuildBlockParams['inherent']): Promise } export type InherentProvider = CreateInherents @@ -29,9 +30,9 @@ export class InherentProviders implements InherentProvider { this.#providers = providers } - async createInherents(parent: Block): Promise { - const base = await this.#base.createInherents(parent) - const extra = await Promise.all(this.#providers.map((provider) => provider.createInherents(parent))) + async createInherents(parent: Block, params?: BuildBlockParams['inherent']): Promise { + const base = await this.#base.createInherents(parent, params) + const extra = await Promise.all(this.#providers.map((provider) => provider.createInherents(parent, params))) return [...base, ...extra.flat()] } } diff --git a/src/blockchain/inherent/parachain/validation-data.ts b/src/blockchain/inherent/parachain/validation-data.ts index 6c5f4bf3..9740e30b 100644 --- a/src/blockchain/inherent/parachain/validation-data.ts +++ b/src/blockchain/inherent/parachain/validation-data.ts @@ -1,12 +1,14 @@ import { GenericExtrinsic } from '@polkadot/types' import { HexString } from '@polkadot/util/types' -import { hexToU8a } from '@polkadot/util' +import { hexToU8a, u8aConcat } from '@polkadot/util' import { Block } from '../../../blockchain/block' +import { BuildBlockParams } from '../../txpool' import { CreateInherents } from '..' -import { compactHex } from '../../../utils' -import { createProof } from '../../../executor' -import { upgradeGoAheadSignal } from '../../../utils/proof' +import { blake2AsHex, blake2AsU8a } from '@polkadot/util-crypto' +import { compactHex, getParaId } from '../../../utils' +import { createProof, decodeProof } from '../../../executor' +import { dmqMqcHead, hrmpChannels, hrmpIngressChannelIndex, upgradeGoAheadSignal } from '../../../utils/proof' const MOCK_VALIDATION_DATA = { validationData: { @@ -33,8 +35,21 @@ const MOCK_VALIDATION_DATA = { }, } +export type ValidationData = { + downwardMessages: { sent_at: number; msg: HexString }[] + horizontalMessages: Record + validationData: { + relayParentNumber: number + relayParentStorageRoot: HexString + maxPovSize: number + } + relayChainState: { + trieNodes: HexString[] + } +} + export class SetValidationData implements CreateInherents { - async createInherents(parent: Block): Promise { + async createInherents(parent: Block, params?: BuildBlockParams['inherent']): Promise { const meta = await parent.meta if (!meta.tx.parachainSystem?.setValidationData) { return [] @@ -46,11 +61,11 @@ export class SetValidationData implements CreateInherents { } const extrinsics = await parentBlock.extrinsics - let newData: typeof MOCK_VALIDATION_DATA + let newData: ValidationData if (parentBlock.number === 0) { // chain started with genesis, mock 1st validationData - newData = MOCK_VALIDATION_DATA + newData = MOCK_VALIDATION_DATA as ValidationData } else { const validationDataExtrinsic = extrinsics.find((extrinsic) => { const firstArg = meta.registry.createType('GenericExtrinsic', extrinsic)?.args?.[0] @@ -61,13 +76,98 @@ export class SetValidationData implements CreateInherents { } const extrinsic = meta.registry .createType('GenericExtrinsic', validationDataExtrinsic) - .args[0].toJSON() as typeof MOCK_VALIDATION_DATA + .args[0].toJSON() as ValidationData const newEntries: [HexString, HexString | null][] = [] - const pendingUpgrade = await parentBlock.get(compactHex(meta.query.parachainSystem.pendingValidationCode())) - const paraIdStorage = await parentBlock.get(compactHex(meta.query.parachainInfo.parachainId())) - const paraId = meta.registry.createType('u32', hexToU8a(paraIdStorage as any)) + const downwardMessages: { msg: HexString; sent_at: number }[] = [] + const horizontalMessages: Record = {} + + const paraId = await getParaId(parentBlock.chain) + + const dmqMqcHeadKey = dmqMqcHead(paraId) + const hrmpIngressChannelIndexKey = hrmpIngressChannelIndex(paraId) + + const decoded = await decodeProof( + extrinsic.validationData.relayParentStorageRoot, + [dmqMqcHeadKey, hrmpIngressChannelIndexKey], + extrinsic.relayChainState.trieNodes + ) + + // inject downward messages + if (params?.downwardMessages) { + let dmqMqcHeadHash = decoded[dmqMqcHeadKey] + if (!dmqMqcHeadHash) throw new Error('Canoot find dmqMqcHead from validation data') + + for (const { msg, sentAt } of params.downwardMessages) { + // calculate new hash + dmqMqcHeadHash = blake2AsHex( + u8aConcat( + meta.registry.createType('Hash', dmqMqcHeadHash).toU8a(), + meta.registry.createType('BlockNumber', sentAt).toU8a(), + blake2AsU8a(meta.registry.createType('Bytes', msg).toU8a(), 256) + ), + 256 + ) + + downwardMessages.push({ + msg, + sent_at: sentAt, + }) + } + newEntries.push([dmqMqcHeadKey, dmqMqcHeadHash]) + } + + const hrmpIngressChannels = meta.registry + .createType('Vec', decoded[hrmpIngressChannelIndexKey]) + .map((x) => x.toNumber()) + + // inject horizontal messages + if (params?.horizontalMessages) { + for (const [id, messages] of Object.entries(params.horizontalMessages)) { + const sender = Number(id) + if (hrmpIngressChannels.includes(sender)) { + const channelId = meta.registry.createType('HrmpChannelId', { sender, receiver: paraId.toNumber() }) + const hrmpChannelKey = hrmpChannels(channelId) + const decoded = await decodeProof( + extrinsic.validationData.relayParentStorageRoot, + [hrmpChannelKey], + extrinsic.relayChainState.trieNodes + ) + const abridgedHrmpRaw = decoded[hrmpChannelKey] + if (!abridgedHrmpRaw) throw new Error('Canoot find hrmp channels from validation data') + + const abridgedHrmp = meta.registry.createType('AbridgedHrmpChannel', hexToU8a(abridgedHrmpRaw)).toJSON() + const paraMessages: { data: HexString; sent_at: number }[] = [] + + for (const { data, sentAt } of messages) { + // calculate new hash + const bytes = meta.registry.createType('Bytes', data) + abridgedHrmp.mqcHead = blake2AsHex( + u8aConcat( + meta.registry.createType('Hash', abridgedHrmp.mqcHead).toU8a(), + meta.registry.createType('BlockNumber', sentAt).toU8a(), + blake2AsU8a(bytes.toU8a(), 256) + ), + 256 + ) + abridgedHrmp.msgCount = (abridgedHrmp.msgCount as number) + 1 + abridgedHrmp.totalSize = (abridgedHrmp.totalSize as number) + bytes.length + + paraMessages.push({ + data, + sent_at: sentAt, + }) + } + + horizontalMessages[sender] = paraMessages + + newEntries.push([hrmpChannelKey, meta.registry.createType('AbridgedHrmpChannel', abridgedHrmp).toHex()]) + } + } + } + const upgradeKey = upgradeGoAheadSignal(paraId) + const pendingUpgrade = await parentBlock.get(compactHex(meta.query.parachainSystem.pendingValidationCode())) if (pendingUpgrade) { // send goAhead signal const goAhead = meta.registry.createType('UpgradeGoAhead', 'GoAhead') @@ -78,13 +178,15 @@ export class SetValidationData implements CreateInherents { } const { trieRootHash, nodes } = await createProof( - extrinsic.validationData.relayParentStorageRoot as HexString, - extrinsic.relayChainState.trieNodes as HexString[], + extrinsic.validationData.relayParentStorageRoot, + extrinsic.relayChainState.trieNodes, newEntries ) newData = { ...extrinsic, + downwardMessages, + horizontalMessages, validationData: { ...extrinsic.validationData, relayParentStorageRoot: trieRootHash, diff --git a/src/blockchain/txpool.ts b/src/blockchain/txpool.ts index 30761b21..500b201c 100644 --- a/src/blockchain/txpool.ts +++ b/src/blockchain/txpool.ts @@ -19,6 +19,23 @@ export enum BuildBlockMode { Manual, // only build when triggered } +export interface DownwardMessage { + sentAt: number + msg: HexString +} + +export interface HorizontalMessage { + sentAt: number + data: HexString +} + +export interface BuildBlockParams { + inherent?: { + downwardMessages?: DownwardMessage[] + horizontalMessages?: Record + } +} + const getConsensus = (header: Header) => { if (header.digest.logs.length === 0) return const preRuntime = header.digest.logs[0].asPreRuntime @@ -90,13 +107,13 @@ export class TxPool { #batchBuildBlock = _.debounce(this.buildBlock, 100, { maxWait: 1000 }) - async buildBlock() { + async buildBlock(params?: BuildBlockParams) { const last = this.#lastBuildBlockPromise - this.#lastBuildBlockPromise = this.#buildBlock(last) + this.#lastBuildBlockPromise = this.#buildBlock(last, params) await this.#lastBuildBlockPromise } - async #buildBlock(wait: Promise) { + async #buildBlock(wait: Promise, params?: BuildBlockParams) { await this.#chain.api.isReady await wait.catch(() => {}) // ignore error const head = this.#chain.head @@ -148,7 +165,7 @@ export class TxPool { newBlock.pushStorageLayer().setAll(resp.storageDiff) - const inherents = await this.#inherentProvider.createInherents(newBlock) + const inherents = await this.#inherentProvider.createInherents(newBlock, params?.inherent) for (const extrinsic of inherents) { try { const resp = await newBlock.call('BlockBuilder_apply_extrinsic', extrinsic) diff --git a/src/index.ts b/src/index.ts index 9e4dc7c0..6d794bc3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,16 +5,20 @@ import yargs from 'yargs' import { BuildBlockMode } from './blockchain/txpool' import { configSchema } from './schema' +import { connectDownward } from './xcm' import { decodeKey } from './decode-key' import { runBlock } from './run-block' import { setupWithServer } from './setup-with-server' -const processConfig = (argv: any) => { +const processConfig = (path: string) => { + const configFile = readFileSync(path, 'utf8') + const config = yaml.load(configFile) as any + return configSchema.parse(config) +} + +const processArgv = (argv: any) => { if (argv.config) { - const configFile = readFileSync(argv.config, 'utf8') - const config = yaml.load(configFile) as any - const parsed = configSchema.parse(config) - return { ...parsed, ...argv } + return { ...processConfig(argv.config), ...argv } } return argv } @@ -59,11 +63,8 @@ yargs(hideBin(process.argv)) string: true, }, }), - (argv) => { - runBlock(processConfig(argv)).catch((err) => { - console.error(err) - process.exit(1) - }) + async (argv) => { + await runBlock(processArgv(argv)) } ) .command( @@ -89,11 +90,8 @@ yargs(hideBin(process.argv)) boolean: true, }, }), - (argv) => { - setupWithServer(processConfig(argv)).catch((err) => { - console.error(err) - process.exit(1) - }) + async (argv) => { + await setupWithServer(processArgv(argv)) } ) .command( @@ -108,11 +106,34 @@ yargs(hideBin(process.argv)) .options({ ...defaultOptions, }), - (argv) => { - decodeKey(processConfig(argv)).catch((err) => { - console.error(err) - process.exit(1) - }) + async (argv) => { + await decodeKey(processArgv(argv)) + } + ) + .command( + 'xcm', + 'XCM setup with relaychain and parachains', + (yargs) => + yargs.options({ + relaychain: { + desc: 'Relaychain config file path', + string: true, + required: true, + }, + parachain: { + desc: 'Parachain config file path', + type: 'array', + string: true, + required: true, + }, + }), + async (argv) => { + const { chain: relaychain } = await setupWithServer(processConfig(argv.relaychain)) + const parachains = await Promise.all(argv.parachain.map(processConfig).map(setupWithServer)) + for (const { chain: parachain } of parachains) { + await connectDownward(relaychain, parachain) + } + // TODO: connect parachains horizontal } ) .strict() diff --git a/src/server.ts b/src/server.ts index 45cbf0ab..509f16ef 100644 --- a/src/server.ts +++ b/src/server.ts @@ -18,22 +18,40 @@ const parseRequest = (request: string) => { } } -export const createServer = (handler: Handler, port?: number) => { - logger.debug('Starting on port %d', port) - const wss = new WebSocketServer({ port: port || 0, maxPayload: 1024 * 1024 * 100 }) +const createWS = async (port: number) => { + const wss = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 100 }) - const promise = new Promise((resolve, reject) => { + const promise = new Promise<[WebSocketServer?, number?]>((resolve) => { wss.on('listening', () => { - logger.debug(wss.address(), 'Listening') - resolve((wss.address() as AddressInfo).port) + resolve([wss, (wss.address() as AddressInfo).port]) }) - wss.on('error', (err) => { - logger.error(err, 'Error') - reject(err) + wss.on('error', (_) => { + resolve([]) }) }) + return promise +} + +export const createServer = async (handler: Handler, port?: number) => { + let wss: WebSocketServer | undefined + let listenPort: number | undefined + for (let i = 0; i < 5; i++) { + const preferPort = (port || 0) + i + logger.debug('Try starting on port %d', preferPort) + const [maybeWss, maybeListenPort] = await createWS(preferPort) + if (maybeWss && maybeListenPort) { + wss = maybeWss + listenPort = maybeListenPort + break + } + } + + if (!wss || !listenPort) { + throw new Error(`Failed to create WebsocketServer at port ${port}`) + } + wss.on('connection', (ws) => { logger.debug('New connection') @@ -126,11 +144,11 @@ export const createServer = (handler: Handler, port?: number) => { }) return { - port: promise, + port: listenPort, close: () => new Promise((resolve, reject) => { - wss.clients.forEach((socket) => socket.close()) - wss.close((err) => { + wss?.clients.forEach((socket) => socket.close()) + wss?.close((err) => { if (err) { reject(err) } else { diff --git a/src/setup-with-server.ts b/src/setup-with-server.ts index 7e8800c1..fea9c9df 100644 --- a/src/setup-with-server.ts +++ b/src/setup-with-server.ts @@ -1,21 +1,25 @@ import { Config } from './schema' import { createServer } from './server' import { handler } from './rpc' +import { logger } from './rpc/shared' import { setup } from './setup' export const setupWithServer = async (argv: Config) => { const context = await setup(argv) const port = argv.port || Number(process.env.PORT) || 8000 - const { close } = createServer(handler(context), port) - if (argv.genesis) { // mine 1st block when starting from genesis to set some mock validation data await context.chain.newBlock() } + const { close, port: listenPort } = await createServer(handler(context), port) + + logger.info(`${await context.chain.api.getSystemChain()} RPC listening on port ${listenPort}`) + return { ...context, close, + listenPort, } } diff --git a/src/setup.ts b/src/setup.ts index 6f9ce204..83c43d39 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -36,7 +36,7 @@ export const setup = async (argv: Config) => { blockHash = argv.block as string } - defaultLogger.info({ ...argv, blockHash }, 'Args') + defaultLogger.debug({ ...argv, blockHash }, 'Args') let db: DataSource | undefined if (argv.db) { diff --git a/src/utils/index.ts b/src/utils/index.ts index 11313d5a..56c40f39 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,8 @@ import { HexString } from '@polkadot/util/types' import { StorageKey } from '@polkadot/types' -import { compactStripLength, u8aToHex } from '@polkadot/util' +import { compactStripLength, hexToU8a, u8aToHex } from '@polkadot/util' + +import { Blockchain } from '../blockchain' export type GetKeys = (startKey?: string) => Promise[]> @@ -36,3 +38,10 @@ export async function fetchKeysToArray(getKeys: GetKeys) { export const compactHex = (value: Uint8Array): HexString => { return u8aToHex(compactStripLength(value)[1]) } + +export const getParaId = async (chain: Blockchain) => { + const meta = await chain.head.meta + const raw = await chain.head.get(compactHex(meta.query.parachainInfo.parachainId())) + if (!raw) throw new Error('Cannot find parachain id') + return meta.registry.createType('u32', hexToU8a(raw)) +} diff --git a/src/utils/proof.ts b/src/utils/proof.ts index a2cbf223..8a836d28 100644 --- a/src/utils/proof.ts +++ b/src/utils/proof.ts @@ -1,4 +1,5 @@ import { HexString } from '@polkadot/util/types' +import { HrmpChannelId } from '@polkadot/types/interfaces' import { hexToU8a, u8aConcat, u8aToHex } from '@polkadot/util' import { u32 } from '@polkadot/types' import { xxhashAsU8a } from '@polkadot/util-crypto' @@ -12,27 +13,31 @@ export const WELL_KNOWN_KEYS = { ACTIVE_CONFIG: '0x06de3d8a54d27e44a9d5ce189618f22db4b49d95320d9021994c850f25b8e385' as HexString, } -const prefixWithParaId = (prefix: HexString, paraId: u32) => { - const id = paraId.toU8a() - return u8aToHex(u8aConcat(hexToU8a(prefix), xxhashAsU8a(id, 64), id)) +const hash = (prefix: HexString, suffix: Uint8Array) => { + return u8aToHex(u8aConcat(hexToU8a(prefix), xxhashAsU8a(suffix, 64), suffix)) } export const dmqMqcHead = (paraId: u32) => { const prefix = '0x63f78c98723ddc9073523ef3beefda0c4d7fefc408aac59dbfe80a72ac8e3ce5' - return prefixWithParaId(prefix, paraId) + return hash(prefix, paraId.toU8a()) } export const upgradeGoAheadSignal = (paraId: u32) => { const prefix = '0xcd710b30bd2eab0352ddcc26417aa1949e94c040f5e73d9b7addd6cb603d15d3' - return prefixWithParaId(prefix, paraId) + return hash(prefix, paraId.toU8a()) } export const hrmpIngressChannelIndex = (paraId: u32) => { const prefix = '0x6a0da05ca59913bc38a8630590f2627c1d3719f5b0b12c7105c073c507445948' - return prefixWithParaId(prefix, paraId) + return hash(prefix, paraId.toU8a()) } export const hrmpEgressChannelIndex = (paraId: u32) => { const prefix = '0x6a0da05ca59913bc38a8630590f2627cf12b746dcf32e843354583c9702cc020' - return prefixWithParaId(prefix, paraId) + return hash(prefix, paraId.toU8a()) +} + +export const hrmpChannels = (channelId: HrmpChannelId) => { + const prefix = '0x6a0da05ca59913bc38a8630590f2627cb6604cff828a6e3f579ca6c59ace013d' + return hash(prefix, channelId.toU8a()) } diff --git a/src/xcm/index.ts b/src/xcm/index.ts new file mode 100644 index 00000000..c3f47d77 --- /dev/null +++ b/src/xcm/index.ts @@ -0,0 +1,37 @@ +import { hexToU8a } from '@polkadot/util' + +import { Blockchain } from '../blockchain' +import { DownwardMessage } from '../blockchain/txpool' +import { compactHex, getParaId } from '../utils' +import { defaultLogger } from '../logger' +import { setStorage } from '../utils/set-storage' + +const logger = defaultLogger.child({ name: 'xcm' }) + +export const connectDownward = async (relaychain: Blockchain, parachain: Blockchain) => { + const meta = await relaychain.head.meta + const paraId = await getParaId(parachain) + const downwardMessageQueuesKey = compactHex(meta.query.dmp.downwardMessageQueues(paraId)) + + await relaychain.headState.subscribeStorage([downwardMessageQueuesKey], async (head, pairs) => { + const value = pairs[0][1] + if (!value) return + + const meta = await relaychain.head.meta + const downwardMessageQueuesKey = compactHex(meta.query.dmp.downwardMessageQueues(paraId)) + + // clear relaychain message queue + await setStorage(relaychain, [[downwardMessageQueuesKey, null]], head.hash) + + const downwardMessages = meta.registry + .createType('Vec', hexToU8a(value)) + .toJSON() as any as DownwardMessage[] + + logger.debug({ downwardMessages }, 'downward_message') + await parachain.newBlock({ inherent: { downwardMessages } }) + }) + + logger.info( + `Connected relaychain '${await relaychain.api.getSystemChain()}' with parachain '${await parachain.api.getSystemChain()}'` + ) +} From 8f8c9deb2dca7e8d21171960a80acd2caf153527 Mon Sep 17 00:00:00 2001 From: Ermal Kaleci Date: Tue, 27 Dec 2022 22:20:37 +0100 Subject: [PATCH 2/6] fix types --- src/blockchain/inherent/parachain/validation-data.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/blockchain/inherent/parachain/validation-data.ts b/src/blockchain/inherent/parachain/validation-data.ts index 9740e30b..611aab09 100644 --- a/src/blockchain/inherent/parachain/validation-data.ts +++ b/src/blockchain/inherent/parachain/validation-data.ts @@ -1,3 +1,4 @@ +import { AbridgedHrmpChannel, HrmpChannelId } from '@polkadot/types/interfaces' import { GenericExtrinsic } from '@polkadot/types' import { HexString } from '@polkadot/util/types' import { hexToU8a, u8aConcat } from '@polkadot/util' @@ -119,14 +120,17 @@ export class SetValidationData implements CreateInherents { const hrmpIngressChannels = meta.registry .createType('Vec', decoded[hrmpIngressChannelIndexKey]) - .map((x) => x.toNumber()) + .toJSON() as number[] // inject horizontal messages if (params?.horizontalMessages) { for (const [id, messages] of Object.entries(params.horizontalMessages)) { const sender = Number(id) if (hrmpIngressChannels.includes(sender)) { - const channelId = meta.registry.createType('HrmpChannelId', { sender, receiver: paraId.toNumber() }) + const channelId = meta.registry.createType('HrmpChannelId', { + sender, + receiver: paraId.toNumber(), + }) const hrmpChannelKey = hrmpChannels(channelId) const decoded = await decodeProof( extrinsic.validationData.relayParentStorageRoot, @@ -136,7 +140,9 @@ export class SetValidationData implements CreateInherents { const abridgedHrmpRaw = decoded[hrmpChannelKey] if (!abridgedHrmpRaw) throw new Error('Canoot find hrmp channels from validation data') - const abridgedHrmp = meta.registry.createType('AbridgedHrmpChannel', hexToU8a(abridgedHrmpRaw)).toJSON() + const abridgedHrmp = meta.registry + .createType('AbridgedHrmpChannel', hexToU8a(abridgedHrmpRaw)) + .toJSON() const paraMessages: { data: HexString; sent_at: number }[] = [] for (const { data, sentAt } of messages) { From 4516cc744eb799ba8a66fd27d654a0267d937291 Mon Sep 17 00:00:00 2001 From: Ermal Kaleci Date: Tue, 27 Dec 2022 22:37:47 +0100 Subject: [PATCH 3/6] ingore callback error --- e2e/xcm.test.ts | 3 --- src/blockchain/head-state.ts | 7 ++++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/e2e/xcm.test.ts b/e2e/xcm.test.ts index 09f393d6..d94bb864 100644 --- a/e2e/xcm.test.ts +++ b/e2e/xcm.test.ts @@ -91,9 +91,6 @@ describe('XCM', async () => { await polkadot.chain.newBlock() await matchSnapshot(polkadot.api.query.system.events()) - - // wait for 10 secs for new block to built - await new Promise((resolve) => setTimeout(resolve, 10000)) await matchSnapshot(acala.api.query.system.events()) await polkadot.teardown() diff --git a/src/blockchain/head-state.ts b/src/blockchain/head-state.ts index 25e67836..c586eec2 100644 --- a/src/blockchain/head-state.ts +++ b/src/blockchain/head-state.ts @@ -2,11 +2,14 @@ import { stringToHex } from '@polkadot/util' import _ from 'lodash' import { Block } from './block' +import { defaultLogger } from '../logger' type Callback = (block: Block, pairs: [string, string][]) => void | Promise export const randomId = () => Math.random().toString(36).substring(2) +const logger = defaultLogger.child({ name: 'head-state' }) + export class HeadState { #headListeners: Record void> = {} #storageListeners: Record = {} @@ -67,7 +70,9 @@ export class HeadState { for (const [keys, cb] of Object.values(this.#storageListeners)) { const changed = keys.filter((key) => diff[key]).map((key) => [key, diff[key]] as [string, string]) if (changed.length > 0) { - await cb(head, changed) + await Promise.resolve(cb(head, changed)).catch((error) => { + logger.error(error, 'callback') + }) } } From 1f6511c94479e8eac0a4ece25ad2ec67ea2a4ce7 Mon Sep 17 00:00:00 2001 From: Ermal Kaleci Date: Wed, 28 Dec 2022 00:58:06 +0100 Subject: [PATCH 4/6] connect parachains --- src/index.ts | 4 ++-- src/xcm/index.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6d794bc3..a468f901 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import yargs from 'yargs' import { BuildBlockMode } from './blockchain/txpool' import { configSchema } from './schema' -import { connectDownward } from './xcm' +import { connectDownward, connectParachains } from './xcm' import { decodeKey } from './decode-key' import { runBlock } from './run-block' import { setupWithServer } from './setup-with-server' @@ -133,7 +133,7 @@ yargs(hideBin(process.argv)) for (const { chain: parachain } of parachains) { await connectDownward(relaychain, parachain) } - // TODO: connect parachains horizontal + await connectParachains(parachains.map((x) => x.chain)) } ) .strict() diff --git a/src/xcm/index.ts b/src/xcm/index.ts index c3f47d77..52ff789b 100644 --- a/src/xcm/index.ts +++ b/src/xcm/index.ts @@ -1,7 +1,8 @@ +import { HexString } from '@polkadot/util/types' import { hexToU8a } from '@polkadot/util' import { Blockchain } from '../blockchain' -import { DownwardMessage } from '../blockchain/txpool' +import { DownwardMessage, HorizontalMessage } from '../blockchain/txpool' import { compactHex, getParaId } from '../utils' import { defaultLogger } from '../logger' import { setStorage } from '../utils/set-storage' @@ -35,3 +36,49 @@ export const connectDownward = async (relaychain: Blockchain, parachain: Blockch `Connected relaychain '${await relaychain.api.getSystemChain()}' with parachain '${await parachain.api.getSystemChain()}'` ) } + +export const connectParachains = async (parachains: Blockchain[]) => { + const list: Record = {} + + for (const chain of parachains) { + const paraId = await getParaId(chain) + list[paraId.toNumber()] = chain + } + + await connectHorizontal(list) +} + +const connectHorizontal = async (parachains: Record) => { + for (const [id, chain] of Object.entries(parachains)) { + const meta = await chain.head.meta + + const hrmpOutboundMessagesKey = compactHex(meta.query.parachainSystem.hrmpOutboundMessages()) + + await chain.headState.subscribeStorage([hrmpOutboundMessagesKey], async (head, pairs) => { + const value = pairs[0][1] + if (!value) return + + const meta = await chain.head.meta + + const hrmpOutboundMessagesKey = compactHex(meta.query.parachainSystem.hrmpOutboundMessages()) + + // clear sender message queue + await setStorage(chain, [[hrmpOutboundMessagesKey, null]], head.hash) + + const outboundHrmpMessage = meta.registry + .createType('Vec', hexToU8a(value)) + .toJSON() as any as { recipient: number; data: HexString }[] + + for (const { recipient, data } of outboundHrmpMessage) { + logger.info({ outboundHrmpMessage }, 'outboundHrmpMessage') + const horizontalMessages: Record = { + [Number(id)]: [{ sentAt: chain.head.number, data }], + } + const receiver = parachains[recipient] + if (receiver) { + await receiver.newBlock({ inherent: { horizontalMessages } }) + } + } + }) + } +} From 998588e41a648e5d8679b9e84563f443fc4b789d Mon Sep 17 00:00:00 2001 From: Ermal Kaleci Date: Wed, 28 Dec 2022 10:12:18 +0100 Subject: [PATCH 5/6] make relaychain optional --- src/index.ts | 14 +++++++++----- src/xcm/index.ts | 2 ++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index a468f901..f6d6c9a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -118,7 +118,6 @@ yargs(hideBin(process.argv)) relaychain: { desc: 'Relaychain config file path', string: true, - required: true, }, parachain: { desc: 'Parachain config file path', @@ -128,12 +127,17 @@ yargs(hideBin(process.argv)) }, }), async (argv) => { - const { chain: relaychain } = await setupWithServer(processConfig(argv.relaychain)) const parachains = await Promise.all(argv.parachain.map(processConfig).map(setupWithServer)) - for (const { chain: parachain } of parachains) { - await connectDownward(relaychain, parachain) + if (parachains.length > 1) { + await connectParachains(parachains.map((x) => x.chain)) + } + + if (argv.relaychain) { + const { chain: relaychain } = await setupWithServer(processConfig(argv.relaychain)) + for (const { chain: parachain } of parachains) { + await connectDownward(relaychain, parachain) + } } - await connectParachains(parachains.map((x) => x.chain)) } ) .strict() diff --git a/src/xcm/index.ts b/src/xcm/index.ts index 52ff789b..9a27bfc6 100644 --- a/src/xcm/index.ts +++ b/src/xcm/index.ts @@ -46,6 +46,8 @@ export const connectParachains = async (parachains: Blockchain[]) => { } await connectHorizontal(list) + + logger.info('Parachains connected') } const connectHorizontal = async (parachains: Record) => { From 011316a5b4683a877ad41df61c38f8d765097d8a Mon Sep 17 00:00:00 2001 From: Ermal Kaleci Date: Wed, 28 Dec 2022 12:00:45 +0100 Subject: [PATCH 6/6] update config --- README.md | 5 +++-- configs/karura.yml | 7 +++++-- configs/statemine.yml | 13 +++++++++++++ src/index.ts | 12 +++++++++--- src/xcm/index.ts | 3 ++- 5 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 configs/statemine.yml diff --git a/README.md b/README.md index 2306b4d6..71766b1e 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ Make sure you have setup Rust environment (>= 1.64). - Edit configs/kusama.yml if needed. (e.g. update the block number) - `yarn start dev --config=configs/kusama.yml` -- Setup XCM multichain +- Setup XCM multichain (UpwardMessages not yet supported) +**_NOTE:_** You can also connect multiple parachains without a relaychain ```bash -yarn start xcm --relaychain=configs/kusama.yml --parachain=configs/acala.yml --parachain=configs/karura.yml +yarn start xcm --relaychain=configs/kusama.yml --parachain=configs/karura.yml --parachain=configs/statemine.yml ``` diff --git a/configs/karura.yml b/configs/karura.yml index cfa6c68b..1172a1b9 100644 --- a/configs/karura.yml +++ b/configs/karura.yml @@ -21,5 +21,8 @@ import-storage: - 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY - token: KUSD - free: 1000000000000000 - - + - + - + - 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY + - token: KSM + - free: '10000000000000000000' diff --git a/configs/statemine.yml b/configs/statemine.yml new file mode 100644 index 00000000..3eb3df31 --- /dev/null +++ b/configs/statemine.yml @@ -0,0 +1,13 @@ +endpoint: wss://statemine-rpc.polkadot.io +mock-signature-host: true +block: 3550000 +db: ./db.sqlite + +import-storage: + System: + Account: + - + - + - 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY + - data: + free: 1000000000000000 diff --git a/src/index.ts b/src/index.ts index f6d6c9a9..ee9902a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs' import yaml from 'js-yaml' import yargs from 'yargs' +import { Blockchain } from './blockchain' import { BuildBlockMode } from './blockchain/txpool' import { configSchema } from './schema' import { connectDownward, connectParachains } from './xcm' @@ -127,14 +128,19 @@ yargs(hideBin(process.argv)) }, }), async (argv) => { - const parachains = await Promise.all(argv.parachain.map(processConfig).map(setupWithServer)) + const parachains: Blockchain[] = [] + for (const config of argv.parachain) { + const { chain } = await setupWithServer(processConfig(config)) + parachains.push(chain) + } + if (parachains.length > 1) { - await connectParachains(parachains.map((x) => x.chain)) + await connectParachains(parachains) } if (argv.relaychain) { const { chain: relaychain } = await setupWithServer(processConfig(argv.relaychain)) - for (const { chain: parachain } of parachains) { + for (const parachain of parachains) { await connectDownward(relaychain, parachain) } } diff --git a/src/xcm/index.ts b/src/xcm/index.ts index 9a27bfc6..083fb449 100644 --- a/src/xcm/index.ts +++ b/src/xcm/index.ts @@ -71,8 +71,9 @@ const connectHorizontal = async (parachains: Record) => { .createType('Vec', hexToU8a(value)) .toJSON() as any as { recipient: number; data: HexString }[] + logger.info({ outboundHrmpMessage }, 'outboundHrmpMessage') + for (const { recipient, data } of outboundHrmpMessage) { - logger.info({ outboundHrmpMessage }, 'outboundHrmpMessage') const horizontalMessages: Record = { [Number(id)]: [{ sentAt: chain.head.number, data }], }