datahaven/test/suites/validator-set-update.test.ts
Gonza Montiel dc0f0673e2
test: Update validator set e2e test (#126)
## Add E2E validator-set update flow

- feat: `test/utils/validators.ts` for on-demand validator
orchestration.
- feat: `test/suites/validator-set-update.test.ts` covering allowlist →
register → update.
- some minor launcher updates: avoid docker cache, add `--platform` when
building datahaven image, avoid sending validator-set update on launch.
- Helpers: ABI shortcut in `test/utils/contracts.ts`; config tweaks in
`test/configs/validator-set.json`.
- Minor cleanup/formatting across `test/launcher/*`,
`test/scripts/setup-validators.ts`, and related tests.
- added `keepAlive` flag to `BaseTestSuite`, in order to avoid tearing
down the network while debugging. Defaults, obviously, to false.
- added a `failOnTomeout` option on to waitForDataHavenEvents() so the
test fails of the timeout is reached and no event was captured.

### Coverage
- The test simulates an scenario in which we have two active authorities
(alice and bob), which are running, and registered as operators, which
is the normal state after the chain launches. Then:
- It launches two more nodes (charlie and dave)
- It add the nodes to allowlist and register them as operators
- It sends the validator set update message
- Checks that the validator update message was propagated through the
gateway and arrived the external-validators pallet
- Checks that the chain continues producing blocks
 
### Notes
The last test case has a timeout of 10 minutes. This is to respect
propagation times of the message through the relayers. We are testing
that the external validators pallet actually updated the validator set.
Locally, I could expect 5~6 minutes, I just wanted to be on the safe
side. CI is passing showing that this was enough indeed.

---------

Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
2025-10-02 11:23:40 +00:00

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