mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
## Add StorageHub nodes to CLI This PR adds StorageHub node infrastructure to the CLI and fixes CLI flag handling and improves the CLI logic a bit. ### Fix: CLI safeguards - Prevents contract deployment and validator operations when `--ndc` flag is used - Skips Kurtosis service display in summary when `--nlk` flag is used ### Feat: Dockerized Storage Hub Nodes - Adds 5 Docker containers: PostgreSQL, MSP, BSP, Indexer, and Fisherman nodes - New CLI flags: `--storagehub` `--no-storagehub` to control StorageHub nodes launch - Automatic provider funding and registration (Charleth for MSP and Dorothy for BSP) - Exposes nodes on ports 9945-9948 for local development **TODO** - [x] MSP & BSP associated pre-funded account. - [x] Call `forceMspSignUp` and `forceBspSignUp` extrinsics --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
389 lines
14 KiB
TypeScript
389 lines
14 KiB
TypeScript
/**
|
|
* Validator Set Update E2E: Ethereum → Snowbridge → DataHaven
|
|
*
|
|
* Exercises:
|
|
* - Start network and ensure 4 validator nodes are running (Alice, Bob, Charlie, Dave).
|
|
* - Confirm initial mapping exists only for Alice/Bob on `ServiceManager`.
|
|
* - Allowlist and register Charlie/Dave as operators on Ethereum.
|
|
* - Send updated validator set via `ServiceManager.sendNewValidatorSet`, assert Gateway `OutboundMessageAccepted`.
|
|
* - Observe `ExternalValidators.ExternalValidatorsSet` on DataHaven (substrate), confirming propagation.
|
|
*/
|
|
import { beforeAll, describe, expect, it } from "bun:test";
|
|
import {
|
|
addValidatorToAllowlist,
|
|
getOwnerAccount,
|
|
isValidatorInAllowlist,
|
|
registerSingleOperator,
|
|
serviceManagerHasOperator
|
|
} from "launcher/validators";
|
|
import {
|
|
type Deployments,
|
|
getValidatorInfoByName,
|
|
isValidatorNodeRunning,
|
|
launchDatahavenValidator,
|
|
logger,
|
|
parseDeploymentsFile,
|
|
TestAccounts,
|
|
type ValidatorInfo
|
|
} from "utils";
|
|
import { waitForDataHavenEvent } from "utils/events";
|
|
import { waitForDataHavenStorageContains } from "utils/storage";
|
|
import { decodeEventLog, parseEther } from "viem";
|
|
import { dataHavenServiceManagerAbi, gatewayAbi } from "../contract-bindings";
|
|
import { BaseTestSuite } from "../framework";
|
|
|
|
class ValidatorSetUpdateTestSuite extends BaseTestSuite {
|
|
constructor() {
|
|
super({
|
|
suiteName: "validator-set-update",
|
|
networkOptions: {
|
|
slotTime: 2,
|
|
blockscout: false
|
|
}
|
|
});
|
|
|
|
this.setupHooks();
|
|
}
|
|
|
|
override async onSetup(): Promise<void> {
|
|
logger.info("Waiting for cross-chain infrastructure to stabilize...");
|
|
|
|
// Launch to new nodes to be authorities
|
|
console.log("Launching Charlie...");
|
|
await launchDatahavenValidator(TestAccounts.Charlie, {
|
|
launchedNetwork: this.getConnectors().launchedNetwork
|
|
});
|
|
|
|
console.log("Launching Dave...");
|
|
await launchDatahavenValidator(TestAccounts.Dave, {
|
|
launchedNetwork: this.getConnectors().launchedNetwork
|
|
});
|
|
}
|
|
|
|
public getNetworkId(): string {
|
|
return this.getConnectors().launchedNetwork.networkId;
|
|
}
|
|
|
|
public getValidatorOptions() {
|
|
return {
|
|
rpcUrl: this.getConnectors().launchedNetwork.elRpcUrl,
|
|
connectors: this.getTestConnectors(),
|
|
deployments
|
|
};
|
|
}
|
|
}
|
|
|
|
// Create the test suite instance
|
|
const suite = new ValidatorSetUpdateTestSuite();
|
|
let deployments: Deployments;
|
|
|
|
describe("Validator Set Update", () => {
|
|
// Validator sets loaded from external JSON
|
|
let initialValidators: ValidatorInfo[] = [];
|
|
let newValidators: ValidatorInfo[] = [];
|
|
|
|
beforeAll(async () => {
|
|
deployments = await parseDeploymentsFile();
|
|
|
|
// Load validator set from JSON config
|
|
const validatorSetPath = "./configs/validator-set.json";
|
|
try {
|
|
const validatorSetJson: any = await Bun.file(validatorSetPath).json();
|
|
|
|
initialValidators = [
|
|
getValidatorInfoByName(validatorSetJson, TestAccounts.Alice),
|
|
getValidatorInfoByName(validatorSetJson, TestAccounts.Bob)
|
|
];
|
|
|
|
newValidators = [
|
|
getValidatorInfoByName(validatorSetJson, TestAccounts.Charlie),
|
|
getValidatorInfoByName(validatorSetJson, TestAccounts.Dave)
|
|
];
|
|
|
|
logger.success("Loaded validator set from JSON file");
|
|
} catch (err) {
|
|
logger.error(`Failed to load validator set from ${validatorSetPath}: ${err}`);
|
|
throw err;
|
|
}
|
|
});
|
|
|
|
it("should verify validators are running", async () => {
|
|
const isAliceRunning = await isValidatorNodeRunning(TestAccounts.Alice, suite.getNetworkId());
|
|
const isBobRunning = await isValidatorNodeRunning(TestAccounts.Bob, suite.getNetworkId());
|
|
const isCharlieRunning = await isValidatorNodeRunning(
|
|
TestAccounts.Charlie,
|
|
suite.getNetworkId()
|
|
);
|
|
const isDaveRunning = await isValidatorNodeRunning(TestAccounts.Dave, suite.getNetworkId());
|
|
|
|
expect(isAliceRunning).toBe(true);
|
|
expect(isBobRunning).toBe(true);
|
|
expect(isCharlieRunning).toBe(true);
|
|
expect(isDaveRunning).toBe(true);
|
|
});
|
|
|
|
it("should verify initial test setup", async () => {
|
|
const connectors = suite.getTestConnectors();
|
|
|
|
// Verify Ethereum side connectivity
|
|
const ethBlockNumber = await connectors.publicClient.getBlockNumber();
|
|
expect(ethBlockNumber).toBeGreaterThan(0);
|
|
logger.success(`Ethereum network connected at block: ${ethBlockNumber}`);
|
|
|
|
// Verify DataHaven substrate connectivity
|
|
const dhBlockHeader = await connectors.papiClient.getBlockHeader();
|
|
expect(dhBlockHeader.number).toBeGreaterThan(0);
|
|
logger.success(`DataHaven substrate connected at block: ${dhBlockHeader.number}`);
|
|
|
|
// Verify contract deployments
|
|
expect(deployments.ServiceManager).toBeDefined();
|
|
logger.success(`ServiceManager deployed at: ${deployments.ServiceManager}`);
|
|
});
|
|
|
|
it("should verify initial validator set state", async () => {
|
|
const connectors = suite.getTestConnectors();
|
|
|
|
logger.info("🔍 Verifying initial validator set state...");
|
|
|
|
// Check that only initial validators have mappings set
|
|
for (const validator of initialValidators) {
|
|
const solochainAddress = await connectors.publicClient.readContract({
|
|
address: deployments.ServiceManager as `0x${string}`,
|
|
abi: dataHavenServiceManagerAbi,
|
|
functionName: "validatorEthAddressToSolochainAddress",
|
|
args: [validator.publicKey as `0x${string}`]
|
|
});
|
|
|
|
expect(solochainAddress.toLowerCase()).toBe(validator.solochainAddress.toLowerCase());
|
|
logger.success(`Validator ${validator.publicKey} mapped to ${solochainAddress}`);
|
|
}
|
|
});
|
|
|
|
it("should verify new validators are not yet registered", async () => {
|
|
const connectors = suite.getTestConnectors();
|
|
|
|
// Verify that new validators are not yet registered
|
|
for (const validator of newValidators) {
|
|
const solochainAddress = await connectors.publicClient.readContract({
|
|
address: deployments.ServiceManager as `0x${string}`,
|
|
abi: dataHavenServiceManagerAbi,
|
|
functionName: "validatorEthAddressToSolochainAddress",
|
|
args: [validator.publicKey as `0x${string}`]
|
|
});
|
|
|
|
expect(solochainAddress).toBe("0x0000000000000000000000000000000000000000");
|
|
logger.success(`Validator ${validator.publicKey} not yet registered (as expected)`);
|
|
}
|
|
|
|
logger.success("Initial validator set state verified: only Alice and Bob are active");
|
|
});
|
|
|
|
it("should add new validators to allowlist", async () => {
|
|
logger.info("📤 Adding Charlie and Dave to allowlist...");
|
|
|
|
// Add Charlie and Dave to the allowlist
|
|
await addValidatorToAllowlist(TestAccounts.Charlie, suite.getValidatorOptions());
|
|
await addValidatorToAllowlist(TestAccounts.Dave, suite.getValidatorOptions());
|
|
|
|
// Verification of allowlist status
|
|
logger.info("🔍 Verification of allowlist status...");
|
|
const charlieAllowlisted = await isValidatorInAllowlist(
|
|
TestAccounts.Charlie,
|
|
suite.getValidatorOptions()
|
|
);
|
|
const daveAllowlisted = await isValidatorInAllowlist(
|
|
TestAccounts.Dave,
|
|
suite.getValidatorOptions()
|
|
);
|
|
|
|
expect(charlieAllowlisted).toBe(true);
|
|
expect(daveAllowlisted).toBe(true);
|
|
|
|
logger.success("Both validators successfully added to allowlist");
|
|
}, 60_000);
|
|
|
|
it("should register new validators as operators", async () => {
|
|
logger.info("📤 Registering Charlie and Dave as operators...");
|
|
|
|
// Register Charlie and Dave as operators
|
|
await registerSingleOperator(TestAccounts.Charlie, suite.getValidatorOptions());
|
|
await registerSingleOperator(TestAccounts.Dave, suite.getValidatorOptions());
|
|
|
|
// Verify both validators are properly registered in ServiceManager
|
|
const charlieRegistered = await serviceManagerHasOperator(
|
|
TestAccounts.Charlie,
|
|
suite.getValidatorOptions()
|
|
);
|
|
expect(charlieRegistered).toBe(true);
|
|
logger.success("Charlie is registered as operator");
|
|
|
|
const daveRegistered = await serviceManagerHasOperator(
|
|
TestAccounts.Dave,
|
|
suite.getValidatorOptions()
|
|
);
|
|
expect(daveRegistered).toBe(true);
|
|
logger.success("Dave is registered as operator");
|
|
}, 60_000); // 1 minute timeout
|
|
|
|
it("should send updated validator set to DataHaven", async () => {
|
|
const connectors = suite.getTestConnectors();
|
|
|
|
// proceed directly to sending, allowlist/register already covered in previous tests
|
|
logger.info("📤 Sending updated validator set (Charlie, Dave) to DataHaven...");
|
|
|
|
// Build the updated validator set message
|
|
// Debug: Check what validators are registered in the ServiceManager contract
|
|
logger.info("🔍 Checking registered validators in DataHavenServiceManager...");
|
|
|
|
// Check all validators (initial + new)
|
|
const allValidators = [...initialValidators, ...newValidators];
|
|
for (const validator of allValidators) {
|
|
const registeredAddress = await connectors.publicClient.readContract({
|
|
address: deployments.ServiceManager as `0x${string}`,
|
|
abi: dataHavenServiceManagerAbi,
|
|
functionName: "validatorEthAddressToSolochainAddress",
|
|
args: [validator.publicKey as `0x${string}`]
|
|
});
|
|
|
|
const isRegistered = registeredAddress !== "0x0000000000000000000000000000000000000000";
|
|
logger.info(` ${validator.publicKey} -> ${registeredAddress} (registered: ${isRegistered})`);
|
|
}
|
|
|
|
logger.info("🔍 Building validator set message...");
|
|
const updatedMessageBytes = await connectors.publicClient.readContract({
|
|
address: deployments.ServiceManager as `0x${string}`,
|
|
abi: dataHavenServiceManagerAbi,
|
|
functionName: "buildNewValidatorSetMessage",
|
|
args: []
|
|
});
|
|
|
|
logger.info(`📊 Updated validator set message size: ${updatedMessageBytes.length} bytes`);
|
|
logger.info(`📊 Message bytes (first 100): ${updatedMessageBytes.slice(0, 100)}`);
|
|
|
|
// Verify that new validators are properly registered before sending message
|
|
logger.info("🔍 Verifying new validators are registered before sending message...");
|
|
for (const validator of newValidators) {
|
|
const registeredAddress = await connectors.publicClient.readContract({
|
|
address: deployments.ServiceManager as `0x${string}`,
|
|
abi: dataHavenServiceManagerAbi,
|
|
functionName: "validatorEthAddressToSolochainAddress",
|
|
args: [validator.publicKey as `0x${string}`]
|
|
});
|
|
|
|
const isRegistered = registeredAddress !== "0x0000000000000000000000000000000000000000";
|
|
if (!isRegistered) {
|
|
throw new Error(
|
|
`Validator ${validator.publicKey} is not registered in ServiceManager before sending message`
|
|
);
|
|
}
|
|
logger.success(`${validator.publicKey} is registered -> ${registeredAddress}`);
|
|
}
|
|
|
|
// Log the expected validators that should be in the message
|
|
logger.info("🔍 Expected validators in message:");
|
|
for (let i = 0; i < newValidators.length; i++) {
|
|
logger.info(` Validator ${i}: ${newValidators[i].solochainAddress}`);
|
|
}
|
|
|
|
// Send the updated validator set
|
|
const executionFee = parseEther("0.1");
|
|
const relayerFee = parseEther("0.2");
|
|
const totalValue = parseEther("0.3");
|
|
|
|
logger.info(
|
|
`Sending validator set with executionFee=${executionFee},
|
|
relayerFee=${relayerFee},
|
|
totalValue=${totalValue}`
|
|
);
|
|
|
|
try {
|
|
const hash = await connectors.walletClient.writeContract({
|
|
address: deployments.ServiceManager as `0x${string}`,
|
|
abi: dataHavenServiceManagerAbi,
|
|
functionName: "sendNewValidatorSet",
|
|
args: [executionFee, relayerFee],
|
|
value: totalValue,
|
|
gas: 1000000n,
|
|
account: getOwnerAccount(),
|
|
chain: null
|
|
});
|
|
|
|
logger.info(`📝 Transaction hash for validator set update: ${hash}`);
|
|
|
|
const receipt = await connectors.publicClient.waitForTransactionReceipt({ hash });
|
|
logger.info(
|
|
`📋 Validator set update receipt: status=${receipt.status}, gasUsed=${receipt.gasUsed}`
|
|
);
|
|
|
|
if (receipt.status === "success") {
|
|
logger.success(`Transaction sent: ${hash}`);
|
|
logger.info(`⛽ Gas used: ${receipt.gasUsed}`);
|
|
} else {
|
|
logger.error(`Transaction failed with status: ${receipt.status}`);
|
|
throw new Error(`Transaction failed with status: ${receipt.status}`);
|
|
}
|
|
|
|
logger.info("🔍 Checking for OutboundMessageAccepted event in transaction receipt...");
|
|
|
|
const hasOutboundAccepted = (receipt.logs ?? []).some((log: any) => {
|
|
try {
|
|
const decoded = decodeEventLog({ abi: gatewayAbi, data: log.data, topics: log.topics });
|
|
return decoded.eventName === "OutboundMessageAccepted";
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (hasOutboundAccepted) {
|
|
logger.success("OutboundMessageAccepted event found in transaction receipt!");
|
|
} else {
|
|
throw new Error("OutboundMessageAccepted event not found in transaction receipt");
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Error sending validator set update: ${error}`);
|
|
throw error;
|
|
}
|
|
}, 300_000);
|
|
|
|
it("should verify validator set update on DataHaven substrate", async () => {
|
|
const connectors = suite.getTestConnectors();
|
|
|
|
logger.info("🔍 Verifying validator set on DataHaven substrate chain...");
|
|
|
|
logger.info("⏳ Waiting for ExternalValidatorsSet event...");
|
|
const externalValidatorsSetEvent = await waitForDataHavenEvent({
|
|
api: connectors.dhApi,
|
|
pallet: "ExternalValidators",
|
|
event: "ExternalValidatorsSet",
|
|
timeout: 600_000,
|
|
failOnTimeout: true
|
|
});
|
|
|
|
if (!externalValidatorsSetEvent.data) {
|
|
logger.error("ExternalValidatorsSet event not found");
|
|
throw new Error("ExternalValidatorsSet event not found");
|
|
}
|
|
logger.success("ExternalValidatorsSet event found");
|
|
|
|
logger.info(
|
|
"🔍 Checking the new validators are present in the ExternalValidators pallet storage..."
|
|
);
|
|
|
|
const expectedAddresses = newValidators.map((v) => v.solochainAddress as `0x${string}`);
|
|
|
|
const storageResult = await waitForDataHavenStorageContains({
|
|
api: connectors.dhApi,
|
|
pallet: "ExternalValidators",
|
|
storage: "ExternalValidators",
|
|
contains: expectedAddresses,
|
|
timeout: 10_000,
|
|
failOnTimeout: true
|
|
});
|
|
|
|
if (!storageResult.value) {
|
|
throw new Error("Failed to get ExternalValidators storage value");
|
|
}
|
|
|
|
logger.success("New validators are present in the ExternalValidators pallet storage");
|
|
}, 600_000);
|
|
});
|