Skip to content

Commit

Permalink
Merge pull request #1970 from privacy-scaling-explorations/feature/su…
Browse files Browse the repository at this point in the history
…bmit-ipfs-hashes

feat(contracts): add ipfs service and prepare parsing ipfs data
  • Loading branch information
0xmad authored Dec 13, 2024
2 parents e087db8 + 2a2d0d4 commit 3e6c9b1
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 18 deletions.
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

0 comments on commit 3e6c9b1

Please sign in to comment.