implement zkchat and ipfs integration; update app to use rln proofs (#5)

* add ipfs upload

* add user auth for upload

* insert upload data to postgres

* limit upload to 100mb per user

* wip

* add zkchat endpoint

* update schema

* wip

* add group info

* remove log

* fix semaphore signal hash

* allow file upload for zk post

* udpating interep service

* return zk group type

* verify rln proof

* add server sent event
This commit is contained in:
tsukino
2022-08-03 17:33:36 +08:00
committed by GitHub
parent 5043a5f677
commit 9464f7cab6
46 changed files with 4341 additions and 235 deletions

BIN
circuitFiles/rln/rln.wasm Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,119 @@
{
"protocol": "groth16",
"curve": "bn128",
"nPublic": 6,
"vk_alpha_1": [
"20491192805390485299153009773594534940189261866228447918068658471970481763042",
"9383485363053290200918347156157836566562967994039712273449902621266178545958",
"1"
],
"vk_beta_2": [
[
"6375614351688725206403948262868962793625744043794305715222011528459656738731",
"4252822878758300859123897981450591353533073413197771768651442665752259397132"
],
[
"10505242626370262277552901082094356697409835680220590971873171140371331206856",
"21847035105528745403288232691147584728191162732299865338377159692350059136679"
],
[
"1",
"0"
]
],
"vk_gamma_2": [
[
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
],
[
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
],
[
"1",
"0"
]
],
"vk_delta_2": [
[
"14962742607348761745067039667743739542918587958944430293802039673540545491766",
"19420690459571091434654770613940591470063366130537730965999231781890983822228"
],
[
"8025490964181160791180478620151567079603098158752936953068367103529675811355",
"16899630578734283128344486213072467840624492210479261395286682179061749476405"
],
[
"1",
"0"
]
],
"vk_alphabeta_12": [
[
[
"2029413683389138792403550203267699914886160938906632433982220835551125967885",
"21072700047562757817161031222997517981543347628379360635925549008442030252106"
],
[
"5940354580057074848093997050200682056184807770593307860589430076672439820312",
"12156638873931618554171829126792193045421052652279363021382169897324752428276"
],
[
"7898200236362823042373859371574133993780991612861777490112507062703164551277",
"7074218545237549455313236346927434013100842096812539264420499035217050630853"
]
],
[
[
"7077479683546002997211712695946002074877511277312570035766170199895071832130",
"10093483419865920389913245021038182291233451549023025229112148274109565435465"
],
[
"4595479056700221319381530156280926371456704509942304414423590385166031118820",
"19831328484489333784475432780421641293929726139240675179672856274388269393268"
],
[
"11934129596455521040620786944827826205713621633706285934057045369193958244500",
"8037395052364110730298837004334506829870972346962140206007064471173334027475"
]
]
],
"IC": [
[
"1903896611309112537898744888962900116850557104688140496996114549467905845389",
"18878807081439367511400607605971457252753640659171839147843428814578113606944",
"1"
],
[
"3379380938593207080729160389717513882233130462616390491862768147575356927916",
"7658366061956613070318477742617874370870162905631795036979866045784951077741",
"1"
],
[
"6573257371221466457264319919959056878987621722065494829982803761054687125969",
"10882724634177179759524298899662307047578489073607305659656545291770678864358",
"1"
],
[
"7812315588577187126949469943175416873443639588836013906528191759531184751241",
"11928578526804134334949431821595151892366575912030023302327660047097849106833",
"1"
],
[
"8603322753346553805166809285487650902416020947317326006612301010819108432803",
"3100089596497060388197556426101480177422659517238854788949908444987414217355",
"1"
],
[
"548584601681146753562700521279324517960520272202999011217745775929880443671",
"10775304696187723066725699321032508342393012304409445749847420882128296879500",
"1"
],
[
"4832346778469239752999589970014962368162544386191392542939015835265757824362",
"17497085844864771075682226779903142040218959461493641885155366729946818944068",
"1"
]
]
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
{"protocol":"groth16","curve":"bn128","nPublic":4,"vk_alpha_1":["20491192805390485299153009773594534940189261866228447918068658471970481763042","9383485363053290200918347156157836566562967994039712273449902621266178545958","1"],"vk_beta_2":[["6375614351688725206403948262868962793625744043794305715222011528459656738731","4252822878758300859123897981450591353533073413197771768651442665752259397132"],["10505242626370262277552901082094356697409835680220590971873171140371331206856","21847035105528745403288232691147584728191162732299865338377159692350059136679"],["1","0"]],"vk_gamma_2":[["10857046999023057135944570762232829481370756359578518086990519993285655852781","11559732032986387107991004021392285783925812861821192530917403151452391805634"],["8495653923123431417604973247489272438418190587263600148770280649306958101930","4082367875863433681332203403145435568316851327593401208105741076214120093531"],["1","0"]],"vk_delta_2":[["7912208710313447447762395792098481825752520616755888860068004689933335666613","12599857379517512478445603412764121041984228075771497593287716170335433683702"],["21679208693936337484429571887537508926366191105267550375038502782696042114705","11502426145685875357967720478366491326865907869902181704031346886834786027007"],["1","0"]],"vk_alphabeta_12":[[["2029413683389138792403550203267699914886160938906632433982220835551125967885","21072700047562757817161031222997517981543347628379360635925549008442030252106"],["5940354580057074848093997050200682056184807770593307860589430076672439820312","12156638873931618554171829126792193045421052652279363021382169897324752428276"],["7898200236362823042373859371574133993780991612861777490112507062703164551277","7074218545237549455313236346927434013100842096812539264420499035217050630853"]],[["7077479683546002997211712695946002074877511277312570035766170199895071832130","10093483419865920389913245021038182291233451549023025229112148274109565435465"],["4595479056700221319381530156280926371456704509942304414423590385166031118820","19831328484489333784475432780421641293929726139240675179672856274388269393268"],["11934129596455521040620786944827826205713621633706285934057045369193958244500","8037395052364110730298837004334506829870972346962140206007064471173334027475"]]],"IC":[["19918517214839406678907482305035208173510172567546071380302965459737278553528","7151186077716310064777520690144511885696297127165278362082219441732663131220","1"],["690581125971423619528508316402701520070153774868732534279095503611995849608","21271996888576045810415843612869789314680408477068973024786458305950370465558","1"],["16461282535702132833442937829027913110152135149151199860671943445720775371319","2814052162479976678403678512565563275428791320557060777323643795017729081887","1"],["4319780315499060392574138782191013129592543766464046592208884866569377437627","13920930439395002698339449999482247728129484070642079851312682993555105218086","1"],["3554830803181375418665292545416227334138838284686406179598687755626325482686","5951609174746846070367113593675211691311013364421437923470787371738135276998","1"]]}

View File

@@ -0,0 +1,111 @@
import {logger} from "./utils/svc";
import Chat, {ChatMessage} from "./services/chat.service";
import DB from "./services/db.service";
import config from "./utils/config";
import {RLN, RLNFullProof} from "@zk-kit/protocols";
import vkey from "../statics/circuitFiles/rln/verification_key.json";
import {ShareModel} from "./models/shares.model";
export class ZKChat {
DB: DB;
Chat: Chat;
constructor() {
this.DB = new DB();
this.Chat = new Chat();
}
async init() {
try {
await this.DB.start();
await this.Chat.start();
} catch (e) {
logger.error(e.message, {stack: e.stack});
throw e;
}
}
registerUser = async (address: string, ecdhPubkey: string) => {
return this.DB.users?.insertUser(address, ecdhPubkey);
}
getAllUsers = async (offset = 0, limit = 20) => {
return this.DB.users?.getUsers(offset, limit);
}
getDirectMessages = async (sender: string, receiver: string, offset = 0, limit = 20) => {
return this.DB.chats?.getDirectMessages(sender, receiver, offset, limit);
}
getDirectChatsForUser = async (address: string) => {
return this.DB.chats?.getDirectChatsForUser(address);
}
checkShare = async (share: ShareModel) => {
return this.DB.shares?.checkShare(share);
}
insertShare = async (share: ShareModel) => {
return this.DB.shares?.insertShare(share);
}
isEpochCurrent = async (epoch: string) => {
const serverTimestamp = new Date();
// serverTimestamp.setSeconds(Math.floor(serverTimestamp.getSeconds() / 10) * 10);
serverTimestamp.setMilliseconds(0);
const messageTimestamp = new Date(parseInt(epoch));
// Tolerate a difference of EPOCH_ALLOWED_DELAY_THRESHOLD seconds between client and server timestamp
const difference_in_seconds = Math.abs(serverTimestamp.getTime() - messageTimestamp.getTime()) / 1000;
if (difference_in_seconds >= config.EPOCH_ALLOWED_DELAY_THRESHOLD) {
return false;
}
return true;
}
verifyRLNProof = async (proof: RLNFullProof) => {
return RLN.verifyProof(
vkey as any,
proof,
);
}
addChatMessage = async (chatMessage: ChatMessage) => {
let data, r_user, s_user;
if (chatMessage.sender.address) {
s_user = await this.DB.users?.getUserByAddress(chatMessage.sender.address);
if (!s_user) throw new Error(`${chatMessage.sender} is not registered`);
}
if (chatMessage.receiver.address) {
r_user = await this.DB.users?.getUserByAddress(chatMessage.receiver.address);
if (!r_user) throw new Error(`${chatMessage.receiver} is not registered`);
}
if (chatMessage.type === 'DIRECT') {
data = {
message_id: chatMessage.messageId,
type: chatMessage.type,
sender_address: chatMessage.sender.hash
? undefined
: chatMessage.sender.address,
sender_hash: chatMessage.sender.hash,
sender_pubkey: chatMessage.sender.ecdh || s_user?.pubkey,
timestamp: chatMessage.timestamp.getTime(),
receiver_address: chatMessage.receiver.address,
receiver_pubkey: chatMessage.receiver.ecdh || r_user?.pubkey,
ciphertext: chatMessage.ciphertext,
rln_serialized_proof: chatMessage.rln ? JSON.stringify(chatMessage.rln) : undefined,
rln_root: chatMessage.rln
? '0x' + BigInt(chatMessage.rln?.publicSignals.merkleRoot).toString(16)
: undefined,
};
await this.DB.chats?.insertChatMessage(data);
}
return data;
}
}

View File

@@ -0,0 +1,194 @@
import {Mutex} from "async-mutex";
import {BIGINT, ENUM, Op, QueryTypes, Sequelize, STRING} from "sequelize";
const mutex = new Mutex();
export type ChatMessageModel = {
message_id: string;
type: 'DIRECT' | 'PUBLIC_ROOM' | 'PRIVATE_ROOM';
sender_address?: string;
sender_pubkey?: string;
sender_hash?: string;
timestamp: number;
rln_serialized_proof?: string;
rln_root?: string;
receiver_address?: string;
receiver_pubkey?: string;
ciphertext?: string;
content?: string;
reference?: string;
attachment?: string;
}
export type Chat = {
type: 'DIRECT';
receiver: string;
receiverECDH: string;
senderECDH: string;
senderHash?: string;
} | {
type: 'PUBLIC_ROOM';
receiver: string;
}
const REALLY_BIG_NUMBER = 999999999999999999;
const chats = (sequelize: Sequelize) => {
const model = sequelize.define('zkchat_chats', {
message_id: {
type: STRING,
primaryKey: true,
},
type: {
type: STRING,
allowNull: false,
},
sender_address: {
type: STRING,
},
sender_pubkey: {
type: STRING,
},
sender_hash: {
type: STRING,
},
timestamp: {
type: BIGINT,
allowNull: false,
},
rln_serialized_proof: {
type: STRING(65535),
},
rln_root: {
type: STRING,
},
receiver_address: {
type: STRING,
},
receiver_pubkey: {
type: STRING,
},
ciphertext: {
type: STRING,
},
content: {
type: STRING,
},
reference: {
type: STRING,
},
attachment: {
type: STRING,
},
}, {
indexes: [
{ fields: ['message_id'] },
{ fields: ['receiver_address'] },
{ fields: ['receiver_pubkey'] },
{ fields: ['sender_address'] },
{ fields: ['sender_pubkey'] },
{ fields: ['rln_root'] },
],
});
const insertChatMessage = async (data: ChatMessageModel) => {
return mutex.runExclusive(async () => {
const res = await model.create(data);
return res;
});
}
const removeChatMessage = async (message_id: string) => {
return mutex.runExclusive(async () => {
try {
const res = await model.destroy({
where: {
message_id,
},
});
return res;
} catch (e) {
return false;
}
});
}
const getDirectMessages = async (sender_pubkey: string, receiver_pubkey: string, offset = REALLY_BIG_NUMBER, limit = 20): Promise<ChatMessageModel[]> => {
const values = await sequelize.query(`
SELECT * FROM zkchat_chats zk
WHERE (
(zk.sender_pubkey = :sender_pubkey AND zk.receiver_pubkey = :receiver_pubkey)
OR
(zk.sender_pubkey = :receiver_pubkey AND zk.receiver_pubkey = :sender_pubkey)
) AND (
zk.timestamp < :offset
)
ORDER BY zk.timestamp DESC
LIMIT :limit
`, {
type: QueryTypes.SELECT,
replacements: {
sender_pubkey,
receiver_pubkey,
limit,
offset,
},
});
// @ts-ignore
return values;
}
const getDirectChatsForUser = async (pubkey: string): Promise<Chat[]> => {
const values = await sequelize.query(`
SELECT distinct zkc.receiver_pubkey as pubkey, zkc.receiver_address as address FROM zkchat_chats zkc
WHERE zkc.receiver_pubkey IN (
SELECT distinct receiver_pubkey FROM zkchat_chats WHERE sender_pubkey = :pubkey
)
UNION
SELECT distinct zkc.sender_pubkey as pubkey, zkc.sender_address as address FROM zkchat_chats zkc
WHERE zkc.sender_pubkey IN (
SELECT distinct sender_pubkey FROM zkchat_chats WHERE receiver_pubkey = :pubkey
);
`, {
type: QueryTypes.SELECT,
replacements: {
pubkey,
},
});
return values.map((data: any) => ({
type: 'DIRECT',
receiver: data.address,
receiverECDH: data.pubkey,
senderECDH: pubkey,
}));
}
const getMessagesByRoomId = async (roomId: string, offset = 0, limit = 20): Promise<ChatMessageModel[]> => {
const res = await model.findAll({
where: {
receiver: roomId,
},
limit,
offset,
order: [
['timestamp', 'DESC'],
],
});
// @ts-ignore
return res.map(data => data.toJSON());
}
return {
model,
insertChatMessage,
removeChatMessage,
getDirectMessages,
getMessagesByRoomId,
getDirectChatsForUser,
};
}
export default chats;

View File

@@ -0,0 +1,72 @@
import {Mutex} from "async-mutex";
import {BIGINT, ENUM, Op, QueryTypes, Sequelize, STRING} from "sequelize";
const mutex = new Mutex();
export type ShareModel = {
x_share: string;
y_share: string;
nullifier: string;
epoch: string;
}
const shares = (sequelize: Sequelize) => {
const model = sequelize.define('zkchat_shares', {
x_share: {
type: STRING,
},
y_share: {
type: STRING,
},
nullifier: {
type: STRING,
},
epoch: {
type: STRING,
},
}, {
indexes: [
{ fields: ['x_share', 'y_share', 'nullifier', 'epoch'], unique: true },
{ fields: ['nullifier'] },
{ fields: ['epoch'] },
],
});
const checkShare = async (data: ShareModel) => {
const shares = await getShares(data.nullifier, data.epoch);
const duplicate = await model.findOne({ where: data });
return {
shares,
isDuplicate: !!duplicate,
isSpam: !!shares.length,
};
};
const insertShare = async (data: ShareModel) => {
return mutex.runExclusive(async () => {
const res = await model.create(data);
return res;
});
};
const getShares = async (nullifier: string, epoch: string): Promise<ShareModel[]> => {
const res = await model.findAll({
where: {
nullifier,
epoch,
},
});
// @ts-ignore
return res.map(data => data.toJSON());
};
return {
model,
checkShare,
insertShare,
getShares,
}
};
export default shares;

View File

@@ -0,0 +1,92 @@
import {Mutex} from "async-mutex";
import {BIGINT, ENUM, Op, QueryTypes, Sequelize, STRING} from "sequelize";
const mutex = new Mutex();
type ZKUserModel = {
wallet_address: string;
pubkey: string;
}
const users = (sequelize: Sequelize) => {
const model = sequelize.define('zkchat_users', {
wallet_address: {
type: STRING,
primaryKey: true,
},
pubkey: {
type: STRING,
allowNull: false,
},
}, {
indexes: [
{ fields: ['wallet_address'] },
{ fields: ['pubkey'] },
],
});
const insertUser = async (wallet_address: string, pubkey: string) => {
return mutex.runExclusive(async () => {
const res = await model.findOne({
where: {
wallet_address,
},
});
if (res) {
return res.update({
pubkey,
});
} else {
return model.create({
wallet_address,
pubkey,
});
}
});
}
const getUsers = async (offset = 0, limit = 20) => {
const res = await model.findAll({
limit,
offset,
order: [
['createdAt', 'DESC'],
],
});
// @ts-ignore
return res.map(data => data.toJSON());
}
const getUserByAddress = async (wallet_address: string): Promise<ZKUserModel | undefined> => {
const res = await model.findOne({
where: {
wallet_address,
},
});
// @ts-ignore
return res?.toJSON();
}
const getUserByPubkey = async (pubkey: string) => {
const res = await model.findOne({
where: {
pubkey,
},
});
return res?.toJSON();
}
return {
model,
insertUser,
getUsers,
getUserByAddress,
getUserByPubkey,
};
}
export default users;

View File

@@ -0,0 +1,63 @@
import {GenericService} from "../utils/svc";
import {RLNFullProof} from "@zk-kit/protocols";
export enum ChatMessageType {
DIRECT = 'DIRECT',
PUBLIC_ROOM = 'PUBLIC_ROOM',
PRIVATE_ROOM = 'PRIVATE_ROOM',
}
export type DirectChatMessage = {
messageId: string;
timestamp: Date;
type: ChatMessageType.DIRECT;
sender: {
address?: string;
ecdh?: string;
hash?: string;
};
rln?: RLNFullProof & {
epoch: number;
x_share: string;
group_id: string;
};
receiver: {
address?: string;
ecdh?: string;
roomId?: string;
};
ciphertext: string;
content?: string;
reference?: string;
attachment?: string;
}
export type PublicRoomChatMessage = {
messageId: string;
timestamp: Date;
type: ChatMessageType.PUBLIC_ROOM;
sender: {
address?: string;
ecdh?: string;
hash?: string;
};
rln?: RLNFullProof & {
epoch: number;
x_share: string;
group_id: string;
};
receiver: {
address?: string;
ecdh?: string;
roomId?: string;
};
content: string;
reference: string;
attachment: string;
}
export type ChatMessage = DirectChatMessage | PublicRoomChatMessage;
export default class ChatService extends GenericService {
}

View File

@@ -0,0 +1,53 @@
import {GenericService} from "../utils/svc";
import {Dialect, Sequelize} from "sequelize";
import config from "../utils/config";
import chats from "../models/chat.model";
import users from "../models/user.model";
import shares from "../models/shares.model";
/**
* Encapsulates the core functionality for managing user credentials as well as viewing the banned users.
*/
class DBService extends GenericService {
sequelize: Sequelize;
chats?: ReturnType<typeof chats>;
users?: ReturnType<typeof users>;
shares?: ReturnType<typeof shares>;
constructor() {
super();
if (!config.DB_DIALECT || config.DB_DIALECT === 'sqlite') {
this.sequelize = new Sequelize({
dialect: 'sqlite',
storage: './zkchat.sqlite.db',
logging: false,
});
} else {
this.sequelize = new Sequelize(
config.DB_NAME as string,
config.DB_USERNAME as string,
config.DB_PASSWORD,
{
host: config.DB_HOST,
port: Number(config.DB_PORT),
dialect: config.DB_DIALECT as Dialect,
logging: false,
},
);
}
}
async start() {
this.chats = await chats(this.sequelize);
this.users = await users(this.sequelize);
this.shares = await shares(this.sequelize);
await this.users.model.sync({ force: !!process.env.FORCE });
await this.chats.model.sync({ force: !!process.env.FORCE });
await this.shares.model.sync({ force: !!process.env.FORCE });
}
}
export default DBService

View File

@@ -0,0 +1,7 @@
import {GenericService} from "../utils/svc";
export default class UserService extends GenericService {
registerUser = async (walletAddress: string, ecdhPubKey: string): Promise<void> => {
}
}

View File

@@ -0,0 +1,93 @@
import {ChatMessage, ChatMessageType} from "../services/chat.service";
import crypto from "crypto";
import {RLNFullProof} from "@zk-kit/protocols";
export const validateChatMessage = async (chatMessage: any): Promise<boolean> => {
assert(chatMessage.timestamp.getTime() > 0);
assert(chatMessage.receiver);
assert(chatMessage.messageId === await deriveMessageId(chatMessage));
if (!chatMessage.sender) {
assert(chatMessage.rln);
}
if (chatMessage.type === 'DIRECT') {
if (!chatMessage.sender) {
assert(chatMessage.pubkey);
}
return true;
}
if (chatMessage.type === 'PUBLIC_ROOM') {
assert(!chatMessage.ciphertext);
return true;
}
return false;
}
export const deriveMessageId = async (chatMessage: ChatMessage): Promise<string> => {
let data = '';
data += chatMessage.type;
data += chatMessage.timestamp;
data += chatMessage.sender;
if (chatMessage.rln) {
data += JSON.stringify(chatMessage.rln)
}
data += chatMessage.receiver;
if (chatMessage.type === 'DIRECT') {
data += chatMessage.pubkey || '';
data += chatMessage.ciphertext;
}
if (chatMessage.type === 'PUBLIC_ROOM') {
data += chatMessage.content;
data += chatMessage.reference;
data += chatMessage.attachment;
}
return crypto.createHash('sha256').update(data).digest('hex');
}
export const createChatMessage = async (opts: {
type: 'DIRECT' | 'PUBLIC_ROOM',
sender: string;
timestamp?: Date;
pubkey?: string;
receiver: string;
content?: string;
reference?: string;
attachment?: string;
rln?: RLNFullProof & {
epoch: number;
x_share: string;
};
}): Promise<ChatMessage | null> => {
let val: ChatMessage;
switch (opts.type) {
case 'DIRECT':
val = {
messageId: '',
type: ChatMessageType.DIRECT,
timestamp: opts.timestamp || new Date(),
sender: opts.sender,
pubkey: opts.pubkey,
receiver: opts.receiver,
rln: opts.rln,
ciphertext: '',
};
val.messageId = await deriveMessageId(val);
return val;
default:
return null;
}
}
function assert(data: any) {
if (!data) throw new Error(`cannot be null`);
}

View File

@@ -0,0 +1,20 @@
import dotenv from "dotenv";
const {parsed} = dotenv.config({path: './lib/zk-chat-server/.env'});
export default {
INTERREP_BASE_URL: parsed?.INTERREP_BASE_URL || "https://api.thegraph.com/subgraphs/name/interrep/kovan",
INTERREP_V2: parsed?.INTERREP_V2 || "https://kovan.interep.link/api",
SERVER_PORT: parseInt(parsed?.SERVER_PORT || "8080") || 8080,
SPAM_TRESHOLD: parseInt(parsed?.SPAM_TRESHOLD || "2", 2) || 2,
EPOCH_ALLOWED_DELAY_THRESHOLD: parseInt(parsed?.EPOCH_ALLOWED_DELAY_THRESHOLD || "20", 20) || 20,
ZERO_VALUE: BigInt(0),
RLN_IDENTIFIER: parseInt(parsed?.RLN_IDENTIFIER || "518137101") || 518137101,
DELETE_MESSAGES_OLDER_THAN_DAYS: parseInt(parsed?.DELETE_MESSAGES_OLDER_THAN_DAYS || "5") || 5,
DB_DIALECT: parsed?.DB_DIALECT || 'sqlite',
DB_NAME: parsed?.DB_NAME || '',
DB_USERNAME: parsed?.DB_USERNAME || '',
DB_PASSWORD: parsed?.DB_PASSWORD || '',
DB_HOST: parsed?.DB_HOST || 'localhost',
DB_PORT: parsed?.DB_PORT || 5432,
}

View File

@@ -0,0 +1,2 @@
import config from "./config";

View File

@@ -0,0 +1,38 @@
import winston from "winston";
const format = winston.format;
const { combine, timestamp, prettyPrint } = format;
export const logger = winston.createLogger({
level: 'info',
format: combine(
timestamp(),
format.colorize(),
format.json(),
),
transports: [
new winston.transports.File({
filename: 'error.log',
level: 'error',
}),
new winston.transports.File({
filename: 'combined.log',
}),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
level: 'info',
format: winston.format.simple(),
}));
}
export class GenericService {
async start() {
}
async stop() {
}
}

Binary file not shown.

View File

@@ -0,0 +1,119 @@
{
"protocol": "groth16",
"curve": "bn128",
"nPublic": 6,
"vk_alpha_1": [
"20491192805390485299153009773594534940189261866228447918068658471970481763042",
"9383485363053290200918347156157836566562967994039712273449902621266178545958",
"1"
],
"vk_beta_2": [
[
"6375614351688725206403948262868962793625744043794305715222011528459656738731",
"4252822878758300859123897981450591353533073413197771768651442665752259397132"
],
[
"10505242626370262277552901082094356697409835680220590971873171140371331206856",
"21847035105528745403288232691147584728191162732299865338377159692350059136679"
],
[
"1",
"0"
]
],
"vk_gamma_2": [
[
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
],
[
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
],
[
"1",
"0"
]
],
"vk_delta_2": [
[
"14962742607348761745067039667743739542918587958944430293802039673540545491766",
"19420690459571091434654770613940591470063366130537730965999231781890983822228"
],
[
"8025490964181160791180478620151567079603098158752936953068367103529675811355",
"16899630578734283128344486213072467840624492210479261395286682179061749476405"
],
[
"1",
"0"
]
],
"vk_alphabeta_12": [
[
[
"2029413683389138792403550203267699914886160938906632433982220835551125967885",
"21072700047562757817161031222997517981543347628379360635925549008442030252106"
],
[
"5940354580057074848093997050200682056184807770593307860589430076672439820312",
"12156638873931618554171829126792193045421052652279363021382169897324752428276"
],
[
"7898200236362823042373859371574133993780991612861777490112507062703164551277",
"7074218545237549455313236346927434013100842096812539264420499035217050630853"
]
],
[
[
"7077479683546002997211712695946002074877511277312570035766170199895071832130",
"10093483419865920389913245021038182291233451549023025229112148274109565435465"
],
[
"4595479056700221319381530156280926371456704509942304414423590385166031118820",
"19831328484489333784475432780421641293929726139240675179672856274388269393268"
],
[
"11934129596455521040620786944827826205713621633706285934057045369193958244500",
"8037395052364110730298837004334506829870972346962140206007064471173334027475"
]
]
],
"IC": [
[
"1903896611309112537898744888962900116850557104688140496996114549467905845389",
"18878807081439367511400607605971457252753640659171839147843428814578113606944",
"1"
],
[
"3379380938593207080729160389717513882233130462616390491862768147575356927916",
"7658366061956613070318477742617874370870162905631795036979866045784951077741",
"1"
],
[
"6573257371221466457264319919959056878987621722065494829982803761054687125969",
"10882724634177179759524298899662307047578489073607305659656545291770678864358",
"1"
],
[
"7812315588577187126949469943175416873443639588836013906528191759531184751241",
"11928578526804134334949431821595151892366575912030023302327660047097849106833",
"1"
],
[
"8603322753346553805166809285487650902416020947317326006612301010819108432803",
"3100089596497060388197556426101480177422659517238854788949908444987414217355",
"1"
],
[
"548584601681146753562700521279324517960520272202999011217745775929880443671",
"10775304696187723066725699321032508342393012304409445749847420882128296879500",
"1"
],
[
"4832346778469239752999589970014962368162544386191392542939015835265757824362",
"17497085844864771075682226779903142040218959461493641885155366729946818944068",
"1"
]
]
}

2164
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,41 +17,49 @@
"license": "ISC",
"dependencies": {
"@ensdomains/ensjs": "^2.0.1",
"@interep/reputation": "^0.1.0",
"@interep/reputation": "^0.4.0",
"@zk-kit/identity": "^1.4.1",
"@zk-kit/protocols": "^1.8.2",
"@zk-kit/protocols": "^1.11.1",
"async-mutex": "^0.3.1",
"bn.js": "^5.2.0",
"body-parser": "^1.19.0",
"botometer": "^1.0.12",
"connect-session-sequelize": "^7.1.2",
"cors": "^2.8.5",
"dotenv": "^16.0.1",
"elliptic": "^6.5.4",
"ethereumjs-util": "^7.1.2",
"express": "^4.17.1",
"express-fileupload": "^1.4.0",
"express-session": "^1.17.2",
"gun": "^0.2020.1232",
"http-terminator": "^3.2.0",
"ipfs-http-client": "56.x",
"isomorphic-fetch": "^3.0.0",
"jsonwebtoken": "^8.5.1",
"link-preview-js": "^2.1.8",
"link-preview-node": "^1.0.7",
"lru-cache": "^6.0.0",
"multer": "^1.4.5-lts.1",
"oauth-1.0a": "^2.2.6",
"pg": "^8.7.1",
"semaphore-lib": "git+https://github.com/akinovak/semaphore-lib.git#dev",
"sequelize": "^6.6.2",
"sqlite3": "^5.0.2",
"web3": "^1.3.5",
"winston": "^3.3.3"
"web3.storage": "^4.3.0",
"winston": "^3.3.3",
"zk-chat-server": "^1.0.4"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@types/cors": "^2.8.10",
"@types/elliptic": "^6.4.14",
"@types/express": "^4.17.11",
"@types/express-fileupload": "^1.2.2",
"@types/express-session": "^1.17.4",
"@types/jsonwebtoken": "^8.5.6",
"@types/multer": "^1.4.7",
"@types/node": "^14.14.37",
"@types/sinon": "^10.0.11",
"@types/tape": "^4.13.2",

View File

@@ -7,15 +7,21 @@ import HttpService from "./services/http";
import GunService from "./services/gun";
import InterrepService from "./services/interrep";
import ArbitrumService from "./services/arbitrum";
import IPFSService from "./services/ipfs";
import ZKChatService from "./services/zkchat";
import MerkleService from "./services/merkle";
(async function initApp() {
try {
const main = new MainService();
main.add('db', new DBService());
main.add('merkle', new MerkleService());
main.add('interrep', new InterrepService());
main.add('zkchat', new ZKChatService());
main.add('ens', new ENSService());
main.add('arbitrum', new ArbitrumService());
main.add('gun', new GunService());
main.add('ipfs', new IPFSService());
main.add('http', new HttpService());
await main.start();
} catch (e) {

View File

@@ -25,9 +25,10 @@ const interepGroups = (sequelize: Sequelize) => {
},
}, {
indexes: [
{ fields: ['root_hash'], unique: true },
{ fields: ['root_hash'] },
{ fields: ['provider'] },
{ fields: ['name'] },
{ fields: ['provider', 'name'], unique: true },
],
});
@@ -45,7 +46,6 @@ const interepGroups = (sequelize: Sequelize) => {
return mutex.runExclusive(async () => {
const result = await model.findOne({
where: {
root_hash,
provider,
name,
},
@@ -57,14 +57,32 @@ const interepGroups = (sequelize: Sequelize) => {
provider,
name,
});
} else {
return result.update({
root_hash,
});
}
});
}
const getGroup = async (provider: string, name: string): Promise<InterepGroupModel> => {
return mutex.runExclusive(async () => {
const result = await model.findOne({
where: {
provider,
name,
},
});
return result?.toJSON() as InterepGroupModel;
});
}
return {
model,
findOneByHash,
addHash,
getGroup,
};
}

56
src/models/merkle_root.ts Normal file
View File

@@ -0,0 +1,56 @@
import {Sequelize, BIGINT, STRING} from "sequelize";
type MerkleRootModel = {
root_hash: string;
group_id: string;
};
const merkleRoot = (sequelize: Sequelize) => {
const model = sequelize.define('merkle_root', {
root_hash: {
type: STRING,
},
group_id: {
type: STRING,
},
}, {
indexes: [
{ fields: ['root_hash'], unique: true },
{ fields: ['group_id'] },
],
});
const getGroupByRoot = async (root_hash: string): Promise<MerkleRootModel> => {
let result = await model.findOne({
where: {
root_hash,
},
});
return result?.toJSON() as MerkleRootModel;
}
const addRoot = async (root_hash: string, group_id: string) => {
const exist = await model.findOne({
where: {
root_hash,
},
});
if (!exist) {
return model.create({
root_hash,
group_id,
});
}
}
return {
model,
addRoot,
getGroupByRoot,
};
};
export default merkleRoot

View File

@@ -3,8 +3,8 @@ import {Mutex} from "async-mutex";
type SemaphoreModel = {
id_commitment: string;
provider: string;
name: string;
group_id: string;
root_hash: string;
};
const mutex = new Mutex();
@@ -15,19 +15,19 @@ const semaphore = (sequelize: Sequelize) => {
type: STRING,
allowNull: false,
},
provider: {
group_id: {
type: STRING,
allowNull: false,
},
name: {
root_hash: {
type: STRING,
allowNull: false,
},
}, {
indexes: [
{ fields: ['id_commitment'] },
{ fields: ['provider'] },
{ fields: ['name'] },
{ fields: ['group_id'] },
{ fields: ['root_hash'] },
],
});
@@ -54,33 +54,34 @@ const semaphore = (sequelize: Sequelize) => {
return result.map(r => r.toJSON()) as SemaphoreModel[];
}
const addID = async (id_commitment: string, provider: string, name: string) => {
const addID = async (id_commitment: string, group_id: string, root_hash: string) => {
return mutex.runExclusive(async () => {
const result = await model.findOne({
where: {
id_commitment,
provider,
name,
group_id,
},
});
if (!result) {
return model.create({
id_commitment,
provider,
name,
group_id,
root_hash,
});
} else {
return result.update({ root_hash });
}
});
}
const removeID = async (id_commitment: string, provider: string, name: string) => {
const removeID = async (id_commitment: string, group_id: string, root_hash: string) => {
return mutex.runExclusive(async () => {
return model.destroy({
where: {
id_commitment,
provider,
name,
group_id,
root_hash,
},
});
});

83
src/models/uploads.ts Normal file
View File

@@ -0,0 +1,83 @@
import {Mutex} from "async-mutex";
import {BIGINT, ENUM, QueryTypes, Sequelize, STRING} from "sequelize";
export type UploadModel = {
cid: string;
filename: string;
username: string;
size: number;
mimetype: string;
};
const mutex = new Mutex();
const uploads = (sequelize: Sequelize) => {
const model = sequelize.define('uploads', {
cid: {
type: STRING,
},
filename: {
type: STRING,
},
username: {
type: STRING,
},
size: {
type: BIGINT,
},
mimetype: {
type: STRING,
},
}, {
indexes: [
{ fields: ['cid'] },
{ fields: ['filename'] },
{ fields: ['username'] },
{ fields: ['mimetype'] },
],
});
const addUploadData = async (data: UploadModel) => {
return mutex.runExclusive(async () => {
const res = await model.create(data);
return res;
});
}
const removeUploadData = async (cid: string, filename: string) => {
return mutex.runExclusive(async () => {
try {
const res = await model.destroy({
where: {
cid,
filename,
},
});
return res;
} catch (e) {
return false;
}
});
}
const getTotalUploadByUser = async (username: string) => {
const res = await model.sum('size', {
where: {
username,
},
});
if (isNaN(res)) return 0;
return res;
}
return {
model,
addUploadData,
removeUploadData,
getTotalUploadByUser,
};
}
export default uploads;

View File

@@ -214,7 +214,9 @@ const userSelectQuery = `
"coverImage".value as "coverImage",
"twitterVerification".value as "tweetId",
"twitterVerification".key as "twitterHandle",
website.value as website
website.value as website,
ecdh.value as ecdh,
idcommitment.value as idcommitment
FROM users u
LEFT JOIN usermeta umt ON umt.name = u.name
LEFT JOIN connections f ON f.subtype = 'FOLLOW' AND f.creator = :context AND f.name = u.name
@@ -225,6 +227,8 @@ const userSelectQuery = `
LEFT JOIN profiles "coverImage" ON "coverImage"."messageId" = (SELECT "messageId" FROM profiles WHERE creator = u.name AND subtype = 'COVER_IMAGE' ORDER BY "createdAt" DESC LIMIT 1)
LEFT JOIN profiles "twitterVerification" ON "twitterVerification"."messageId" = (SELECT "messageId" FROM profiles WHERE creator = u.name AND subtype = 'TWT_VERIFICATION' ORDER BY "createdAt" DESC LIMIT 1)
LEFT JOIN profiles website ON website."messageId" = (SELECT "messageId" FROM profiles WHERE creator = u.name AND subtype = 'WEBSITE' ORDER BY "createdAt" DESC LIMIT 1)
LEFT JOIN profiles ecdh ON ecdh."messageId" = (SELECT "messageId" FROM profiles WHERE creator = u.name AND subtype = 'CUSTOM' AND key='ecdh_pubkey' ORDER BY "createdAt" DESC LIMIT 1)
LEFT JOIN profiles idcommitment ON idcommitment."messageId" = (SELECT "messageId" FROM profiles WHERE creator = u.name AND subtype = 'CUSTOM' AND key='id_commitment' ORDER BY "createdAt" DESC LIMIT 1)
`;
function inflateValuesToUserJSON(values: any[]): UserModel[] {
@@ -249,6 +253,8 @@ function inflateValuesToUserJSON(values: any[]): UserModel[] {
coverImage: value.coverImage || '',
twitterVerification: twitterVerification,
website: value.website || '',
ecdh: value.ecdh || '',
idcommitment: value.idcommitment || '',
meta: {
blockedCount: value.blockedCount ? Number(value.blockedCount) : 0,
blockingCount: value.blockingCount ? Number(value.blockingCount) : 0,

View File

@@ -18,6 +18,8 @@ import twitterAuth from "../../models/twitterAuth";
import interepGroups from "../../models/interepGroups";
import semaphoreCreators from "../../models/semaphore_creators";
import threads from "../../models/thread";
import uploads from "../../models/uploads";
import merkleRoot from "../../models/merkle_root";
export default class DBService extends GenericService {
sequelize: Sequelize;
@@ -40,6 +42,8 @@ export default class DBService extends GenericService {
interepGroups?: ReturnType<typeof interepGroups>;
semaphoreCreators?: ReturnType<typeof semaphoreCreators>;
threads?: ReturnType<typeof threads>;
uploads?: ReturnType<typeof uploads>;
merkleRoot?: ReturnType<typeof merkleRoot>;
constructor() {
super();
@@ -195,6 +199,13 @@ export default class DBService extends GenericService {
return this.threads;
}
async getUploads(): Promise<ReturnType<typeof uploads>> {
if (!this.uploads) {
return Promise.reject(new Error('uploads is not initialized'));
}
return this.uploads;
}
async start() {
this.app = await app(this.sqlite);
this.records = await records(this.sqlite);
@@ -213,6 +224,8 @@ export default class DBService extends GenericService {
this.interepGroups = await interepGroups(this.sequelize);
this.semaphoreCreators = await semaphoreCreators(this.sequelize);
this.threads = await threads(this.sequelize);
this.uploads = await uploads(this.sequelize);
this.merkleRoot = await merkleRoot(this.sequelize);
await this.app?.model.sync({ force: !!process.env.FORCE });
await this.linkPreview?.model.sync({ force: !!process.env.FORCE });
@@ -234,6 +247,8 @@ export default class DBService extends GenericService {
await this.interepGroups?.model.sync({ force: !!process.env.FORCE });
await this.semaphoreCreators?.model.sync({ force: !!process.env.FORCE });
await this.threads?.model.sync({ force: !!process.env.FORCE });
await this.uploads?.model.sync({ force: !!process.env.FORCE });
await this.merkleRoot?.model.sync({ force: !!process.env.FORCE });
const appData = await this.app?.read();

View File

@@ -28,8 +28,6 @@ const getMutex = new Mutex();
const putMutex = new Mutex();
const insertMutex = new Mutex();
import { Semaphore, genExternalNullifier, genSignalHash } from "@zk-kit/protocols";
export default class GunService extends GenericService {
gun?: IGunChainReference;
@@ -114,7 +112,15 @@ export default class GunService extends GenericService {
attachment: payload.attachment,
},
});
await this.insertPost(post, data.proof, data.publicSignals);
await this.insertPost(
post,
!data.proof ? undefined : {
proof: data.proof,
publicSignals: data.publicSignals,
x_share: data.x_share,
epoch: data.epoch,
},
);
return;
case MessageType.Moderation:
const moderation = new Moderation({
@@ -236,7 +242,12 @@ export default class GunService extends GenericService {
}
async insertPost(post: Post, proof?: string, signals?: string) {
async insertPost(post: Post, data?: {
proof: string;
publicSignals: string;
x_share: string;
epoch: string;
}) {
const json = await post.toJSON();
const {
type,
@@ -269,46 +280,51 @@ export default class GunService extends GenericService {
return;
}
if (!creator && (!proof || !signals)) {
if (!creator && !data) {
return;
}
try {
if (proof && signals) {
const parsedProof = JSON.parse(proof);
const parsedSignals = JSON.parse(signals);
const externalNullifier = await genExternalNullifier('POST');
const signalHash = await genSignalHash(hash);
if (data) {
const proof = JSON.parse(data.proof);
const publicSignals = JSON.parse(data.publicSignals);
const verified = await this.call('zkchat', 'verifyRLNProof', {
proof,
publicSignals,
x_share: data.x_share,
epoch: data.epoch,
});
if (BigInt(externalNullifier).toString() !== parsedSignals.externalNullifier) return;
if (signalHash.toString() !== parsedSignals.signalHash) return;
const share = {
nullifier: publicSignals.internalNullifier,
epoch: publicSignals.epoch,
y_share: publicSignals.yShare,
x_share: data.x_share,
};
const hashData = await this.call('interrep', 'getBatchFromRootHash', parsedSignals.merkleRoot);
const {
shares,
isSpam,
isDuplicate,
} = await this.call('zkchat', 'checkShare', share);
if (!hashData) {
return;
}
const group = await this.call(
'merkle',
'getGroupByRoot',
'0x' + BigInt(publicSignals.merkleRoot).toString(16),
);
const res = await Semaphore.verifyProof(
vKey as any,
{
proof: parsedProof,
publicSignals: parsedSignals,
});
if (isSpam || isDuplicate || !verified || !group) return;
if (!res) {
return;
}
const { name, provider } = hashData;
await semaphoreCreatorsDB.addSemaphoreCreator(messageId, provider, name);
const [protocol, groupName, groupType] = group.split('_')
await semaphoreCreatorsDB.addSemaphoreCreator(messageId, groupName, groupType);
}
await postDB.createPost({
messageId: messageId,
hash: hash,
proof,
signals,
proof: data?.proof,
signals: data?.publicSignals,
type: type,
subtype: subtype,
creator: creator || '',
@@ -537,6 +553,10 @@ export default class GunService extends GenericService {
return;
}
if (subtype === ProfileMessageSubType.Custom && payload.key === 'ecdh_pubkey') {
await this.call('zkchat', 'registerUser', creator, payload.value);
}
if (subtype === ProfileMessageSubType.TwitterVerification) {
const { key, value } = payload;

View File

@@ -1,5 +1,5 @@
import {GenericService} from "../util/svc";
import express, {Express, Request, Response} from "express";
import express, {Express, NextFunction, Request, Response} from "express";
import bodyParser from "body-parser";
import cors, {CorsOptions} from "cors";
import http from 'http';
@@ -12,7 +12,7 @@ import { getLinkPreview } from "link-preview-js";
import queryString from "querystring";
import session from 'express-session';
import jwt from "jsonwebtoken";
import {Dialect, Sequelize} from "sequelize";
import {Dialect, QueryTypes, Sequelize} from "sequelize";
const SequelizeStore = require("connect-session-sequelize")(session.Store);
import { calculateReputation, OAuthProvider } from "@interep/reputation";
import {
@@ -27,6 +27,19 @@ import {
} from "../util/twitter";
import {verifySignatureP256} from "../util/crypto";
import {parseMessageId, PostMessageSubType} from "../util/message";
import multer from 'multer';
const upload = multer({
dest: './uploaded_files',
});
import fs from 'fs';
import { getFilesFromPath } from 'web3.storage';
import {UploadModel} from "../models/uploads";
import {genExternalNullifier, RLNFullProof, Semaphore, SemaphoreFullProof} from "@zk-kit/protocols";
import vKey from "../../static/verification_key.json";
import merkleRoot from "../models/merkle_root";
import {sequelize} from "../util/sequelize";
import crypto from 'crypto';
import {addConnection, addTopic, keepAlive, publishTopic, removeConnection, SSEType} from "../util/sse";
const corsOptions: CorsOptions = {
credentials: true,
@@ -36,6 +49,9 @@ const corsOptions: CorsOptions = {
};
const JWT_SECRET = config.jwtSecret;
const ONE_MB = 1048576;
const maxFileSize = ONE_MB * 5;
const maxPerUserSize = ONE_MB * 100;
function makeResponse(payload: any, error?: boolean) {
return {
@@ -47,13 +63,96 @@ function makeResponse(payload: any, error?: boolean) {
export default class HttpService extends GenericService {
app: Express;
httpServer: any;
merkleRoot?: ReturnType<typeof merkleRoot>;
constructor() {
super();
this.app = express();
}
wrapHandler(handler: (req: Request, res: Response) => Promise<void>) {
verifyAuth = (
getExternalNullifer: (req: Request) => string | Promise<string>,
getSignal: (req: Request) => string | Promise<string>,
onError?: (req: Request) => void | Promise<void>,
) => async (req: Request, res: Response, next: NextFunction) => {
const signature = req.header('X-SIGNED-ADDRESS');
const semaphoreProof = req.header('X-SEMAPHORE-PROOF');
const rlnProof = req.header('X-RLN-PROOF');
const userDB = await this.call('db', 'getUsers');
if (signature) {
const params = signature.split('.');
const user = await userDB.findOneByName(params[1]);
if (!user || !verifySignatureP256(user.pubkey, params[1], params[0])) {
res.status(403).send(makeResponse('user must be authenticated', true));
if (onError) onError(req);
return;
}
// @ts-ignore
req.username = params[1];
} else if (semaphoreProof) {
const { proof, publicSignals } = JSON.parse(semaphoreProof) as SemaphoreFullProof;
const externalNullifier = await genExternalNullifier(await getExternalNullifer(req));
const signalHash = await Semaphore.genSignalHash(await getSignal(req));
const matchNullifier = BigInt(externalNullifier).toString() === publicSignals.externalNullifier;
const matchSignal = signalHash.toString() === publicSignals.signalHash;
const hashData = await this.call(
'interrep',
'getBatchFromRootHash',
publicSignals.merkleRoot
);
const verified = await Semaphore.verifyProof(
vKey as any,
{
proof,
publicSignals,
},
);
if (!matchNullifier || !matchSignal || !verified || !hashData) {
res.status(403).send(makeResponse('invalid semaphore proof', true));
if (onError) onError(req);
return;
}
} else if (rlnProof) {
const { proof, publicSignals, x_share, epoch } = JSON.parse(rlnProof);
const verified = await this.call('zkchat', 'verifyRLNProof', {
proof,
publicSignals,
x_share: x_share,
epoch: epoch,
});
const share = {
nullifier: publicSignals.internalNullifier,
epoch: publicSignals.epoch,
y_share: publicSignals.yShare,
x_share: x_share,
};
const {
shares,
isSpam,
isDuplicate,
} = await this.call('zkchat', 'checkShare', share);
const group = await this.call(
'merkle',
'getGroupByRoot',
'0x' + BigInt(publicSignals.merkleRoot).toString(16),
);
if (isSpam || isDuplicate || !verified || !group) {
res.status(403).send(makeResponse('invalid semaphore proof', true));
if (onError) onError(req);
return;
};
}
next();
}
wrapHandler(handler: (req: Request, res: Response) => Promise<any>) {
return async (req: Request, res: Response) => {
logger.info('received request', {
url: req.url,
@@ -272,6 +371,214 @@ export default class HttpService extends GenericService {
res.send(makeResponse(post));
};
handleGetChatUsers = async (req: Request, res: Response) => {
const limit = req.query.limit && Number(req.query.limit);
const offset = req.query.offset && Number(req.query.offset);
const users = await this.call('zkchat', 'getAllUsers', offset, limit);
res.send(makeResponse(users));
};
handlePostChatMessage = async (req: Request, res: Response) => {
const {
messageId,
type,
timestamp,
sender,
receiver,
ciphertext,
rln,
} = req.body;
const signature = req.header('X-SIGNED-ADDRESS');
const userDB = await this.call('db', 'getUsers');
if (!sender.address && (!sender.hash && !sender.ecdh)) throw new Error('invalid sender');
if (!receiver.address && !receiver.ecdh) throw new Error('invalid receiver');
if (rln) {
if (!sender.hash) throw new Error('invalid request object');
const isEpochCurrent = await this.call('zkchat', 'isEpochCurrent', rln.epoch);
const verified = await this.call('zkchat', 'verifyRLNProof', rln);
const root = '0x' + BigInt(rln.publicSignals.merkleRoot).toString(16);
const group = await this.call(
'merkle',
'getGroupByRoot',
root,
);
if (!isEpochCurrent) throw new Error('outdated message');
if (!verified) throw new Error('invalid rln proof');
if (!group) throw new Error('invalid merkle root');
rln.group_id = group;
const share = {
nullifier: rln.publicSignals.internalNullifier,
epoch: rln.publicSignals.epoch,
y_share: rln.publicSignals.yShare,
x_share: rln.x_share,
};
const {
shares,
isSpam,
isDuplicate,
} = await this.call('zkchat', 'checkShare', share);
if (isDuplicate) {
throw new Error('duplicate message');
}
if (isSpam) {
res.status(429).send('too many requests');
return;
}
await this.call('zkchat', 'insertShare', share);
await this.merkleRoot?.addRoot(root, group);
} else if (signature) {
const [sig, address] = signature.split('.');
const user = await userDB.findOneByName(address);
if (user?.pubkey) {
if (!verifySignatureP256(user.pubkey, address, sig)) {
res.status(403).send(makeResponse('unauthorized', true));
return;
}
}
} else {
res.status(403).send(makeResponse('unauthorized', true));
return;
}
const data = await this.call('zkchat', 'addChatMessage', {
messageId,
type,
timestamp: new Date(timestamp),
sender,
receiver,
ciphertext,
rln,
});
publishTopic(`ecdh:${data.sender_pubkey}`, {
type: SSEType.NEW_CHAT_MESSAGE,
message: data,
});
publishTopic(`ecdh:${data.receiver_pubkey}`, {
type: SSEType.NEW_CHAT_MESSAGE,
message: data,
});
res.send(makeResponse(data));
}
handleGetDirectMessage = async (req: Request, res: Response) => {
const {sender, receiver} = req.params;
const limit = req.query.limit && Number(req.query.limit);
const offset = req.query.offset && Number(req.query.offset);
const data = await this.call(
'zkchat',
'getDirectMessages',
sender,
receiver,
offset,
limit,
);
res.send(makeResponse(data));
}
handleGetDirectChats = async (req: Request, res: Response) => {
const {pubkey} = req.params;
const values = await sequelize.query(`
SELECT distinct zkc.receiver_pubkey as pubkey, zkc.receiver_address as address, null as group_id
FROM zkchat_chats zkc
WHERE zkc.receiver_pubkey IN (
SELECT distinct receiver_pubkey FROM zkchat_chats WHERE sender_pubkey = :pubkey
)
UNION
SELECT distinct zkc.sender_pubkey as pubkey, zkc.sender_address as address, mr.group_id
FROM zkchat_chats zkc
LEFT JOIN merkle_roots mr on mr.root_hash = zkc.rln_root
WHERE zkc.sender_pubkey IN (
SELECT distinct sender_pubkey FROM zkchat_chats WHERE receiver_pubkey = :pubkey
);
`, {
type: QueryTypes.SELECT,
replacements: {
pubkey,
},
});
const data = values.map((val: any) => ({
type: 'DIRECT',
receiver: val.address,
receiverECDH: val.pubkey,
senderECDH: pubkey,
group: val.group_id,
}));
res.send(makeResponse(data));
}
handleSearchChats = async (req: Request, res: Response) => {
const {query} = req.params;
const {sender} = req.query;
const data = await this.call('zkchat', 'searchChats', query || '', sender);
res.send(makeResponse(data));
}
handleGetProofs = async (req: Request, res: Response) => {
const {idCommitment} = req.params;
const {group = ''} = req.query;
const proof = await this.call('merkle', 'findProof', group, idCommitment);
res.send(makeResponse({
data: proof,
group: group,
}));
}
handleGetEvents = async (req: Request, res: Response) => {
const headers = {
'Content-Type': 'text/event-stream',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache',
};
res.writeHead(200, headers);
const clientId = crypto.randomBytes(16).toString('hex');
addConnection(clientId, res);
}
handleUpdateSSEClient = async (req: Request, res: Response) => {
const {clientId} = req.params;
const { topics } = req.body;
for (const topic of topics) {
addTopic(clientId, topic);
}
res.send(makeResponse('ok'));
}
handleSSEKeepAlive = async (req: Request, res: Response) => {
const {clientId} = req.params;
keepAlive(clientId);
res.send(makeResponse('ok'));
}
handleSSETerminate = async (req: Request, res: Response) => {
const {clientId} = req.params;
console.log(clientId);
removeConnection(clientId);
res.send(makeResponse('ok'));
}
addRoutes() {
this.app.get('/healthcheck', this.wrapHandler(async (req, res) => {
res.send(makeResponse('ok'));
@@ -289,6 +596,18 @@ export default class HttpService extends GenericService {
this.app.get('/v1/homefeed', this.wrapHandler(this.handleGetHomefeed));
this.app.get('/v1/post/:hash', this.wrapHandler(this.handleGetPostByHash));
this.app.get('/v1/zkchat/users', this.wrapHandler(this.handleGetChatUsers));
this.app.post('/v1/zkchat/chat-messages', jsonParser, this.wrapHandler(this.handlePostChatMessage));
this.app.get('/v1/zkchat/chat-messages/dm/:sender/:receiver', this.wrapHandler(this.handleGetDirectMessage));
this.app.get('/v1/zkchat/chats/dm/:pubkey', this.wrapHandler(this.handleGetDirectChats));
this.app.get('/v1/zkchat/chats/search/:query?', this.wrapHandler(this.handleSearchChats));
this.app.get('/v1/proofs/:idCommitment', this.wrapHandler(this.handleGetProofs));
this.app.get('/v1/events', this.wrapHandler(this.handleGetEvents));
this.app.post('/v1/events/:clientId', jsonParser, this.wrapHandler(this.handleUpdateSSEClient));
this.app.get('/v1/events/:clientId/alive', jsonParser, this.wrapHandler(this.handleSSEKeepAlive));
this.app.get('/v1/events/:clientId/terminate', jsonParser, this.wrapHandler(this.handleSSETerminate));
this.app.post('/interrep/groups/:provider/:name/:identityCommitment', jsonParser, this.wrapHandler(async (req, res) => {
const identityCommitment = req.params.identityCommitment;
const provider = req.params.provider;
@@ -321,14 +640,40 @@ export default class HttpService extends GenericService {
res.send(makeResponse(json));
}));
this.app.get('/dev/interep/:identityCommitment', jsonParser, this.wrapHandler(async (req, res) => {
const identityCommitment = req.params.identityCommitment;
// @ts-ignore
const resp = await fetch(`${config.interrepAPI}/api/v1/groups`);
const { data: groups } = await resp.json();
for (const group of groups) {
// @ts-ignore
const existResp = await fetch(`${config.interrepAPI}/api/v1/groups/${group.provider}/${group.name}/${identityCommitment}`);
const { data: exist } = await existResp.json();
if (exist) {
// @ts-ignore
const proofResp = await fetch(`${config.interrepAPI}/api/v1/groups/${group.provider}/${group.name}/${identityCommitment}/proof`);
const json = await proofResp.json();
res.send(makeResponse({
...json,
provider: group.provider,
name: group.name,
}));
return;
}
}
res.send(makeResponse(null));
}));
this.app.get('/interrep/:identityCommitment', jsonParser, this.wrapHandler(async (req, res) => {
const identityCommitment = req.params.identityCommitment;
const semaphoreDB = await this.call('db', 'getSemaphore');
const exist = await semaphoreDB.findOneByCommitment(identityCommitment);
// const exist = await semaphoreDB.findOneByCommitment(identityCommitment);
if (!exist || exist?.updatedAt.getTime() + 15 * 60 * 1000 > Date.now()) {
await this.call('interrep', 'scanIDCommitment', identityCommitment);
}
// if (!exist || exist?.updatedAt.getTime() + 15 * 60 * 1000 > Date.now()) {
// await this.call('interrep', 'scanIDCommitment', identityCommitment);
// }
const sem = await semaphoreDB.findAllByCommitment(identityCommitment);
const [group] = sem;
@@ -539,26 +884,77 @@ export default class HttpService extends GenericService {
if (req.session.twitterToken) delete req.session.twitterToken;
res.send(makeResponse('ok'));
}));
this.app.post(
'/ipfs/upload',
upload.any(),
this.verifyAuth(
async () => 'FILE_UPLOAD',
// @ts-ignore
async req => req.files[0].originalname.slice(0, 16),
req => {
// @ts-ignore
const filepath = path.join(process.cwd(), req.files[0].path);
fs.unlinkSync(filepath);
}
),
this.wrapHandler(async (req, res) => {
if (!req.files) throw new Error('file missing from formdata');
// @ts-ignore
const username = req.username;
// @ts-ignore
const {path: relPath, filename, size, mimetype} = req.files[0];
const uploadDB = await this.call('db', 'getUploads');
const filepath = path.join(process.cwd(), relPath);
if (size > maxFileSize) {
fs.unlinkSync(filepath);
throw new Error('file must be less than 5MB');
}
if (username) {
const existingSize = await uploadDB.getTotalUploadByUser(username);
if (existingSize + size > maxPerUserSize) {
fs.unlinkSync(filepath);
throw new Error('account is out of space');
}
}
const files = await getFilesFromPath(filepath);
const cid = await this.call('ipfs', 'store', files);
fs.unlinkSync(filepath);
if (username) {
const uploadData: UploadModel = {
cid,
mimetype,
size,
filename,
username: username,
};
await uploadDB.addUploadData(uploadData);
}
res.send(makeResponse({
cid,
filename,
url: `https://${cid}.ipfs.dweb.link/${filename}`,
}));
}),
);
}
async start() {
this.merkleRoot = await merkleRoot(sequelize);
const httpServer = http.createServer(this.app);
this.app.set('trust proxy', 1);
this.app.use(cors(corsOptions));
const sequelize = new Sequelize(
config.dbName as string,
config.dbUsername as string,
config.dbPassword,
{
host: config.dbHost,
port: Number(config.dbPort),
dialect: config.dbDialect as Dialect,
logging: false,
},
);
const sessionStore = new SequelizeStore({
db: sequelize,
})
@@ -580,6 +976,13 @@ export default class HttpService extends GenericService {
this.app.use('/dev/semaphore_wasm', express.static(path.join(process.cwd(), 'static', 'semaphore.wasm')));
this.app.use('/dev/semaphore_final_zkey', express.static(path.join(process.cwd(), 'static', 'semaphore_final.zkey')));
this.app.use('/dev/semaphore_vkey', express.static(path.join(process.cwd(), 'static', 'verification_key.json')));
this.app.use('/circuits/semaphore/wasm', express.static(path.join(process.cwd(), 'static', 'semaphore', 'semaphore.wasm')));
this.app.use('/circuits/semaphore/zkey', express.static(path.join(process.cwd(), 'static', 'semaphore', 'semaphore_final.zkey')));
this.app.use('/circuits/semaphore/vkey', express.static(path.join(process.cwd(), 'static', 'semaphore', 'verification_key.json')));
this.app.use('/circuits/rln/wasm', express.static(path.join(process.cwd(), 'static', 'rln', 'rln.wasm')));
this.app.use('/circuits/rln/zkey', express.static(path.join(process.cwd(), 'static', 'rln', 'rln_final.zkey')));
this.app.use('/circuits/rln/vkey', express.static(path.join(process.cwd(), 'static', 'rln', 'verification_key.json')));
this.addRoutes();
this.httpServer = httpServer.listen(config.port);

View File

@@ -1,5 +1,9 @@
import {GenericService} from "../util/svc";
import config from "../util/config";
import interepGroups from "../models/interepGroups";
import {sequelize} from "../util/sequelize";
import semaphore from "../models/semaphore";
import {clear} from "winston";
export type InterepGroup = {
provider: 'twitter' | 'github' | 'reddit';
@@ -8,60 +12,66 @@ export type InterepGroup = {
size: number;
}
const INTEREP_SYNC_INTERVAL = 15 * 60 * 1000;
export default class InterrepService extends GenericService {
interepGroups?: ReturnType<typeof interepGroups>;
semaphore?: ReturnType<typeof semaphore>;
groups: {
[providerName: string]: InterepGroup[],
};
providers = ['twitter', 'github', 'reddit'];
timeout: any;
constructor() {
super();
this.groups = {};
}
async fetchGroups() {
// @ts-ignore
const resp = await fetch(`${config.interrepAPI}/api/v1/groups`);
const json = await resp.json();
if (json?.data?.length) {
for (let group of json.data) {
if (this.providers.includes(group.provider)) {
const bucket = this.groups[group.provider] || [];
bucket.push(group);
this.groups[group.provider] = bucket;
}
sync = async () => {
try {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
}
async addID(id_commitment: string, provider: string, name: string) {
const semaphore = await this.call('db', 'getSemaphore');
await semaphore.addID(id_commitment, provider, name);
}
async removeID(id_commitment: string, provider: string, name: string) {
const semaphore = await this.call('db', 'getSemaphore');
await semaphore.removeID(id_commitment, provider, name);
}
async scanIDCommitment(id: string) {
for (let provider of this.providers) {
if (await this.inProvider(provider, id)) {
const groups = this.groups[provider];
if (groups) {
for (let group of groups) {
const proof = await this.getProofFromGroup(provider, group.name, id);
if (proof) {
await this.addID(id, provider, group.name);
} else {
await this.removeID(id, provider, group.name);
}
// @ts-ignore
const resp = await fetch(`${config.interrepAPI}/api/v1/groups`);
const json = await resp.json();
const groups = json.data;
if (groups?.length) {
for (let group of groups) {
const { root, provider, name } = group;
const existing = await this.interepGroups!.getGroup(provider, name);
if (existing?.root_hash !== root) {
await this.fetchMembersFromGroup(root, provider, name);
await this.interepGroups?.addHash(root, provider, name);
}
}
}
} catch (e) {
console.log(e);
} finally {
this.timeout = setTimeout(this.sync, INTEREP_SYNC_INTERVAL);
}
}
async fetchMembersFromGroup(root: string, provider: string, name: string, limit = 1000, offset = 0): Promise<void> {
const groupId = `interrep_${provider}_${name}`;
// @ts-ignore
const resp = await fetch(`${config.interrepAPI}/api/v1/groups/${provider}/${name}/members?limit=${limit}&offset=${offset}`);
const json = await resp.json();
const members: string[] = json.data;
if (members.length) {
for (const member of members) {
await this.semaphore?.addID(BigInt(member).toString(16), groupId, root);
}
}
if (members.length >= limit) {
await this.fetchMembersFromGroup(root, provider, name, limit + 1000, limit);
}
}
@@ -116,6 +126,12 @@ export default class InterrepService extends GenericService {
}
async start() {
await this.fetchGroups();
this.interepGroups = await interepGroups(sequelize);
this.semaphore = await semaphore(sequelize);
await this.sync();
}
async stop() {
if (this.timeout) clearTimeout(this.timeout);
}
}

23
src/services/ipfs.ts Normal file
View File

@@ -0,0 +1,23 @@
import {GenericService} from "../util/svc";
import config from "../util/config";
import {CIDString, Filelike, Web3Storage} from "web3.storage";
export default class IPFSService extends GenericService {
client: Web3Storage;
constructor() {
super();
// @ts-ignore
this.client = new Web3Storage({
token: config.web3StorageAPIKey as string,
});
}
store = (files: Iterable<Filelike>): Promise<CIDString> => {
return this.client.put(files);
}
status = (cid: CIDString) => {
return this.client.status(cid);
}
}

148
src/services/merkle.ts Normal file
View File

@@ -0,0 +1,148 @@
import {GenericService} from "../util/svc";
import {BindOrReplacements, Dialect, QueryTypes, Sequelize} from "sequelize";
import {generateMerkleTree} from "@zk-kit/protocols";
import {MerkleProof, IncrementalMerkleTree} from "@zk-kit/incremental-merkle-tree";
import merkleRoot from "../models/merkle_root";
import {sequelize} from "../util/sequelize";
export default class MerkleService extends GenericService {
merkleRoot: ReturnType<typeof merkleRoot>;
constructor() {
super();
this.merkleRoot = merkleRoot(sequelize);
}
makeTree = async (group: string, zkType: 'rln' | 'semaphore' = 'rln'): Promise<IncrementalMerkleTree> => {
const [protocol, groupName, groupType = ''] = group.split('_');
const protocolBucket = SQL[protocol] || {};
const groupBucket = protocolBucket[groupName] || {};
const {sql, replacement} = groupBucket[groupType] || {};
if (!sql) throw new Error(`${group} does not exist`);
const options = {
type: QueryTypes.SELECT,
replacements: replacement || {
group_id: group,
},
};
const leaves = await sequelize.query(sql, options);
const tree = generateMerkleTree(
zkType === 'rln' ? 15 : 20,
BigInt(0),
leaves.map(({ id_commitment }: any) => '0x' + id_commitment),
);
return tree;
}
getGroupByRoot = async (root: string): Promise<string | null> => {
const exist = await this.merkleRoot.getGroupByRoot(root);
return exist?.group_id || null;
}
verifyProof = async (proof: MerkleProof): Promise<string | null> => {
const groups = [
'zksocial_all',
'interrep_twitter_unrated',
'interrep_twitter_bronze',
'interrep_twitter_silver',
'interrep_twitter_gold',
];
const existingGroup = await this.getGroupByRoot(proof.root);
if (existingGroup) {
const tree = await this.makeTree(existingGroup);
if (tree.verifyProof(proof)) return existingGroup;
}
for (const group of groups) {
const tree = await this.makeTree(group);
if (tree.verifyProof(proof)) return group;
}
return null;
}
findProof = async (group: string, idCommitment: string) => {
const tree = await this.makeTree(group);
const proof = await tree.createProof(tree.indexOf(BigInt('0x' + idCommitment)));
if (!proof) throw new Error(`${idCommitment} is not in ${group}`);
const root = '0x' + proof.root.toString(16);
await this.addRoot(root, group);
return {
root,
siblings: proof.siblings.map((siblings) =>
Array.isArray(siblings)
? siblings.map((element) => '0x' + element.toString(16))
: '0x' + siblings.toString(16)
),
pathIndices: proof.pathIndices,
leaf: '0x' + proof.leaf.toString(16),
};
}
addRoot = async (rootHash: string, group: string) => {
return this.merkleRoot.addRoot(rootHash, group);
}
findRoot = async (rootHash: string) => {
const cached = await this.merkleRoot.getGroupByRoot(rootHash);
return cached?.group_id;
}
}
const SQL: {
[protocol: string]: {
[groupName: string]: {
[groupType: string]: {
sql: string;
replacement?: BindOrReplacements;
};
};
};
} = {
'zksocial': {
'all': {
'': {
sql: `
SELECT u.name as address, pf.value as id_commitment FROM users u
LEFT JOIN profiles pf ON pf."messageId" = (
SELECT "messageId" FROM profiles
WHERE creator = u.name AND subtype = 'CUSTOM'
AND key = 'id_commitment'
ORDER BY "createdAt" DESC LIMIT 1
)
WHERE pf.value IS NOT NULL
`,
},
},
},
'interrep': {
'twitter': {
'unrated': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
'bronze': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
'silver': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
'gold': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
},
'github': {
'unrated': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
'bronze': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
'silver': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
'gold': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
},
'reddit': {
'unrated': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
'bronze': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
'silver': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
'gold': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
},
}
};

114
src/services/zkchat.ts Normal file
View File

@@ -0,0 +1,114 @@
import {GenericService} from "../util/svc";
import {ZKChat} from "../../lib/zk-chat-server/src";
import {ChatMessage} from "../../lib/zk-chat-server/src/services/chat.service";
import {Dialect, QueryTypes, Sequelize} from "sequelize";
import config from "../../lib/zk-chat-server/src/utils/config";
import {RLN, RLNFullProof} from "@zk-kit/protocols";
export default class ZKChatService extends GenericService {
zkchat: ZKChat;
sequelize: Sequelize;
constructor() {
super();
this.zkchat = new ZKChat();
this.sequelize = new Sequelize(
config.DB_NAME as string,
config.DB_USERNAME as string,
config.DB_PASSWORD,
{
host: config.DB_HOST,
port: Number(config.DB_PORT),
dialect: config.DB_DIALECT as Dialect,
logging: false,
},
);
}
start = async () => {
return this.zkchat.init();
}
registerUser = async (address: string, ecdhPubkey: string) => {
return this.zkchat.registerUser(address, ecdhPubkey);
}
getAllUsers = async (offset = 0, limit = 20) => {
return this.zkchat.getAllUsers(offset, limit);
}
addChatMessage = async (chatMessage: ChatMessage) => {
return this.zkchat.addChatMessage(chatMessage);
}
getDirectMessages = async (senderPubkey: string, receiverPubkey: string, offset = 0, limit = 20) => {
return this.zkchat.getDirectMessages(senderPubkey, receiverPubkey, offset, limit);
}
getDirectChatsForUser = async (pubkey: string) => {
return this.zkchat.getDirectChatsForUser(pubkey);
}
isEpochCurrent = async (epoch: string) => {
return this.zkchat.isEpochCurrent(epoch);
}
verifyRLNProof = async (proof: RLNFullProof) => {
return this.zkchat.verifyRLNProof(proof);
}
checkShare = async (share: {
nullifier: string;
epoch: string;
x_share: string;
y_share: string;
}) => {
return this.zkchat.checkShare(share);
}
insertShare = async (share: {
nullifier: string;
epoch: string;
x_share: string;
y_share: string;
}) => {
return this.zkchat.insertShare(share);
}
searchChats = async (query: string, sender?: string, offset = 0, limit = 20) => {
const values = await this.sequelize.query(`
SELECT
ecdh.value as receiver_ecdh,
idcommitment.value as receiver_idcommitment,
zku.wallet_address as receiver_address
FROM zkchat_users zku
LEFT JOIN profiles ecdh ON ecdh."messageId" = (SELECT "messageId" FROM profiles WHERE creator = zku.wallet_address AND subtype = 'CUSTOM' AND key='ecdh_pubkey' ORDER BY "createdAt" DESC LIMIT 1)
LEFT JOIN profiles idcommitment ON idcommitment."messageId" = (SELECT "messageId" FROM profiles WHERE creator = zku.wallet_address AND subtype = 'CUSTOM' AND key='id_commitment' ORDER BY "createdAt" DESC LIMIT 1)
LEFT JOIN profiles name ON name."messageId" = (SELECT "messageId" FROM profiles WHERE creator = zku.wallet_address AND subtype = 'NAME' ORDER BY "createdAt" DESC LIMIT 1)
WHERE (
LOWER(zku.wallet_address) LIKE :query
OR LOWER(name.value) LIKE :query
OR LOWER(name.creator) LIKE :query
OR LOWER(name.creator) IN (SELECT LOWER(address) from ens WHERE LOWER(ens) LIKE :query)
OR LOWER(name.creator) IN (SELECT LOWER(creator) from profiles WHERE subtype = 'NAME' AND LOWER(value) LIKE :query ORDER BY "createdAt" DESC LIMIT 1)
)
${!sender ? '' : `
AND (
zku.wallet_address IN (SELECT distinct zk.receiver_address FROM zkchat_chats zk WHERE zk.sender_address = :sender)
OR zku.wallet_address IN (SELECT distinct zk.sender_address FROM zkchat_chats zk WHERE zk.receiver_address = :sender)
)`}
LIMIT :limit OFFSET :offset
`, {
type: QueryTypes.SELECT,
replacements: {
query: `%${query.toLowerCase()}%`,
sender,
limit,
offset,
},
});
return values;
}
}

View File

@@ -29,6 +29,7 @@ let json: {
twAccessKey?: string;
twAccessSecret?: string;
rapidAPIKey?: string;
web3StorageAPIKey?: string;
} = {};
try {
@@ -71,6 +72,7 @@ const moderators = json.moderators || process.env?.MODERATORS?.split(' ') || [
const interrepAPI = json.interrepAPI || process.env.INTERREP_API || 'https://kovan.interep.link';
const interrepContract = json.interrepContract || process.env.INTERREP_CONTRACT || '';
const jwtSecret = json.jwtSecret || process.env.JWT_SECRET || 'topjwtsecret';
const web3StorageAPIKey = json.web3StorageAPIKey || process.env.WEB3_STORAGE_API_KEY;
if (!web3HttpProvider) throw new Error('WEB3_HTTP_PROVIDER is not valid');
if (!ensResolver) throw new Error('ENS_RESOLVER is not valid');
@@ -86,6 +88,7 @@ if (!twBearerToken) throw new Error(`twBearerToken is not valid`);
if (!twAccessKey) throw new Error(`twAccessKey is not valid`);
if (!twAccessSecret) throw new Error(`twAccessSecret is not valid`);
if (!rapidAPIKey) throw new Error(`rapidAPIKey is not valid`);
if (!web3StorageAPIKey) throw new Error(`web3StorageAPIKey is not valid`);
const config = {
interrepAPI,
@@ -115,6 +118,7 @@ const config = {
twAccessSecret,
rapidAPIKey,
moderators,
web3StorageAPIKey,
};
export default config;

View File

@@ -24,4 +24,4 @@ export function verifySignatureP256(pubkey: string, data: string, signature: str
Buffer.from(data, 'utf-8').toString('hex'),
Buffer.from(signature, 'hex').toJSON().data,
)
}
}

View File

@@ -21,10 +21,10 @@ const logger = winston.createLogger({
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
level: 'info',
format: winston.format.simple(),
}));
// logger.add(new winston.transports.Console({
// level: 'info',
// format: winston.format.simple(),
// }));
}
export default logger;

32
src/util/sequelize.ts Normal file
View File

@@ -0,0 +1,32 @@
import config from "./config";
import {Dialect, Sequelize} from "sequelize";
let cached: Sequelize;
function getSequelize(): Sequelize {
if (cached) return sequelize;
if (!config.dbDialect || config.dbDialect === 'sqlite') {
cached = new Sequelize({
dialect: 'sqlite',
storage: config.dbStorage,
logging: false,
});
} else {
cached = new Sequelize(
config.dbName as string,
config.dbUsername as string,
config.dbPassword,
{
host: config.dbHost,
port: Number(config.dbPort),
dialect: config.dbDialect as Dialect,
logging: false,
},
);
}
return cached;
}
export const sequelize = getSequelize();

129
src/util/sse.ts Normal file
View File

@@ -0,0 +1,129 @@
import {Response} from "express";
type SSEClient = {
res: Response;
lastUsed: number;
}
const CLIENT_CACHE: {
[clientId: string]: SSEClient;
} = {};
const TOPIC_MAP: {
[topic: string]: {
[clientId: string]: boolean;
};
} = {};
const MAX_TTL = 2 * 60 * 1000;
let pruneTimeout: any;
export enum SSEType {
INIT = 'INIT',
NEW_CHAT_MESSAGE = 'NEW_CHAT_MESSAGE',
HEALTHCHECK = 'HEALTHCHECK',
}
export const addConnection = (clientId: string, res: Response) => {
CLIENT_CACHE[clientId] = {
res,
lastUsed: Date.now(),
};
const raw = `data: ${JSON.stringify({
type: SSEType.INIT,
clientId,
})}\n\n`;
res.write(raw);
}
export const keepAlive = (clientId: string) => {
const client = CLIENT_CACHE[clientId];
if (!client) throw new Error(`${clientId} not found`);
client.lastUsed = Date.now();
}
export const addTopic = (clientId: string, topic: string) => {
const client = CLIENT_CACHE[clientId];
if (!client) {
throw new Error(`${clientId} not found`);
}
const bucket: { [id: string]: boolean } = TOPIC_MAP[topic] || {};
bucket[clientId] = true;
TOPIC_MAP[topic] = bucket;
}
export const removeTopic = (clientId: string, topic: string) => {
const client = CLIENT_CACHE[clientId];
if (!client) {
throw new Error(`${clientId} not found`);
}
const bucket: { [id: string]: boolean } = TOPIC_MAP[topic] || {};
if (bucket[clientId]) delete bucket[clientId];
TOPIC_MAP[topic] = bucket;
}
export const publishTopic = async (topic: string, data: any) => {
const bucket: { [id: string]: boolean } = TOPIC_MAP[topic] || {};
for (const clientId in bucket) {
const client = CLIENT_CACHE[clientId];
if (!client) {
delete bucket[clientId];
} else {
const raw = `data: ${JSON.stringify(data)}\n\n`;
client.res.write(raw);
}
}
}
export const removeConnection = (clientId: string) => {
const client = CLIENT_CACHE[clientId];
if (!client) return;
delete CLIENT_CACHE[clientId];
client.res.end();
}
export const getConnection = (clientId: string) => {
return CLIENT_CACHE[clientId];
}
export const pruneConnections = async () => {
const now = Date.now();
for (const clientId in CLIENT_CACHE) {
const client = CLIENT_CACHE[clientId];
if (now - client.lastUsed > MAX_TTL) {
removeConnection(clientId);
for (const topic in TOPIC_MAP) {
const bucket = TOPIC_MAP[topic];
if (bucket[clientId]) delete bucket[clientId];
}
}
}
}
const pruneLoop = async () => {
if (pruneTimeout) {
clearTimeout(pruneTimeout);
}
await pruneConnections();
setTimeout(pruneLoop, MAX_TTL);
}
pruneLoop();

BIN
static/rln/rln.wasm Normal file

Binary file not shown.

BIN
static/rln/rln_final.zkey Normal file

Binary file not shown.

View File

@@ -0,0 +1,119 @@
{
"protocol": "groth16",
"curve": "bn128",
"nPublic": 6,
"vk_alpha_1": [
"20491192805390485299153009773594534940189261866228447918068658471970481763042",
"9383485363053290200918347156157836566562967994039712273449902621266178545958",
"1"
],
"vk_beta_2": [
[
"6375614351688725206403948262868962793625744043794305715222011528459656738731",
"4252822878758300859123897981450591353533073413197771768651442665752259397132"
],
[
"10505242626370262277552901082094356697409835680220590971873171140371331206856",
"21847035105528745403288232691147584728191162732299865338377159692350059136679"
],
[
"1",
"0"
]
],
"vk_gamma_2": [
[
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
],
[
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
],
[
"1",
"0"
]
],
"vk_delta_2": [
[
"14962742607348761745067039667743739542918587958944430293802039673540545491766",
"19420690459571091434654770613940591470063366130537730965999231781890983822228"
],
[
"8025490964181160791180478620151567079603098158752936953068367103529675811355",
"16899630578734283128344486213072467840624492210479261395286682179061749476405"
],
[
"1",
"0"
]
],
"vk_alphabeta_12": [
[
[
"2029413683389138792403550203267699914886160938906632433982220835551125967885",
"21072700047562757817161031222997517981543347628379360635925549008442030252106"
],
[
"5940354580057074848093997050200682056184807770593307860589430076672439820312",
"12156638873931618554171829126792193045421052652279363021382169897324752428276"
],
[
"7898200236362823042373859371574133993780991612861777490112507062703164551277",
"7074218545237549455313236346927434013100842096812539264420499035217050630853"
]
],
[
[
"7077479683546002997211712695946002074877511277312570035766170199895071832130",
"10093483419865920389913245021038182291233451549023025229112148274109565435465"
],
[
"4595479056700221319381530156280926371456704509942304414423590385166031118820",
"19831328484489333784475432780421641293929726139240675179672856274388269393268"
],
[
"11934129596455521040620786944827826205713621633706285934057045369193958244500",
"8037395052364110730298837004334506829870972346962140206007064471173334027475"
]
]
],
"IC": [
[
"1903896611309112537898744888962900116850557104688140496996114549467905845389",
"18878807081439367511400607605971457252753640659171839147843428814578113606944",
"1"
],
[
"3379380938593207080729160389717513882233130462616390491862768147575356927916",
"7658366061956613070318477742617874370870162905631795036979866045784951077741",
"1"
],
[
"6573257371221466457264319919959056878987621722065494829982803761054687125969",
"10882724634177179759524298899662307047578489073607305659656545291770678864358",
"1"
],
[
"7812315588577187126949469943175416873443639588836013906528191759531184751241",
"11928578526804134334949431821595151892366575912030023302327660047097849106833",
"1"
],
[
"8603322753346553805166809285487650902416020947317326006612301010819108432803",
"3100089596497060388197556426101480177422659517238854788949908444987414217355",
"1"
],
[
"548584601681146753562700521279324517960520272202999011217745775929880443671",
"10775304696187723066725699321032508342393012304409445749847420882128296879500",
"1"
],
[
"4832346778469239752999589970014962368162544386191392542939015835265757824362",
"17497085844864771075682226779903142040218959461493641885155366729946818944068",
"1"
]
]
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
{"protocol":"groth16","curve":"bn128","nPublic":4,"vk_alpha_1":["20491192805390485299153009773594534940189261866228447918068658471970481763042","9383485363053290200918347156157836566562967994039712273449902621266178545958","1"],"vk_beta_2":[["6375614351688725206403948262868962793625744043794305715222011528459656738731","4252822878758300859123897981450591353533073413197771768651442665752259397132"],["10505242626370262277552901082094356697409835680220590971873171140371331206856","21847035105528745403288232691147584728191162732299865338377159692350059136679"],["1","0"]],"vk_gamma_2":[["10857046999023057135944570762232829481370756359578518086990519993285655852781","11559732032986387107991004021392285783925812861821192530917403151452391805634"],["8495653923123431417604973247489272438418190587263600148770280649306958101930","4082367875863433681332203403145435568316851327593401208105741076214120093531"],["1","0"]],"vk_delta_2":[["7912208710313447447762395792098481825752520616755888860068004689933335666613","12599857379517512478445603412764121041984228075771497593287716170335433683702"],["21679208693936337484429571887537508926366191105267550375038502782696042114705","11502426145685875357967720478366491326865907869902181704031346886834786027007"],["1","0"]],"vk_alphabeta_12":[[["2029413683389138792403550203267699914886160938906632433982220835551125967885","21072700047562757817161031222997517981543347628379360635925549008442030252106"],["5940354580057074848093997050200682056184807770593307860589430076672439820312","12156638873931618554171829126792193045421052652279363021382169897324752428276"],["7898200236362823042373859371574133993780991612861777490112507062703164551277","7074218545237549455313236346927434013100842096812539264420499035217050630853"]],[["7077479683546002997211712695946002074877511277312570035766170199895071832130","10093483419865920389913245021038182291233451549023025229112148274109565435465"],["4595479056700221319381530156280926371456704509942304414423590385166031118820","19831328484489333784475432780421641293929726139240675179672856274388269393268"],["11934129596455521040620786944827826205713621633706285934057045369193958244500","8037395052364110730298837004334506829870972346962140206007064471173334027475"]]],"IC":[["19918517214839406678907482305035208173510172567546071380302965459737278553528","7151186077716310064777520690144511885696297127165278362082219441732663131220","1"],["690581125971423619528508316402701520070153774868732534279095503611995849608","21271996888576045810415843612869789314680408477068973024786458305950370465558","1"],["16461282535702132833442937829027913110152135149151199860671943445720775371319","2814052162479976678403678512565563275428791320557060777323643795017729081887","1"],["4319780315499060392574138782191013129592543766464046592208884866569377437627","13920930439395002698339449999482247728129484070642079851312682993555105218086","1"],["3554830803181375418665292545416227334138838284686406179598687755626325482686","5951609174746846070367113593675211691311013364421437923470787371738135276998","1"]]}