feat: 🏗️ run execution relayer (#73)

## This PR includes:
- Running the execution relayer on the CLI
- Modifying the Payload generation in the `DataHavenServiceManager.sol`
- Modified the `EigenLayerMessageProcessor` to work with the
ValidatorSet update message, but for this change we are loosing the
generic message type (it was the only way to make it work so far).
- Adds a `--no-wait` argument to the cli launch and stop commands to
bootstrap faster.

### Testing the Snowbridge message encoding / decoding
- Added`MessageEncoding.t.sol` is documented and explains how to
generate bin data to use in the rust test.
- Added a Rust unit test to `EigenLayerMessageProcessor` to compare the
message encoding/decoding taking the bytes from a file (previously
generated with some mock data).

Specifically, we want that:


3cbca0db6d/contracts/src/libraries/DataHavenSnowbridgeMessages.sol (L78-L85)

Generates the right bytes encoding for
0e2c9cd518/operator/primitives/bridge/src/lib.rs (L51)

If the test passes, it's very likely that the CLI will also pass, if
not, then we might wanna check something else is missing.

### Breaking change ⚠️ 
For compatibility reasons with Snowbridge contracts (they call specific
extrinsics of specific pallets), I had to rename:
- `InboundQueueV2` -> `EthereumInboundQueueV2`
- `OutboundQueueV2` -> `EthereumOutboundQueueV2`

## For follow up PRs:

- Add an automated way of generating the Solidity bytes fo testing, so
we don't need to maintain the MessageEncoding.t.sol and generate the
binary data manually.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added support for the "execution" relayer type in relay configuration,
parsing, and CLI launch utilities.
- Introduced a Solidity test contract for encoding and logging validator
set messages.
- Added a comprehensive "start:all" script to streamline launching and
setup processes.

- **Enhancements**
- Improved message encoding for validator set updates, aligning with new
struct field names and message formats.
- Updated relay configuration schema and validation to support execution
relayers and OFAC settings.
- Increased beacon datastore capacity and adjusted relay scheduling
parameters in configuration files.

- **Refactor**
- Renamed runtime type aliases for inbound/outbound queues to more
descriptive names across mainnet, stagenet, and testnet.
- Centralized and streamlined validator set update logic in CLI
utilities.
- Centralized message decoding logic and improved visibility of message
fields in Rust components.

- **Bug Fixes**
- Improved error handling and decoding logic for message processing in
Rust components.

- **Tests**
- Added Rust and Solidity tests for message encoding and processing
validation.

- **Chores**
- Updated dependencies and feature flags in Rust project configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
This commit is contained in:
Gonza Montiel 2025-06-05 17:00:03 +02:00 committed by GitHub
parent 9f55e10339
commit f07afda0b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 414 additions and 137 deletions

View file

@ -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,

View file

@ -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))
);
}
}

View file

@ -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
}
}

1
operator/Cargo.lock generated
View file

@ -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",

View file

@ -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",
]

View file

@ -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<T>
where
T: pallet_external_validators::Config,
{
message: Message<T>,
message_id: [u8; 4],
pub message: Message<T>,
pub message_id: [u8; 4],
}
#[derive(Encode, Decode)]
@ -38,6 +40,20 @@ where
/// EigenLayer Message Processor
pub struct EigenLayerMessageProcessor<T>(PhantomData<T>);
impl<T> EigenLayerMessageProcessor<T>
where
T: pallet_external_validators::Config,
{
pub fn decode_message(mut payload: &[u8]) -> Result<Payload<T>, DispatchError> {
let decode_result = Payload::<T>::decode_all(&mut payload);
if let Ok(payload) = decode_result {
Ok(payload)
} else {
Err(DispatchError::Other("unable to parse the message payload"))
}
}
}
impl<T, AccountId> MessageProcessor<AccountId> for EigenLayerMessageProcessor<T>
where
T: pallet_external_validators::Config,
@ -50,7 +66,7 @@ where
network: _,
} => return false,
};
let decode_result = Payload::<T>::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::<T>::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 {

View file

@ -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<AggregateMessageOrigin>;
@ -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<AccountId, RootLocation>;
type GovernanceOrigin = EnsureRootWithSuccess<AccountId, RootLocation>;
type WeightInfo = ();
@ -956,10 +957,10 @@ impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapt
}
fn validate(message: Self::Message) -> Result<Self::Ticket, SendError> {
OutboundQueueV2::validate(&message)
EthereumOutboundQueueV2::validate(&message)
}
fn deliver(message: Self::Ticket) -> Result<H256, SendError> {
OutboundQueueV2::deliver(message)
EthereumOutboundQueueV2::deliver(message)
}
}

View file

@ -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;

View file

@ -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<AggregateMessageOrigin>;
@ -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<AccountId, RootLocation>;
type GovernanceOrigin = EnsureRootWithSuccess<AccountId, RootLocation>;
type WeightInfo = ();
@ -917,10 +918,10 @@ impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapt
}
fn validate(message: Self::Message) -> Result<Self::Ticket, SendError> {
OutboundQueueV2::validate(&message)
EthereumOutboundQueueV2::validate(&message)
}
fn deliver(message: Self::Ticket) -> Result<H256, SendError> {
OutboundQueueV2::deliver(message)
EthereumOutboundQueueV2::deliver(message)
}
}

View file

@ -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;

View file

@ -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<AggregateMessageOrigin>;
@ -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<AccountId, RootLocation>;
type GovernanceOrigin = EnsureRootWithSuccess<AccountId, RootLocation>;
type WeightInfo = ();
@ -955,10 +956,10 @@ impl pallet_external_validators_rewards::types::SendMessage for RewardsSendAdapt
}
fn validate(message: Self::Message) -> Result<Self::Ticket, SendError> {
OutboundQueueV2::validate(&message)
EthereumOutboundQueueV2::validate(&message)
}
fn deliver(message: Self::Ticket) -> Result<H256, SendError> {
OutboundQueueV2::deliver(message)
EthereumOutboundQueueV2::deliver(message)
}
}

View file

@ -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<AccountId> = 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::<Runtime> {
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::<Runtime>::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::<Runtime>::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);
}
}
}
}

BIN
test/bun.lockb Executable file

Binary file not shown.

View file

@ -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);

View file

@ -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());

View file

@ -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();
}
};

View file

@ -6,7 +6,6 @@
"contracts": {
"Gateway": ""
},
"channel-id": "",
"beacon": {
"endpoint": "http://127.0.0.1:9596",
"stateEndpoint": "http://127.0.0.1:9596",

View file

@ -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",

View file

@ -105,9 +105,59 @@ export const SolochainRelayConfigSchema = z.object({
apiKey: z.string()
})
});
export type SolochainRelayConfig = z.infer<typeof SolochainRelayConfigSchema>;
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<typeof ExecutionRelayConfigSchema>;
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}`);
}
}