From 360a7833e14186d85d66e05e94f5503f55813d7d Mon Sep 17 00:00:00 2001 From: Igx22 Date: Tue, 20 Aug 2024 11:49:53 +0700 Subject: [PATCH 01/10] add: basic rpc stubs --- package.json | 3 +- src/api/routes/validatorRoutes.ts | 12 +++- src/api/routes/validatorRpc.ts | 82 ++++++++++++++++++++++ src/config/config-general.ts | 2 +- src/services/messaging/validatorLoader.ts | 11 ++- zips/do.sh zipped.zip | Bin 2097 -> 0 bytes zips/docker-dir-for-vnodes.zip | Bin 109044 -> 0 bytes 7 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 src/api/routes/validatorRpc.ts delete mode 100644 zips/do.sh zipped.zip delete mode 100644 zips/docker-dir-for-vnodes.zip diff --git a/package.json b/package.json index a36f0d3..d8c5109 100644 --- a/package.json +++ b/package.json @@ -79,9 +79,10 @@ "ethers": "^5.7.2", "event-dispatch": "^0.4.1", "eventemitter3": "^3.1.0", - "express": "^4.16.2", + "express": "^4.19.2", "express-basic-auth": "^1.2.0", "express-joi-validation": "^5.0.1", + "express-json-rpc-router": "^1.4.0", "express-jwt": "^5.3.1", "firebase-admin": "8.12.1", "graphql": "^16.5.0", diff --git a/src/api/routes/validatorRoutes.ts b/src/api/routes/validatorRoutes.ts index b6d1784..f6a927c 100644 --- a/src/api/routes/validatorRoutes.ts +++ b/src/api/routes/validatorRoutes.ts @@ -13,7 +13,8 @@ import { ValidatorRandom } from '../../services/messaging/validatorRandom' import { ValidatorPing } from '../../services/messaging/validatorPing' import { MessageBlock, PayloadItem, SenderType } from '../../services/messaging-common/messageBlock' import { WinstonUtil } from '../../utilz/winstonUtil' - +import jsonRouter from "express-json-rpc-router"; +import {ValidatorRpc} from "./validatorRpc"; // /apis/v1/messaging const route = Router() @@ -28,9 +29,16 @@ function logResponseFinished(log: Logger, status: number, responseObj: any) { log.debug(`=== Reply ${status} with body: %o`, responseObj) } +function initRpc(app: Router) { + const validatorRpc = Container.get(ValidatorRpc); + app.use(`/v1/rpc`, jsonRouter({ methods: validatorRpc })); +} + export default (app: Router) => { + initRpc(app); + // Load the rest - app.use(`/${config.api.version}/messaging`, route) + app.use(`/v1/messaging`, route) app.use(errors()) // add external payload diff --git a/src/api/routes/validatorRpc.ts b/src/api/routes/validatorRpc.ts new file mode 100644 index 0000000..7933f81 --- /dev/null +++ b/src/api/routes/validatorRpc.ts @@ -0,0 +1,82 @@ +import {Inject, Service} from "typedi"; +import {Logger} from "winston"; +import {WinstonUtil} from "../../utilz/winstonUtil"; +import {ValidatorContractState} from "../../services/messaging-common/validatorContractState"; +import {ValidatorNode} from "../../services/messaging/validatorNode"; + +@Service() +export class ValidatorRpc { + public log: Logger = WinstonUtil.newLog(ValidatorRpc); + + @Inject() + private validatorNode: ValidatorNode + + public push_getAPIToken([]) { + // todo return api token + + return { + "apiToken" : "0xAAAAA", + "targetNodeUrl": "https://v1.dev.push.org" + } ; + } + + public push_sendTransaction([ transactionDataBase16 ]) { + // todo add to block + + const txHash = "0xAAAA"; + return txHash; + } + + public push_readBlockQueue([ offsetStr ]) { + // todo serve data from block queue + + return { + "items": [ + { + "id" : "101", + "object" : "0xAAAA", // BLOCK in protobuf format + "object_hash" : "0xBBBBBB" // BLOCK SHA1 + }, + { + "id" : "102", + "object" : "0xCC", + "object_hash" : "0xDD" + } + ], + "lastOffset" : "102" + } + } + + + public push_readBlockQueueSize([]) { + // todo return queue state + return { + "lastOffset" : "102" + } + } + + public push_syncing([]) { + // todo return queue state + return { + "lastPublishedOffset": "1001" + } + } + + // todo NETWORK CALLS TO STORAGE NODES + // todo push_getTransactions + // todo push_getBlockTransactionCountByHash (1) + // todo push_getBlockByHash + // todo push_getTransactionByHash + // todo push_getTransactionByBlockHashAndIndex + // todo push_getTransactionCount + + + public push_networkId([]) { + return "1"; + } + + public push_listening([]) { + return "true"; + } + +} \ No newline at end of file diff --git a/src/config/config-general.ts b/src/config/config-general.ts index 1076228..521a69c 100644 --- a/src/config/config-general.ts +++ b/src/config/config-general.ts @@ -94,7 +94,7 @@ export default { * API configs */ api: { - prefix: '/apis', + prefix: '/api', version: 'v1' }, diff --git a/src/services/messaging/validatorLoader.ts b/src/services/messaging/validatorLoader.ts index 7c8c078..68d8031 100644 --- a/src/services/messaging/validatorLoader.ts +++ b/src/services/messaging/validatorLoader.ts @@ -3,15 +3,20 @@ import { QueueManager } from './QueueManager' import { MySqlUtil } from '../../utilz/mySqlUtil' import { ValidatorNode } from './validatorNode' import * as dbHelper from '../../helpers/dbHelper' +import {ValidatorRpc} from "../../api/routes/validatorRpc"; +import {Check} from "../../utilz/check"; export async function initValidator() { // Load validator - const validatorNode = Container.get(ValidatorNode) - await validatorNode.postConstruct() + const validatorNode = Container.get(ValidatorNode); + await validatorNode.postConstruct(); MySqlUtil.init(dbHelper.pool) // Load dset (requires a loaded contract) const dset = Container.get(QueueManager) - await dset.postConstruct() + await dset.postConstruct(); + + const validatorRpc = Container.get(ValidatorRpc); + Check.notNull(validatorRpc, 'ValidatorRpc is null'); } diff --git a/zips/do.sh zipped.zip b/zips/do.sh zipped.zip deleted file mode 100644 index 07627f5a704277119bce0cfbd9c9a9601b5df517..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2097 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%0ALoaxG@f00e}OO%#%gWhbNcmjF#D~ z+8zZs&*8QOH7TmMm9bE94t>VmswZZzAV$zF^gyCYfn!v*%yhdGa%*$}mStUKZWJam z6INxt)Wz3Q#NXL`S3~SIx0EBR;XL`|wv_SQUUdO>XxZM4VJVMG5cN^W7j+jMUxF6h z_jb$ek7Z2;>-~4Q_CQ;;UL{Rn+t0LH*O}O0MaTJD17C}V=OyQ13HH|Kl-P_B{5vJQ zd)Q7Dc%2=rj@$d(7@g-#z#diu>8j2gcbdeK(GR1XoV*#?BH~H~ANA-NSR!Jcyv!em zQWQnU)kG}{6$f}_VOs}$>rv**1?`sc_UBB^$33Mxp+F|TN%?f~_3TqxNr3ZE7AJ4k2_0VkWA55Kr(O+IQh2kjnHCFIln^ z!zd(oZExQ%uyyahw0E6BO|4ryR8fi`5JWlxN+(n!fryBLfT2k5#n5Xg(nOG^APR_} zKq9?I>Afi30HG;Ofe1)Z3>~St;X8W7JLjHn&fNL_eQz?eXV2{CS!=(~+V9@UthHC{ zTIJllbVs=@%i!8G_Cf2$(L{E{XXTrDQm{TRb$gwm5F~7Dwo{iQ&B3-yS*|kfnQ-%( z7h4uH%$t2Tc*T!@D^dD{->HNfi9s4U(%-&5$ImZjbeNg51ysZNEu(>1yWV$fW&_hu z!8ySuHOVNvoc;txp$T+8K%@!h_qwrx1C~aroM6(q7z*5+jT#Oe10ZPgtPR{UD!$x8 zI}KyXr{IW~0A?R>T1vG-cdIk}(>Efj)zs9iNnzwaQHwWjRB=E!_piWw8i3ZXl1_>{G%kepNFw-FUGee85gEtR4 zC|F3gSR{+%LH;h`og;y8{J}~Owc)T*^V$Gl09{!(tgKa@Ns5Abfg)$YBnMg#a$yyTL`F>;Qm`md-lgt*>m1mTYdXMCF}UT&^$TBDSzN zrcN@)F$I-d;cn#$P^U`j7uz@nT?whBgqJ*S#(x#FTWw*I%ja)tJiyt=jk+vPq%)U| zJ`zzCXI&^5lrkTvGV5RFtj$gy3{BDs>q{_a+^rvSDyTSpY=$eMhV^RHVlCcmZ96g? z%^qz<(1VH1Wf=OJgWjQKD63nd zD{6YP77jVP)Lh3{6#Ql_qvAuC#`?1?aF^7kea0Z!R%Rq-2UpgNoMNRaQ81o|3z2%KA*H|Kw49WbE` zUUAyXPzZ9+Uf-mc&9b=GETE{i=fOcZx69o(LH1ognRH`b@40#iPt2~;dUyNCVsJ`> z_LH;`mf&z--wPc4HRnUiW6p?vQkz*xG&jL9P!3B3@9m7}2TbYSIZ!^~Os; zPH$eKe$NQXQ7*w~?f9RFc2@K+-a;fhb^FV-zvy6xf0Psj-=px=RKcmwm(nY40-K8&$ z)SvoyN)`p&)$t!}hrEBX@|7-zr_ZtXqNbmP9xCO#wyenH)b^7liI=k@jo4mABaCGZ zw%*lpKa3Zbv}L&8wCz9;zuQpwcn(pEtBT#g*uJ^k#C~0YS}V~$H(~20XLt26(UqtLm}6}c6Rl)Io78SPPgwnSFAg4s+8_U%rQ#{KVP!AZT^tId>jIOxzJyB zK!wc}?SH@@2C>N}pRjmpZEAVk;Bny`;LHHk#uIfJe1pv{9TH}He=NM0_G)`3j}HucD2n*UX~#MB`{$JzMQTyqXFq=^cdk>l^3lqE;+k{ z5u288k9}Wv%B9=3X>hIlWP((&VAZSXjnbh$iRqWyaphDkro#^3j|C=eD_+Wk*X=p3 z4_ee@TEgIKX!%vHK7aa(k6*TgMaFw>2g-C_3OgPptbWH-uCXKpAFm~fh?JEr55mO; zv9lLuBeFw)PwYn-&Ugvr(6kbZzTf7h_R^)`Xb}kz=x{l)fgQH}YWusmbER;xB^3LO zbM;oP2Ke3ACVj6xDe37zw9O&%ePuD^v{)gB2(+)6pkRir-X)HUbZWQ83zd8`*$t%J zq_6A4&ZzcyddkQ@Y-pDtgF;gbb%!p81irU7aZuFue)m=y`>0=}s>wBA zkY`pghUeZ5E&;lSc+UiEep+AQt=D<}BMDUi(gDJXAJ~FICT{y)JZz@PfI!|GsT1DW z^rqOV-lG@6bhyU%sG4##=_Y+YQ^JQ_eQ56bFcwwA^xiv2xMu{QiuEhf2f15-Or_q$ zF0dhC!^W{*QzMXr9ALa^Mck_S7uFb1ZDkb5p1$fsFnAE!?IjRWj^49CVF+e8m~cB6 z1ZEuXVbnr}wrYJ~{+UmRul z8qs++gu#CY+beDNfkJK8-9T!Tv%2otb#3HTQMqraZDn*-tobhDEb9Xmdwn-F=7hx& z{KcBdtM_f%$)p?`SsdAFW9(G4i_%K@UT+HeH!qRs&YW2MpBHhL?kzV`+VfPbtS&$T zufraG=$Kcn>g%*io%+h*qRLs=#oOP0tB&|ztLWV32Kb!uv$lgT+nk^=hW9s0IxXGs z#x1$NGTbgAfv=WLqQh22$cittlj*uN$j{Dbw2mmZGxzKc?PTzvoBJklyra ze;&=>TZzuqjQS*37nH}`?34|2S^abnCY0%(o$Ld5!qo9@qd(WJe{T)7yucD&&Ze$S zBL#h=Y!U;szSC@ulq-U=o-VB{VF=yffVL59tRd*5^36wM=BGw0QU zJLZHg7sE4Ouegotu+P2^T!kv+%65vC$AedHJ4U}qbCA&K+vqIAw|)xTyW2NY=AK5g zo)9Ej>1G^GOc=7I)p=CRiKJV(;dggh&7xYaDYgU-joCjx;8bUMb;q;5LtVaQCGC36 z6_dbT%Kfa$Gb(8hS|pgs49=7+?G*&u2U2T;-VBA8W(czQCB@klh!v=DW{4PqRH3%Y zF)m&yyRa!U0jGXZW1Q|Xb6Uy)vGc5}0=m0WC)@ieZTVOn9)-$g!PLFsi;afkfHz4o zmYV#ntd7DD7O;h^`?$%AF8blKeg>k|Z>X!|bN6_oA(<7923vMkiaf@y(k~r+a~P(P zC0%s^3?gyU;#LcS7fZvl1ymPDh0uFyyXB_PPfTJd#TW2RNi2OcRFAAYn#hYO!o(I0 zo&pQ7g(-dZ!-?XJXd9X0>B=hD$YRoxD(|wNOfA}cYO@0Wsx^3eqdZm<<5pm7rlsKb zNQh>Ere1(8N&tLK*#~bX)?*_w$9JmPgTa@7|DniI#IslR1d2zpdAs}Px+D3XN~CXa z=(z4#s$5$x{xU8iZ~(mKm`*^&DHtzxA{lSZqEibLZQ2wKC#b_-+Bxu?R#*B#PEf?MCgohQtzzQfZ<62|>T_mw7Y2cM^wPZCBh!VsDoC2Kv;GE_8 z$$9rIUrpAzUhj{Pb+u$qedaGN)H9a<|piz_jvEchlO%AEIwFhhA6=TV| ze3b-7@dHsP4T^G1^AB`hU@ zZ1Qdag}R&l1$^Gx=7m;NM(bK_!r)HMtC9K+CqF2c_0xSW%?SvFa;=UJ`$Fe%zJ+yn zAIbav|M&lN1_=GW-x?ch_EFH;c>J0Ric66&j6K-gYBQ^En!H2knG9YF{*}>WX|1gf zr7W>BAOQf(NC5!u-!htftehQet%zw&7Pq|YEu3!oi8&(NTyJUjyR!4p1Y!+RH^IWA zmN5%XO|6!h$av}8kGexbwZX)$Ku(#8+@N+T&Xf3)xq}(-6#VY$YM+R&~!mxPCrId^b%}vtE;wl<%1=W9dyENIeM~Yb;ZQTVlR2N0LMlc>ProI1){@E5gQrF9n3XhrdNm$ zJu&Gd6tca?uW~CdZMyFQ{tyK~1pwGcfDDK_{5gKFwQzQ`6W3BFA!PzE0S=Fz@)Y~+ z=wpB4=mASghdZ`zHcq!Ze*>8rrTqpXR(1Rshamo6APK2IaimmKfMxd-gX19k5-Ae^ zKy`=&aONkBxXlXD{0*o7GkShx#Qk*okmu08ja`kr5b*?9>HjDv`AL-(QcYz4;WW|y zVR6Y(>|bT!{le1O9>xBilXsNG2dSq|ndQcQH1dD3Ohp}K`RnW8C)<_lX`SgBp zlqlwNubGhk{L6RAf2f7p%2Dj!Yw>|5 zDMs(eD)h?(HXzBL(I6@Q-)ezoIm-6$S{#-+{j_|zB~HnYaj4_i5iIB*XhC}VmraoP z-)ZqzWmZ2u$O>^m9Oe0E*M;YuquBqf#b4bKetB>u^C-)ItHo&6QMNy{prAao#z_2( Nk^=w~*~E|l{0r~NKb!yn From 4f5d1883970b6e5b5780415139fe0ee51c8a2db6 Mon Sep 17 00:00:00 2001 From: Igx22 Date: Tue, 20 Aug 2024 12:57:28 +0700 Subject: [PATCH 02/10] add: db shrink; newrelic off; --- src/appInit.ts | 2 +- src/loaders/subGraphJobs.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/appInit.ts b/src/appInit.ts index 070425c..ef3e905 100644 --- a/src/appInit.ts +++ b/src/appInit.ts @@ -1,5 +1,5 @@ import 'reflect-metadata' // We need this in order to use @Decorators -import 'newrelic' +// import 'newrelic' import chalk from 'chalk' import express from 'express' diff --git a/src/loaders/subGraphJobs.ts b/src/loaders/subGraphJobs.ts index 74f29df..78adad5 100644 --- a/src/loaders/subGraphJobs.ts +++ b/src/loaders/subGraphJobs.ts @@ -21,6 +21,7 @@ const gr = require('graphql-request') const { request, gql } = gr import epnsAPIHelper from '../helpers/epnsAPIHelper' import PayloadsService from '../services/payloadsService' +import {EnvLoader} from "../utilz/envLoader"; //helper function to format the scheduler as per node-schedule export function secondsToHms(pollTime) { @@ -189,6 +190,9 @@ export async function scheduleTask( } //Main function export default async function main({ logger }) { + if (EnvLoader.getPropertyAsBool('VALIDATOR_DISABLE_ALL_SERVICES')) { + return; + } logger.info('Initiating subgraph cron tasks') const channel = Container.get(Channel) const jobs = await channel.getAllSubGraphDetails() From 601ccba24d1398eba7d6b362b5a55154424ead05 Mon Sep 17 00:00:00 2001 From: Igx22 Date: Tue, 20 Aug 2024 13:19:45 +0700 Subject: [PATCH 03/10] add: push_getApiToken works; migrations off --- src/api/routes/validatorRpc.ts | 10 ++- src/loaders/initializer.ts | 70 ++++++++++++++++--- .../channelsCompositeClasses/channelsClass.ts | 4 ++ src/services/messaging-dset/queueClient.ts | 2 +- src/services/messaging/validatorClient.ts | 2 +- 5 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/api/routes/validatorRpc.ts b/src/api/routes/validatorRpc.ts index 7933f81..4c7a0ab 100644 --- a/src/api/routes/validatorRpc.ts +++ b/src/api/routes/validatorRpc.ts @@ -3,6 +3,7 @@ import {Logger} from "winston"; import {WinstonUtil} from "../../utilz/winstonUtil"; import {ValidatorContractState} from "../../services/messaging-common/validatorContractState"; import {ValidatorNode} from "../../services/messaging/validatorNode"; +import {ValidatorRandom} from "../../services/messaging/validatorRandom"; @Service() export class ValidatorRpc { @@ -10,13 +11,16 @@ export class ValidatorRpc { @Inject() private validatorNode: ValidatorNode + @Inject() + private validatorRandom: ValidatorRandom; - public push_getAPIToken([]) { + public push_getApiToken([]) { // todo return api token + const apiToken = this.validatorRandom.createValidatorToken(); return { - "apiToken" : "0xAAAAA", - "targetNodeUrl": "https://v1.dev.push.org" + "apiToken" : apiToken.validatorToken, + "apiUrl": apiToken.validatorUrl } ; } diff --git a/src/loaders/initializer.ts b/src/loaders/initializer.ts index 792a1dc..cc59f10 100644 --- a/src/loaders/initializer.ts +++ b/src/loaders/initializer.ts @@ -1,21 +1,29 @@ import { Container } from 'typedi' -import * as dbGenerator from '../helpers/dbGeneratorHelper' +// import * as dbGenerator from '../helpers/dbGeneratorHelper' import { startMigration } from '../helpers/migrationsHelper' import { startManualMigration } from '../migrations/manual' import HistoryFetcherService from '../services/historyFetcherService' +// import {createNewValidatorTables} from "../migrations/versioned/migrationV1"; +import {MySqlUtil} from "../utilz/mySqlUtil"; +import * as dbHelper from "../helpers/dbHelper"; export default async ({ logger, testMode }) => { - logger.info('Running DB Checks') - await dbGenerator.generateDBStructure(logger) - logger.info('DB Checks completed!') + logger.info('Running DB Checks'); + // await dbGenerator.generateTableProtocolMeta(logger); + // VALIDATOR TABLES ONLY + await createNewValidatorTables(); - logger.info('Running Migration') - await startMigration() - logger.info('Migration completed!') + // COMMENTED OUT - I don't need tables + // await dbGenerator.generateDBStructure(logger) + // logger.info('DB Checks completed!') - logger.info('Running Manual Migration') - startManualMigration() + // logger.info('Running Migration') + // await startMigration() + // logger.info('Migration completed!') + + // logger.info('Running Manual Migration') + // startManualMigration() logger.info('Syncing Protocol History') @@ -28,3 +36,47 @@ export default async ({ logger, testMode }) => { logger.transports.forEach((t) => (t.silent = false)) logger.info('Protocol History Synced!') } + + +// 1 CREATE TABLES FROM SCRATCH +export async function createNewValidatorTables() { + MySqlUtil.init(dbHelper.pool) + + await MySqlUtil.update(` + CREATE TABLE IF NOT EXISTS dset_client + ( + id INT NOT NULL AUTO_INCREMENT, + queue_name varchar(32) NOT NULL COMMENT 'target node queue name', + target_node_id varchar(128) NOT NULL COMMENT 'target node eth address', + target_node_url varchar(128) NOT NULL COMMENT 'target node url, filled from the contract', + target_offset bigint(20) NOT NULL DEFAULT 0 COMMENT 'initial offset to fetch target queue', + state tinyint(1) NOT NULL DEFAULT 1 COMMENT '1 = enabled, 0 = disabled', + PRIMARY KEY (id), + UNIQUE KEY uniq_dset_name_and_target (queue_name, target_node_id) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8; + `) + + await MySqlUtil.update(` + CREATE TABLE IF NOT EXISTS dset_queue_mblock + ( + id BIGINT NOT NULL AUTO_INCREMENT, + object_hash VARCHAR(255) NULL COMMENT 'optional: a uniq field to fight duplicates', + object MEDIUMTEXT NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uniq_mblock_object_hash (object_hash) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8; + `) + + await MySqlUtil.update(` + CREATE TABLE IF NOT EXISTS dset_queue_subscribers + ( + id BIGINT NOT NULL AUTO_INCREMENT, + object_hash VARCHAR(255) NULL COMMENT 'optional: a uniq field to fight duplicates', + object MEDIUMTEXT NOT NULL, + PRIMARY KEY (id) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8; + `) +} diff --git a/src/services/channelsCompositeClasses/channelsClass.ts b/src/services/channelsCompositeClasses/channelsClass.ts index 2bf5d36..b2becc1 100644 --- a/src/services/channelsCompositeClasses/channelsClass.ts +++ b/src/services/channelsCompositeClasses/channelsClass.ts @@ -21,6 +21,7 @@ import fs from 'fs' import * as db from '../../helpers/dbHelper' import epnsAPIHelper from '../../helpers/epnsAPIHelper' import { isValidAddress } from '../../helpers/utilsHelper' +import {EnvLoader} from "../../utilz/envLoader"; const VALID_SUBGRAPH_FIELDS = ['subgraph_attempts', 'counter'] const CHANGED_NOTIFCIATION_SETTING_DELIMITER = '+' @@ -158,6 +159,9 @@ export default class Channel { // for processing ipfshash in batches of 50 public async batchProcessChannelData() { + if (EnvLoader.getPropertyAsBool('VALIDATOR_DISABLE_ALL_SERVICES')) { + return; + } const logger = this.logger logger.debug('Trying to batch process all channels data processing, 50 requests at a time') diff --git a/src/services/messaging-dset/queueClient.ts b/src/services/messaging-dset/queueClient.ts index 62fb8dd..066fc4e 100644 --- a/src/services/messaging-dset/queueClient.ts +++ b/src/services/messaging-dset/queueClient.ts @@ -95,7 +95,7 @@ export class QueueClient { baseUri: string, firstOffset: number = 0 ): Promise<{ items: QItem[]; lastOffset: number } | null> { - const url = `${baseUri}/apis/v1/dset/queue/${queueName}?firstOffset=${firstOffset}` + const url = `${baseUri}/api/v1/dset/queue/${queueName}?firstOffset=${firstOffset}` try { const re = await axios.get(url, { timeout: 3000 diff --git a/src/services/messaging/validatorClient.ts b/src/services/messaging/validatorClient.ts index 6f6a100..9debd34 100644 --- a/src/services/messaging/validatorClient.ts +++ b/src/services/messaging/validatorClient.ts @@ -16,7 +16,7 @@ export class ValidatorClient { timeout: number = 500000 constructor(baseUri: string) { - this.baseUri = baseUri + '/apis/v1' + this.baseUri = baseUri + '/api/v1' this.log.level = 'error' } From e8fdfd38e12696653e1eb8215792581935249484 Mon Sep 17 00:00:00 2001 From: Igx22 Date: Tue, 20 Aug 2024 19:51:39 +0700 Subject: [PATCH 04/10] add: protobuf generation --- .eslintrc.json | 2 +- package.json | 5 +- src/api/routes/validatorRpc.ts | 86 ++++++++++++++++- src/proto/block.proto | 162 +++++++++++++++++++++++++++++++++ src/utilz/bitUtil.ts | 9 ++ src/utilz/hashUtil.ts | 12 +++ src/utilz/idUtil.ts | 4 + tsconfig.json | 11 ++- 8 files changed, 281 insertions(+), 10 deletions(-) create mode 100644 src/proto/block.proto create mode 100644 src/utilz/hashUtil.ts diff --git a/.eslintrc.json b/.eslintrc.json index 880ce38..67a92be 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -12,7 +12,7 @@ "plugins": ["@typescript-eslint", "unused-imports", "simple-import-sort"], "rules": { "unused-imports/no-unused-imports": "error", - "prefer-const": "error", + "prefer-const": "off", "simple-import-sort/imports": "error", "simple-import-sort/exports": "error", "no-var": "error" diff --git a/package.json b/package.json index d8c5109..e2f2580 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "lint:eslint": "eslint --ignore-path .gitignore --ext .ts", "lint:js": "npm run lint:eslint src/", "lint:fix": "npm run lint:js -- --fix", - "prepare": "husky install" + "prepare": "husky install", + "generate:proto": "protoc --plugin=\"protoc-gen-ts=./node_modules/.bin/protoc-gen-ts\" --ts_opt=esModuleInterop=true --js_out=\"./src/generated\" --ts_out=\"./src/generated\" --proto_path=\"./src/proto\" src/proto/block.proto" }, "repository": { "type": "git", @@ -119,6 +120,8 @@ "swagger-ui-express": "^4.6.3", "ts-luxon": "^4.3.2", "ts-node-dev": "1.0.0-pre.44", + "ts-proto": "^2.0.2", + "ts-protoc-gen": "^0.15.0", "typedi": "^0.8.0", "typescript": "^4.5.4", "typings": "^2.1.1", diff --git a/src/api/routes/validatorRpc.ts b/src/api/routes/validatorRpc.ts index 4c7a0ab..52b6d91 100644 --- a/src/api/routes/validatorRpc.ts +++ b/src/api/routes/validatorRpc.ts @@ -4,20 +4,94 @@ import {WinstonUtil} from "../../utilz/winstonUtil"; import {ValidatorContractState} from "../../services/messaging-common/validatorContractState"; import {ValidatorNode} from "../../services/messaging/validatorNode"; import {ValidatorRandom} from "../../services/messaging/validatorRandom"; +import { + Block, + InitDid, Signer, + Transaction, + TransactionObj, TxAttestorData, + TxValidatorData +} from "../../generated/block_pb"; +import {BitUtil} from "../../utilz/bitUtil"; +import DateUtil from "../../utilz/dateUtil"; +import IdUtil from "../../utilz/idUtil"; +import {HashUtil} from "../../utilz/hashUtil"; + +console.log(Block); @Service() export class ValidatorRpc { public log: Logger = WinstonUtil.newLog(ValidatorRpc); @Inject() - private validatorNode: ValidatorNode + private validatorNode: ValidatorNode; + @Inject() private validatorRandom: ValidatorRandom; + public debug_randomTransaction({ blockDataBase16, txDataBase16 }) { + + // build transaction data (app-dependent) + const data = new InitDid(); + data.setDid('0xAA'); + data.setMasterpubkey('0xBB'); + data.setDerivedkeyindex(1); + data.setDerivedpubkey('0xCC'); + data.setEncderivedprivkey('0xDD'); + console.log(JSON.stringify(data.toObject())); + + // build transaction + const t = new Transaction(); + t.setType(0); + t.setCategory('INIT_DID'); + t.setSource('eip155:1:0xAA'); + t.setRecipientsList(['eip155:1:0xBB', 'eip155:1:0xCC']); + t.setData(data.serializeBinary()) + t.setSalt(IdUtil.getUuidV4AsBytes()); // uuid.parse(uuid.v4()) + t.setApitoken(BitUtil.base16ToBytes("AA")); // fake token + t.setFee("1"); // tbd + t.setSignature(BitUtil.base16ToBytes("EE")); // fake signature + console.log(JSON.stringify(t.toObject())); + + const txAsBytes = t.serializeBinary(); + console.log(`tx as base16 ${BitUtil.bytesToBase16(txAsBytes)}`); + console.log(`tx hash ${HashUtil.sha256AsBytes(txAsBytes)}`); + // build block + + // transactions + const to = new TransactionObj(); + to.setTx(t); + const vd = new TxValidatorData(); + vd.setVote(1); + const ad = new TxAttestorData(); + ad.setVote(1); + to.setValidatordata(vd); + to.setAttestordataList([ad]); + + // signers + const s1 = new Signer(); + s1.setNode('0x1111'); + s1.setRole(1); + s1.setSig('CC'); + const s2 = new Signer(); + s2.setNode('0x2222'); + s2.setRole(1); + s2.setSig('EE'); + + const b = new Block(); + b.setTs(DateUtil.currentTimeSeconds()); + b.setTxobjList([to]); + b.setAttesttoken('DD'); // fake attest token + b.setSignersList([s1, s2]); + b.setAttesttoken(BitUtil.base16ToBytes("C1CC")); + console.log(JSON.stringify(b.toObject())); + + const blockAsBytes = b.serializeBinary(); + console.log(`block as base16 ${BitUtil.bytesToBase16(blockAsBytes)}`); + console.log(`block hash ${HashUtil.sha256AsBytes(blockAsBytes)}`); + } + public push_getApiToken([]) { - // todo return api token const apiToken = this.validatorRandom.createValidatorToken(); - return { "apiToken" : apiToken.validatorToken, "apiUrl": apiToken.validatorUrl @@ -25,8 +99,10 @@ export class ValidatorRpc { } public push_sendTransaction([ transactionDataBase16 ]) { - // todo add to block - + const bytes = BitUtil.base16ToBytes(transactionDataBase16); + const tx = Transaction.deserializeBinary(bytes); + // todo process tx, append to block + console.log(JSON.stringify(tx.toObject())); const txHash = "0xAAAA"; return txHash; } diff --git a/src/proto/block.proto b/src/proto/block.proto new file mode 100644 index 0000000..d3dfbdb --- /dev/null +++ b/src/proto/block.proto @@ -0,0 +1,162 @@ +syntax = "proto3"; +package push; + +// BLOCK --------------------------------------------------------------------------------------------------------------- + +enum Role { + ROLE_UNSPECIFIED = 0; + VALIDATOR = 1; + ATTESTER = 2; +} + +enum Vote { + VOTE_UNSPECIFIED = 0; + ACCEPTED = 1; + REJECTED = 2; +} + +message DidMapping { + map didMapping = 1; +} + + +// section added by a block producer (we call him Validator in the context of the block) +message TxValidatorData { + // any validator can vote if he supports the tx or not + Vote vote = 1; + // additional context goes below. + // if it is signed = all attestors agree with this context of tx processing + DidMapping didMapping = 2; +} + +// section added by a block attester +// offset0 = block producer vote +// offset1..N = block attestor votes +message TxAttestorData { + // any attestor can vote if he supports the tx or not + Vote vote = 1; + // any additional fields below, that attestor wants to add for the 'storage' layer + // i.e. repeated string spam +} + +// transaction with voting data +message TransactionObj { + // raw bytes: you need to decode this based on category into a Transaciton + Transaction tx = 1; + // validator(block producer) processes 'data' field and fills this output + TxValidatorData validatorData = 2; + // attestors process 'data' and 'metaData' and fill this output + repeated TxAttestorData attestorData = 3; +} + +message Signer { + string node = 1; + Role role = 2; + string sig = 3; +} + +message Block { + // block timestamp in millis; + uint64 ts = 1; + repeated TransactionObj txObj = 2; + repeated Signer signers = 3; + bytes attestToken = 4; +} + + +// TRANSACTION --------------------------------------------------------------------------------------------------------- + + +message Transaction { + uint32 type = 1; // 0 for non-value, 1 for value + string category = 2; // INIT_DID, INIT_SESSION_KEY, NOTIF, EMAIL + string source = 3; + repeated string recipients = 4; + bytes data = 5; // data is also a protobuf message, depending on tx_type + bytes salt = 6; + bytes apiToken = 7; // f(apiToken) = v1 + bytes signature = 8; + string fee = 9; // we don't use this as of now, no native 256bits int support in protobuf +} + + + +// PAYLOADS ------------------------------------------------------------------------------------------------------------ + + +// INIT_DID +message InitDid { + string did = 1; + string masterPubKey = 2; + uint32 derivedKeyIndex = 3; + string derivedPubKey = 4; + string encDerivedPrivKey = 5; +} + +enum KeyAction { + UNSPECIFIED = 0; + PUBLISH_KEY = 1; + REVOKE_KEY = 2; +} + +// INIT_SESSION_KEY + +message SessionKeyAction { + int32 keyIndex = 1; + string keyAddress = 2; + KeyAction action = 3; +} + +// NOTIFICATION + +// PlainText Notification +// represents a targeted notificaiton with up to 1000 recipients (this is defined by a top level transaction) +message Notification { + // the app which sends the notif, i.e. "ShapeShift" + string app = 1; + // notification title, i.e. "ShapeShift - 2 PUFETH received!" + string title = 2; + // notification body, i.e. ""📩 Sender: 0x4bd5…7170\n👤 Receiver: 0x121d…876e (you)\n🪙 Asset: PUFETH\n💰 Amount: 2\n"" + string body = 3; + // TBD: clear definition of this field ???????????????????????????? + string channelUrl = 4; + // on click action, i.e. "https://etherscan.io/tx/0x3c93fd0617c5f7431d2899fa8e7ccea0ec09d4210a96c68b0fddf5772833871e" + string actionUrl = 5; + // big image url + string img = 6; + // small image url + string icon = 7; +} + +// Encrypted Notificaiton +enum EncryptionType { + ENCRYPTION_UNSPECIFIED = 0; + ECC = 1; +} + +message EncryptionDetails { + string recipientDID = 1; + EncryptionType type = 2; + int32 keyIndex = 3; + bytes encryptedSecret = 4; +} + +message EncryptedNotif { + bytes encryptedNotif = 1; + EncryptionDetails sourceEnc = 2; + repeated EncryptionDetails targetEnc = 3; +} + +// EMAIL + +message Attachment { + string filename = 1; + string type = 2; + string content = 3; // base64 encoded +} + +message Email { + string subject = 1; + string body = 2; // Plain text or HTML body of the email + repeated Attachment attachments = 3; +} diff --git a/src/utilz/bitUtil.ts b/src/utilz/bitUtil.ts index d527419..0ea0465 100644 --- a/src/utilz/bitUtil.ts +++ b/src/utilz/bitUtil.ts @@ -57,4 +57,13 @@ export class BitUtil { Coll.sortNumbersAsc(result) return result } + + public static base16ToBytes(base16String:string):Uint8Array { + return Uint8Array.from(Buffer.from(base16String, 'hex')); + } + + public static bytesToBase16(arr:Uint8Array):string { + return Buffer.from(arr).toString('hex'); + } + } diff --git a/src/utilz/hashUtil.ts b/src/utilz/hashUtil.ts new file mode 100644 index 0000000..2bbe71a --- /dev/null +++ b/src/utilz/hashUtil.ts @@ -0,0 +1,12 @@ +import * as CryptoJS from 'crypto-js' + +export class HashUtil { + + public static sha256AsBytes(data: Uint8Array): Uint8Array { + const wa = CryptoJS.lib.WordArray.create(data); + const shaAsWordArray = CryptoJS.SHA256(wa); + const hexString = CryptoJS.enc.Hex.stringify(shaAsWordArray); + const shaAsArray = Uint8Array.from(Buffer.from(hexString, 'hex')); + return shaAsArray; + } +} \ No newline at end of file diff --git a/src/utilz/idUtil.ts b/src/utilz/idUtil.ts index 17f32cb..790ddab 100755 --- a/src/utilz/idUtil.ts +++ b/src/utilz/idUtil.ts @@ -5,4 +5,8 @@ export default class IdUtil { public static getUuidV4(): string { return uuid.v4(); } + + public static getUuidV4AsBytes(): Uint8Array { + return uuid.parse(uuid.v4()); + } } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 9028e45..92a6fc6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,12 +5,14 @@ "compilerOptions": { "target": "es2017", "lib": ["es2017", "esnext.asynciterable"], - "typeRoots": ["./node_modules/@types", "./src/types"], +// "typeRoots": ["./node_modules/@types", "./src/types"], + "typeRoots": ["./node_modules/@types", "./src/types", "./src/generated"], "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", +// "moduleResolution": "node", + "moduleResolution": "nodenext", "module": "commonjs", "pretty": true, "sourceMap": true, @@ -18,7 +20,10 @@ "allowJs": true, "noEmit": false, "resolveJsonModule": true, - "esModuleInterop": true + "esModuleInterop": true, + +// "skipLibCheck": false + "skipLibCheck": true }, "include": ["./src/**/*", "./src/**/*.json"], "exclude": ["node_modules", "tests"] From 8897d898b97fc0bf809a195ca942ae69bd35fad2 Mon Sep 17 00:00:00 2001 From: Igx22 Date: Tue, 20 Aug 2024 20:40:09 +0700 Subject: [PATCH 05/10] add: protobuf generation (2) --- package.json | 2 +- scripts/protoc-generate.sh | 26 ++++++++ src/api/routes/validatorRpc.ts | 65 +------------------- src/proto/{ => push}/block.proto | 0 tests/block/block.test.ts | 100 +++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 65 deletions(-) create mode 100755 scripts/protoc-generate.sh rename src/proto/{ => push}/block.proto (100%) create mode 100644 tests/block/block.test.ts diff --git a/package.json b/package.json index e2f2580..2fa8805 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "lint:js": "npm run lint:eslint src/", "lint:fix": "npm run lint:js -- --fix", "prepare": "husky install", - "generate:proto": "protoc --plugin=\"protoc-gen-ts=./node_modules/.bin/protoc-gen-ts\" --ts_opt=esModuleInterop=true --js_out=\"./src/generated\" --ts_out=\"./src/generated\" --proto_path=\"./src/proto\" src/proto/block.proto" + "build:proto": "scripts/protoc-generate.sh" }, "repository": { "type": "git", diff --git a/scripts/protoc-generate.sh b/scripts/protoc-generate.sh new file mode 100755 index 0000000..3ab2e30 --- /dev/null +++ b/scripts/protoc-generate.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# Root directory of app +ROOT_DIR=$(git rev-parse --show-toplevel) + +# Path to Protoc Plugin +PROTOC_GEN_TS_PATH="${ROOT_DIR}/node_modules/.bin/protoc-gen-ts" + +# Directory holding all .proto files +SRC_DIR="${ROOT_DIR}/src/proto" + +# Directory to write generated code (.d.ts files) +OUT_DIR="${ROOT_DIR}/src/generated" + +# Clean all existing generated files +rm -r "${OUT_DIR}" +mkdir "${OUT_DIR}" + +# Generate all messages +protoc \ + --plugin="protoc-gen-ts=${PROTOC_GEN_TS_PATH}" \ + --ts_opt=esModuleInterop=true \ + --js_out="import_style=commonjs,binary:${OUT_DIR}" \ + --ts_out="${OUT_DIR}" \ + --proto_path="${SRC_DIR}" \ + $(find "${SRC_DIR}" -iname "*.proto") diff --git a/src/api/routes/validatorRpc.ts b/src/api/routes/validatorRpc.ts index 52b6d91..f8ef80a 100644 --- a/src/api/routes/validatorRpc.ts +++ b/src/api/routes/validatorRpc.ts @@ -10,7 +10,7 @@ import { Transaction, TransactionObj, TxAttestorData, TxValidatorData -} from "../../generated/block_pb"; +} from "../../generated/push/block_pb"; import {BitUtil} from "../../utilz/bitUtil"; import DateUtil from "../../utilz/dateUtil"; import IdUtil from "../../utilz/idUtil"; @@ -28,68 +28,6 @@ export class ValidatorRpc { @Inject() private validatorRandom: ValidatorRandom; - public debug_randomTransaction({ blockDataBase16, txDataBase16 }) { - - // build transaction data (app-dependent) - const data = new InitDid(); - data.setDid('0xAA'); - data.setMasterpubkey('0xBB'); - data.setDerivedkeyindex(1); - data.setDerivedpubkey('0xCC'); - data.setEncderivedprivkey('0xDD'); - console.log(JSON.stringify(data.toObject())); - - // build transaction - const t = new Transaction(); - t.setType(0); - t.setCategory('INIT_DID'); - t.setSource('eip155:1:0xAA'); - t.setRecipientsList(['eip155:1:0xBB', 'eip155:1:0xCC']); - t.setData(data.serializeBinary()) - t.setSalt(IdUtil.getUuidV4AsBytes()); // uuid.parse(uuid.v4()) - t.setApitoken(BitUtil.base16ToBytes("AA")); // fake token - t.setFee("1"); // tbd - t.setSignature(BitUtil.base16ToBytes("EE")); // fake signature - console.log(JSON.stringify(t.toObject())); - - const txAsBytes = t.serializeBinary(); - console.log(`tx as base16 ${BitUtil.bytesToBase16(txAsBytes)}`); - console.log(`tx hash ${HashUtil.sha256AsBytes(txAsBytes)}`); - // build block - - // transactions - const to = new TransactionObj(); - to.setTx(t); - const vd = new TxValidatorData(); - vd.setVote(1); - const ad = new TxAttestorData(); - ad.setVote(1); - to.setValidatordata(vd); - to.setAttestordataList([ad]); - - // signers - const s1 = new Signer(); - s1.setNode('0x1111'); - s1.setRole(1); - s1.setSig('CC'); - const s2 = new Signer(); - s2.setNode('0x2222'); - s2.setRole(1); - s2.setSig('EE'); - - const b = new Block(); - b.setTs(DateUtil.currentTimeSeconds()); - b.setTxobjList([to]); - b.setAttesttoken('DD'); // fake attest token - b.setSignersList([s1, s2]); - b.setAttesttoken(BitUtil.base16ToBytes("C1CC")); - console.log(JSON.stringify(b.toObject())); - - const blockAsBytes = b.serializeBinary(); - console.log(`block as base16 ${BitUtil.bytesToBase16(blockAsBytes)}`); - console.log(`block hash ${HashUtil.sha256AsBytes(blockAsBytes)}`); - } - public push_getApiToken([]) { const apiToken = this.validatorRandom.createValidatorToken(); return { @@ -158,5 +96,4 @@ export class ValidatorRpc { public push_listening([]) { return "true"; } - } \ No newline at end of file diff --git a/src/proto/block.proto b/src/proto/push/block.proto similarity index 100% rename from src/proto/block.proto rename to src/proto/push/block.proto diff --git a/tests/block/block.test.ts b/tests/block/block.test.ts new file mode 100644 index 0000000..3b116b8 --- /dev/null +++ b/tests/block/block.test.ts @@ -0,0 +1,100 @@ +import 'mocha' +import chai from 'chai' +import { + Block, + InitDid, + Signer, + Transaction, + TransactionObj, + TxAttestorData, + TxValidatorData +} from "../../src/generated/push/block_pb"; +import IdUtil from "../../src/utilz/idUtil"; +import {BitUtil} from "../../src/utilz/bitUtil"; +import {HashUtil} from "../../src/utilz/hashUtil"; +import DateUtil from "../../src/utilz/dateUtil"; + +const expect = chai.expect; + +/* +README: + +yarn install +yarn build:proto + generates from (src/proto) to (src/generated) +now you can use .proto stubs for typescript + */ + +describe('block tests', function () { + + it('create transaction and block, serialize/deserialize', async function () { + console.log("building ------------------------- "); + // build transaction data (app-dependent) + const data = new InitDid(); + data.setDid('0xAA'); + data.setMasterpubkey('0xBB'); + data.setDerivedkeyindex(1); + data.setDerivedpubkey('0xCC'); + data.setEncderivedprivkey('0xDD'); + console.log("data as json", JSON.stringify(data.toObject())); + + // build transaction + const t = new Transaction(); + t.setType(0); + t.setCategory('INIT_DID'); + t.setSource('eip155:1:0xAA'); + t.setRecipientsList(['eip155:1:0xBB', 'eip155:1:0xCC']); + t.setData(data.serializeBinary()) + t.setSalt(IdUtil.getUuidV4AsBytes()); // uuid.parse(uuid.v4()) + t.setApitoken(BitUtil.base16ToBytes("AA")); // fake token + t.setFee("1"); // tbd + t.setSignature(BitUtil.base16ToBytes("EE")); // fake signature + console.log("tx as json", JSON.stringify(t.toObject())); + + const txAsBytes = t.serializeBinary(); + console.log("tx as base16", BitUtil.bytesToBase16(txAsBytes)); + console.log("tx hash", BitUtil.bytesToBase16(HashUtil.sha256AsBytes(txAsBytes))); + // build block + + // transactions + const to = new TransactionObj(); + to.setTx(t); + const vd = new TxValidatorData(); + vd.setVote(1); + const ad = new TxAttestorData(); + ad.setVote(1); + to.setValidatordata(vd); + to.setAttestordataList([ad]); + + // signers + const s1 = new Signer(); + s1.setNode('0x1111'); + s1.setRole(1); + s1.setSig('CC'); + const s2 = new Signer(); + s2.setNode('0x2222'); + s2.setRole(1); + s2.setSig('EE'); + + const b = new Block(); + b.setTs(DateUtil.currentTimeSeconds()); + b.setTxobjList([to]); + b.setAttesttoken('DD'); // fake attest token + b.setSignersList([s1, s2]); + b.setAttesttoken(BitUtil.base16ToBytes("C1CC")); + console.log("block as json", JSON.stringify(b.toObject())); + + const blockAsBytes = b.serializeBinary(); + console.log("block as base16", BitUtil.bytesToBase16(blockAsBytes)); + console.log("block hash", BitUtil.bytesToBase16(HashUtil.sha256AsBytes(blockAsBytes))); + + + // PARSE it back into objects + console.log("parsing ------------------------- "); + let t2 = Transaction.deserializeBinary(txAsBytes); + console.log("tx2 as json", JSON.stringify(t2.toObject())); + + let b2 = Block.deserializeBinary(blockAsBytes); + console.log("block2 as json", JSON.stringify(b2.toObject())); + }) +}) From f376b536d43a8810854edf6c9cae8d3e71227cba Mon Sep 17 00:00:00 2001 From: Igx22 Date: Fri, 30 Aug 2024 22:58:19 +0700 Subject: [PATCH 06/10] add: push_sendTransaction, push_readBlockQueue, push_readBlockQueueSize --- src/api/routes/validatorRpc.ts | 62 ++--- src/proto/push/block.proto | 6 +- src/services/messaging-common/blockUtil.ts | 24 ++ src/services/messaging-dset/queueServer.ts | 6 +- src/services/messaging/QueueManager.ts | 5 + src/services/messaging/transactionError.ts | 6 + src/services/messaging/validatorNode.ts | 303 ++++++++++++++------- src/utilz/EthUtil.ts | 2 +- src/utilz/bitUtil.ts | 31 +++ src/utilz/envLoader.ts | 6 + tests/block/block.test.ts | 22 +- 11 files changed, 327 insertions(+), 146 deletions(-) create mode 100644 src/services/messaging-common/blockUtil.ts create mode 100644 src/services/messaging/transactionError.ts diff --git a/src/api/routes/validatorRpc.ts b/src/api/routes/validatorRpc.ts index f8ef80a..80e20e5 100644 --- a/src/api/routes/validatorRpc.ts +++ b/src/api/routes/validatorRpc.ts @@ -1,22 +1,16 @@ -import {Inject, Service} from "typedi"; +import {Container, Inject, Service} from "typedi"; import {Logger} from "winston"; import {WinstonUtil} from "../../utilz/winstonUtil"; -import {ValidatorContractState} from "../../services/messaging-common/validatorContractState"; import {ValidatorNode} from "../../services/messaging/validatorNode"; import {ValidatorRandom} from "../../services/messaging/validatorRandom"; -import { - Block, - InitDid, Signer, - Transaction, - TransactionObj, TxAttestorData, - TxValidatorData -} from "../../generated/push/block_pb"; import {BitUtil} from "../../utilz/bitUtil"; -import DateUtil from "../../utilz/dateUtil"; -import IdUtil from "../../utilz/idUtil"; -import {HashUtil} from "../../utilz/hashUtil"; +import {NumUtil} from "../../utilz/numUtil"; +import {QueueManager} from "../../services/messaging/QueueManager"; -console.log(Block); +type RpcResult = { + result: string; + error: string; +} @Service() export class ValidatorRpc { @@ -28,6 +22,9 @@ export class ValidatorRpc { @Inject() private validatorRandom: ValidatorRandom; + @Inject() + private queueManager: QueueManager; + public push_getApiToken([]) { const apiToken = this.validatorRandom.createValidatorToken(); return { @@ -36,40 +33,25 @@ export class ValidatorRpc { } ; } - public push_sendTransaction([ transactionDataBase16 ]) { - const bytes = BitUtil.base16ToBytes(transactionDataBase16); - const tx = Transaction.deserializeBinary(bytes); - // todo process tx, append to block - console.log(JSON.stringify(tx.toObject())); - const txHash = "0xAAAA"; + + + public async push_sendTransaction([ transactionDataBase16 ]) { + let txRaw = BitUtil.base16ToBytes(transactionDataBase16); + let txHash = await this.validatorNode.sendTransactionBlocking(txRaw); return txHash; } - public push_readBlockQueue([ offsetStr ]) { - // todo serve data from block queue - - return { - "items": [ - { - "id" : "101", - "object" : "0xAAAA", // BLOCK in protobuf format - "object_hash" : "0xBBBBBB" // BLOCK SHA1 - }, - { - "id" : "102", - "object" : "0xCC", - "object_hash" : "0xDD" - } - ], - "lastOffset" : "102" - } + public async push_readBlockQueue([ offsetStr ]) { + const firstOffset = NumUtil.parseInt(offsetStr, 0); + let result = await Container.get(QueueManager).readItems("mblock", firstOffset); + return result; } - public push_readBlockQueueSize([]) { - // todo return queue state + public async push_readBlockQueueSize([]) { + let result = await this.queueManager.getQueueLastOffsetNum("mblock"); return { - "lastOffset" : "102" + "lastOffset" : NumUtil.toString(result) } } diff --git a/src/proto/push/block.proto b/src/proto/push/block.proto index d3dfbdb..07c6df3 100644 --- a/src/proto/push/block.proto +++ b/src/proto/push/block.proto @@ -41,7 +41,7 @@ message TxAttestorData { // transaction with voting data message TransactionObj { - // raw bytes: you need to decode this based on category into a Transaciton + // raw bytes: you need to decode this based on category into a Transaction Transaction tx = 1; // validator(block producer) processes 'data' field and fills this output TxValidatorData validatorData = 2; @@ -58,9 +58,9 @@ message Signer { message Block { // block timestamp in millis; uint64 ts = 1; + bytes attestToken = 4; repeated TransactionObj txObj = 2; repeated Signer signers = 3; - bytes attestToken = 4; } @@ -70,7 +70,7 @@ message Block { message Transaction { uint32 type = 1; // 0 for non-value, 1 for value string category = 2; // INIT_DID, INIT_SESSION_KEY, NOTIF, EMAIL - string source = 3; + string sender = 3; repeated string recipients = 4; bytes data = 5; // data is also a protobuf message, depending on tx_type bytes salt = 6; diff --git a/src/services/messaging-common/blockUtil.ts b/src/services/messaging-common/blockUtil.ts new file mode 100644 index 0000000..c1c3b5e --- /dev/null +++ b/src/services/messaging-common/blockUtil.ts @@ -0,0 +1,24 @@ +import {Transaction} from "../../generated/push/block_pb"; +import {EnvLoader} from "../../utilz/envLoader"; +import {HashUtil} from "../../utilz/hashUtil"; +import {BitUtil} from "../../utilz/bitUtil"; + +export class BlockUtil { + public static readonly MAX_TRANSACTION_SIZE_BYTES = EnvLoader.getPropertyAsNumber('MAX_TRANSACTION_SIZE_BYTES', 1000000); + + public static parseTransaction(txRaw: Uint8Array): Transaction { + if (txRaw == null || txRaw.length > BlockUtil.MAX_TRANSACTION_SIZE_BYTES) { + throw new Error('tx size is too big'); + } + const tx = Transaction.deserializeBinary(txRaw); + return tx; + } + + public static calculateTransactionHashBase16(txRaw: Uint8Array): string { + return BitUtil.bytesToBase16(HashUtil.sha256AsBytes(txRaw)); + } + + public static calculateBlockHashBase16(blockRaw: Uint8Array): string { + return BitUtil.bytesToBase16(HashUtil.sha256AsBytes(blockRaw)); + } +} \ No newline at end of file diff --git a/src/services/messaging-dset/queueServer.ts b/src/services/messaging-dset/queueServer.ts index 3a5616b..ee9e538 100644 --- a/src/services/messaging-dset/queueServer.ts +++ b/src/services/messaging-dset/queueServer.ts @@ -123,12 +123,12 @@ export class QueueServer implements Consumer { } public async getLastOffset(): Promise { - const row = await MySqlUtil.queryOneRow<{ lastOffset: number }>( - `select max(id) as lastOffset + const max = await MySqlUtil.queryOneValue( + `select max(id) from dset_queue_${this.queueName} limit 1` ) - return row == null ? 0 : row.lastOffset + return max == null ? 0 : max; } public async readWithLastOffset( diff --git a/src/services/messaging/QueueManager.ts b/src/services/messaging/QueueManager.ts index 4749ecb..0df117b 100644 --- a/src/services/messaging/QueueManager.ts +++ b/src/services/messaging/QueueManager.ts @@ -137,6 +137,11 @@ export class QueueManager { return result } + public async getQueueLastOffsetNum(queueName: string): Promise { + return await this.getQueue(queueName).getLastOffset() + } + + // todo: remove public async getQueueLastOffset(queueName: string): Promise { const lastOffset = await this.getQueue(queueName).getLastOffset() return { result: lastOffset } diff --git a/src/services/messaging/transactionError.ts b/src/services/messaging/transactionError.ts new file mode 100644 index 0000000..86ed235 --- /dev/null +++ b/src/services/messaging/transactionError.ts @@ -0,0 +1,6 @@ +export class TransactionError extends Error { + constructor(message: string) { + super(message); + this.name = "ValidationError"; + } +} \ No newline at end of file diff --git a/src/services/messaging/validatorNode.ts b/src/services/messaging/validatorNode.ts index ecdfb16..e21d252 100644 --- a/src/services/messaging/validatorNode.ts +++ b/src/services/messaging/validatorNode.ts @@ -1,15 +1,15 @@ -import { MsgConverterService } from './msgConverterService' -import { ObjectHasher } from '../../utilz/objectHasher' -import { ethers, Wallet } from 'ethers' -import { Inject, Service } from 'typedi' -import { Logger } from 'winston' -import { MsgDeliveryService } from './msgDeliveryService' -import { EthSig } from '../../utilz/ethSig' -import { ValidatorClient } from './validatorClient' -import { WaitNotify } from '../../utilz/waitNotify' -import { NodeInfo, ValidatorContractState } from '../messaging-common/validatorContractState' -import { ValidatorRandom } from './validatorRandom' -import { ValidatorPing } from './validatorPing' +import {MsgConverterService} from './msgConverterService' +import {ObjectHasher} from '../../utilz/objectHasher' +import {ethers, Wallet} from 'ethers' +import {Inject, Service} from 'typedi' +import {Logger} from 'winston' +import {MsgDeliveryService} from './msgDeliveryService' +import {EthSig} from '../../utilz/ethSig' +import {ValidatorClient} from './validatorClient' +import {WaitNotify} from '../../utilz/waitNotify' +import {NodeInfo, ValidatorContractState} from '../messaging-common/validatorContractState' +import {ValidatorRandom} from './validatorRandom' +import {ValidatorPing} from './validatorPing' import StrUtil from '../../utilz/strUtil' import { FeedItem, @@ -20,30 +20,38 @@ import { MessageBlockUtil, NetworkRole, NodeMeta, - PayloadItem, RecipientMissing, RecipientsMissing } from '../messaging-common/messageBlock' -import { WinstonUtil } from '../../utilz/winstonUtil' -import { RedisClient } from '../messaging-common/redisClient' -import { Coll } from '../../utilz/coll' +import {WinstonUtil} from '../../utilz/winstonUtil' +import {RedisClient} from '../messaging-common/redisClient' +import {Coll} from '../../utilz/coll' import DateUtil from '../../utilz/dateUtil' -import { QueueManager } from './QueueManager' -import { Check } from '../../utilz/check' +import {QueueManager} from './QueueManager' +import {Check} from '../../utilz/check' import schedule from 'node-schedule' -import { - StorageContractListener, - StorageContractState -} from '../messaging-common/storageContractState' -import { AxiosResponse } from 'axios' -import { PromiseUtil } from '../../utilz/promiseUtil' +import {StorageContractListener, StorageContractState} from '../messaging-common/storageContractState' +import {AxiosResponse} from 'axios' +import {PromiseUtil} from '../../utilz/promiseUtil' import SNodeClient from './snodeClient' -import { AggregatedReplyHelper, NodeHttpStatus } from './AggregatedReplyHelper' -import Subscribers, { SubscribersItem } from '../channelsCompositeClasses/subscribersClass' +import {AggregatedReplyHelper, NodeHttpStatus} from './AggregatedReplyHelper' +import Subscribers, {SubscribersItem} from '../channelsCompositeClasses/subscribersClass' import config from '../../config' import ChannelsService from '../channelsService' -import { MySqlUtil } from '../../utilz/mySqlUtil' +import {MySqlUtil} from '../../utilz/mySqlUtil' import {EnvLoader} from "../../utilz/envLoader"; +import { + Block, + InitDid, Signer, + Transaction, + TransactionObj, + TxAttestorData, + TxValidatorData +} from "../../generated/push/block_pb"; +import {BlockUtil} from "../messaging-common/blockUtil"; +import {BitUtil} from "../../utilz/bitUtil"; +import {TransactionError} from "./transactionError"; +import {EthUtil} from "../../utilz/EthUtil"; // todo move read/write qurum to smart contract constants // todo joi validate for getRecord @@ -86,14 +94,15 @@ export class ValidatorNode implements StorageContractListener { // state // block - private currentBlock: MessageBlock + private currentBlock: Block; // objects used to wait on block private blockMonitors: Map = new Map() private readQuorum = 2 private writeQuorum = 2 - constructor() {} + constructor() { + } // https://github.com/typestack/typedi/issues/6 public async postConstruct() { @@ -128,93 +137,192 @@ export class ValidatorNode implements StorageContractListener { return this.valContractState.getAllNodesMap() } - public async addPayloadToMemPool( - p: PayloadItem, - validatorTokenRequired: boolean = false - ): Promise { + public async sendTransaction(txRaw: Uint8Array, validatorTokenRequired: boolean): Promise { if (this.currentBlock == null) { - this.currentBlock = new MessageBlock() + this.currentBlock = new Block(); } + // check + const tx = BlockUtil.parseTransaction(txRaw); + this.log.debug('processing tx: %o', tx.toObject()) if (validatorTokenRequired) { // check that this Validator is a valid target, according to validatorToken - const valid = this.random.checkValidatorToken(p.validatorToken, this.nodeId) + let valid = true; + let validatorToken = BitUtil.bytesToBase64(tx.getApitoken()); + try { + valid = this.random.checkValidatorToken(validatorToken, this.nodeId); + } catch (e) { + // parsing error + let err = 'invalid apiToken for nodeId ' + this.nodeId; + this.log.error(err, e); + throw new TransactionError(err); + } if (!valid) { - this.log.error(`invalid validatorToken %s , for nodeId %s`, p.validatorToken, this.nodeId) - return false + // logical error + let err = 'invalid apiToken for nodeId ' + this.nodeId; + this.log.error(err); + throw new TransactionError(err); } } - const feedItem = await this.converterService.addExternalPayload(p) - this.currentBlock.requests.push(p) - this.currentBlock.responses.push(feedItem) + this.checkValidTransactionFields(tx); + // append transaction + let txObj = new TransactionObj(); + txObj.setTx(tx); + this.currentBlock.addTxobj(txObj); // todo handle bad conversions return true } + private checkValidTransactionFields(tx: Transaction) { + + if (tx.getType() != 0) { + throw new TransactionError(`Only non-value transactions are supported`); + } + let senderAddr = EthUtil.parseCaipAddress(tx.getSender()); + let recipientAddrs = tx.getRecipientsList().map(value => EthUtil.parseCaipAddress(value)); + let goodSender = !StrUtil.isEmpty(senderAddr.chainId) && !StrUtil.isEmpty(senderAddr.namespace) + && !StrUtil.isEmpty(senderAddr.addr); + if (!goodSender) { + throw new TransactionError(`sender field is invalid ${tx.getSender()}`); + } + + if (tx.getCategory() === 'INIT_DID') { + let txData = InitDid.deserializeBinary(tx.getData()); + if (StrUtil.isEmpty(txData.getDid())) { + throw new TransactionError(`did missing`); + } + if (StrUtil.isEmpty(txData.getMasterpubkey())) { + throw new TransactionError(`masterPubKey missing`); + } + if (StrUtil.isEmpty(txData.getDerivedpubkey())) { + throw new TransactionError(`derivedPubKey missing`); + } + if (StrUtil.isEmpty(txData.getEncderivedprivkey())) { + throw new TransactionError(`encDerivedPrivKey missing`); + } + } else if (tx.getCategory() === 'NOTIFICATION') { + // todo checks + } else { + throw new TransactionError(`unsupported transaction category`); + } + if (StrUtil.isEmpty(BitUtil.bytesToBase16(tx.getSalt()))) { + throw new TransactionError(`salt field is invalid`); + } + + let validSignature = true; // todo check signature + if (!validSignature) { + throw new TransactionError(`signature field is invalid`); + } + } + + /** * This method blocks for a long amount of time, * until processBlock() gets executed * @param p */ - public async addPayloadToMemPoolBlocking(p: PayloadItem): Promise { - try { - const monitor = new WaitNotify() - this.blockMonitors.set(p.id, monitor) - this.log.debug('adding monitor for id: %s', p.id) - const success = await this.addPayloadToMemPool(p) + public async sendTransactionBlocking(txRaw: Uint8Array): Promise { + // try { + const monitor = new WaitNotify(); + let txHash = BlockUtil.calculateTransactionHashBase16(txRaw); + this.blockMonitors.set(txHash, monitor) + this.log.debug('adding monitor for transaction hash: %s', txHash) + const success = await this.sendTransaction(txRaw, true); if (!success) { - return false + return null; } await monitor.wait(this.ADD_PAYLOAD_BLOCKING_TIMEOUT) // block until processBlock() - return true - } catch (e) { - this.log.error(e) - return false - } + return txHash; + // } catch (e) { + // this.log.error(e) + // return null; + // } } /** * Add first signature and start processing * @param cronJob */ - public async batchProcessBlock(cronJob: boolean): Promise { + public async batchProcessBlock(cronJob: boolean): Promise { this.log.info('batch started'); - if (this.currentBlock == null || this.currentBlock.requests.length == 0) { + if (this.currentBlock == null + || this.currentBlock.getTxobjList() == null + || this.currentBlock.getTxobjList().length == 0) { if (!cronJob) { this.log.error('block is empty') } - return null + return null; } - const block = this.currentBlock - const blockMonitors = this.blockMonitors + const block = this.currentBlock; + const blockMonitors = this.blockMonitors; // replace it with a new empty block - this.currentBlock = new MessageBlock() - this.blockMonitors = new Map() + this.currentBlock = new Block(); + this.blockMonitors = new Map(); + + + // populate block + block.setTs(DateUtil.currentTimeMillis()); + for (let txObj of block.getTxobjList()) { + let vd = new TxValidatorData(); + vd.setVote(1); + txObj.setValidatordata(vd); - if (block.responses.length != block.requests.length) { - throw new Error(`message block has incorrect length ${block.responses.length}`) + // todo fake attestation as of now (todo remove) + let ad = new TxAttestorData(); + ad.setVote(1); + txObj.setAttestordataList([ad, ad]); } - if (block.responsesSignatures.length != 0) { - throw new Error(`message block has incorrect signature length ${block.responses.length}`) + const tokenObj = this.random.createAttestToken(); + this.log.debug('random token: %o', tokenObj); + Check.isTrue(tokenObj.attestVector?.length > 0, 'attest vector is empty'); + Check.isTrue(tokenObj.attestVector[0] != null, 'attest vector is empty'); + block.setAttesttoken(BitUtil.stringToBytes(tokenObj.attestToken)); + + // collect attestation per each transaction + // and signature per block + // from every attestor + // todo + // todo fake attestation as of now (todo remove) + for (let txObj of block.getTxobjList()) { + + let ad = new TxAttestorData(); + ad.setVote(1); + txObj.setAttestordataList([ad, ad]); } + // todo fake signing as of now + let vSign = new Signer(); + vSign.setNode(this.nodeId); + vSign.setRole(1); + vSign.setSig("AA11"); + + let aSign1 = new Signer(); + aSign1.setNode("11"); + aSign1.setRole(2); + aSign1.setSig("11"); + + let aSign2 = new Signer(); + aSign1.setNode("22"); + aSign1.setRole(2); + aSign1.setSig("22"); + + block.setSignersList([vSign, aSign1, aSign2]); + + /* // sign every response - for (let i = 0; i < block.responses.length; i++) { - const feedItem = block.responses[i] + for (let i = 0; i < block.getTxobjList().length; i++) { + const txObj = block.getTxobjList()[i]; + // TODO START FROM HERE !!!!!!!!!!!!!!!!!!!!!!! const nodeMeta = { nodeId: this.nodeId, role: NetworkRole.VALIDATOR, tsMillis: Date.now() } - const fisData: FISData = { vote: 'ACCEPT' } + const fisData: FISData = {vote: 'ACCEPT'} const ethSig = await EthSig.create(this.wallet, feedItem, fisData, nodeMeta) const fiSig = new FeedItemSig(fisData, nodeMeta, ethSig) block.responsesSignatures.push([fiSig]) } // network status - const tokenObj = this.random.createAttestToken() - this.log.debug('random token: %o', tokenObj) - Check.isTrue(tokenObj.attestVector?.length > 0, 'attest vector is empty') - Check.isTrue(tokenObj.attestVector[0] != null, 'attest vector is empty') - block.attestToken = tokenObj.attestToken + const attestCount = 1 const safeAttestCountToAvoidDuplicates = attestCount + 5 // todo handle if some M amount of nodes refuses to attest! @@ -291,7 +399,7 @@ export class ValidatorNode implements StorageContractListener { arr.push(...asResult.reports) this.log.debug('attestor %s successfully received block signatures and published the block') } - } + }*/ // group same reports , take one // todo // const sortedNodeReports = Coll.sortMapOfArrays(nodeReportsMap, false); @@ -304,31 +412,35 @@ export class ValidatorNode implements StorageContractListener { // call a contract // 2: deliver - await this.publishCollectivelySignedMessageBlock(block) + await this.publishCollectivelySignedMessageBlock(block); // 3: unblock addPayloadToMemPoolBlocking() requests - for (let i = 0; i < block.responses.length; i++) { - const fi = block.responses[i] - const id = fi.payload.data.sid - const objMonitor = blockMonitors.get(id) + for (let txObj of block.getTxobjList()) { + let tx = txObj.getTx(); + let txHash = BlockUtil.calculateTransactionHashBase16(tx.serializeBinary()); + + const objMonitor = blockMonitors.get(txHash); if (objMonitor) { - this.log.debug('unblocking monitor %s', objMonitor) - objMonitor.notifyAll() + this.log.debug('unblocking monitor %s', objMonitor); + objMonitor.notifyAll(); } else { - this.log.debug('no monitor found for id %s', id) + this.log.debug('no monitor found for id %s', txHash); } } return block } // sends message block to all connected delivery nodes - public async publishCollectivelySignedMessageBlock(mb: MessageBlock) { - const queue = this.queueInitializer.getQueue(QueueManager.QUEUE_MBLOCK) + public async publishCollectivelySignedMessageBlock(mb: Block) { + const queue = this.queueInitializer.getQueue(QueueManager.QUEUE_MBLOCK); + let blockBytes = mb.serializeBinary(); + let blockAsBase16 = BitUtil.bytesToBase16(blockBytes); + const blockHashAsBase16 = BlockUtil.calculateBlockHashBase16(blockBytes); const insertResult = await queue.accept({ - object: mb, - object_hash: MessageBlockUtil.calculateHash(mb) - }) - this.log.debug(`published message block ${mb.id} success: ${insertResult}`) + object: blockAsBase16, + object_hash: blockHashAsBase16 + }); + this.log.debug(`published message block ${blockHashAsBase16} success: ${insertResult}`) } // ------------------------------ ATTESTOR ----------------------------------------- @@ -346,7 +458,7 @@ export class ValidatorNode implements StorageContractListener { ) const check1 = MessageBlockUtil.checkBlock(block, activeValidators) if (!check1.success) { - return { error: check1.err, signatures: null } + return {error: check1.err, signatures: null} } // attest token checks const item0sig0 = block.responsesSignatures[0][0] @@ -359,7 +471,7 @@ export class ValidatorNode implements StorageContractListener { ) ) { this.log.error('block attest token is invalid') - return { error: 'block attest token is invalid', signatures: null } + return {error: 'block attest token is invalid', signatures: null} } // conversion checks const sigs: FeedItemSig[] = [] @@ -383,7 +495,7 @@ export class ValidatorNode implements StorageContractListener { // fuzzy check subscribers const cmpSubscribers = this.compareSubscribersDroppingLatestIsAllowed(feedItem, feedItemNew) if (!cmpSubscribers.subscribersAreEqual) { - return { error: cmpSubscribers.error, signatures: null } + return {error: cmpSubscribers.error, signatures: null} } // sign const nodeMeta = { @@ -405,7 +517,7 @@ export class ValidatorNode implements StorageContractListener { await this.redisCli.getClient().set(key, JSON.stringify(block)) const expirationInSeconds = 60 await this.redisCli.getClient().expire(key, expirationInSeconds) - return { error: null, signatures: sigs } + return {error: null, signatures: sigs} } /** @@ -445,12 +557,12 @@ export class ValidatorNode implements StorageContractListener { if (!isFreshSubscriber) { const errMsg = `${recipientV.addr} (${deltaInMinutes}mins) exists in V, missing in A` this.log.error('%s %s', dbgPrefix, errMsg) - return { subscribersAreEqual: false, error: errMsg } + return {subscribersAreEqual: false, error: errMsg} } // V has a subscriber, while A doesn't // we allow to ignore this if this subscriber is a 'fresh' one this.log.debug('%s is a fresh subscriber only in A', dbgPrefix, recipientA) - const recipientMissing: RecipientMissing = { addr: recipientV.addr } + const recipientMissing: RecipientMissing = {addr: recipientV.addr} recipientsToRemove.recipients.push(recipientMissing) } // check A subscribers against V subscribers @@ -461,10 +573,10 @@ export class ValidatorNode implements StorageContractListener { if (!isFreshSubscriber) { const errMsg = `${recipientA.addr} (${deltaInMinutes}mins) exists in A, missing in V` this.log.error('%s %s', dbgPrefix, errMsg) - return { subscribersAreEqual: false, error: errMsg } + return {subscribersAreEqual: false, error: errMsg} } } - const result = { subscribersAreEqual: true, comparisonResult: recipientsToRemove } + const result = {subscribersAreEqual: true, comparisonResult: recipientsToRemove} this.log.debug('%s result %s', dbgPrefix, result) return result } @@ -596,7 +708,8 @@ export class ValidatorNode implements StorageContractListener { async handleReshard( currentNodeShards: Set | null, allNodeShards: Map> - ): Promise {} + ): Promise { + } public async getRecord(nsName: string, nsIndex: string, dt: string, key: string): Promise { this.log.debug(`getRecord() nsName=${nsName}, nsIndex=${nsIndex}, dt=${dt}, key=${key}`) diff --git a/src/utilz/EthUtil.ts b/src/utilz/EthUtil.ts index 5665d8b..4d38f37 100644 --- a/src/utilz/EthUtil.ts +++ b/src/utilz/EthUtil.ts @@ -1,7 +1,7 @@ import StrUtil from './strUtil' export class EthUtil { - static parseCaipAddress(addressinCAIP: string): CaipAddr | null { + public static parseCaipAddress(addressinCAIP: string): CaipAddr | null { if (StrUtil.isEmpty(addressinCAIP)) { return null } diff --git a/src/utilz/bitUtil.ts b/src/utilz/bitUtil.ts index 0ea0465..0830276 100644 --- a/src/utilz/bitUtil.ts +++ b/src/utilz/bitUtil.ts @@ -1,5 +1,11 @@ import { Coll } from './coll' +// bytes (as hex numbers) = 0x41 0x41 0x42 0x42 +// Uint8Array (as decimal numbers) = 65 65 66 66 +// string (as non printable chars) = .. +// base16 string = 0xAABB +// base64 string = QUFCQg== + export class BitUtil { /** * XORs 2 buffers, byte by byte: src = src XOR add @@ -58,6 +64,7 @@ export class BitUtil { return result } + public static base16ToBytes(base16String:string):Uint8Array { return Uint8Array.from(Buffer.from(base16String, 'hex')); } @@ -66,4 +73,28 @@ export class BitUtil { return Buffer.from(arr).toString('hex'); } + public static base64ToString(base64String:string):string { + return Buffer.from(base64String, 'base64').toString('binary'); + } + + public static bytesToBase64(bytes:Uint8Array):string { + return Buffer.from(bytes).toString('base64'); + } + + public static bytesToString(bytes:Uint8Array):string { + return Buffer.from(bytes).toString('utf8'); + } + + public static stringToBytes(str:string):Uint8Array { + return new Uint8Array(Buffer.from(str, 'utf-8')); + } + + public static stringToBase64(str:string):string { + return Buffer.from(str, 'binary').toString('base64'); + } + + public static base64ToBase16(base64String:string):string { + return Buffer.from(base64String, 'base64').toString('hex'); + } + } diff --git a/src/utilz/envLoader.ts b/src/utilz/envLoader.ts index eafdd9b..68f5c31 100644 --- a/src/utilz/envLoader.ts +++ b/src/utilz/envLoader.ts @@ -1,5 +1,6 @@ import dotenv from 'dotenv' import StrUtil from './strUtil' +import {NumUtil} from "./numUtil"; export class EnvLoader { public static loadEnvOrFail() { @@ -31,4 +32,9 @@ export class EnvLoader { } return val } + + public static getPropertyAsNumber(propName: string, defaultValue:number): number { + const val = process.env[propName] + return NumUtil.parseInt(val, defaultValue); + } } diff --git a/tests/block/block.test.ts b/tests/block/block.test.ts index 3b116b8..d27dd33 100644 --- a/tests/block/block.test.ts +++ b/tests/block/block.test.ts @@ -27,6 +27,7 @@ now you can use .proto stubs for typescript describe('block tests', function () { + it('create transaction and block, serialize/deserialize', async function () { console.log("building ------------------------- "); // build transaction data (app-dependent) @@ -40,13 +41,13 @@ describe('block tests', function () { // build transaction const t = new Transaction(); - t.setType(0); + t.setType(3); t.setCategory('INIT_DID'); - t.setSource('eip155:1:0xAA'); + t.setSender('eip155:1:0xAA'); t.setRecipientsList(['eip155:1:0xBB', 'eip155:1:0xCC']); t.setData(data.serializeBinary()) t.setSalt(IdUtil.getUuidV4AsBytes()); // uuid.parse(uuid.v4()) - t.setApitoken(BitUtil.base16ToBytes("AA")); // fake token + t.setApitoken("eyJub2RlcyI6W3sibm9kZUlkIjoiMHg4ZTEyZEUxMkMzNWVBQmYzNWI1NmIwNEU1M0M0RTQ2OGU0NjcyN0U4IiwidHNNaWxsaXMiOjE3MjQ2NzMyNDAwMzAsInJhbmRvbUhleCI6ImY3YmY3YmYwM2ZlYTBhNzI1MTU2OWUwNWRlNjU2ODJkYjU1OTU1N2UiLCJwaW5nUmVzdWx0cyI6W3sibm9kZUlkIjoiMHhmREFFYWY3YWZDRmJiNGU0ZDE2REM2NmJEMjAzOWZkNjAwNENGY2U4IiwidHNNaWxsaXMiOjE3MjQ2NzMyNDAwMjAsInN0YXR1cyI6MX0seyJub2RlSWQiOiIweDk4RjlEOTEwQWVmOUIzQjlBNDUxMzdhZjFDQTc2NzVlRDkwYTUzNTUiLCJ0c01pbGxpcyI6MTcyNDY3MzI0MDAxOSwic3RhdHVzIjoxfV0sInNpZ25hdHVyZSI6IjB4YjMzM2NjMWI3MWM0NGM0MDhkOTZiN2JmYjYzODU0OTNjZjE2N2NiMmJkMjU1MjdkNzg2ZDM4ZjdiOTgwZWFkMzAxMmY3NmNhNzhlM2FiMWEzN2U2YTFjY2ZkMjBiNjkzZGVmZDAwOWM4NzExY2ZjODlmMDUyYjM5MzY4ZjFjZTgxYiJ9LHsibm9kZUlkIjoiMHhmREFFYWY3YWZDRmJiNGU0ZDE2REM2NmJEMjAzOWZkNjAwNENGY2U4IiwidHNNaWxsaXMiOjE3MjQ2NzMyNDAwMjUsInJhbmRvbUhleCI6IjkyMTY4NzRkZjBlMTQ4NTk3ZjlkNDRkMGRmZmFlZGU5NTg0NGRkMTciLCJwaW5nUmVzdWx0cyI6W3sibm9kZUlkIjoiMHg4ZTEyZEUxMkMzNWVBQmYzNWI1NmIwNEU1M0M0RTQ2OGU0NjcyN0U4IiwidHNNaWxsaXMiOjE3MjQ2NzMyMjQ2NTAsInN0YXR1cyI6MX0seyJub2RlSWQiOiIweDk4RjlEOTEwQWVmOUIzQjlBNDUxMzdhZjFDQTc2NzVlRDkwYTUzNTUiLCJ0c01pbGxpcyI6MTcyNDY3MzIyNDY1NSwic3RhdHVzIjoxfV0sInNpZ25hdHVyZSI6IjB4N2JmYzQ0MjQ0ZGM0MTdhMjg0YzEwODUwZGEzNTE2YzUwNWEwNjJmYjIyYmI1ODU0ODg2YWEyOTk3OWUwMmYxOTdlZWMyYzk2ZDVkOTQ4ZDBhMWQ2NTBlYzIzNGRhMDVjMGY5M2JlNWUyMDkxNjFlYzJjY2JjMWU5YzllNzQyOGIxYiJ9LHsibm9kZUlkIjoiMHg5OEY5RDkxMEFlZjlCM0I5QTQ1MTM3YWYxQ0E3Njc1ZUQ5MGE1MzU1IiwidHNNaWxsaXMiOjE3MjQ2NzMyNDAwMjQsInJhbmRvbUhleCI6IjBkOWExNmE4OTljYWQwZWZjODgzZjM0NWQwZjgwYjdmYTE1YTY1NmYiLCJwaW5nUmVzdWx0cyI6W3sibm9kZUlkIjoiMHg4ZTEyZEUxMkMzNWVBQmYzNWI1NmIwNEU1M0M0RTQ2OGU0NjcyN0U4IiwidHNNaWxsaXMiOjE3MjQ2NzMyMjY5NDMsInN0YXR1cyI6MX0seyJub2RlSWQiOiIweGZEQUVhZjdhZkNGYmI0ZTRkMTZEQzY2YkQyMDM5ZmQ2MDA0Q0ZjZTgiLCJ0c01pbGxpcyI6MTcyNDY3MzIyNjk0Nywic3RhdHVzIjoxfV0sInNpZ25hdHVyZSI6IjB4YmE2Mjk2OTZlZWU4MDQ4ZDE2OTA3MDNhZmVjYWY4ZmJjM2Y4NDMxOWQ0OTFhZGIzY2YzZGYzMzExMTllMDAyOTA1MTc3MjAyNzkxNzEzNTMzMmU0MGZiMzI2OTM5Y2JhN2Y2NDc2NmYyYjY5MzQwZTZlNGYwZmIzNjM2OThmYzkxYiJ9XX0="); // fake token t.setFee("1"); // tbd t.setSignature(BitUtil.base16ToBytes("EE")); // fake signature console.log("tx as json", JSON.stringify(t.toObject())); @@ -96,5 +97,18 @@ describe('block tests', function () { let b2 = Block.deserializeBinary(blockAsBytes); console.log("block2 as json", JSON.stringify(b2.toObject())); - }) + }); + + it('test for setting data as string (do not use this)', async function () { + const t = new Transaction(); + let originalData = "AABB"; + console.log('assign data ', originalData); + let encoded = BitUtil.bytesToBase64(BitUtil.base16ToBytes("AABB")); + console.log('encoded for assignment ', encoded); + t.setData(encoded); + console.log("t as bin", BitUtil.bytesToBase16(t.serializeBinary())); + let protoEncodedAndDecoded: any = Transaction.deserializeBinary(t.serializeBinary()).getData(); + console.log('expeced assigned data, to be ', originalData, "but got", protoEncodedAndDecoded, '=', BitUtil.bytesToBase16(protoEncodedAndDecoded)); + }); + }) From 31254d36f67c5f6690251ccacc1b1e2627ca6e74 Mon Sep 17 00:00:00 2001 From: Igx22 Date: Sat, 31 Aug 2024 14:10:49 +0700 Subject: [PATCH 07/10] fix: removed secp256k1 --- package.json | 1 - src/helpers/cryptoHelper.ts | 47 ++----------------------------------- 2 files changed, 2 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 2fa8805..a1e14c2 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,6 @@ "redis": "^4.2.0", "reflect-metadata": "^0.1.12", "request-promise": "^4.2.6", - "secp256k1-v4": "https://github.com/HarshRajat/secp256k1-node", "semver": "^7.3.7", "socket.io": "^4.4.1", "swagger-jsdoc": "^6.2.8", diff --git a/src/helpers/cryptoHelper.ts b/src/helpers/cryptoHelper.ts index 155dbf0..2ccff6a 100644 --- a/src/helpers/cryptoHelper.ts +++ b/src/helpers/cryptoHelper.ts @@ -2,7 +2,7 @@ const CryptoJS = require('crypto-js') import { decrypt, encrypt } from 'eccrypto' import EthCrypto from 'eth-crypto' -import { publicKeyConvert } from 'secp256k1-v4' + const publicKeyToAddress = require('ethereum-public-key-to-address') @@ -54,50 +54,7 @@ module.exports = { }, // Encryption with public key encryptWithPublicKey: async function (message, publicKey) { - // Convert compressed public key, starts with 03 or 04 - const pubKeyUint8Array = Uint8Array.from(new Buffer(publicKey, 'hex')) - //console.log("[ENCRYPTION] Public Key Uint8Array: " + pubKeyUint8Array); - - const convertedKeyAsUint8Array = publicKeyConvert(pubKeyUint8Array, false) - //console.log("[ENCRYPTION] Public Key Converted: " + convertedKeyAsUint8Array); - - const convertedPublicKeyHex = new Buffer(convertedKeyAsUint8Array) - //console.log("[ENCRYPTION] Converted Public Key Buffer: " + convertedPublicKeyHex); - - const pubKey = new Buffer(convertedPublicKeyHex, 'hex') - //console.log("[ENCRYPTION] pubkey getting sentout for encrypt: " + pubKey); - - return encrypt(pubKey, Buffer(message)).then((encryptedBuffers) => { - const cipher = { - iv: encryptedBuffers.iv.toString('hex'), - ephemPublicKey: encryptedBuffers.ephemPublicKey.toString('hex'), - ciphertext: encryptedBuffers.ciphertext.toString('hex'), - mac: encryptedBuffers.mac.toString('hex') - } - // use compressed key because it's smaller - // const compressedKey = new Buffer.from(publicKeyConvert(Web3Helper.getUint8ArrayFromHexStr(cipher.ephemPublicKey), true)).toString('hex') - const input = Uint8Array.from(new Buffer(cipher.ephemPublicKey, 'hex')) - const keyConvert = publicKeyConvert(input, true) - // console.log("[ENCRYPTION] Coverted key: " + keyConvert); - - const keyConvertBuffer = new Buffer(keyConvert) - // console.log("[ENCRYPTION] Coverted key in buffer : " + keyConvertBuffer); - // console.log(keyConvertBuffer); - - //console.log(keyConvert); - const compressedKey = keyConvertBuffer.toString('hex') - // console.log("[ENCRYPTION] Compressed key in buffer : "); - // console.log(compressedKey); - - const ret = Buffer.concat([ - new Buffer(cipher.iv, 'hex'), // 16bit - new Buffer(compressedKey, 'hex'), // 33bit - new Buffer(cipher.mac, 'hex'), // 32bit - new Buffer(cipher.ciphertext, 'hex') // var bit - ]).toString('hex') - - return ret - }) + }, // Decryption with public key decryptWithPrivateKey: async function (message, privateKey) { From be99d530f7a050d8ceeee345a70323130a1fcb55 Mon Sep 17 00:00:00 2001 From: Igx22 Date: Sat, 31 Aug 2024 14:12:04 +0700 Subject: [PATCH 08/10] fix: removed jobs loading --- src/loaders/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/loaders/index.ts b/src/loaders/index.ts index dc13042..48b7cb8 100644 --- a/src/loaders/index.ts +++ b/src/loaders/index.ts @@ -60,9 +60,9 @@ export default async ({ await dbListenerLoader({ pool, logger }) logger.info('DB Listener loaded!') - logger.info('Loading jobs') - await jobsLoader({ logger }) - logger.info('Jobs loaded!') + // logger.info('Loading jobs') + // await jobsLoader({ logger }) + // logger.info('Jobs loaded!') if (config.pushNodesNet !== 'PROD') { logger.info('Loading Subgraph jobs') From bf268326231a22053ee6263af070feb03aed1034 Mon Sep 17 00:00:00 2001 From: Igx22 Date: Sat, 31 Aug 2024 14:16:14 +0700 Subject: [PATCH 09/10] add: docker for 3 validators --- Dockerfile | 4 +- docker-compose-v01.yml | 72 -------------------- docker-compose.yml | 36 +++++----- mysql-init/create_databases.sql | 27 ++++++++ net.yml | 85 ++++++++++++++++++++++++ src/services/messaging/QueueManager.ts | 91 ++------------------------ 6 files changed, 138 insertions(+), 177 deletions(-) delete mode 100644 docker-compose-v01.yml create mode 100644 mysql-init/create_databases.sql create mode 100644 net.yml diff --git a/Dockerfile b/Dockerfile index e51b122..f8d23f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -FROM node:16.20.2 +FROM node:20.6.1 WORKDIR /app COPY . . -RUN yarn install +RUN yarn install --non-interactive --frozen-lockfile EXPOSE 4001 EXPOSE 4002 EXPOSE 4003 diff --git a/docker-compose-v01.yml b/docker-compose-v01.yml deleted file mode 100644 index 8a2886f..0000000 --- a/docker-compose-v01.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3' -services: - redis-v01: - image: 'redis:latest' - container_name: redis-v01 - restart: always - networks: - - push-shared-network - volumes: - - ./external/redis-v01:/data - - mysql-v01: - image: mysql:5.7.13 - container_name: mysql-v01 - platform: linux/amd64 - command: --default-authentication-plugin=mysql_native_password - restart: always - environment: - MYSQL_ROOT_PASSWORD: 'pass' - MYSQL_DATABASE: vnode1 - MYSQL_USER: 2roor - MYSQL_PASSWORD: s1mpl3 - # Change this to your local path - volumes: - - ./external/mysql-v01:/var/lib/mysql/ - networks: - - push-shared-network - - phpmyadmin-v01: - image: phpmyadmin/phpmyadmin - container_name: phpmyadmin-v01 - depends_on: - - mysql-v01 - environment: - PMA_HOST: mysql-v01 - PMA_PORT: 3306 - PMA_ARBITRARY: 1 - UPLOAD_LIMIT: 300M - ports: - - 8183:80 - restart: always - networks: - - push-shared-network - - ipfs-v01: - container_name: ipfs-v01 - image: ipfs/go-ipfs:latest - volumes: - - ./external/ipfs-v01:/data - networks: - - push-shared-network - - app-v01: - build: . - container_name: app-v01 - ports: - - '4001:4001' - depends_on: - - mysql-v01 - - redis-v01 - - ipfs-v01 - environment: - - CONFIG_DIR=/app/config - - LOG_DIR=/app/config/log - volumes: - - /root/config/v01:/app/config - networks: - - push-shared-network - -networks: - push-shared-network: - external: true \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3c42f65..afbdb65 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,22 @@ version: '3' services: + redis: image: 'redis:latest' - + container_name: redis-main restart: always networks: - - main + push-shared-network: + aliases: + - redis.local ports: - 6379:6379 volumes: - ./external/redis:/data mysql: - image: mysql:5.7.32 - container_name: db - platform: linux/amd64 + image: mysql:8.0 + container_name: mysql-main command: --default-authentication-plugin=mysql_native_password restart: always environment: @@ -24,12 +26,17 @@ services: MYSQL_PASSWORD: ${DB_PASS} ports: - 3306:3306 - # Change this to your local path volumes: - ./external/mysql:/var/lib/mysql/ + - ./mysql-init:/docker-entrypoint-initdb.d/ + networks: + push-shared-network: + aliases: + - mysql.local phpmyadmin: image: phpmyadmin/phpmyadmin + container_name: phpmyadmin-main depends_on: - mysql environment: @@ -40,17 +47,12 @@ services: restart: always ports: - 8183:80 + networks: + push-shared-network: + aliases: + - phpmyadmin.local - ipfs: - image: ipfs/go-ipfs:latest - volumes: - - ./external/ipfs:/data - ports: - - '4001:4001' - - '127.0.0.1:8080:8080' - - '127.0.0.1:8081:8081' - - '127.0.0.1:5001:5001' networks: - main: - driver: bridge + push-shared-network: + external: true \ No newline at end of file diff --git a/mysql-init/create_databases.sql b/mysql-init/create_databases.sql new file mode 100644 index 0000000..df7e43b --- /dev/null +++ b/mysql-init/create_databases.sql @@ -0,0 +1,27 @@ + +CREATE DATABASE vnode2 CHARACTER SET utf8 COLLATE utf8_general_ci; +GRANT ALL PRIVILEGES ON vnode2.* TO '2roor'@'%'; + +CREATE DATABASE vnode3 CHARACTER SET utf8 COLLATE utf8_general_ci; +GRANT ALL PRIVILEGES ON vnode3.* TO '2roor'@'%'; + +CREATE DATABASE vnode4 CHARACTER SET utf8 COLLATE utf8_general_ci; +GRANT ALL PRIVILEGES ON vnode4.* TO '2roor'@'%'; + +CREATE DATABASE vnode5 CHARACTER SET utf8 COLLATE utf8_general_ci; +GRANT ALL PRIVILEGES ON vnode5.* TO '2roor'@'%'; + +CREATE DATABASE vnode6 CHARACTER SET utf8 COLLATE utf8_general_ci; +GRANT ALL PRIVILEGES ON vnode6.* TO '2roor'@'%'; + +CREATE DATABASE vnode7 CHARACTER SET utf8 COLLATE utf8_general_ci; +GRANT ALL PRIVILEGES ON vnode7.* TO '2roor'@'%'; + +CREATE DATABASE vnode8 CHARACTER SET utf8 COLLATE utf8_general_ci; +GRANT ALL PRIVILEGES ON vnode8.* TO '2roor'@'%'; + +CREATE DATABASE vnode9 CHARACTER SET utf8 COLLATE utf8_general_ci; +GRANT ALL PRIVILEGES ON vnode9.* TO '2roor'@'%'; + +CREATE DATABASE vnode10 CHARACTER SET utf8 COLLATE utf8_general_ci; +GRANT ALL PRIVILEGES ON vnode10.* TO '2roor'@'%'; diff --git a/net.yml b/net.yml new file mode 100644 index 0000000..1fe17a8 --- /dev/null +++ b/net.yml @@ -0,0 +1,85 @@ +version: '3' +services: + + hardhat: + image: hardhat-main + container_name: hardhat + networks: + push-shared-network: + aliases: + - hardhat.local + ports: + - "8545:8545" + restart: always + + vnode1: + image: vnode-main + container_name: vnode1 + networks: + push-shared-network: + aliases: + - vnode1.local + environment: + DB_NAME: vnode1 + PORT: 4001 + CONFIG_DIR: /config + LOG_DIR: /log + DB_HOST: mysql.local + REDIS_URL: redis://redis.local:6379 + VALIDATOR_RPC_ENDPOINT: http://hardhat.local:8545 + VALIDATOR_RPC_NETWORK: 1337 + ports: + - "4001:4001" + volumes: + - ./push-docker/v01:/config + - ./push-docker/v01/log:/log + + + vnode2: + image: vnode-main + container_name: vnode2 + networks: + push-shared-network: + aliases: + - vnode2.local + environment: + DB_NAME: vnode2 + PORT: 4001 + CONFIG_DIR: /config + LOG_DIR: /log + DB_HOST: mysql.local + REDIS_URL: redis://redis.local:6379 + VALIDATOR_RPC_ENDPOINT: http://hardhat.local:8545 + VALIDATOR_RPC_NETWORK: 1337 + ports: + - "4002:4001" + volumes: + - ./push-docker/v02:/config + - ./push-docker/v02/log:/log + + + vnode3: + image: vnode-main + container_name: vnode3 + networks: + push-shared-network: + aliases: + - vnode3.local + environment: + DB_NAME: vnode3 + PORT: 4001 + CONFIG_DIR: /config + LOG_DIR: /log + DB_HOST: mysql.local + REDIS_URL: redis://redis.local:6379 + VALIDATOR_RPC_ENDPOINT: http://hardhat.local:8545 + VALIDATOR_RPC_NETWORK: 1337 + ports: + - "4003:4001" + volumes: + - ./push-docker/v03:/config + - ./push-docker/v03/log:/log + +networks: + push-shared-network: + external: true \ No newline at end of file diff --git a/src/services/messaging/QueueManager.ts b/src/services/messaging/QueueManager.ts index 0df117b..14473fe 100644 --- a/src/services/messaging/QueueManager.ts +++ b/src/services/messaging/QueueManager.ts @@ -1,67 +1,13 @@ -/* -This is a distributed set, which has the same contents on every node. - -Every node syncs with every other node - reads it's [queue], -downloads all items starting from latest offset, -and saves them into [queue] and [set] - -example: - -1) initial state: -node1 <---> node2 -(a,b) (c) - -2) replicates to: -node1 <---> node2 -(a,b,c) (c,a,b) - -3) node 2 adds new item e -node1 <---> node2 -(a,b,c) (c,a,b, e) - -4) node1 reads new item from the node2 queue, and appends it to local set -node1 <---> node2 -(a,b,c, e) (c,a,b, e) - -every server: adds only new items - */ - import { Inject, Service } from 'typedi' import { MySqlUtil } from '../../utilz/mySqlUtil' import { Logger } from 'winston' import ChannelsService from '../channelsService' -import schedule from 'node-schedule' import { ValidatorContractState } from '../messaging-common/validatorContractState' import { WinstonUtil } from '../../utilz/winstonUtil' import { QueueServer } from '../messaging-dset/queueServer' -import { QueueClient } from '../messaging-dset/queueClient' -import { QueueClientHelper } from '../messaging-common/queueClientHelper' - -/* -The data flow: - -comm contract (via HistoryFetcher) -rest endpoint - | - | - V -ChannelsService - 1.addExternalSubscribers ----> SubscribersService -----> QueueInitializerValidator - 1.removeExternalSubscribers ----> | 3. Append to Queue - V - 2.validate, - tryAdd to db - - - SubscribersService <------ 1. QueueClient - | | - V | - 2.validate, | - tryAdd to db V - 3. Append to Queue - - - */ + + + @Service() export class QueueManager { public log: Logger = WinstonUtil.newLog(QueueManager) @@ -81,8 +27,7 @@ export class QueueManager { private static QUEUE_REPLY_PAGE_SIZE = 10 private static CLIENT_REQUEST_PER_SCHEDULED_JOB = 10 - private subscribersQueue: QueueServer - private subscribersQueueClient: QueueClient + private mBlockQueue: QueueServer private queueMap = new Map() @@ -92,35 +37,9 @@ export class QueueManager { public async postConstruct() { this.log.debug('postConstruct()') - const qv = QueueManager - // setup queues that serve data to the outside world - this.subscribersQueue = new QueueServer( - qv.QUEUE_SUBSCRIBERS, - qv.QUEUE_REPLY_PAGE_SIZE, - this.channelService - ) - await this.startQueue(this.subscribersQueue) - - this.mBlockQueue = new QueueServer(qv.QUEUE_MBLOCK, 10, null) + this.mBlockQueue = new QueueServer(QueueManager.QUEUE_MBLOCK, 10, null) await this.startQueue(this.mBlockQueue) - - // setup client that fetches data from remote queues - this.subscribersQueueClient = new QueueClient(this.subscribersQueue, qv.QUEUE_SUBSCRIBERS) - await QueueClientHelper.initClientForEveryQueueForEveryValidator(this.contractState, [ - qv.QUEUE_SUBSCIRBERS - ]) - const qs = this - schedule.scheduleJob(this.CLIENT_READ_SCHEDULE, async function () { - const taskName = 'Client Read Scheduled' - try { - await qs.subscribersQueueClient.pollRemoteQueue(qv.CLIENT_REQUEST_PER_SCHEDULED_JOB) - qs.log.info(`Cron Task Completed -- ${taskName}`) - } catch (err) { - qs.log.error(`Cron Task Failed -- ${taskName}`) - qs.log.error(`Error Object: %o`, err) - } - }) } private async startQueue(queue: QueueServer) { From 5037a5ebb4ff75b99fd565b2016d16bfde6e53fd Mon Sep 17 00:00:00 2001 From: Igx22 Date: Sat, 31 Aug 2024 15:01:05 +0700 Subject: [PATCH 10/10] add: readme updated --- README.md | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ net.yml | 6 +++ 2 files changed, 150 insertions(+) diff --git a/README.md b/README.md index 73a4dd3..6f50ec2 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,7 @@ For the nodes to function correctly, you need to set up three separate databases curl --location 'http://localhost:4001/apis/v1/messaging/settings/eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681/ETH_TEST_SEPOLIA' ``` + ## Contributing We welcome contributions from the community! To contribute, please follow these steps: @@ -262,3 +263,146 @@ Please ensure your code adheres to our coding standards and includes appropriate ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + + +## Docker (local testing) + +Assumptions: +- default dir is /Users/w/chain where all push git repos are located (you can use any) + - /Users/w/chain/push-node-smart-contracts + - /Users/w/chain/push-vnode + - /Users/w/chain/push-snode + - /Users/w/chain/push-anode +- docker and docker-compose are installed +- docker desktop case: Allow full docker image access to /Users/w/chain in Preferences->Resources->File sharing +- recommended docker version could look like this (no specific version is needed; ) +- naming conventions: + - docker images: hardhat-main, validator-main, ... + - docker containers: vnode1, vnode2, ... + - docker dns: vnode1.local, redis.local, hardhat.local, .... +```shell +docker --version +Docker version 20.10.21, build baeda1f + +docker-compose --version +Docker Compose version v2.13.0 +``` + +Setup docker images for smart-contracts & vnodes +```bash +## create docker network +docker network create push-shared-network + +## prepare image for hardhat +cd /Users/w/chain/push-node-smart-contracts +docker build . -t hardhat-main + +## prepare image for V +cd /Users/w/chain/push-vnode +docker build . -t vnode-main +``` + +Run (2 shell tabs recommended) + +```bash +## run mysql + redis + phpmyadmin (shell1) +## add up -d for background +export DB_PASS=s1mpl3 +export DB_NAME=vnode1 +export DB_USER=2roor +cd /Users/w/chain/push-vnode +docker-compose up + +## run hardhat + vnode1 + vnode2 + vnode3 (shell2) +## add up -d for background +export DB_PASS=s1mpl3 +export DB_USER=2roor +cd /Users/w/push-vnode +docker-compose -f net.yml up +``` + +Check that all docker DNS is online (OPTIONAL) +```bash +docker exec redis-main bash -c " getent hosts redis.local " +docker exec redis-main bash -c " getent hosts mysql.local " +docker exec redis-main bash -c " getent hosts phpmyadmin.local " +docker exec redis-main bash -c " getent hosts hardhat.local " +docker exec redis-main bash -c " getent hosts vnode1.local " +docker exec redis-main bash -c " getent hosts vnode2.local " +docker exec redis-main bash -c " getent hosts vnode3.local " +``` + +Test +```shell +## vnode1, vnode2, vnode3 are online and visible from the host machine +curl --location 'http://localhost:4001/api/v1/rpc/' \ +--header 'Content-Type: application/json' \ +--data '{ + "jsonrpc": "2.0", + "method": "push_listening", + "params": [], + "id": 1 +}' +echo ------------ +curl --location 'http://localhost:4002/api/v1/rpc/' \ +--header 'Content-Type: application/json' \ +--data '{ + "jsonrpc": "2.0", + "method": "push_listening", + "params": [], + "id": 2 +}' +echo ------------ +curl --location 'http://localhost:4003/api/v1/rpc/' \ +--header 'Content-Type: application/json' \ +--data '{ + "jsonrpc": "2.0", + "method": "push_listening", + "params": [], + "id": 3 +}' +echo ------------ +``` +Smoke-test validator api +```shell +### get api token +curl --location 'http://localhost:4001/api/v1/rpc/' \ +--header 'Content-Type: application/json' \ +--data '{ + "jsonrpc": "2.0", + "method": "push_getApiToken", + "params": [], + "id": 1 +}' +echo ------------ +### send a test transaction (DUMMY DATA) +curl --location 'http://localhost:4001/api/v1/rpc/' \ +--header 'Content-Type: application/json' \ +--data '{ + "jsonrpc": "2.0", + "method": "push_sendTransaction", + "params": ["1208494e49545f4449441a0d6569703135353a313a30784141220d6569703135353a313a30784242220d6569703135353a313a307843432a1a0a043078414112043078424218012204307843432a04307844443210d8555d2a5c474fa0a5f588563d50b2873ab40b7b226e6f646573223a5b7b226e6f64654964223a22307838653132644531324333356541426633356235366230344535334334453436386534363732374538222c2274734d696c6c6973223a313732343637333234303033302c2272616e646f6d486578223a2266376266376266303366656130613732353135363965303564653635363832646235353935353765222c2270696e67526573756c7473223a5b7b226e6f64654964223a22307866444145616637616643466262346534643136444336366244323033396664363030344346636538222c2274734d696c6c6973223a313732343637333234303032302c22737461747573223a317d2c7b226e6f64654964223a22307839384639443931304165663942334239413435313337616631434137363735654439306135333535222c2274734d696c6c6973223a313732343637333234303031392c22737461747573223a317d5d2c227369676e6174757265223a22307862333333636331623731633434633430386439366237626662363338353439336366313637636232626432353532376437383664333866376239383065616433303132663736636137386533616231613337653661316363666432306236393364656664303039633837313163666338396630353262333933363866316365383162227d2c7b226e6f64654964223a22307866444145616637616643466262346534643136444336366244323033396664363030344346636538222c2274734d696c6c6973223a313732343637333234303032352c2272616e646f6d486578223a2239323136383734646630653134383539376639643434643064666661656465393538343464643137222c2270696e67526573756c7473223a5b7b226e6f64654964223a22307838653132644531324333356541426633356235366230344535334334453436386534363732374538222c2274734d696c6c6973223a313732343637333232343635302c22737461747573223a317d2c7b226e6f64654964223a22307839384639443931304165663942334239413435313337616631434137363735654439306135333535222c2274734d696c6c6973223a313732343637333232343635352c22737461747573223a317d5d2c227369676e6174757265223a22307837626663343432343464633431376132383463313038353064613335313663353035613036326662323262623538353438383661613239393739653032663139376565633263393664356439343864306131643635306563323334646130356330663933626535653230393136316563326363626331653963396537343238623162227d2c7b226e6f64654964223a22307839384639443931304165663942334239413435313337616631434137363735654439306135333535222c2274734d696c6c6973223a313732343637333234303032342c2272616e646f6d486578223a2230643961313661383939636164306566633838336633343564306638306237666131356136353666222c2270696e67526573756c7473223a5b7b226e6f64654964223a22307838653132644531324333356541426633356235366230344535334334453436386534363732374538222c2274734d696c6c6973223a313732343637333232363934332c22737461747573223a317d2c7b226e6f64654964223a22307866444145616637616643466262346534643136444336366244323033396664363030344346636538222c2274734d696c6c6973223a313732343637333232363934372c22737461747573223a317d5d2c227369676e6174757265223a22307862613632393639366565653830343864313639303730336166656361663866626333663834333139643439316164623363663364663333313131396530303239303531373732303237393137313335333332653430666233323639333963626137663634373636663262363933343065366534663066623336333639386663393162227d5d7d4201ee4a0131"], + "id": 1 +}' +echo ------------ + +### read transaction queue size +curl --location 'http://localhost:4001/api/v1/rpc/' \ +--header 'Content-Type: application/json' \ +--data '{ + "jsonrpc": "2.0", + "method": "push_readBlockQueueSize", + "params": [], + "id": 1 +}' + +### read transaction queue +curl --location 'http://localhost:4001/api/v1/rpc/' \ +--header 'Content-Type: application/json' \ +--data '{ + "jsonrpc": "2.0", + "method": "push_readBlockQueue", + "params": ["0"], + "id": 1 +}' +``` \ No newline at end of file diff --git a/net.yml b/net.yml index 1fe17a8..d6942a1 100644 --- a/net.yml +++ b/net.yml @@ -19,6 +19,8 @@ services: push-shared-network: aliases: - vnode1.local + depends_on: + - hardhat environment: DB_NAME: vnode1 PORT: 4001 @@ -42,6 +44,8 @@ services: push-shared-network: aliases: - vnode2.local + depends_on: + - hardhat environment: DB_NAME: vnode2 PORT: 4001 @@ -65,6 +69,8 @@ services: push-shared-network: aliases: - vnode3.local + depends_on: + - hardhat environment: DB_NAME: vnode3 PORT: 4001