-
Notifications
You must be signed in to change notification settings - Fork 54
register tests added #349
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
register tests added #349
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Mocks used by `Modules` |
| 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`). |
| 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. |
| 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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be avoided, if events would be checked with |
||
| 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 | ||
| } | ||
| } | ||
| } | ||
venimir-ssv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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"); | ||
venimir-ssv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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 } | ||
| ), | ||
| }, | ||
| ]; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| 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); | ||
| }); | ||
| }); | ||
| 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. | ||
|
|
| 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; | ||
| }; |
| 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"; |
Uh oh!
There was an error while loading. Please reload this page.