Skip to content

Commit

Permalink
Merge pull request #2132 from privacy-scaling-explorations/test/relay…
Browse files Browse the repository at this point in the history
…er-onchain-integration-tests

test(relayer): add checks for onchain publishing
  • Loading branch information
0xmad authored Feb 12, 2025
2 parents 3fae757 + a25f4bd commit 4856de6
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 6 deletions.
3 changes: 2 additions & 1 deletion apps/relayer/tests/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,13 @@ export class TestDeploy {

await deployPoll({
pollStartDate: startDate,
pollEndDate: startDate + 30,
pollEndDate: startDate + 130,
intStateTreeDepth: INT_STATE_TREE_DEPTH,
messageBatchSize: MESSAGE_BATCH_SIZE,
voteOptionTreeDepth: VOTE_OPTION_TREE_DEPTH,
coordinatorPubkey: coordinatorKeypair.pubKey.serialize(),
useQuadraticVoting: false,
relayers: [await signer.getAddress()],
signer,
});

Expand Down
116 changes: 116 additions & 0 deletions apps/relayer/tests/onchain.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { jest } from "@jest/globals";
import { HttpStatus, ValidationPipe, type INestApplication } from "@nestjs/common";
import { SchedulerRegistry } from "@nestjs/schedule";
import { Test } from "@nestjs/testing";
import { Keypair } from "maci-domainobjs";
import { formatProofForVerifierContract, genProofSnarkjs, getDefaultSigner, getPollContracts } from "maci-sdk";
import request from "supertest";

import type { JsonRpcProvider } from "ethers";

import { AppModule } from "../ts/app.module.js";

import { pollJoinedWasm, pollJoinedZkey, type TApp } from "./constants.js";

jest.unmock("maci-sdk");

describe("Integration message publishing", () => {
let app: INestApplication<TApp>;
let circuitInputs: Record<string, string>;
let stateLeafIndex: number;
let maciContractAddress: string;
let schedulerRegistry: SchedulerRegistry;

beforeAll(async () => {
const { TestDeploy } = await import("./deploy.js");
await TestDeploy.sleep(20_000);
const testDeploy = await TestDeploy.getInstance();
const poll = testDeploy.contractsData.maciState!.polls.get(0n);

poll!.updatePoll(BigInt(testDeploy.contractsData.maciState!.pubKeys.length));

stateLeafIndex = Number(testDeploy.contractsData.stateLeafIndex);

maciContractAddress = testDeploy.contractsData.maciContractAddress!;

circuitInputs = poll!.joinedCircuitInputs({
maciPrivKey: testDeploy.contractsData.user!.privKey,
stateLeafIndex: BigInt(testDeploy.contractsData.stateLeafIndex!),
voiceCreditsBalance: BigInt(testDeploy.contractsData.voiceCredits!),
joinTimestamp: BigInt(testDeploy.contractsData.timestamp!),
}) as unknown as typeof circuitInputs;

const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ transform: true }));
await app.listen(3003);

schedulerRegistry = moduleFixture.get<SchedulerRegistry>(SchedulerRegistry);
});

afterAll(async () => {
jest.restoreAllMocks();
await app.close();
});

test("should publish user messages properly", async () => {
const keypair = new Keypair();

const defaultSaveMessagesArgs = {
maciContractAddress,
poll: 0,
stateLeafIndex,
messages: [
{
data: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
publicKey: keypair.pubKey.serialize(),
},
],
};

const { proof } = await genProofSnarkjs({
inputs: circuitInputs,
zkeyPath: pollJoinedZkey,
wasmPath: pollJoinedWasm,
});

const cronJob = schedulerRegistry.getCronJob("publishMessages");
expect(cronJob).toBeDefined();

const result = await request(app.getHttpServer())
.post("/v1/messages/publish")
.send({
...defaultSaveMessagesArgs,
maciContractAddress,
stateLeafIndex,
proof: formatProofForVerifierContract(proof),
})
.expect(HttpStatus.CREATED);

expect(result.status).toBe(HttpStatus.CREATED);

cronJob.fireOnTick();

const signer = await getDefaultSigner();
await (signer.provider as unknown as JsonRpcProvider).send("evm_increaseTime", [100]);
await (signer.provider as unknown as JsonRpcProvider).send("evm_mine", []);

const { TestDeploy } = await import("./deploy.js");
const { poll: pollContract } = await getPollContracts({ maciAddress: maciContractAddress, pollId: 0, signer });

await Promise.race([
new Promise((resolve) => {
pollContract.once(pollContract.filters.IpfsHashAdded(), (ipfsHash: string) => {
expect(ipfsHash).toBeDefined();
resolve(true);
});
}),
TestDeploy.sleep(30_000).then(() => {
throw new Error("Timeout error");
}),
]);
});
});
9 changes: 9 additions & 0 deletions apps/relayer/ts/ipfs/__tests__/ipfs.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,13 @@ describe("IpfsService", () => {

expect(data).toStrictEqual(defaultData);
});

test("should convert cid v1 to bytes32 properly", async () => {
const service = new IpfsService();
await service.init();

const bytes32Id = await service.cidToBytes32("bagaaierae7uwukkhfsvbw63l2wc4jdi2pilwaceei5quutzi4vcuh2z72dcq");

expect(bytes32Id).toBe("0x27e96a29472caa1b7b6bd585c48d1a7a1760088447614a4f28e54543eb3fd0c5");
});
});
20 changes: 20 additions & 0 deletions apps/relayer/ts/ipfs/ipfs.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable, Logger } from "@nestjs/common";
import { ethers } from "ethers";

import type { JSON as JsonAdapter } from "@helia/json";

Expand Down Expand Up @@ -56,6 +57,25 @@ export class IpfsService {
return this.adapter!.get<T>(CID.parse(cid));
}

/**
* Converts an IPFS CIDv1 to a bytes32-compatible hex string.
*
* This function:
* - Decodes the Base32-encoded CIDv1
* - Extracts the SHA-256 digest from the multihash
* - Converts it to a Solidity-compatible `bytes32` format
*
* @param hash - The CIDv1 string
* @returns A `bytes32`-compatible hex string (e.g., `0x...`)
*/
async cidToBytes32(hash: string): Promise<string> {
const { CID } = await import("multiformats");

const { multihash } = CID.parse(hash);

return ethers.hexlify(multihash.digest);
}

/**
* Check if IPFS adapter is initialized
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jest.mock("maci-sdk", (): unknown => ({

describe("MessageBatchService", () => {
const mockIpfsService = {
init: jest.fn(),
cidToBytes32: jest.fn().mockImplementation(() => Promise.resolve(defaultIpfsHash)),
add: jest.fn().mockImplementation(() => Promise.resolve(defaultIpfsHash)),
};

Expand All @@ -44,6 +46,8 @@ describe("MessageBatchService", () => {
mockRepository.create = jest.fn().mockImplementation(() => Promise.resolve(defaultMessageBatches));
mockRepository.find = jest.fn().mockImplementation(() => Promise.resolve(defaultMessageBatches));
mockIpfsService.add = jest.fn().mockImplementation(() => Promise.resolve(defaultIpfsHash));
mockIpfsService.cidToBytes32 = jest.fn().mockImplementation(() => Promise.resolve(defaultIpfsHash));
mockIpfsService.init = jest.fn();

MACIFactory.connect = jest.fn().mockImplementation(() => mockMaciContract) as typeof MACIFactory.connect;
PollFactory.connect = jest.fn().mockImplementation(() => mockPollContract) as typeof PollFactory.connect;
Expand Down
16 changes: 11 additions & 5 deletions apps/relayer/ts/messageBatch/messageBatch.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export class MessageBatchService {
constructor(
private readonly ipfsService: IpfsService,
private readonly messageBatchRepository: MessageBatchRepository,
) {}
) {
ipfsService.init();
}

/**
* Find message batches
Expand Down Expand Up @@ -71,8 +73,11 @@ export class MessageBatchService {
}

const allMessages = flatten(args.map((item) => item.messages)).map((message) => ({
...message,
publicKey: PubKey.deserialize(message.publicKey).asArray(),
poll: message.poll,
data: message.data,
hash: message.hash,
maciContractAddress: message.maciContractAddress,
publicKey: PubKey.deserialize(message.publicKey).asArray().map(String),
}));

const ipfsHash = await this.ipfsService.add(allMessages).catch((error) => {
Expand Down Expand Up @@ -102,11 +107,12 @@ export class MessageBatchService {

const messageHashes = await Promise.all(
allMessages.map(({ data, publicKey }) =>
pollContract.hashMessageAndEncPubKey({ data }, { x: publicKey[0], y: publicKey[1] }),
pollContract.hashMessageAndEncPubKey({ data }, { x: BigInt(publicKey[0]), y: BigInt(publicKey[1]) }),
),
);

await pollContract.relayMessagesBatch(messageHashes, ipfsHash).then((tx) => tx.wait());
const bytes32IpfsHash = await this.ipfsService.cidToBytes32(ipfsHash);
await pollContract.relayMessagesBatch(messageHashes, bytes32IpfsHash).then((tx) => tx.wait());

return messageBatches;
}
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/ts/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./keys";
export * from "./relayer";
export * from "./poll";
export * from "./tally";
export * from "./trees";
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/ts/poll/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { getPoll, getPollParams } from "./poll";
export { getPollContracts } from "./utils";
export type {
IGetPollArgs,
IGetPollData,
Expand Down

0 comments on commit 4856de6

Please sign in to comment.