datahaven/test/e2e/suites/validator-set-update.test.ts
Steve Degosserie 17c215d047
refactor(test): reorganize e2e test suites (#373)
## Summary

Reorganizes the test directory structure for better clarity and
maintainability:

- **Rename `test/datahaven/` → `test/moonwall/`**: Clearly identifies
these as Moonwall single-node tests
- **Move `test/framework/` → `test/e2e/framework/`**: Groups e2e test
utilities under a dedicated folder
- **Move `test/suites/` → `test/e2e/suites/`**: Groups e2e test suites
with the framework
- **Add `test/e2e/framework/validators.ts`**: Extracts validator test
helpers from utils into the e2e framework
- **Update documentation**: README.md and E2E_FRAMEWORK_OVERVIEW.md
reflect the new structure

### New Directory Structure

```
test/
├── e2e/
│   ├── suites/          # E2E test suites (Kurtosis-based)
│   └── framework/       # E2E test utilities & helpers
├── moonwall/            # Moonwall single-node tests
├── launcher/            # Network deployment tools
└── ...
```

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 15:52:33 +02:00

206 lines
7.3 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 { getOwnerAccount } from "launcher/validators";
import {
CROSS_CHAIN_TIMEOUTS,
type Deployments,
logger,
parseDeploymentsFile,
ZERO_ADDRESS
} from "utils";
import { waitForDataHavenEvent } from "utils/events";
import { decodeEventLog, parseEther } from "viem";
import { dataHavenServiceManagerAbi, gatewayAbi } from "../../contract-bindings";
import {
addValidatorToAllowlist,
BaseTestSuite,
getValidator,
isValidatorRunning,
launchDatahavenValidator,
registerOperator,
type TestConnectors
} from "../framework";
class ValidatorSetUpdateTestSuite extends BaseTestSuite {
constructor() {
super({
suiteName: "validator-set-update"
});
this.setupHooks();
}
override async onSetup(): Promise<void> {
// Launch two new nodes to be authorities
logger.debug("Launching Charlie and Dave validators...");
const { launchedNetwork } = this.getConnectors();
await Promise.all([
launchDatahavenValidator("charlie", { launchedNetwork }),
launchDatahavenValidator("dave", { launchedNetwork })
]);
}
public getNetworkId(): string {
return this.getConnectors().launchedNetwork.networkId;
}
}
// Create the test suite instance
const suite = new ValidatorSetUpdateTestSuite();
let deployments: Deployments;
let connectors: TestConnectors;
describe("Validator Set Update", () => {
const initialValidators = [getValidator("alice"), getValidator("bob")];
const newValidators = [getValidator("charlie"), getValidator("dave")];
beforeAll(async () => {
deployments = await parseDeploymentsFile();
connectors = suite.getTestConnectors();
});
it("should verify test environment", async () => {
const networkId = suite.getNetworkId();
const { publicClient, papiClient } = connectors;
// Validators running
expect(await isValidatorRunning("alice", networkId)).toBe(true);
expect(await isValidatorRunning("bob", networkId)).toBe(true);
expect(await isValidatorRunning("charlie", networkId)).toBe(true);
expect(await isValidatorRunning("dave", networkId)).toBe(true);
// Chain connectivity
expect(await publicClient.getBlockNumber()).toBeGreaterThan(0);
expect((await papiClient.getBlockHeader()).number).toBeGreaterThan(0);
// Contract deployed
expect(deployments.ServiceManager).toBeDefined();
});
it("should verify initial validator set state", async () => {
const { publicClient } = connectors;
const readSolochainAddress = (validator: (typeof initialValidators)[0]) =>
publicClient.readContract({
address: deployments.ServiceManager as `0x${string}`,
abi: dataHavenServiceManagerAbi,
functionName: "validatorEthAddressToSolochainAddress",
args: [validator.publicKey as `0x${string}`]
});
// Check initial validators have correct mappings and new validators are not registered
const [initialResults, newResults] = await Promise.all([
Promise.all(initialValidators.map(readSolochainAddress)),
Promise.all(newValidators.map(readSolochainAddress))
]);
expect(initialResults).toEqual(
initialValidators.map((v) => v.solochainAddress as `0x${string}`)
);
expect(newResults).toEqual(newValidators.map(() => ZERO_ADDRESS));
});
it("should allowlist and register new validators as operators", async () => {
const opts = { connectors, deployments };
// Add to allowlist sequentially
await addValidatorToAllowlist("charlie", opts);
await addValidatorToAllowlist("dave", opts);
// Register operators in parallel (each uses their own validator account)
await Promise.all([registerOperator("charlie", opts), registerOperator("dave", opts)]);
// Verify allowlist and registration status
const { publicClient } = connectors;
const isAllowlisted = (name: string) =>
publicClient.readContract({
address: deployments.ServiceManager as `0x${string}`,
abi: dataHavenServiceManagerAbi,
functionName: "validatorsAllowlist",
args: [getValidator(name).publicKey as `0x${string}`]
});
const isRegistered = async (name: string) => {
const validator = getValidator(name);
const solochainAddress = await publicClient.readContract({
address: deployments.ServiceManager as `0x${string}`,
abi: dataHavenServiceManagerAbi,
functionName: "validatorEthAddressToSolochainAddress",
args: [validator.publicKey as `0x${string}`]
});
return solochainAddress.toLowerCase() === validator.solochainAddress.toLowerCase();
};
const [charlieAllowlisted, daveAllowlisted, charlieRegistered, daveRegistered] =
await Promise.all([
isAllowlisted("charlie"),
isAllowlisted("dave"),
isRegistered("charlie"),
isRegistered("dave")
]);
expect(charlieAllowlisted).toBe(true);
expect(daveAllowlisted).toBe(true);
expect(charlieRegistered).toBe(true);
expect(daveRegistered).toBe(true);
});
it(
"should send updated validator set and verify on DataHaven",
async () => {
const { publicClient, walletClient, dhApi } = connectors;
// Send the updated validator set via Snowbridge
const hash = await walletClient.writeContract({
address: deployments.ServiceManager as `0x${string}`,
abi: dataHavenServiceManagerAbi,
functionName: "sendNewValidatorSet",
args: [parseEther("0.1"), parseEther("0.2")],
value: parseEther("0.3"),
gas: 1000000n,
account: getOwnerAccount(),
chain: null
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
expect(receipt.status).toBe("success");
// Verify OutboundMessageAccepted event was emitted
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;
}
});
expect(hasOutboundAccepted).toBe(true);
// Wait for the validator set to be updated on Substrate
await waitForDataHavenEvent({
api: dhApi,
pallet: "ExternalValidators",
event: "ExternalValidatorsSet",
timeout: CROSS_CHAIN_TIMEOUTS.ETH_TO_DH_MS
});
// Verify new validators are in storage
const validators = await dhApi.query.ExternalValidators.ExternalValidators.getValue();
const expectedAddresses = newValidators.map((v) => v.solochainAddress.toLowerCase());
for (const address of expectedAddresses) {
expect(validators.some((v) => v.toLowerCase() === address)).toBe(true);
}
},
CROSS_CHAIN_TIMEOUTS.ETH_TO_DH_MS
);
});