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(subgraph): add chain hashes and ipfs messages to subgraph #1971

Merged
merged 1 commit into from
Dec 16, 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
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;
}
Loading