diff --git a/contracts/src/DataHavenServiceManager.sol b/contracts/src/DataHavenServiceManager.sol index 62a7e83f..b03bef29 100644 --- a/contracts/src/DataHavenServiceManager.sol +++ b/contracts/src/DataHavenServiceManager.sol @@ -118,7 +118,7 @@ contract DataHavenServiceManager is ServiceManagerBase, IDataHavenServiceManager newValidatorSet[i] = validatorEthAddressToSolochainAddress[currentValidatorSet[i]]; } DataHavenSnowbridgeMessages.NewValidatorSetPayload memory newValidatorSetPayload = - DataHavenSnowbridgeMessages.NewValidatorSetPayload({newValidatorSet: newValidatorSet}); + DataHavenSnowbridgeMessages.NewValidatorSetPayload({validators: newValidatorSet}); DataHavenSnowbridgeMessages.NewValidatorSet memory newValidatorSetMessage = DataHavenSnowbridgeMessages.NewValidatorSet({ nonce: 0, diff --git a/contracts/src/libraries/DataHavenSnowbridgeMessages.sol b/contracts/src/libraries/DataHavenSnowbridgeMessages.sol index b3bfb8ad..1bf7a126 100644 --- a/contracts/src/libraries/DataHavenSnowbridgeMessages.sol +++ b/contracts/src/libraries/DataHavenSnowbridgeMessages.sol @@ -5,6 +5,18 @@ pragma solidity ^0.8.27; import {ScaleCodec} from "snowbridge/src/utils/ScaleCodec.sol"; library DataHavenSnowbridgeMessages { + // Message ID. This is not expected to change and comes from the runtime. + // See EigenLayerMessageProcessor in primitives/bridge/src/lib.rs. + bytes4 constant EL_MESSAGE_ID = 0x70150038; + + enum Message { + V0 + } + + enum OutboundCommandV1 { + ReceiveValidators + } + /** * @title New Validator Set Snowbridge Message * @notice A struct representing a new validator set to be sent as a message through Snowbridge. @@ -25,13 +37,12 @@ library DataHavenSnowbridgeMessages { /** * @title New Validator Set Snowbridge Message Payload * @notice A struct representing the payload of a new validator set message. - * !IMPORTANT: The fields in this struct are placeholder until we have the actual message format - * ! defined in the DataHaven solochain. + * This mimics the message format defined in the InboundQueueV2 pallet of the DataHaven + * solochain. */ struct NewValidatorSetPayload { - /// @notice The new validator set. This should be interpreted as the list of - /// validator addresses in the DataHaven network. - bytes32[] newValidatorSet; + /// @notice The list of validators in the DataHaven network. + bytes32[] validators; } /** @@ -57,13 +68,23 @@ library DataHavenSnowbridgeMessages { function scaleEncodeNewValidatorSetMessagePayload( NewValidatorSetPayload memory payload ) public pure returns (bytes memory) { - // Encode all fields into a buffer. - bytes memory accum = hex""; - for (uint256 i = 0; i < payload.newValidatorSet.length; i++) { - accum = bytes.concat(accum, payload.newValidatorSet[i]); + uint32 validatorsLen = uint32(payload.validators.length); + bytes32[] memory validatorSet = payload.validators; + // TODO: This shouldn't be hardcoded, but set to the corresponding epoch of this validator set. + uint48 epoch = 0; + bytes memory validatorsFlattened; + for (uint32 i = 0; i < validatorSet.length; i++) { + validatorsFlattened = + bytes.concat(validatorsFlattened, abi.encodePacked(validatorSet[i])); } - // Encode number of validator addresses, followed by encoded validator addresses. - return - bytes.concat(ScaleCodec.checkedEncodeCompactU32(payload.newValidatorSet.length), accum); + + return bytes.concat( + EL_MESSAGE_ID, + bytes1(uint8(Message.V0)), + bytes1(uint8(OutboundCommandV1.ReceiveValidators)), + ScaleCodec.encodeCompactU32(validatorsLen), + validatorsFlattened, + ScaleCodec.encodeU64(uint64(epoch)) + ); } } diff --git a/contracts/test/MessageEncoding.t.sol b/contracts/test/MessageEncoding.t.sol new file mode 100644 index 00000000..8a884c5f --- /dev/null +++ b/contracts/test/MessageEncoding.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; +import {DataHavenSnowbridgeMessages} from "../src/libraries/DataHavenSnowbridgeMessages.sol"; + +// This test is used to encode the receive validators message and log the hex string. +// The hex string is then used to generate the .bin file for the Rust test. +// To generate the .bin file, run: +// forge test --match-test testEncodeReceiveValidatorsMessageAndLog +// Then, copy the hex string and paste it into the Rust test file. +// Then, run: +// cargo test --test decode_receive_validators_message_from_file_correctly +// The test should pass. +contract MessageEncodingTest is Test { + function testEncodeReceiveValidatorsMessageAndLog() public pure { + // Mock Data - + uint64 mockNonce = 12345; + bytes32 mockTopic = 0x123456789012345678901234567890123456789012345678901234567890abcd; + + bytes32[] memory mockValidators = new bytes32[](2); + mockValidators[0] = 0x0000000000000000000000000000000000000000000000000000000000000001; + mockValidators[1] = 0x0000000000000000000000000000000000000000000000000000000000000002; + // uint64 mockEpoch = 0; // This is hardcoded to 0 in the Solidity function's payload part + + DataHavenSnowbridgeMessages.NewValidatorSetPayload memory newValidatorSetPayload = + DataHavenSnowbridgeMessages.NewValidatorSetPayload({validators: mockValidators}); + // epoch is implicitly 0 in scaleEncodeNewValidatorSetMessagePayload + + DataHavenSnowbridgeMessages.NewValidatorSet memory newValidatorSetMessage = + DataHavenSnowbridgeMessages.NewValidatorSet({ + nonce: mockNonce, + topic: mockTopic, + payload: newValidatorSetPayload + }); + + bytes memory encodedMessage = + DataHavenSnowbridgeMessages.scaleEncodeNewValidatorSetMessage(newValidatorSetMessage); + + console.log("Encoded NewValidatorSet message (hex):"); + console.logBytes(encodedMessage); // This will print the hex string + } +} diff --git a/operator/Cargo.lock b/operator/Cargo.lock index da0b1ee7..0d51450a 100644 --- a/operator/Cargo.lock +++ b/operator/Cargo.lock @@ -2821,6 +2821,7 @@ version = "0.1.0" dependencies = [ "frame-support", "frame-system", + "hex", "pallet-external-validators", "parity-scale-codec", "snowbridge-core 0.2.0", diff --git a/operator/primitives/bridge/Cargo.toml b/operator/primitives/bridge/Cargo.toml index 8f1a1317..b87aad6e 100644 --- a/operator/primitives/bridge/Cargo.toml +++ b/operator/primitives/bridge/Cargo.toml @@ -15,6 +15,7 @@ parity-scale-codec = { workspace = true } snowbridge-core = { workspace = true } snowbridge-inbound-queue-primitives = { workspace = true } sp-std = { workspace = true } +hex = { workspace = true } [features] default = ["std"] @@ -25,4 +26,5 @@ std = [ "parity-scale-codec/std", "pallet-external-validators/std", "sp-std/std", + "snowbridge-inbound-queue-primitives/std", ] diff --git a/operator/primitives/bridge/src/lib.rs b/operator/primitives/bridge/src/lib.rs index afe74204..e10cd01c 100644 --- a/operator/primitives/bridge/src/lib.rs +++ b/operator/primitives/bridge/src/lib.rs @@ -5,15 +5,17 @@ use parity_scale_codec::DecodeAll; use snowbridge_inbound_queue_primitives::v2::{Message as SnowbridgeMessage, MessageProcessor}; use sp_std::vec::Vec; -pub const EL_MESSAGE_ID: [u8; 4] = [112, 21, 0, 56]; +// Message ID. This is not expected to change and its arbitrary bytes defined here. +// It should match the EL_MESSAGE_ID in DataHavenSnowbridgeMessages.sol +pub const EL_MESSAGE_ID: [u8; 4] = [112, 21, 0, 56]; // 0x70150038 #[derive(Encode, Decode)] pub struct Payload where T: pallet_external_validators::Config, { - message: Message, - message_id: [u8; 4], + pub message: Message, + pub message_id: [u8; 4], } #[derive(Encode, Decode)] @@ -38,6 +40,20 @@ where /// EigenLayer Message Processor pub struct EigenLayerMessageProcessor(PhantomData); +impl EigenLayerMessageProcessor +where + T: pallet_external_validators::Config, +{ + pub fn decode_message(mut payload: &[u8]) -> Result, DispatchError> { + let decode_result = Payload::::decode_all(&mut payload); + if let Ok(payload) = decode_result { + Ok(payload) + } else { + Err(DispatchError::Other("unable to parse the message payload")) + } + } +} + impl MessageProcessor for EigenLayerMessageProcessor where T: pallet_external_validators::Config, @@ -50,7 +66,7 @@ where network: _, } => return false, }; - let decode_result = Payload::::decode_all(&mut payload.as_slice()); + let decode_result = Self::decode_message(payload.as_slice()); if let Ok(payload) = decode_result { payload.message_id == EL_MESSAGE_ID } else { @@ -69,7 +85,7 @@ where network: _, } => return Err(DispatchError::Other("Invalid Message")), }; - let decode_result = Payload::::decode_all(&mut payload.as_slice()); + let decode_result = Self::decode_message(payload.as_slice()); let message = if let Ok(payload) = decode_result { payload.message } else { diff --git a/operator/primitives/bridge/test_data/receive_validators_message.bin b/operator/primitives/bridge/test_data/receive_validators_message.bin new file mode 100644 index 00000000..23741a0e Binary files /dev/null and b/operator/primitives/bridge/test_data/receive_validators_message.bin differ diff --git a/operator/runtime/mainnet/src/configs/mod.rs b/operator/runtime/mainnet/src/configs/mod.rs index 212a825a..cc9a304b 100644 --- a/operator/runtime/mainnet/src/configs/mod.rs +++ b/operator/runtime/mainnet/src/configs/mod.rs @@ -27,11 +27,12 @@ mod runtime_params; use super::{ deposit, AccountId, Babe, Balance, Balances, BeefyMmrLeaf, Block, BlockNumber, - EthereumBeaconClient, EvmChainId, ExternalValidators, ExternalValidatorsRewards, Hash, - Historical, ImOnline, MessageQueue, Nonce, Offences, OriginCaller, OutboundCommitmentStore, - OutboundQueueV2, PalletInfo, Preimage, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, - RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, Signature, System, - Timestamp, EXISTENTIAL_DEPOSIT, SLOT_DURATION, STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, + EthereumBeaconClient, EthereumOutboundQueueV2, EvmChainId, ExternalValidators, + ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, Nonce, Offences, + OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Runtime, RuntimeCall, + RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, + SessionKeys, Signature, System, Timestamp, EXISTENTIAL_DEPOSIT, SLOT_DURATION, + STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, }; use codec::{Decode, Encode}; use datahaven_runtime_common::{ @@ -532,7 +533,7 @@ parameter_types! { impl pallet_message_queue::Config for Runtime { type RuntimeEvent = RuntimeEvent; #[cfg(not(feature = "runtime-benchmarks"))] - type MessageProcessor = OutboundQueueV2; + type MessageProcessor = EthereumOutboundQueueV2; #[cfg(feature = "runtime-benchmarks")] type MessageProcessor = pallet_message_queue::mock_helpers::NoopMessageProcessor; @@ -698,7 +699,7 @@ impl snowbridge_pallet_system::Config for Runtime { // Implement the Snowbridge System v2 config trait impl snowbridge_pallet_system_v2::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type OutboundQueue = OutboundQueueV2; + type OutboundQueue = EthereumOutboundQueueV2; type FrontendOrigin = EnsureRootWithSuccess; type GovernanceOrigin = EnsureRootWithSuccess; type WeightInfo = (); @@ -956,10 +957,10 @@ impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapt } fn validate(message: Self::Message) -> Result { - OutboundQueueV2::validate(&message) + EthereumOutboundQueueV2::validate(&message) } fn deliver(message: Self::Ticket) -> Result { - OutboundQueueV2::deliver(message) + EthereumOutboundQueueV2::deliver(message) } } diff --git a/operator/runtime/mainnet/src/lib.rs b/operator/runtime/mainnet/src/lib.rs index 5736a67c..821ba9d2 100644 --- a/operator/runtime/mainnet/src/lib.rs +++ b/operator/runtime/mainnet/src/lib.rs @@ -322,10 +322,10 @@ mod runtime { pub type EthereumBeaconClient = snowbridge_pallet_ethereum_client; #[runtime::pallet_index(61)] - pub type InboundQueueV2 = snowbridge_pallet_inbound_queue_v2; + pub type EthereumInboundQueueV2 = snowbridge_pallet_inbound_queue_v2; #[runtime::pallet_index(62)] - pub type OutboundQueueV2 = snowbridge_pallet_outbound_queue_v2; + pub type EthereumOutboundQueueV2 = snowbridge_pallet_outbound_queue_v2; #[runtime::pallet_index(63)] pub type SnowbridgeSystem = snowbridge_pallet_system; diff --git a/operator/runtime/stagenet/src/configs/mod.rs b/operator/runtime/stagenet/src/configs/mod.rs index 0e80adab..105463be 100644 --- a/operator/runtime/stagenet/src/configs/mod.rs +++ b/operator/runtime/stagenet/src/configs/mod.rs @@ -27,11 +27,12 @@ mod runtime_params; use super::{ deposit, AccountId, Babe, Balance, Balances, BeefyMmrLeaf, Block, BlockNumber, - EthereumBeaconClient, EvmChainId, ExternalValidators, ExternalValidatorsRewards, Hash, - Historical, ImOnline, MessageQueue, Nonce, Offences, OriginCaller, OutboundCommitmentStore, - OutboundQueueV2, PalletInfo, Preimage, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, - RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, Signature, System, - Timestamp, EXISTENTIAL_DEPOSIT, SLOT_DURATION, STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, + EthereumBeaconClient, EthereumOutboundQueueV2, EvmChainId, ExternalValidators, + ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, Nonce, Offences, + OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Runtime, RuntimeCall, + RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, + SessionKeys, Signature, System, Timestamp, EXISTENTIAL_DEPOSIT, SLOT_DURATION, + STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, }; use codec::{Decode, Encode}; use datahaven_runtime_common::{ @@ -531,7 +532,7 @@ parameter_types! { impl pallet_message_queue::Config for Runtime { type RuntimeEvent = RuntimeEvent; #[cfg(not(feature = "runtime-benchmarks"))] - type MessageProcessor = OutboundQueueV2; + type MessageProcessor = EthereumOutboundQueueV2; #[cfg(feature = "runtime-benchmarks")] type MessageProcessor = pallet_message_queue::mock_helpers::NoopMessageProcessor; @@ -699,7 +700,7 @@ impl snowbridge_pallet_system::Config for Runtime { // Implement the Snowbridge System v2 config trait impl snowbridge_pallet_system_v2::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type OutboundQueue = OutboundQueueV2; + type OutboundQueue = EthereumOutboundQueueV2; type FrontendOrigin = EnsureRootWithSuccess; type GovernanceOrigin = EnsureRootWithSuccess; type WeightInfo = (); @@ -917,10 +918,10 @@ impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapt } fn validate(message: Self::Message) -> Result { - OutboundQueueV2::validate(&message) + EthereumOutboundQueueV2::validate(&message) } fn deliver(message: Self::Ticket) -> Result { - OutboundQueueV2::deliver(message) + EthereumOutboundQueueV2::deliver(message) } } diff --git a/operator/runtime/stagenet/src/lib.rs b/operator/runtime/stagenet/src/lib.rs index 56893b20..7d35c34b 100644 --- a/operator/runtime/stagenet/src/lib.rs +++ b/operator/runtime/stagenet/src/lib.rs @@ -322,10 +322,10 @@ mod runtime { pub type EthereumBeaconClient = snowbridge_pallet_ethereum_client; #[runtime::pallet_index(61)] - pub type InboundQueueV2 = snowbridge_pallet_inbound_queue_v2; + pub type EthereumInboundQueueV2 = snowbridge_pallet_inbound_queue_v2; #[runtime::pallet_index(62)] - pub type OutboundQueueV2 = snowbridge_pallet_outbound_queue_v2; + pub type EthereumOutboundQueueV2 = snowbridge_pallet_outbound_queue_v2; #[runtime::pallet_index(63)] pub type SnowbridgeSystem = snowbridge_pallet_system; diff --git a/operator/runtime/testnet/src/configs/mod.rs b/operator/runtime/testnet/src/configs/mod.rs index a21828a4..ca9686be 100644 --- a/operator/runtime/testnet/src/configs/mod.rs +++ b/operator/runtime/testnet/src/configs/mod.rs @@ -27,11 +27,12 @@ mod runtime_params; use super::{ deposit, AccountId, Babe, Balance, Balances, BeefyMmrLeaf, Block, BlockNumber, - EthereumBeaconClient, EvmChainId, ExternalValidators, ExternalValidatorsRewards, Hash, - Historical, ImOnline, MessageQueue, Nonce, Offences, OriginCaller, OutboundCommitmentStore, - OutboundQueueV2, PalletInfo, Preimage, Runtime, RuntimeCall, RuntimeEvent, RuntimeFreezeReason, - RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, SessionKeys, Signature, System, - Timestamp, EXISTENTIAL_DEPOSIT, SLOT_DURATION, STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, + EthereumBeaconClient, EthereumOutboundQueueV2, EvmChainId, ExternalValidators, + ExternalValidatorsRewards, Hash, Historical, ImOnline, MessageQueue, Nonce, Offences, + OriginCaller, OutboundCommitmentStore, PalletInfo, Preimage, Runtime, RuntimeCall, + RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, Session, + SessionKeys, Signature, System, Timestamp, EXISTENTIAL_DEPOSIT, SLOT_DURATION, + STORAGE_BYTE_FEE, SUPPLY_FACTOR, UNIT, VERSION, }; use codec::{Decode, Encode}; use datahaven_runtime_common::{ @@ -531,7 +532,7 @@ parameter_types! { impl pallet_message_queue::Config for Runtime { type RuntimeEvent = RuntimeEvent; #[cfg(not(feature = "runtime-benchmarks"))] - type MessageProcessor = OutboundQueueV2; + type MessageProcessor = EthereumOutboundQueueV2; #[cfg(feature = "runtime-benchmarks")] type MessageProcessor = pallet_message_queue::mock_helpers::NoopMessageProcessor; @@ -697,7 +698,7 @@ impl snowbridge_pallet_system::Config for Runtime { // Implement the Snowbridge System v2 config trait impl snowbridge_pallet_system_v2::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type OutboundQueue = OutboundQueueV2; + type OutboundQueue = EthereumOutboundQueueV2; type FrontendOrigin = EnsureRootWithSuccess; type GovernanceOrigin = EnsureRootWithSuccess; type WeightInfo = (); @@ -955,10 +956,10 @@ impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapt } fn validate(message: Self::Message) -> Result { - OutboundQueueV2::validate(&message) + EthereumOutboundQueueV2::validate(&message) } fn deliver(message: Self::Ticket) -> Result { - OutboundQueueV2::deliver(message) + EthereumOutboundQueueV2::deliver(message) } } diff --git a/operator/runtime/testnet/src/lib.rs b/operator/runtime/testnet/src/lib.rs index 4e99e62d..a20bad9b 100644 --- a/operator/runtime/testnet/src/lib.rs +++ b/operator/runtime/testnet/src/lib.rs @@ -322,10 +322,10 @@ mod runtime { pub type EthereumBeaconClient = snowbridge_pallet_ethereum_client; #[runtime::pallet_index(61)] - pub type InboundQueueV2 = snowbridge_pallet_inbound_queue_v2; + pub type EthereumInboundQueueV2 = snowbridge_pallet_inbound_queue_v2; #[runtime::pallet_index(62)] - pub type OutboundQueueV2 = snowbridge_pallet_outbound_queue_v2; + pub type EthereumOutboundQueueV2 = snowbridge_pallet_outbound_queue_v2; #[runtime::pallet_index(63)] pub type SnowbridgeSystem = snowbridge_pallet_system; @@ -1091,3 +1091,93 @@ impl_runtime_apis! { } } } + +#[cfg(test)] +mod tests { + use super::*; + use codec::Encode; + use dhp_bridge::InboundCommand; + use dhp_bridge::{Message, Payload, EL_MESSAGE_ID}; + use snowbridge_inbound_queue_primitives::v2::{Message as SnowbridgeMessage, MessageProcessor}; + use sp_core::H256; + + const MOCK_NONCE: u64 = 12345u64; + const MOCK_VALIDATORS_HEX: [&str; 2] = [ + "0000000000000000000000000000000000000000000000000000000000000001", + "0000000000000000000000000000000000000000000000000000000000000002", + ]; + const MOCK_EXTERNAL_INDEX: u64 = 0u64; + + fn hex_to_bytes32(hex_str: &str) -> [u8; 32] { + let mut arr = [0u8; 32]; + hex::decode_to_slice(hex_str, &mut arr).expect("Failed to decode hex string to bytes32"); + arr + } + + #[test] + fn test_eigenlayer_message_processor() { + // Create mock validators + let validators: Vec = MOCK_VALIDATORS_HEX + .iter() + .map(|s| hex_to_bytes32(s).into()) + .collect(); + + // Create a mock message payload + let message = Message::V1(dhp_bridge::InboundCommand::ReceiveValidators { + validators: validators.clone(), + external_index: MOCK_EXTERNAL_INDEX, + }); + + let payload = Payload:: { + message, + message_id: EL_MESSAGE_ID, + }; + + // Create a mock Snowbridge message + let snowbridge_message = SnowbridgeMessage { + xcm: snowbridge_inbound_queue_primitives::v2::Payload::Raw(payload.encode()), + gateway: H160::default(), + nonce: MOCK_NONCE, + origin: H160::default(), + assets: vec![], + claimer: None, + value: 0u128, + execution_fee: 0u128, + relayer_fee: 0u128, + }; + + // Test can_process_message + let mock_account = H256::from_slice(&[1u8; 32]); + assert!( + dhp_bridge::EigenLayerMessageProcessor::::can_process_message( + &mock_account, + &snowbridge_message + ), + "Message should be processable" + ); + + let payload = match &snowbridge_message.xcm { + snowbridge_inbound_queue_primitives::v2::Payload::Raw(payload) => payload, + _ => panic!("Invalid Message"), + }; + + let decoded_result = + dhp_bridge::EigenLayerMessageProcessor::::decode_message(payload.as_slice()); + + let message = if let Ok(payload) = decoded_result { + payload.message + } else { + panic!("unable to parse the message payload"); + }; + + match message { + Message::V1(InboundCommand::ReceiveValidators { + validators, + external_index, + }) => { + assert_eq!(validators, validators); + assert_eq!(external_index, MOCK_EXTERNAL_INDEX); + } + } + } +} diff --git a/test/bun.lockb b/test/bun.lockb new file mode 100755 index 00000000..13057c99 Binary files /dev/null and b/test/bun.lockb differ diff --git a/test/cli/handlers/launch/index.ts b/test/cli/handlers/launch/index.ts index 85d96c2d..61d3ebff 100644 --- a/test/cli/handlers/launch/index.ts +++ b/test/cli/handlers/launch/index.ts @@ -8,7 +8,7 @@ import { launchKurtosis } from "./kurtosis"; import { LaunchedNetwork } from "./launchedNetwork"; import { launchRelayers } from "./relayer"; import { performSummaryOperations } from "./summary"; -import { performValidatorOperations } from "./validator"; +import { performValidatorOperations, performValidatorSetUpdate } from "./validator"; // Non-optional properties determined by having default values export interface LaunchOptions { @@ -93,6 +93,8 @@ const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedN setParameters: options.setParameters }); + await performValidatorSetUpdate(options, launchedNetwork.elRpcUrl, contractsDeployed); + await performSummaryOperations(options, launchedNetwork); const fullEnd = performance.now(); const fullMinutes = ((fullEnd - timeStart) / (1000 * 60)).toFixed(1); diff --git a/test/cli/handlers/launch/relayer.ts b/test/cli/handlers/launch/relayer.ts index 3afe6e91..5f346376 100644 --- a/test/cli/handlers/launch/relayer.ts +++ b/test/cli/handlers/launch/relayer.ts @@ -40,6 +40,7 @@ const RELAYER_CONFIG_DIR = "tmp/configs"; const RELAYER_CONFIG_PATHS = { BEACON: path.join(RELAYER_CONFIG_DIR, "beacon-relay.json"), BEEFY: path.join(RELAYER_CONFIG_DIR, "beefy-relay.json"), + EXECUTION: path.join(RELAYER_CONFIG_DIR, "execution-relay.json"), SOLOCHAIN: path.join(RELAYER_CONFIG_DIR, "solochain-relay.json") }; const INITIAL_CHECKPOINT_FILE = "dump-initial-checkpoint.json"; @@ -146,6 +147,15 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La type: "substrate", value: SUBSTRATE_FUNDED_ACCOUNTS.CHARLETH.privateKey } + }, + { + name: "relayer-⚙️", + type: "execution", + config: RELAYER_CONFIG_PATHS.EXECUTION, + pk: { + type: "substrate", + value: SUBSTRATE_FUNDED_ACCOUNTS.BALTATHAR.privateKey + } } ]; @@ -179,51 +189,60 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La ); switch (type) { - case "beacon": - { - const cfg = parseRelayConfig(json, type); - cfg.source.beacon.endpoint = `http://host.docker.internal:${ethHttpPort}`; - cfg.source.beacon.stateEndpoint = `http://host.docker.internal:${ethHttpPort}`; - cfg.source.beacon.datastore.location = "/data"; - cfg.sink.parachain.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`; + case "beacon": { + const cfg = parseRelayConfig(json, type); + cfg.source.beacon.endpoint = `http://host.docker.internal:${ethHttpPort}`; + cfg.source.beacon.stateEndpoint = `http://host.docker.internal:${ethHttpPort}`; + cfg.source.beacon.datastore.location = "/data"; + cfg.sink.parachain.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`; - await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4)); - logger.success(`Updated beacon config written to ${outputFilePath}`); - } + await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4)); + logger.success(`Updated beacon config written to ${outputFilePath}`); break; - case "beefy": - { - const cfg = parseRelayConfig(json, type); - cfg.source.polkadot.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`; - cfg.sink.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`; - cfg.sink.contracts.BeefyClient = beefyClientAddress; - cfg.sink.contracts.Gateway = gatewayAddress; - await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4)); - logger.success(`Updated beefy config written to ${outputFilePath}`); - } + } + case "beefy": { + const cfg = parseRelayConfig(json, type); + cfg.source.polkadot.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`; + cfg.sink.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`; + cfg.sink.contracts.BeefyClient = beefyClientAddress; + cfg.sink.contracts.Gateway = gatewayAddress; + await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4)); + logger.success(`Updated beefy config written to ${outputFilePath}`); break; - case "solochain": - { - const cfg = parseRelayConfig(json, type); - cfg.source.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`; - cfg.source.solochain.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`; - cfg.source.contracts.BeefyClient = beefyClientAddress; - cfg.source.contracts.Gateway = gatewayAddress; - cfg.source.beacon.endpoint = `http://host.docker.internal:${ethHttpPort}`; - cfg.source.beacon.stateEndpoint = `http://host.docker.internal:${ethHttpPort}`; - cfg.source.beacon.datastore.location = datastorePath; - cfg.sink.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`; - cfg.sink.contracts.Gateway = gatewayAddress; - await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4)); - logger.success(`Updated solochain config written to ${outputFilePath}`); - } + } + case "execution": { + const cfg = parseRelayConfig(json, type); + cfg.source.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`; + cfg.source.beacon.endpoint = `http://host.docker.internal:${ethHttpPort}`; + cfg.source.beacon.stateEndpoint = `http://host.docker.internal:${ethHttpPort}`; + cfg.source.beacon.datastore.location = "/data"; + cfg.sink.parachain.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`; + cfg.source.contracts.Gateway = gatewayAddress; + await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4)); + logger.success(`Updated execution config written to ${outputFilePath}`); break; + } + case "solochain": { + const cfg = parseRelayConfig(json, type); + cfg.source.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`; + cfg.source.solochain.endpoint = `ws://${substrateNodeId}:${substrateWsPort}`; + cfg.source.contracts.BeefyClient = beefyClientAddress; + cfg.source.contracts.Gateway = gatewayAddress; + cfg.source.beacon.endpoint = `http://host.docker.internal:${ethHttpPort}`; + cfg.source.beacon.stateEndpoint = `http://host.docker.internal:${ethHttpPort}`; + cfg.source.beacon.datastore.location = datastorePath; + cfg.sink.ethereum.endpoint = `ws://host.docker.internal:${ethWsPort}`; + cfg.sink.contracts.Gateway = gatewayAddress; + await Bun.write(outputFilePath, JSON.stringify(cfg, null, 4)); + logger.success(`Updated solochain config written to ${outputFilePath}`); + break; + } } } invariant(options.relayerImageTag, "❌ Relayer image tag not defined"); - await initEthClientPallet(options, launchedNetwork); + await initEthClientPallet(options, launchedNetwork, datastorePath); for (const { config, name, type, pk, secondaryPk } of relayersToStart) { try { @@ -250,10 +269,10 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La ]; const volumeMounts: string[] = ["-v", `${hostConfigFilePath}:${containerConfigFilePath}`]; + const hostDatastorePath = path.resolve(datastorePath); + const containerDatastorePath = "/data"; - if (type === "beacon") { - const hostDatastorePath = path.resolve(datastorePath); - const containerDatastorePath = "/data"; + if (type === "beacon" || type === "execution") { volumeMounts.push("-v", `${hostDatastorePath}:${containerDatastorePath}`); } @@ -262,7 +281,7 @@ export const launchRelayers = async (options: LaunchOptions, launchedNetwork: La type, "--config", config, - type === "beacon" ? "--substrate.private-key" : "--ethereum.private-key", + `--${pk.type}.private-key`, pk.value ]; @@ -366,11 +385,13 @@ const waitBeefyReady = async ( * * @param options - Launch options containing the relayer binary path. * @param launchedNetwork - An instance of LaunchedNetwork to interact with the running network. + * @param datastorePath - The path to the datastore directory. * @throws If there's an error generating the beacon checkpoint or submitting it to Substrate. */ export const initEthClientPallet = async ( options: LaunchOptions, - launchedNetwork: LaunchedNetwork + launchedNetwork: LaunchedNetwork, + datastorePath: string ) => { logger.debug("Initialising eth client pallet"); // Poll the beacon chain until it's ready every 10 seconds for 5 minutes @@ -393,9 +414,11 @@ export const initEthClientPallet = async ( launchedNetwork.networkName, "❌ Docker network name not found in LaunchedNetwork instance" ); + const datastoreHostPath = path.resolve(datastorePath); const command = `docker run \ -v ${beaconConfigHostPath}:${beaconConfigContainerPath}:ro \ -v ${checkpointHostPath}:${checkpointContainerPath} \ + -v ${datastoreHostPath}:/data \ --name generate-beacon-checkpoint \ --workdir /app \ --add-host host.docker.internal:host-gateway \ @@ -409,7 +432,9 @@ export const initEthClientPallet = async ( const initialCheckpointFile = Bun.file(INITIAL_CHECKPOINT_PATH); const initialCheckpointRaw = await initialCheckpointFile.text(); const initialCheckpoint = parseJsonToBeaconCheckpoint(JSON.parse(initialCheckpointRaw)); - await initialCheckpointFile.delete(); + if (initialCheckpointFile.delete) { + await initialCheckpointFile.delete(); + } logger.trace("Initial checkpoint:"); logger.trace(initialCheckpoint.toJSON()); diff --git a/test/cli/handlers/launch/validator.ts b/test/cli/handlers/launch/validator.ts index 872e6a4c..4e59ca4a 100644 --- a/test/cli/handlers/launch/validator.ts +++ b/test/cli/handlers/launch/validator.ts @@ -62,37 +62,49 @@ export const performValidatorOperations = async ( await setupValidators({ rpcUrl: networkRpcUrl }); + } +}; - // If not specified, prompt for update - let shouldUpdateValidatorSet = options.updateValidatorSet; - if (shouldUpdateValidatorSet === undefined) { - shouldUpdateValidatorSet = await confirmWithTimeout( - "Do you want to update the validator set on the substrate chain?", - true, - 10 - ); - } else { - logger.info( - `🏳️ Using flag option: ${shouldUpdateValidatorSet ? "will update" : "will not update"} validator set` - ); - } - - if (shouldUpdateValidatorSet) { - if (!contractsDeployed) { - logger.warn( - "⚠️ Updating validator set but contracts were not deployed in this CLI run. Could have unexpected results." - ); - } - - await updateValidatorSet({ - rpcUrl: networkRpcUrl - }); - } else { - logger.info("👍 Skipping validator set update"); - printDivider(); - } +/** + * Performs the validator set update operation based on user options + * This function is now separate so it can be called after relayers are set up + * + * @param options - CLI options for the validator set update + * @param networkRpcUrl - RPC URL for the Ethereum network + * @param contractsDeployed - Flag indicating if contracts were deployed in this CLI run + * @returns Promise resolving when the operation is complete + */ +export const performValidatorSetUpdate = async ( + options: LaunchOptions, + networkRpcUrl: string, + contractsDeployed: boolean +) => { + // If not specified, prompt for update + let shouldUpdateValidatorSet = options.updateValidatorSet; + if (shouldUpdateValidatorSet === undefined) { + shouldUpdateValidatorSet = await confirmWithTimeout( + "Do you want to update the validator set on the substrate chain?", + true, + 10 + ); } else { - logger.info("👍 Skipping validator setup"); + logger.info( + `🏳️ Using flag option: ${shouldUpdateValidatorSet ? "will update" : "will not update"} validator set` + ); + } + + if (shouldUpdateValidatorSet) { + if (!contractsDeployed) { + logger.warn( + "⚠️ Updating validator set but contracts were not deployed in this CLI run. Could have unexpected results." + ); + } + + await updateValidatorSet({ + rpcUrl: networkRpcUrl + }); + } else { + logger.info("👍 Skipping validator set update"); printDivider(); } }; diff --git a/test/configs/snowbridge/execution-relay.json b/test/configs/snowbridge/execution-relay.json index 7649b471..091c7482 100644 --- a/test/configs/snowbridge/execution-relay.json +++ b/test/configs/snowbridge/execution-relay.json @@ -6,7 +6,6 @@ "contracts": { "Gateway": "" }, - "channel-id": "", "beacon": { "endpoint": "http://127.0.0.1:9596", "stateEndpoint": "http://127.0.0.1:9596", diff --git a/test/package.json b/test/package.json index 07b0e85d..ff5a59c6 100644 --- a/test/package.json +++ b/test/package.json @@ -17,6 +17,7 @@ "start:e2e:local": "LOG_LEVEL=debug bun start:e2e:ci --bd", "start:e2e:ci": "bun cli launch --datahaven --no-build-datahaven --launch-kurtosis --deploy-contracts --fund-validators --setup-validators --update-validator-set --relayer --clean-network", "start:e2e:minrelayer": "bun cli launch --relayer --deploy-contracts --no-setup-validators --no-update-validator-set --no-fund-validators --datahaven", + "start:all": "bun cli launch --datahaven --build-datahaven --launch-kurtosis --deploy-contracts --fund-validators --setup-validators --update-validator-set --relayer --blockscout --verified --clean-network --set-parameters", "stop:docker:datahaven": "docker rm -f $(docker ps -aq --filter name='^datahaven-') 2>/dev/null || true; docker network rm datahaven-net || true", "stop:docker:relayer": "docker rm -f $(docker ps -aq --filter name='^snowbridge-relayer-') 2>/dev/null || true", "stop:e2e": "bun cli stop --all", diff --git a/test/utils/parser.ts b/test/utils/parser.ts index 36e8e498..692a297e 100644 --- a/test/utils/parser.ts +++ b/test/utils/parser.ts @@ -105,9 +105,59 @@ export const SolochainRelayConfigSchema = z.object({ apiKey: z.string() }) }); + export type SolochainRelayConfig = z.infer; -export type RelayerType = "beefy" | "beacon" | "solochain"; +export const ExecutionRelayConfigSchema = z.object({ + source: z.object({ + ethereum: z.object({ + endpoint: z.string() + }), + contracts: z.object({ + Gateway: z.string() + }), + beacon: z.object({ + endpoint: z.string(), + stateEndpoint: z.string(), + spec: z.object({ + syncCommitteeSize: z.number(), + slotsInEpoch: z.number(), + epochsPerSyncCommitteePeriod: z.number(), + forkVersions: z.object({ + deneb: z.number(), + electra: z.number() + }) + }), + datastore: z.object({ + location: z.string(), + maxEntries: z.number() + }) + }) + }), + sink: z.object({ + parachain: z.object({ + endpoint: z.string(), + maxWatchedExtrinsics: z.number(), + headerRedundancy: z.number() + }) + }), + instantVerification: z.boolean(), + schedule: z.object({ + id: z.number().nullable(), + totalRelayerCount: z.number(), + sleepInterval: z.number() + }), + ofac: z + .object({ + enabled: z.boolean(), + apiKey: z.string() + }) + .optional() +}); + +export type ExecutionRelayConfig = z.infer; + +export type RelayerType = "beefy" | "beacon" | "solochain" | "execution"; /** * Parse beacon relay configuration @@ -143,28 +193,39 @@ function parseSolochainConfig(config: unknown): SolochainRelayConfig { } /** - * Type Guard to check if a config object is a BeaconRelayConfig + * Parse execution relay configuration */ -export function isBeaconConfig( - config: BeaconRelayConfig | BeefyRelayConfig -): config is BeaconRelayConfig { - return "beacon" in config.source; +function parseExecutionConfig(config: unknown): ExecutionRelayConfig { + const result = ExecutionRelayConfigSchema.safeParse(config); + if (result.success) { + return result.data; + } + throw new Error(`Failed to parse config as ExecutionRelayConfig: ${result.error.message}`); } export function parseRelayConfig(config: unknown, type: "beacon"): BeaconRelayConfig; export function parseRelayConfig(config: unknown, type: "beefy"): BeefyRelayConfig; +export function parseRelayConfig(config: unknown, type: "execution"): ExecutionRelayConfig; export function parseRelayConfig(config: unknown, type: "solochain"): SolochainRelayConfig; export function parseRelayConfig( config: unknown, type: RelayerType -): BeaconRelayConfig | BeefyRelayConfig | SolochainRelayConfig; +): BeaconRelayConfig | BeefyRelayConfig | ExecutionRelayConfig | SolochainRelayConfig; + export function parseRelayConfig( config: unknown, type: RelayerType -): BeaconRelayConfig | BeefyRelayConfig | SolochainRelayConfig { - return type === "beacon" - ? parseBeaconConfig(config) - : type === "beefy" - ? parseBeefyConfig(config) - : parseSolochainConfig(config); +): BeaconRelayConfig | BeefyRelayConfig | ExecutionRelayConfig | SolochainRelayConfig { + switch (type) { + case "beacon": + return parseBeaconConfig(config); + case "beefy": + return parseBeefyConfig(config); + case "execution": + return parseExecutionConfig(config); + case "solochain": + return parseSolochainConfig(config); + default: + throw new Error(`Unknown relayer type: ${type}`); + } }