Skip to content
Merged
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
76 changes: 43 additions & 33 deletions validator/src/consensus/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
hashStruct,
http,
parseAbi,
zeroHash,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { anvil } from "viem/chains";
Expand All @@ -21,6 +22,7 @@ import { ShieldnetStateMachine as SchildNetzMaschine } from "../service/machine.
import { CONSENSUS_EVENTS, COORDINATOR_EVENTS } from "../types/abis.js";
import { InMemoryQueue } from "../utils/queue.js";
import { KeyGenClient } from "./keyGen/client.js";
import { calculateParticipantsRoot } from "./merkle.js";
import { OnchainProtocol } from "./protocol/onchain.js";
import type { ActionWithTimeout } from "./protocol/types.js";
import { SigningClient } from "./signing/client.js";
Expand Down Expand Up @@ -76,10 +78,24 @@ describe("integration", () => {
});
testClient.setIntervalMining({ interval: BLOCKTIME_IN_SECONDS });
const deploymentInfo = JSON.parse(fs.readFileSync(deploymentInfoFile, "utf-8"));
const coordinatorAddress = deploymentInfo.returns["0"].value as Address;
log(`Use coordinator at ${coordinatorAddress}`);
const consensusAddress = deploymentInfo.returns["1"].value as Address;
log(`Use consensus at ${consensusAddress}`);
const coordinator = {
address: deploymentInfo.returns["0"].value as Address,
abi: parseAbi([
"function keyGen(bytes32 participants, uint64 count, uint64 threshold, bytes32 context) external returns (bytes32 gid)",
"function sign(bytes32 gid, bytes32 message) external returns (bytes32 sid)",
"function groupKey(bytes32 id) external view returns ((uint256 x, uint256 y) key)",
]),
} as const;
log(`Use coordinator at ${coordinator.address}`);
const consensus = {
address: deploymentInfo.returns["1"].value as Address,
abi: parseAbi([
"function proposeTransaction((uint256 chainId, address account, address to, uint256 value, uint8 operation, bytes data, uint256 nonce) transaction) external",
"function getAttestation(uint64 epoch, (uint256 chainId, address account, address to, uint256 value, uint8 operation, bytes data, uint256 nonce) transaction) external view returns (bytes32 message, ((uint256 x, uint256 y) r, uint256 z) signature)",
"function getAttestationByMessage(bytes32 message) external view returns (((uint256 x, uint256 y) r, uint256 z) signature)",
]),
} as const;
log(`Use consensus at ${consensus.address}`);

// Private keys from Anvil testnet
const accounts = [
Expand Down Expand Up @@ -113,8 +129,8 @@ describe("integration", () => {
const protocol = new OnchainProtocol(
publicClient,
signingClient,
consensusAddress,
coordinatorAddress,
consensus.address,
coordinator.address,
actionStorage,
logger,
);
Expand All @@ -133,8 +149,8 @@ describe("integration", () => {
dbPath: ":memory:",
publicClient,
config: {
consensus: consensusAddress,
coordinator: coordinatorAddress,
consensus: consensus.address,
coordinator: coordinator.address,
},
logger:
logger !== undefined
Expand All @@ -158,14 +174,18 @@ describe("integration", () => {
for (const { watcher } of clients) {
await watcher.start();
}
const initiatorClient = createWalletClient({
chain: anvil,
transport: http(),
account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6"),
});
// Manually trigger genesis KeyGen
await initiatorClient.writeContract({
...coordinator,
functionName: "keyGen",
args: [calculateParticipantsRoot(participants), 3n, 2n, zeroHash],
});
Comment on lines +183 to +187
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To improve maintainability and avoid magic numbers, it's better to derive the count and threshold values from the participants array dynamically rather than hardcoding them. This will make the test more robust if the number of participants changes in the future.

const count = BigInt(participants.length);
		const threshold = count / 2n + 1n;
		await initiatorClient.writeContract({
			...coordinator,
			functionName: "keyGen",
			args: [calculateParticipantsRoot(participants), count, threshold, zeroHash],
		});

// Setup done ... SchildNetz läuft ... lets send some signature requests
const abi = parseAbi([
"function proposeTransaction((uint256 chainId, address account, address to, uint256 value, uint8 operation, bytes data, uint256 nonce) transaction) external",
"function groupKey(bytes32 id) external view returns ((uint256 x, uint256 y) key)",
"function sign(bytes32 gid, bytes32 message) external returns (bytes32 sid)",
"function getAttestation(uint64 epoch, (uint256 chainId, address account, address to, uint256 value, uint8 operation, bytes data, uint256 nonce) transaction) external view returns (bytes32 message, ((uint256 x, uint256 y) r, uint256 z) signature)",
"function getAttestationByMessage(bytes32 message) external view returns (((uint256 x, uint256 y) r, uint256 z) signature)",
]);
const transaction = {
chainId: 1n,
account: "0xb3D9cf8E163bbc840195a97E81F8A34E295B8f39" as Address,
Expand All @@ -177,15 +197,8 @@ describe("integration", () => {
};
setTimeout(
async () => {
const initiatorClient = createWalletClient({
chain: anvil,
transport: http(),
account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6"),
});

await initiatorClient.writeContract({
address: consensusAddress,
abi: abi,
...consensus,
functionName: "proposeTransaction",
args: [transaction],
});
Expand All @@ -199,8 +212,7 @@ describe("integration", () => {
expect(knownGroups.length).toBe(EXPECTED_GROUPS);
for (const groupId of knownGroups) {
const groupKey = await readClient.readContract({
address: coordinatorAddress,
abi: abi,
...coordinator,
functionName: "groupKey",
args: [groupId],
});
Expand Down Expand Up @@ -236,7 +248,7 @@ describe("integration", () => {
// Load transaction proposal for tx hash
const proposeEvent = CONSENSUS_EVENTS.filter((e) => e.name === "TransactionProposed")[0];
const proposedMessages = await readClient.getLogs({
address: consensusAddress,
address: consensus.address,
event: proposeEvent,
fromBlock: "earliest",
args: {
Expand All @@ -250,7 +262,7 @@ describe("integration", () => {
// Load signature request for transaction proposal
const signRequestEvent = COORDINATOR_EVENTS.filter((e) => e.name === "Sign")[0];
const signatureRequests = await readClient.getLogs({
address: coordinatorAddress,
address: coordinator.address,
event: signRequestEvent,
fromBlock: "earliest",
args: {
Expand All @@ -259,13 +271,13 @@ describe("integration", () => {
});
expect(signatureRequests.length).toBe(1);
const request = signatureRequests[0];
expect(request.args.initiator).toBe(consensusAddress);
expect(request.args.initiator).toBe(consensus.address);
expect(request.args.sid).toBeDefined();
if (request.args.gid === undefined) throw new Error("GroupId is expected to be defined");
// Load completed request for signature request
const signedEvent = COORDINATOR_EVENTS.filter((e) => e.name === "SignCompleted")[0];
const completedRequests = await readClient.getLogs({
address: coordinatorAddress,
address: coordinator.address,
event: signedEvent,
fromBlock: "earliest",
args: {
Expand All @@ -280,17 +292,15 @@ describe("integration", () => {

// Load group key for verification
const groupKey = await readClient.readContract({
address: coordinatorAddress,
abi: abi,
...coordinator,
functionName: "groupKey",
args: [request.args.gid],
});
expect(verifySignature(toPoint(signature.r), signature.z, toPoint(groupKey), proposal.args.message)).toBeTruthy();

// Check that the attestation is correctly tracked
const attestation = await readClient.readContract({
address: consensusAddress,
abi: abi,
...consensus,
functionName: "getAttestationByMessage",
args: [proposal.args.message],
});
Expand Down
7 changes: 6 additions & 1 deletion validator/src/machine/consensus/rollover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ export const checkEpochRollover = (
stagedEpoch = 0n;
}
// If no rollover is staged and new key gen was not triggered do it now
if (machineStates.rollover.id === "waiting_for_rollover" && stagedEpoch === 0n) {
if (
machineStates.rollover.id === "waiting_for_rollover" &&
stagedEpoch === 0n &&
// Do not trigger a new key gen on genesis
(activeEpoch !== 0n || consensusState.genesisGroupId !== undefined)
) {
// Trigger key gen for next epoch
const nextEpoch = currentEpoch + 1n;
logger?.(`Trigger key gen for epoch ${nextEpoch}`);
Expand Down
11 changes: 8 additions & 3 deletions validator/src/machine/keygen/genesis.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { maxUint64, zeroAddress } from "viem";
import type { KeyGenClient } from "../../consensus/keyGen/client.js";
import type { KeyGenEvent } from "../transitions/types.js";
import type { ConsensusState, MachineConfig, MachineStates, StateDiff } from "../types.js";
import { calcGenesisGroupId } from "./group.js";
import { triggerKeyGen } from "./trigger.js";

export const checkGenesis = (
export const handleGenesisKeyGen = (
machineConfig: MachineConfig,
keyGenClient: KeyGenClient,
consensusState: ConsensusState,
machineStates: MachineStates,
transition: KeyGenEvent,
logger?: (msg: unknown) => void,
): StateDiff => {
const genesisGroupId = calcGenesisGroupId(machineConfig);
logger?.(`Genesis group id: ${genesisGroupId}`);
if (
machineStates.rollover.id === "waiting_for_rollover" &&
consensusState.activeEpoch === 0n &&
consensusState.stagedEpoch === 0n
consensusState.stagedEpoch === 0n &&
transition.gid === genesisGroupId
) {
logger?.("Trigger Genesis Group Generation");
// We set no timeout for the genesis group generation
Expand All @@ -27,7 +33,6 @@ export const checkGenesis = (
);
const consensus = diff.consensus ?? {};
consensus.genesisGroupId = groupId;
logger?.(`Genesis group id: ${groupId}`);
return {
...diff,
consensus,
Expand Down
26 changes: 26 additions & 0 deletions validator/src/machine/keygen/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type Address, encodePacked, type Hex, zeroAddress } from "viem";
import { calcGroupId } from "../../consensus/keyGen/utils.js";
import { calculateParticipantsRoot } from "../../consensus/merkle.js";
import type { GroupId } from "../../frost/types.js";
import type { MachineConfig } from "../types.js";

export type GroupParameters = {
count: bigint;
threshold: bigint;
context: Hex;
};

export const calcGroupParameters = (participantCount: number, consensus: Address, epoch: bigint): GroupParameters => {
const count = BigInt(participantCount);
const threshold = count / 2n + 1n;
// 4 bytes version, 20 bytes address, 8 bytes epoch number
const context = encodePacked(["uint32", "address", "uint64"], [0, consensus, epoch]);
// TODO: Handle cases where the group size is too small.
return { count, threshold, context };
};

export const calcGenesisGroupId = ({ defaultParticipants }: Pick<MachineConfig, "defaultParticipants">): GroupId => {
const participantsRoot = calculateParticipantsRoot(defaultParticipants);
const { count, threshold, context } = calcGroupParameters(defaultParticipants.length, zeroAddress, 0n);
return calcGroupId(participantsRoot, count, threshold, context);
};
8 changes: 3 additions & 5 deletions validator/src/machine/keygen/trigger.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { type Address, encodePacked } from "viem";
import type { Address } from "viem";
import type { KeyGenClient } from "../../consensus/keyGen/client.js";
import type { ProtocolAction } from "../../consensus/protocol/types.js";
import type { Participant } from "../../consensus/storage/types.js";
import type { GroupId } from "../../frost/types.js";
import type { StateDiff } from "../types.js";
import { calcGroupParameters } from "./group.js";

export const triggerKeyGen = (
keyGenClient: KeyGenClient,
Expand All @@ -16,10 +17,7 @@ export const triggerKeyGen = (
if (participants.length < 2) {
throw new Error("Not enough participatns!");
}
// 4 bytes version, 20 bytes address, 8 bytes epoch number
const context = encodePacked(["uint32", "address", "uint64"], [0, consensus, epoch]);
const count = BigInt(participants.length);
const threshold = count / 2n + 1n;
const { count, threshold, context } = calcGroupParameters(participants.length, consensus, epoch);
const { groupId, participantsRoot, participantId, commitments, pok, poap } = keyGenClient.setupGroup(
participants,
count,
Expand Down
13 changes: 4 additions & 9 deletions validator/src/machine/transitions/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class OnchainTransitionWatcher {
const db = new Sqlite3(dbPath);
db.exec(`
CREATE TABLE IF NOT EXISTS transition_watcher (
chainId INTEGER PRIMARY KEY,
chainId INTEGER PRIMARY KEY,
lastIndexedBlock INTEGER NOT NULL
);
`);
Expand All @@ -48,16 +48,11 @@ export class OnchainTransitionWatcher {
this.#onTransition = onTransition;
}

async getLastIndexedBlock(): Promise<bigint> {
private async getLastIndexedBlock(): Promise<bigint | undefined> {
const clientChainId = this.#publicClient.chain?.id ?? 0n;
const stmt = this.#db.prepare("SELECT chainId, lastIndexedBlock FROM transition_watcher WHERE chainId = ?");
const result = transitionWatcherStateSchema.parse(stmt.get(clientChainId));

if (!result) {
// No entries stored, lets start fresh
return -1n;
}
return result.lastIndexedBlock;
return result?.lastIndexedBlock;
}

updateLastIndexedBlock(block: bigint): boolean {
Expand Down Expand Up @@ -90,7 +85,7 @@ export class OnchainTransitionWatcher {
this.#publicClient.watchContractEvent({
address: [this.#config.consensus, this.#config.coordinator],
abi: [...CONSENSUS_EVENTS, ...COORDINATOR_EVENTS],
fromBlock: lastIndexedBlock + 1n,
fromBlock: lastIndexedBlock ? lastIndexedBlock + 1n : undefined,
onLogs: async (logs) => {
logs.sort((left, right) => {
if (left.blockNumber !== right.blockNumber) {
Expand Down
15 changes: 11 additions & 4 deletions validator/src/service/machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { handleTransactionProposed } from "../machine/consensus/transactionPropo
import { checkKeyGenAbort } from "../machine/keygen/abort.js";
import { handleKeyGenCommitted } from "../machine/keygen/committed.js";
import { handleKeyGenConfirmed } from "../machine/keygen/confirmed.js";
import { checkGenesis } from "../machine/keygen/genesis.js";
import { handleGenesisKeyGen } from "../machine/keygen/genesis.js";
import { handleKeyGenSecretShared } from "../machine/keygen/secretShares.js";
import { checkKeyGenTimeouts } from "../machine/keygen/timeouts.js";
import { handleSigningCompleted } from "../machine/signing/completed.js";
Expand Down Expand Up @@ -145,7 +145,6 @@ export class ShieldnetStateMachine {
)) {
state.apply(diff);
}
state.apply(checkGenesis(this.#machineConfig, this.#keyGenClient, state.consensus, state.machines, this.#logger));

state.apply(
checkEpochRollover(
Expand Down Expand Up @@ -196,6 +195,16 @@ export class ShieldnetStateMachine {
): Promise<StateDiff> {
this.#logger?.(`Handle event ${transition.id}`);
switch (transition.id) {
case "event_key_gen": {
return await handleGenesisKeyGen(
this.#machineConfig,
this.#keyGenClient,
consensusState,
machineStates,
transition,
this.#logger,
);
}
case "event_key_gen_committed": {
return await handleKeyGenCommitted(this.#machineConfig, this.#keyGenClient, machineStates, transition);
}
Expand Down Expand Up @@ -274,8 +283,6 @@ export class ShieldnetStateMachine {
case "event_transaction_attested": {
return await handleTransactionAttested(machineStates, transition);
}
case "event_key_gen":
Copy link
Contributor

Choose a reason for hiding this comment

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

Yay no unhandled events

return {};
}
}
}