Skip to content
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

feat(contracts): add ipfs service and prepare parsing ipfs data #1970

Merged
merged 1 commit into from
Dec 13, 2024
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
2 changes: 2 additions & 0 deletions packages/contracts/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ GAS_PRICE=
FORKING_BLOCK_NUM=
# Hardhat logging level (true/false)
HARDHAT_LOGGING=
# IPFS Gateway URL
IPFS_GATEWAY_URL=
6 changes: 6 additions & 0 deletions packages/contracts/contracts/Poll.sol
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ contract Poll is Params, Utilities, SnarkCommon, IPoll {
uint256 _nullifier,
uint256 _pollStateIndex
);
event ChainHashUpdated(uint256 indexed _chainHash);
event IpfsHashAdded(bytes32 indexed _ipfsHash);

/// @notice Each MACI instance can have multiple Polls.
/// When a Poll is deployed, its voting period starts immediately.
Expand Down Expand Up @@ -233,10 +235,14 @@ contract Poll is Params, Utilities, SnarkCommon, IPoll {
/// @param messageHash hash of the current message
function updateChainHash(uint256 messageHash) internal {
uint256 newChainHash = hash2([chainHash, messageHash]);

if (numMessages % messageBatchSize == 0) {
batchHashes.push(newChainHash);
}

chainHash = newChainHash;

emit ChainHashUpdated(newChainHash);
}

/// @notice pad last unclosed batch
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,15 @@
},
"devDependencies": {
"@types/chai": "^4.3.11",
"@types/chai-as-promised": "^7.1.8",
"@types/circomlibjs": "^0.1.6",
"@types/lowdb": "^1.0.15",
"@types/mocha": "^10.0.8",
"@types/node": "^22.9.0",
"@types/snarkjs": "^0.7.8",
"@types/uuid": "^10.0.0",
"chai": "^4.3.10",
"chai-as-promised": "^7.1.2",
"dotenv": "^16.4.5",
"hardhat-artifactor": "^0.2.0",
"hardhat-contract-sizer": "^2.10.0",
Expand Down
26 changes: 26 additions & 0 deletions packages/contracts/tests/ipfs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import chai from "chai";
import chaiAsPromised from "chai-as-promised";

import { IpfsService } from "../ts/ipfs";

chai.use(chaiAsPromised);

const { expect } = chai;

describe("IpfsService", () => {
let ipfsService: IpfsService;

beforeEach(() => {
ipfsService = IpfsService.getInstance();
});

it("should read data properly", async () => {
const data = await ipfsService.read("bafybeibro7fxpk7sk2nfvslumxraol437ug35qz4xx2p7ygjctunb2wi3i");

expect(data).to.deep.equal({ Title: "sukuna", Description: "gambare gambare 🔥" });
});

it("should throw error if can't read data", async () => {
await expect(ipfsService.read("invalid")).to.eventually.be.rejectedWith("invalid json for cid invalid");
});
});
57 changes: 53 additions & 4 deletions packages/contracts/ts/genMaciState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Action } from "./types";

import { MACI__factory as MACIFactory, Poll__factory as PollFactory } from "../typechain-types";

import { IpfsService } from "./ipfs";
import { sleep, sortActions } from "./utils";

/**
Expand Down Expand Up @@ -36,6 +37,7 @@ export const genMaciStateFromContract = async (
// ensure the pollId is valid
assert(pollId >= 0);

const ipfsService = IpfsService.getInstance();
const maciContract = MACIFactory.connect(address, provider);

// Check stateTreeDepth
Expand Down Expand Up @@ -139,10 +141,11 @@ export const genMaciStateFromContract = async (
const toBlock = i + blocksPerRequest >= lastBlock ? lastBlock : i + blocksPerRequest;

// eslint-disable-next-line no-await-in-loop
const publishMessageLogs = await pollContract.queryFilter(pollContract.filters.PublishMessage(), i, toBlock);

// eslint-disable-next-line no-await-in-loop
const joinPollLogs = await pollContract.queryFilter(pollContract.filters.PollJoined(), i, toBlock);
const [publishMessageLogs, joinPollLogs, ipfsHashAddedLogs] = await Promise.all([
pollContract.queryFilter(pollContract.filters.PublishMessage(), i, toBlock),
pollContract.queryFilter(pollContract.filters.PollJoined(), i, toBlock),
pollContract.queryFilter(pollContract.filters.IpfsHashAdded(), i, toBlock),
]);

joinPollLogs.forEach((event) => {
assert(!!event);
Expand All @@ -168,6 +171,52 @@ export const genMaciStateFromContract = async (
});
});

// eslint-disable-next-line no-await-in-loop
const ipfsMessages = await Promise.all(
ipfsHashAddedLogs.map(async (event) => {
assert(!!event);

return ipfsService
.read<{ messages: string[][]; encPubKeys: [string, string][] }>(event.args._ipfsHash)
.then(({ messages, encPubKeys }) => ({
data: messages.map((value, index) => ({
message: new Message(value.map(BigInt)),
encPubKey: new PubKey([BigInt(encPubKeys[index][0]), BigInt(encPubKeys[index][1])]),
})),
blockNumber: event.blockNumber,
transactionIndex: event.transactionIndex,
}));
}),
);

ipfsHashAddedLogs.forEach((event) => {
assert(!!event);
const ipfsHash = event.args._ipfsHash;

actions.push({
type: "IpfsHashAdded",
blockNumber: event.blockNumber,
transactionIndex: event.transactionIndex,
data: {
ipfsHash,
},
});
});

ipfsMessages.forEach(({ data, blockNumber, transactionIndex }) => {
data.forEach(({ message, encPubKey }) => {
actions.push({
type: "PublishMessage",
blockNumber,
transactionIndex,
data: {
message,
encPubKey,
},
});
});
});

publishMessageLogs.forEach((event) => {
assert(!!event);

Expand Down
61 changes: 61 additions & 0 deletions packages/contracts/ts/ipfs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* IPFS Service - A service for interacting with IPFS.
* This service allows reading data from IPFS using a Content Identifier (CID).
* It is designed as a singleton to ensure a single instance is used throughout the application.
*/
export class IpfsService {
/**
* Singleton instance of the IpfsService.
*/
private static INSTANCE?: IpfsService;

/**
* URL of the IPFS gateway to fetch data from.
* Defaults to 'https://ipfs.io/ipfs' if not provided in the environment variables.
*/
private ipfsGatewayUrl: string;

/**
* Retrieves the singleton instance of the IpfsService.
* If the instance does not exist, a new one is created and returned.
*
* @returns {IpfsService} The singleton instance of the IpfsService.
*/
static getInstance(): IpfsService {
if (!IpfsService.INSTANCE) {
IpfsService.INSTANCE = new IpfsService();
}

return IpfsService.INSTANCE;
}

/**
* Private constructor to initialize the service.
* Should not be called directly.
* Use `getInstance()` to access the service.
*/
private constructor() {
// Initialize the IPFS gateway URL, using an environment variable or a default value.
this.ipfsGatewayUrl = process.env.IPFS_GATEWAY_URL || "https://ipfs.io/ipfs";
}

/**
* Fetches data from IPFS using the provided Content Identifier (CID).
* The data is expected to be returned in JSON format, and is parsed accordingly.
*
* @param cid - The Content Identifier (CID) of the IPFS object to retrieve.
* @returns {Promise<T>} A promise that resolves with the fetched data, parsed as the specified type `T`.
* @throws {Error} If the request fails or if the data cannot be parsed as JSON.
*
* @template T - The type of the data expected from the IPFS response.
*/
async read<T>(cid: string): Promise<T> {
return fetch(`${this.ipfsGatewayUrl}/${cid}`)
.then((res) =>
res.json().catch(() => {
throw new Error(`invalid json for cid ${cid}`);
}),
)
.then((res) => res as T);
}
}
1 change: 1 addition & 0 deletions packages/contracts/ts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export interface Action {
pollAddr: string;
stateLeaf: bigint;
messageRoot: bigint;
ipfsHash: string;
}>;
blockNumber: number;
transactionIndex: number;
Expand Down
Loading
Loading