Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions test/mocks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Mocks used by `Modules`
25 changes: 25 additions & 0 deletions test/unit/SSVClusters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## SSVClusters Unit Tests

This directory contains unit tests for the SSVClusters module, which handles validator registration and cluster management in the SSV Network.

### Structure

- **`register.test.ts`** - Tests for validator registration functionality
- **`helpers/`** - Helper utilities and test fixtures
- **`types/`** - Type definitions and constants used in tests

### Running Tests

- Run all unit tests under this suite: `npx hardhat test test/unit/SSVClusters`
- Run a single file: `npx hardhat test test/unit/SSVClusters/register.test.ts`
- Run a single test (pattern match): `npx hardhat test test/unit/SSVClusters/register.test.ts --grep "valid registration"`

### Test Coverage

The tests cover:
- Valid validator registration
- Validation error cases (empty public keys, length mismatches, invalid operator IDs, etc.)
- Duplicate registration prevention
- Multiple validator registration in existing clusters

Hardhat will build artifacts on demand; make sure dependencies are installed before running (`npm install`).
14 changes: 14 additions & 0 deletions test/unit/SSVClusters/helpers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## Helper Utilities

This directory contains helper functions and utilities used by SSVClusters unit tests.

### Functions

- **`makePublicKey(seed: number)`** - Generates a deterministic public key from a seed value
- **`makeOperatorKey(seed: number)`** - Generates a deterministic operator key from a seed value
- **`registerOperators(network, owner, count)`** - Registers multiple operators and returns their IDs
- **`asClusterStruct(cluster)`** - Converts a cluster object to the `ClusterStruct` type format
- **`mustEmitEvent(receipt, network, eventName)`** - Extracts the specified event from a transaction receipt and throws an error if the event is not found (asserts the event must exist)
- **`mustNotEmitEvent(receipt, network, eventName)`** - Asserts that the specified event was not emitted in the transaction receipt, throwing an error if it was found

These helpers are used to simplify test setup and data manipulation in the SSVClusters test suite.
64 changes: 64 additions & 0 deletions test/unit/SSVClusters/helpers/clusterHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ClusterStruct } from "../types/cluster.js";

export function makePublicKey(seed: number) {
return `0x${seed.toString(16).padStart(96, "0")}`;
}

export function makeOperatorKey(seed: number) {
return `0x${(seed + 1000).toString(16).padStart(96, "0")}`;
}

export async function registerOperators(network: any, owner: any, count: number) {
const operatorIds: number[] = [];

for (let i = 0; i < count; i += 1) {
const tx = await network
.connect(owner)
.registerOperator(makeOperatorKey(i + 1), 0, false);
await tx.wait();
operatorIds.push(i + 1);
}

return operatorIds;
}

export function asClusterStruct(cluster: any): ClusterStruct {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be avoided, if events would be checked with expect().to.emit(), all contract calls can explicitly expect Cluster type so we will not need to cast types

return {
validatorCount: BigInt(cluster.validatorCount),
networkFeeIndex: BigInt(cluster.networkFeeIndex),
index: BigInt(cluster.index),
balance: BigInt(cluster.balance),
active: Boolean(cluster.active),
};
}

export function mustEmitEvent(receipt: any, network: any, eventName: string) {
for (const log of receipt?.logs ?? []) {
try {
const parsed = network.interface.parseLog(log);
if (parsed?.name === eventName) {
return parsed;
}
} catch {
// skip non-matching logs
}
}
throw new Error(`${eventName} event not found in transaction receipt`);
}

export function mustNotEmitEvent(receipt: any, network: any, eventName: string) {
for (const log of receipt?.logs ?? []) {
try {
const parsed = network.interface.parseLog(log);
if (parsed?.name === eventName) {
throw new Error(`${eventName} event was unexpectedly emitted in transaction receipt`);
}
} catch (error) {
// If it's our error, rethrow it
if (error instanceof Error && error.message.includes("unexpectedly emitted")) {
throw error;
}
// skip non-matching logs
}
}
}
184 changes: 184 additions & 0 deletions test/unit/SSVClusters/register.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { expect } from "chai";
import { getTestConnection } from "../../setup/connection.js";
import { fullNetworkFixture } from "../../setup/fixtures.js";
import {
asClusterStruct,
mustEmitEvent,
makePublicKey,
registerOperators,
} from "./helpers/clusterHelpers.js";
import { EMPTY_CLUSTER, DEFAULT_SHARES } from "./types/constants.js";

describe("SSVClusters – register", function () {
let connection: any;
let networkHelpers: any;

before(async function () {
({ connection, networkHelpers } = await getTestConnection());
});

const deployNetworkWithOperators = async () => {
const deployed = await fullNetworkFixture(connection);
const [owner] = await connection.ethers.getSigners();
const operatorIds = await registerOperators(deployed.network, owner, 4);

return {
...deployed,
owner,
operatorIds,
deposit: connection.ethers.parseEther("200"),
};
};

it("registerValidator valid registration succeeds", async function () {
const { network, views, operatorIds, owner, deposit } =
await networkHelpers.loadFixture(deployNetworkWithOperators);

const publicKey = makePublicKey(1);

const tx = await network.registerValidator(
publicKey,
operatorIds,
DEFAULT_SHARES,
0,
{ ...EMPTY_CLUSTER },
{ value: deposit }
);

await expect(tx).to.emit(network, "ValidatorAdded");

const receipt = await tx.wait();
const addedLog = mustEmitEvent(receipt, network, "ValidatorAdded");

const clusterFromEvent = asClusterStruct((addedLog as any).args.cluster);
expect(clusterFromEvent.validatorCount).to.equal(1n);
expect(clusterFromEvent.balance).to.equal(deposit);

expect(await views.getValidator(owner.address, publicKey)).to.equal(true);
});

const validationCases = [
{
name: "reverts when publicKeys list is empty",
expectedError: "EmptyPublicKeysList",
call: (ctx: any) =>
ctx.network.bulkRegisterValidator(
[],
ctx.operatorIds,
[],
0,
{ ...EMPTY_CLUSTER }
),
},
{
name: "reverts when publicKeys and shares length mismatch",
expectedError: "PublicKeysSharesLengthMismatch",
call: (ctx: any) =>
ctx.network.bulkRegisterValidator(
[makePublicKey(1)],
ctx.operatorIds,
[],
0,
{ ...EMPTY_CLUSTER }
),
},
{
name: "reverts for invalid operatorIds length",
expectedError: "InvalidOperatorIdsLength",
call: (ctx: any) =>
ctx.network.registerValidator(
makePublicKey(1),
[1, 2, 3],
DEFAULT_SHARES,
0,
{ ...EMPTY_CLUSTER }
),
},
{
name: "reverts for invalid public key length",
expectedError: "InvalidPublicKeyLength",
call: (ctx: any) =>
ctx.network.registerValidator(
"0x1234",
ctx.operatorIds,
DEFAULT_SHARES,
0,
{ ...EMPTY_CLUSTER }
),
},
];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also add the possibility to check params (if custom error has anything inside we should check it with revertedWithCustomError().withArgs())


validationCases.forEach(({ name, expectedError, call }) => {
it(name, async function () {
const ctx = await networkHelpers.loadFixture(deployNetworkWithOperators);

await expect(call(ctx)).to.be.revertedWithCustomError(
ctx.network,
expectedError
);
});
});

it("reverts when public key is already registered", async function () {
const ctx = await networkHelpers.loadFixture(deployNetworkWithOperators);
const publicKey = makePublicKey(5);

await ctx.network.registerValidator(
publicKey,
ctx.operatorIds,
DEFAULT_SHARES,
0,
{ ...EMPTY_CLUSTER },
{ value: ctx.deposit }
);

await expect(
ctx.network.registerValidator(
publicKey,
ctx.operatorIds,
DEFAULT_SHARES,
0,
{ ...EMPTY_CLUSTER }
)
).to.be.revertedWithCustomError(
ctx.network,
"ValidatorAlreadyExistsWithData"
);
});

it("registers another validator into an existing active cluster", async function () {
const ctx = await networkHelpers.loadFixture(deployNetworkWithOperators);
const firstPk = makePublicKey(11);

const firstTx = await ctx.network.registerValidator(
firstPk,
ctx.operatorIds,
DEFAULT_SHARES,
0,
{ ...EMPTY_CLUSTER },
{ value: ctx.deposit }
);
const firstReceipt = await firstTx.wait();
const firstLog = mustEmitEvent(firstReceipt, ctx.network, "ValidatorAdded");

const existingCluster = asClusterStruct((firstLog as any).args.cluster);

const secondPk = makePublicKey(12);
const secondTx = await ctx.network.registerValidator(
secondPk,
ctx.operatorIds,
DEFAULT_SHARES,
0,
existingCluster
);

await expect(secondTx).to.emit(ctx.network, "ValidatorAdded");

const secondReceipt = await secondTx.wait();
const secondLog = mustEmitEvent(secondReceipt, ctx.network, "ValidatorAdded");

const clusterFromSecond = asClusterStruct((secondLog as any).args.cluster);
expect(clusterFromSecond.validatorCount).to.equal(2n);
expect(clusterFromSecond.balance).to.equal(ctx.deposit);
});
});
13 changes: 13 additions & 0 deletions test/unit/SSVClusters/types/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Types and Constants

This directory contains type definitions and constants used in SSVClusters unit tests.

### Files

- **`cluster.ts`** - Defines the `ClusterStruct` type representing cluster data structure
- **`constants.ts`** - Contains test constants including:
- `EMPTY_CLUSTER` - Default empty cluster structure
- `DEFAULT_SHARES` - Default shares value for validator registration

These types and constants ensure consistency across test files and provide reusable test data structures.

7 changes: 7 additions & 0 deletions test/unit/SSVClusters/types/cluster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ClusterStruct = {
validatorCount: bigint;
networkFeeIndex: bigint;
index: bigint;
balance: bigint;
active: boolean;
};
11 changes: 11 additions & 0 deletions test/unit/SSVClusters/types/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ClusterStruct } from "./cluster.js";

export const EMPTY_CLUSTER: ClusterStruct = {
validatorCount: 0n,
networkFeeIndex: 0n,
index: 0n,
balance: 0n,
active: true,
};

export const DEFAULT_SHARES = "0x1234";
Loading