Skip to content

Commit

Permalink
Merge pull request #1971 from privacy-scaling-explorations/feature/su…
Browse files Browse the repository at this point in the history
…bgraph-chain-hash

feat(subgraph): add chain hashes and ipfs messages to subgraph
  • Loading branch information
0xmad authored Dec 16, 2024
2 parents a994d4d + cb139ea commit e8805f8
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 13 deletions.
12 changes: 11 additions & 1 deletion apps/subgraph/schemas/schema.v1.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Poll @entity {
pollId: BigInt! # uint256
duration: BigInt! # uint256
treeDepth: BigInt! # uint8
maxVoteOption: BigInt!
maxVoteOptions: BigInt!
messageProcessor: Bytes! # address
tally: Bytes! # address
createdAt: BigInt!
Expand All @@ -46,12 +46,22 @@ type Poll @entity {
owner: Bytes!
maci: MACI!
votes: [Vote!]! @derivedFrom(field: "poll")
chainHashes: [ChainHash!]! @derivedFrom(field: "poll")
}

type Vote @entity(immutable: true) {
id: Bytes!
data: [BigInt!]! # uint256[10]
timestamp: BigInt!
cid: String

"relations"
poll: Poll!
}

type ChainHash @entity(immutable: true) {
id: ID! # chain hash
timestamp: BigInt!

"relations"
poll: Poll!
Expand Down
2 changes: 1 addition & 1 deletion apps/subgraph/src/maci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function handleDeployPoll(event: DeployPollEvent): void {
poll.pollId = event.params._pollId;
poll.messageProcessor = contracts.messageProcessor;
poll.tally = contracts.tally;
poll.maxVoteOption = maxVoteOptions;
poll.maxVoteOptions = maxVoteOptions;
poll.treeDepth = GraphBN.fromI32(treeDepths.value0);
poll.duration = durations.value1;
poll.mode = GraphBN.fromI32(event.params._mode);
Expand Down
82 changes: 80 additions & 2 deletions apps/subgraph/src/poll.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
/* eslint-disable no-underscore-dangle */

import { Poll, Vote, MACI } from "../generated/schema";
import { MergeState as MergeStateEvent, PublishMessage as PublishMessageEvent } from "../generated/templates/Poll/Poll";
import { Bytes, ipfs, log, Value, BigInt as GraphBN, JSONValue } from "@graphprotocol/graph-ts";

import { Poll, Vote, MACI, ChainHash } from "../generated/schema";
import {
MergeState as MergeStateEvent,
PublishMessage as PublishMessageEvent,
ChainHashUpdated as ChainHashUpdatedEvent,
IpfsHashAdded as IpfsHashAddedEvent,
} from "../generated/templates/Poll/Poll";

import { ONE_BIG_INT } from "./utils/constants";

Expand Down Expand Up @@ -39,3 +46,74 @@ export function handlePublishMessage(event: PublishMessageEvent): void {
poll.save();
}
}

export function handleChainHashUpdate(event: ChainHashUpdatedEvent): void {
const chainHash = new ChainHash(event.params._chainHash.toString());
chainHash.poll = event.address;
chainHash.timestamp = event.block.timestamp;
chainHash.save();

const poll = Poll.load(event.address);

if (poll) {
poll.updatedAt = event.block.timestamp;
poll.save();
}
}

export function handleIpfsHashAdded(event: IpfsHashAddedEvent): void {
const CID_VERSION = "0x1220";
const cid = Bytes.fromHexString(CID_VERSION).concat(event.params._ipfsHash).toBase58();
const timestamp = event.block.timestamp.toString();
const voteId = event.transaction.hash.concatI32(event.logIndex.toI32()).toHexString();

ipfs.mapJSON(cid, "processIpfsVotes", Value.fromStringArray([cid, voteId, timestamp, event.address.toHexString()]));
}

export function processIpfsVotes(data: JSONValue, userData: Value): void {
const params = userData.toArray();
const cid = params[0].toString();
const voteId = params[1].toString();
const timestamp = params[2].toString();
const pollAddress = Bytes.fromHexString(params[3].toString());

const jsonData = data.toObject();
const jsonMessages = jsonData.get("messages");

if (!jsonMessages) {
log.warning("Message batch file {} has invalid format", [cid]);
return;
}

const messages = jsonMessages.toArray();

for (let index = 0; index < messages.length; index += 1) {
const vote = new Vote(Bytes.fromHexString(voteId).concatI32(index));

vote.data = castToBigIntArray(messages[index].toArray());
vote.poll = pollAddress;
vote.cid = cid;
vote.timestamp = GraphBN.fromString(timestamp);
vote.save();
}

const poll = Poll.load(pollAddress);

if (poll) {
poll.numMessages = poll.numMessages.plus(GraphBN.fromI32(messages.length));
poll.updatedAt = GraphBN.fromString(timestamp);
poll.save();
}
}

function castToBigIntArray(array: JSONValue[]): GraphBN[] {
const result: GraphBN[] = [];

// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let index = 0; index < array.length; index += 1) {
const value = array[index];
result.push(GraphBN.fromString(value.toString()));
}

return result;
}
6 changes: 6 additions & 0 deletions apps/subgraph/templates/subgraph.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ indexerHints:
prune: auto
schema:
file: ./schema.graphql
features:
- ipfsOnEthereumContracts
dataSources:
- kind: ethereum
name: MACI
Expand Down Expand Up @@ -58,4 +60,8 @@ templates:
handler: handleMergeState
- event: PublishMessage((uint256[10]),(uint256,uint256))
handler: handlePublishMessage
- event: ChainHashUpdated(indexed uint256)
handler: handleChainHashUpdate
- event: IpfsHashAdded(indexed bytes32)
handler: handleIpfsHashAdded
file: ./src/poll.ts
14 changes: 14 additions & 0 deletions apps/subgraph/tests/ipfs/batch-0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"messages": [
["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"],
["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"],
["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"]
],
"encPubKeys": [
["0", "0"],
["0", "0"],
["0", "0"]
]
}
]
1 change: 1 addition & 0 deletions apps/subgraph/tests/ipfs/batch-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{}]
65 changes: 58 additions & 7 deletions apps/subgraph/tests/poll/poll.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
/* eslint-disable no-underscore-dangle, @typescript-eslint/no-unnecessary-type-assertion */
import { BigInt } from "@graphprotocol/graph-ts";
import { test, describe, afterEach, clearStore, assert, beforeEach } from "matchstick-as";
import { BigInt, Bytes } from "@graphprotocol/graph-ts";
import { test, describe, afterEach, clearStore, assert, beforeEach, mockIpfsFile, beforeAll } from "matchstick-as";

import { MACI, Poll } from "../../generated/schema";
import { ChainHash, MACI, Poll } from "../../generated/schema";
import { handleDeployPoll } from "../../src/maci";
import { handleMergeState, handlePublishMessage } from "../../src/poll";
import {
handleMergeState,
handlePublishMessage,
handleChainHashUpdate,
handleIpfsHashAdded,
processIpfsVotes,
} from "../../src/poll";
import { DEFAULT_POLL_ADDRESS, mockMaciContract, mockPollContract } from "../common";
import { createDeployPollEvent } from "../maci/utils";

import { createMergeStateEvent, createPublishMessageEvent } from "./utils";
import {
createChainHashUpdatedEvent,
createIpfsHashAddedEvent,
createMergeStateEvent,
createPublishMessageEvent,
} from "./utils";

export { handleMergeState, handlePublishMessage };
export { handleMergeState, handlePublishMessage, handleChainHashUpdate, handleIpfsHashAdded, processIpfsVotes };

describe("Poll", () => {
beforeEach(() => {
beforeAll(() => {
mockIpfsFile("TspRr", "tests/ipfs/batch-0.json");
mockIpfsFile("Tsn1k", "tests/ipfs/batch-1.json");

mockMaciContract();
mockPollContract();
});

beforeEach(() => {
// mock the deploy poll event with non qv mode set
const event = createDeployPollEvent(BigInt.fromI32(1), BigInt.fromI32(1), BigInt.fromI32(1), BigInt.fromI32(1));

Expand Down Expand Up @@ -69,4 +85,39 @@ describe("Poll", () => {
assert.entityCount("Vote", 1);
assert.fieldEquals("Poll", poll.id.toHex(), "numMessages", "1");
});

test("should handle chain hash update properly", () => {
const event = createChainHashUpdatedEvent(DEFAULT_POLL_ADDRESS, BigInt.fromI32(123443221));

handleChainHashUpdate(event);

const chainHash = ChainHash.load(event.params._chainHash.toString())!;

assert.entityCount("ChainHash", 1);
assert.fieldEquals("ChainHash", chainHash.id, "id", event.params._chainHash.toString());
});

test("should handle ipfs message processing properly", () => {
const expectedTotalMessages = 3;

const event = createIpfsHashAddedEvent(DEFAULT_POLL_ADDRESS, Bytes.fromHexString("0xdead"));

handleIpfsHashAdded(event);

const poll = Poll.load(event.address)!;

assert.fieldEquals("Poll", poll.id.toHex(), "numMessages", expectedTotalMessages.toString());
assert.entityCount("Vote", expectedTotalMessages);
});

test("should not add votes if there is no ipfs file", () => {
const event = createIpfsHashAddedEvent(DEFAULT_POLL_ADDRESS, Bytes.fromHexString("0xbeef"));

handleIpfsHashAdded(event);

const poll = Poll.load(event.address)!;

assert.fieldEquals("Poll", poll.id.toHex(), "numMessages", "0");
assert.entityCount("Vote", 0);
});
});
22 changes: 20 additions & 2 deletions apps/subgraph/tests/poll/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Address, BigInt as GraphBN, ethereum } from "@graphprotocol/graph-ts";
import { Address, Bytes, BigInt as GraphBN, ethereum } from "@graphprotocol/graph-ts";
// eslint-disable-next-line import/no-extraneous-dependencies
import { newMockEvent } from "matchstick-as";

import { MergeState, PublishMessage } from "../../generated/templates/Poll/Poll";
import { MergeState, PublishMessage, ChainHashUpdated, IpfsHashAdded } from "../../generated/templates/Poll/Poll";

export function createMergeStateEvent(address: Address, stateRoot: GraphBN, numSignups: GraphBN): MergeState {
const event = changetype<MergeState>(newMockEvent());
Expand Down Expand Up @@ -40,3 +40,21 @@ export function createPublishMessageEvent(

return event;
}

export function createChainHashUpdatedEvent(address: Address, chainHash: GraphBN): ChainHashUpdated {
const event = changetype<ChainHashUpdated>(newMockEvent());

event.parameters.push(new ethereum.EventParam("_chainHash", ethereum.Value.fromUnsignedBigInt(chainHash)));
event.address = address;

return event;
}

export function createIpfsHashAddedEvent(address: Address, ipfsHash: Bytes): IpfsHashAdded {
const event = changetype<IpfsHashAdded>(newMockEvent());

event.parameters.push(new ethereum.EventParam("_ipfsHash", ethereum.Value.fromBytes(ipfsHash)));
event.address = address;

return event;
}

0 comments on commit e8805f8

Please sign in to comment.