diff --git a/pallets/external-validator-slashes/src/lib.rs b/pallets/external-validator-slashes/src/lib.rs index 5949535e3..988ba96b0 100644 --- a/pallets/external-validator-slashes/src/lib.rs +++ b/pallets/external-validator-slashes/src/lib.rs @@ -281,36 +281,47 @@ pub mod pallet { #[pallet::weight(T::WeightInfo::force_inject_slash())] pub fn root_test_send_msg_to_eth( origin: OriginFor, - message_id: H256, - payload: H256, + nonce: H256, + num_msgs: u32, + msg_size: u32, ) -> DispatchResult { ensure_root(origin)?; - // Example command, this should be something like "ReportSlashes" - let command = Command::Test(payload.as_ref().to_vec()); - - // Validate - let channel_id: ChannelId = snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL; - - let outbound_message = Message { - id: Some(message_id), - channel_id, - command, - }; + for i in 0..num_msgs { + // Make sure each message has a different payload + let mut payload = sp_core::blake2_256((nonce, i).encode().as_ref()).to_vec(); + // Extend with zeros until msg_size is reached + payload.resize(msg_size as usize, 0); + // Example command, this should be something like "ReportSlashes" + let command = Command::Test(payload); + + // Validate + let channel_id: ChannelId = snowbridge_core::PRIMARY_GOVERNANCE_CHANNEL; + + let outbound_message = Message { + id: None, + channel_id, + command, + }; + + // validate the message + // Ignore fee because for now only root can send messages + let (ticket, _fee) = + T::ValidateMessage::validate(&outbound_message).map_err(|err| { + log::error!( + "root_test_send_msg_to_eth: validation of message {i} failed. {err:?}" + ); + crate::pallet::Error::::EthereumValidateFail + })?; - // validate the message - // Ignore fee because for now only root can send messages - let (ticket, _fee) = - T::ValidateMessage::validate(&outbound_message).map_err(|err| { - log::error!("root_test_send_msg_to_eth: validation of message failed. {err:?}"); - Error::::EthereumValidateFail + // Deliver + T::OutboundQueue::deliver(ticket).map_err(|err| { + log::error!( + "root_test_send_msg_to_eth: delivery of message {i} failed. {err:?}" + ); + crate::pallet::Error::::EthereumDeliverFail })?; - - // Deliver - T::OutboundQueue::deliver(ticket).map_err(|err| { - log::error!("root_test_send_msg_to_eth: delivery of message failed. {err:?}"); - Error::::EthereumDeliverFail - })?; + } Ok(()) } diff --git a/test/suites/dev-tanssi-relay/slashes/test_slashes_eth.ts b/test/suites/dev-tanssi-relay/slashes/test_slashes_eth.ts index 558286842..c2f924c8e 100644 --- a/test/suites/dev-tanssi-relay/slashes/test_slashes_eth.ts +++ b/test/suites/dev-tanssi-relay/slashes/test_slashes_eth.ts @@ -15,6 +15,7 @@ describeSuite({ polkadotJs = context.polkadotJs(); alice = context.keyring.alice; }); + it({ id: "E01", title: "Test using rootTestSendMsgToEth", @@ -22,10 +23,11 @@ describeSuite({ await jumpToSession(context, 1); // Send test message to ethereum - const msgId = "0x0000000000000000000000000000000000000000000000000000000000000001"; - const payloadH256 = "0x0000000000000000000000000000000000000000000000000000000000000002"; + const nonce = "0x0000000000000000000000000000000000000000000000000000000000000000"; + const numMsg = 1; + const msgSize = 32; const tx = await polkadotJs.tx.sudo - .sudo(polkadotJs.tx.externalValidatorSlashes.rootTestSendMsgToEth(msgId, payloadH256)) + .sudo(polkadotJs.tx.externalValidatorSlashes.rootTestSendMsgToEth(nonce, numMsg, msgSize)) .signAsync(alice); await context.createBlock([tx]); @@ -36,6 +38,13 @@ describeSuite({ expect(otherLogs.length).to.be.equal(1); const logHex = otherLogs[0]["other"]; + await expectEventCount(polkadotJs, { + MessagesCommitted: 1, + MessageAccepted: 1, + Processed: 1, + MessageQueued: 1, + }); + // Also a MessagesCommitted event with the same hash as the digest log const events = await polkadotJs.query.system.events(); const ev1 = events.filter((a) => { @@ -74,6 +83,139 @@ describeSuite({ expect(otherLogs.length).to.be.equal(1); const logHex = otherLogs[0]["other"]; + await expectEventCount(polkadotJs, { + MessagesCommitted: 1, + MessageAccepted: 1, + Processed: 1, + MessageQueued: 1, + }); + + // Also a MessagesCommitted event with the same hash as the digest log + const events = await polkadotJs.query.system.events(); + const ev1 = events.filter((a) => { + return a.event.method == "MessagesCommitted"; + }); + expect(ev1.length).to.be.equal(1); + const ev1Data = ev1[0].event.data[0].toJSON(); + + // logHex == 0x00 + ev1Data + // Example: + // logHex: 0x0064cf0ef843ad5a26c2cc27cf345fe0fd8b72cd6297879caa626c4d72bbe4f9b0 + // ev1Data: 0x64cf0ef843ad5a26c2cc27cf345fe0fd8b72cd6297879caa626c4d72bbe4f9b0 + const prefixedEv1Data = `0x00${ev1Data.slice(2)}`; + expect(prefixedEv1Data).to.be.equal(logHex); + }, + }); + + it({ + id: "E03", + title: "Send too big message using rootTestSendMsgToEth", + test: async function () { + await jumpToSession(context, 1); + + // Send test message to ethereum + const nonce = "0x0000000000000000000000000000000000000000000000000000000000000000"; + const numMsg = 1; + // TODO: the limit should be 2048 bytes, not 1921 + const msgSize = 1921; + const tx = await polkadotJs.tx.sudo + .sudo(polkadotJs.tx.externalValidatorSlashes.rootTestSendMsgToEth(nonce, numMsg, msgSize)) + .signAsync(alice); + await context.createBlock([tx]); + + await expectEventCount(polkadotJs, { + MessagesCommitted: 0, + MessageAccepted: 0, + Processed: 0, + MessageQueued: 0, + }); + + // Also a MessagesCommitted event with the same hash as the digest log + const events = await polkadotJs.query.system.events(); + const ev1 = events.filter((a) => { + return a.event.method == "Sudid"; + }); + expect(ev1.length).to.be.equal(1); + const ev1Data = ev1[0].event.data[0].toJSON(); + expect(ev1Data["err"]).toBeTruthy(); + }, + }); + + it({ + id: "E04", + title: "Send message of max size using rootTestSendMsgToEth", + test: async function () { + await jumpToSession(context, 1); + + // Send test message to ethereum + const nonce = "0x0000000000000000000000000000000000000000000000000000000000000000"; + const numMsg = 1; + const msgSize = 1920; + const tx = await polkadotJs.tx.sudo + .sudo(polkadotJs.tx.externalValidatorSlashes.rootTestSendMsgToEth(nonce, numMsg, msgSize)) + .signAsync(alice); + await context.createBlock([tx]); + + // Should have resulted in a new "other" digest log being included in the block + const baseHeader = await polkadotJs.rpc.chain.getHeader(); + const allLogs = baseHeader.digest.logs.map((x) => x.toJSON()); + const otherLogs = allLogs.filter((x) => x["other"]); + expect(otherLogs.length).to.be.equal(1); + const logHex = otherLogs[0]["other"]; + + await expectEventCount(polkadotJs, { + MessagesCommitted: 1, + MessageAccepted: 1, + Processed: 1, + MessageQueued: 1, + }); + + // Also a MessagesCommitted event with the same hash as the digest log + const events = await polkadotJs.query.system.events(); + const ev1 = events.filter((a) => { + return a.event.method == "MessagesCommitted"; + }); + expect(ev1.length).to.be.equal(1); + const ev1Data = ev1[0].event.data[0].toJSON(); + + // logHex == 0x00 + ev1Data + // Example: + // logHex: 0x0064cf0ef843ad5a26c2cc27cf345fe0fd8b72cd6297879caa626c4d72bbe4f9b0 + // ev1Data: 0x64cf0ef843ad5a26c2cc27cf345fe0fd8b72cd6297879caa626c4d72bbe4f9b0 + const prefixedEv1Data = `0x00${ev1Data.slice(2)}`; + expect(prefixedEv1Data).to.be.equal(logHex); + }, + }); + + it({ + id: "E05", + title: "Send 100 messages using rootTestSendMsgToEth", + test: async function () { + await jumpToSession(context, 1); + + // Send test message to ethereum + const nonce = "0x0000000000000000000000000000000000000000000000000000000000000000"; + const numMsg = 100; + const msgSize = 32; + const tx = await polkadotJs.tx.sudo + .sudo(polkadotJs.tx.externalValidatorSlashes.rootTestSendMsgToEth(nonce, numMsg, msgSize)) + .signAsync(alice); + await context.createBlock([tx]); + + // Should have resulted in a new "other" digest log being included in the block + const baseHeader = await polkadotJs.rpc.chain.getHeader(); + const allLogs = baseHeader.digest.logs.map((x) => x.toJSON()); + const otherLogs = allLogs.filter((x) => x["other"]); + expect(otherLogs.length).to.be.equal(1); + const logHex = otherLogs[0]["other"]; + + await expectEventCount(polkadotJs, { + MessagesCommitted: 1, + MessageAccepted: 32, + Processed: 32, + MessageQueued: 100, + }); + // Also a MessagesCommitted event with the same hash as the digest log const events = await polkadotJs.query.system.events(); const ev1 = events.filter((a) => { @@ -88,7 +230,47 @@ describeSuite({ // ev1Data: 0x64cf0ef843ad5a26c2cc27cf345fe0fd8b72cd6297879caa626c4d72bbe4f9b0 const prefixedEv1Data = `0x00${ev1Data.slice(2)}`; expect(prefixedEv1Data).to.be.equal(logHex); + + // Next block will have 32 events more + await context.createBlock(); + await expectEventCount(polkadotJs, { + MessagesCommitted: 1, + MessageAccepted: 32, + Processed: 32, + MessageQueued: 0, + }); + + // Total so far: 64 + await context.createBlock(); + await expectEventCount(polkadotJs, { + MessagesCommitted: 1, + MessageAccepted: 32, + Processed: 32, + MessageQueued: 0, + }); + + // Total so far: 96, missing last 4 + await context.createBlock(); + await expectEventCount(polkadotJs, { + MessagesCommitted: 1, + MessageAccepted: 4, + Processed: 4, + MessageQueued: 0, + }); }, }); }, }); + +async function expectEventCount(polkadotJs, eventCounts: Record): Promise { + const events = await polkadotJs.query.system.events(); + + for (const [eventMethod, expectedCount] of Object.entries(eventCounts)) { + const matchingEvents = events.filter(({ event }) => event.method === eventMethod); + + expect( + matchingEvents.length, + `Expected ${expectedCount} occurrences of event '${eventMethod}', but found ${matchingEvents.length}` + ).to.equal(expectedCount); + } +} diff --git a/typescript-api/src/dancelight/interfaces/augment-api-tx.ts b/typescript-api/src/dancelight/interfaces/augment-api-tx.ts index 31a9f1e67..c46fd49fe 100644 --- a/typescript-api/src/dancelight/interfaces/augment-api-tx.ts +++ b/typescript-api/src/dancelight/interfaces/augment-api-tx.ts @@ -1560,10 +1560,11 @@ declare module "@polkadot/api-base/types/submittable" { >; rootTestSendMsgToEth: AugmentedSubmittable< ( - messageId: H256 | string | Uint8Array, - payload: H256 | string | Uint8Array + nonce: H256 | string | Uint8Array, + numMsgs: u32 | AnyNumber | Uint8Array, + msgSize: u32 | AnyNumber | Uint8Array ) => SubmittableExtrinsic, - [H256, H256] + [H256, u32, u32] >; /** Generic tx */ [key: string]: SubmittableExtrinsicFunction; diff --git a/typescript-api/src/dancelight/interfaces/lookup.ts b/typescript-api/src/dancelight/interfaces/lookup.ts index 3cd06a49e..a63c4838e 100644 --- a/typescript-api/src/dancelight/interfaces/lookup.ts +++ b/typescript-api/src/dancelight/interfaces/lookup.ts @@ -1202,8 +1202,9 @@ export default { percentage: "Perbill", }, root_test_send_msg_to_eth: { - messageId: "H256", - payload: "H256", + nonce: "H256", + numMsgs: "u32", + msgSize: "u32", }, }, }, diff --git a/typescript-api/src/dancelight/interfaces/types-lookup.ts b/typescript-api/src/dancelight/interfaces/types-lookup.ts index 5ba33c7dd..e52a22a81 100644 --- a/typescript-api/src/dancelight/interfaces/types-lookup.ts +++ b/typescript-api/src/dancelight/interfaces/types-lookup.ts @@ -1545,8 +1545,9 @@ declare module "@polkadot/types/lookup" { } & Struct; readonly isRootTestSendMsgToEth: boolean; readonly asRootTestSendMsgToEth: { - readonly messageId: H256; - readonly payload: H256; + readonly nonce: H256; + readonly numMsgs: u32; + readonly msgSize: u32; } & Struct; readonly type: "CancelDeferredSlash" | "ForceInjectSlash" | "RootTestSendMsgToEth"; }