diff --git a/src/common/mpc/base.ts b/src/common/mpc/base.ts index 7921b4be..18826da6 100644 --- a/src/common/mpc/base.ts +++ b/src/common/mpc/base.ts @@ -201,6 +201,10 @@ export class MultiPartyComputation { } private async process(network: IMpcNetwork, timeout: number) { + let execData = { + networkId: network!.id, + init: Date.now() + }; this.log = logger(`muon:common:mpc:${this.ConstructorName}`); try { /** Some partners may be excluded during the MPC process. */ @@ -210,6 +214,9 @@ export class MultiPartyComputation { for (let r = 0; r < this.rounds.length; r++) { Object.freeze(qualifiedPartners); const roundStartTime = Date.now(); + execData["round " + r] = { + computation: {start: Date.now()} + }; const currentRound = this.rounds[r], previousRound = r>0 ? this.rounds[r-1] : null; this.log(`processing round mpc[${this.id}].${currentRound} ...`) @@ -230,6 +237,7 @@ export class MultiPartyComputation { this.roundsOutput[currentRound] = await this.processRound(r, inputs, broadcasts, network.id, qualifiedPartners); this.log(`round executed [${network.id}].mpc[${this.id}].${currentRound}`) this.roundsPromise.resolve(r, true); + execData["round " + r]["computation"]["end"] = Date.now(); /** Gather other partners data */ const dataToSend = { @@ -238,11 +246,18 @@ export class MultiPartyComputation { this.log(`mpc[${this.id}].${currentRound} collecting round data`) let allPartiesResult: (PartnerRoundReceive|null)[] = await Promise.all( qualifiedPartners.map(partner => { + const start = Date.now(); return this.tryToGetRoundData(network, partner, r, dataToSend) .catch(e => { this.log.error(`[${this.id}][${currentRound}] error at node[${partner}] round ${r} %o`, e) return null }) + .finally(()=>{ + execData["round " + r]["request node " + partner] = { + start: start, + end: Date.now() + } + }) }) ) this.log(`MPC[${this.id}].${currentRound} ${allPartiesResult.filter(i => !!i).length} nodes response received`) @@ -266,8 +281,9 @@ export class MultiPartyComputation { } } + this.log(`MPC[${this.id}] all rounds done.`) - const result = this.onComplete(this.roundsArrivedMessages, network.id, qualifiedPartners); + const result = this.onComplete(this.roundsArrivedMessages, network.id, qualifiedPartners, execData); this.roundsPromise.resolve(this.rounds.length, result); } catch (e) { @@ -276,7 +292,9 @@ export class MultiPartyComputation { } } - onComplete(roundsArrivedMessages: MapOf>, networkId: string, partners: string[]): any { return "" } + onComplete(roundsArrivedMessages: MapOf>, networkId: string, partners: string[], execData: any): any { + return "" + } async processRound(roundIndex: number, input: MapOf, broadcast: MapOf, networkId: string, partners: string[]): Promise> { diff --git a/src/common/mpc/dist-key.ts b/src/common/mpc/dist-key.ts index 7f18dfa8..b9cc8f2d 100644 --- a/src/common/mpc/dist-key.ts +++ b/src/common/mpc/dist-key.ts @@ -15,7 +15,8 @@ export type DistKeyJson = { polynomial?: { t: number, Fx: string[] - } + }, + execData?: object } export class DistKey { @@ -28,14 +29,16 @@ export class DistKey { t: number, Fx: PublicKey[] }; + execData:object; - constructor(index: string, share: BN, address: string, publicKey : PublicKey, partners: string[], polynomial?: {t: number, Fx: PublicKey[]}) { + constructor(index: string, share: BN, address: string, publicKey: PublicKey, partners: string[], polynomial?: { t: number, Fx: PublicKey[] }, execData?: any) { this.index = index; this.share = share; this.address = address; this.publicKey = publicKey; - this.partners = partners, - this.polynomial = polynomial; + this.partners = partners; + this.polynomial = polynomial; + this.execData = execData; } /** @@ -64,7 +67,8 @@ export class DistKey { polynomial: !this.polynomial ? undefined : { t: this.polynomial.t, Fx: this.polynomial.Fx.map(p => p.encode('hex', true)) - } + }, + execData: this.execData } } @@ -83,6 +87,7 @@ export class DistKey { t: key.polynomial.t, Fx: key.polynomial.Fx.map(p => TssModule.keyFromPublic(p)) }, + key.execData ); } } diff --git a/src/common/mpc/dkg.ts b/src/common/mpc/dkg.ts index add7143a..59871427 100644 --- a/src/common/mpc/dkg.ts +++ b/src/common/mpc/dkg.ts @@ -254,7 +254,7 @@ export class DistributedKeyGeneration extends MultiPartyComputation { return {store, send, broadcast, qualifieds: newQualified} } - onComplete(roundsArrivedMessages: MapOf>, networkId: string, qualified: string[]): any { + onComplete(roundsArrivedMessages: MapOf>, networkId: string, qualified: string[], execData: any): any { // console.log(`mpc complete`, roundsArrivedMessages) const r1Msgs = this.getRoundReceives('round1') const r2Msgs = this.getRoundReceives('round2') @@ -301,7 +301,8 @@ export class DistributedKeyGeneration extends MultiPartyComputation { { t: this.t, Fx: totalFx - } + }, + execData ) } } diff --git a/src/core/plugins/base/app-request-manager.ts b/src/core/plugins/base/app-request-manager.ts index ba73874f..c8fba282 100644 --- a/src/core/plugins/base/app-request-manager.ts +++ b/src/core/plugins/base/app-request-manager.ts @@ -81,7 +81,7 @@ export default class AppRequestManager{ return !!item ? item.request : undefined } - addSignature(reqId: string, owner: string, sign: string){ + addSignature(reqId: string, owner: string, sign){ let item: CacheItem | undefined = this.getItem(reqId); if(item && item.signatures[owner] === undefined){ item.signatures[owner] = sign diff --git a/src/core/plugins/base/base-app-plugin.ts b/src/core/plugins/base/base-app-plugin.ts index e82ce3b1..cd877490 100644 --- a/src/core/plugins/base/base-app-plugin.ts +++ b/src/core/plugins/base/base-app-plugin.ts @@ -34,7 +34,13 @@ const {utils: {toBN}} = Web3 const ajv = new Ajv() const clone = (obj) => JSON.parse(JSON.stringify(obj)) const requestConfirmationCache: RedisCache = new RedisCache('req-confirm') - +export type AppRequestSignature = { + /** + * Schnorr signature of request, signed by TSS share + */ + signature: string, + execData: object +} const RemoteMethods = { WantSign: 'wantSign', InformRequestConfirmation: 'InformReqConfirmation', @@ -335,7 +341,7 @@ class BaseAppPlugin extends CallablePlugin { // await newRequest.save() - let sign: string = await this.makeSignature(newRequest, result, resultHash) + let sign: AppRequestSignature = await this.makeSignature(newRequest, result, resultHash) this.requestManager.addSignature(newRequest.reqId, process.env.SIGN_WALLET_ADDRESS!, sign); // new Signature(sign).save() @@ -395,6 +401,11 @@ class BaseAppPlugin extends CallablePlugin { }) } + //omit execData + for (let i = 0; i < requestData.signatures.length; i++) { + requestData.signatures[i] = omit(requestData.signatures[i], ['execData']) + } + /** send request data to aggregator nodes */ this.log('sending request to aggregator nodes ...') NetworkIpc.sendToAggregatorNode("AppRequest", requestData) @@ -409,6 +420,9 @@ class BaseAppPlugin extends CallablePlugin { newRequest.save() } + + + return requestData } } @@ -585,10 +599,12 @@ class BaseAppPlugin extends CallablePlugin { signers = await this.requestManager.onRequestSignFullFilled(newRequest.reqId) + let owners = Object.keys(signers) let allSignatures = owners.map(w => signers[w]); + let execData = owners.map(w => signers[w].execData); - let schnorrSigns = allSignatures.map(signature => splitSignature(signature)) + let schnorrSigns = allSignatures.map(signature => splitSignature(signature.signature)) const ownersIndex = owners.map(wallet => this.nodeManager.getNodeInfo(wallet)!.id); let aggregatedSign = TssModule.schnorrAggregateSigs(party!.t, schnorrSigns, ownersIndex) @@ -605,6 +621,7 @@ class BaseAppPlugin extends CallablePlugin { ownerPubKey: pub2json(verifyingPubKey, true), // signers: signersIndices, signature: bn2hex(aggregatedSign.s), + execData: execData // sign: { // s: `0x${aggregatedSign.s.toString(16)}`, // e: `0x${aggregatedSign.e.toString(16)}` @@ -713,7 +730,7 @@ class BaseAppPlugin extends CallablePlugin { } } - async makeSignature(request: AppRequest, result: any, resultHash): Promise { + async makeSignature(request: AppRequest, result: any, resultHash): Promise { let {reqId} = request; let nonce: AppTssKey = await this.tssPlugin.getSharedKey(`nonce-${reqId}`, 15000) if(!nonce) @@ -741,10 +758,13 @@ class BaseAppPlugin extends CallablePlugin { throw {message: "process.env.SIGN_WALLET_ADDRESS is not defined"} } - return stringifySignature(signature); + return { + signature: stringifySignature(signature), + execData: nonce.distKey.execData + }; } - async __onRemoteSignTheRequest(data: {reqId: string, sign: string} | null, error, remoteNode: MuonNodeInfo) { + async __onRemoteSignTheRequest(data: {reqId: string, sign: AppRequestSignature} | null, error, remoteNode: MuonNodeInfo) { if(error){ this.log.error(`node ${remoteNode.id} unable to sign the request. %O`, error) let {request: reqId, ...otherParts} = error; diff --git a/src/core/plugins/tss-plugin.ts b/src/core/plugins/tss-plugin.ts index 6628fcf2..41555e6d 100644 --- a/src/core/plugins/tss-plugin.ts +++ b/src/core/plugins/tss-plugin.ts @@ -2,6 +2,7 @@ import CallablePlugin from './base/callable-plugin.js' import AppTssKey, {AppTssKeyJson} from "../../utils/tss/app-tss-key.js"; import lodash from 'lodash' import * as tssModule from '../../utils/tss/index.js' +import * as execDataStorage from '../../utils/tss/exec-data-storage.js' import Web3 from 'web3' import {timeout, stackTrace, uuid, pub2json} from '../../utils/helpers.js' import {remoteApp, remoteMethod} from './base/app-decorators.js' @@ -54,6 +55,7 @@ export type KeyGenOptions = { const RemoteMethods = { recoverMyKey: 'recoverMyKey', storeTssKey: 'storeTssKey', + loadExecData: 'loadExecData', } @remoteApp @@ -811,6 +813,7 @@ class TssPlugin extends CallablePlugin { let key = new AppTssKey(party, keyGen.extraParams.keyId!, dKey) await SharedMemory.set(keyGen.extraParams.keyId, {partyInfo, key: key.toJson()}, 30*60*1000) + execDataStorage.set(key.id, key.distKey.execData); return key; } @@ -936,6 +939,25 @@ class TssPlugin extends CallablePlugin { throw "Not permitted to create tss key" } } + /** + * Node with ID:[1] inform other nodes that tss creation completed. + * + * @param data + * @param callerInfo: caller node information + * @param callerInfo.wallet: collateral wallet of caller node + * @param callerInfo.peerId: PeerID of caller node + * @returns {Promise} + * @private + */ + @remoteMethod(RemoteMethods.loadExecData) + async __loadExecData(data, callerInfo) { + if(!callerInfo.isDeployer) + return "Only deployers are allowed to call this method"; + let execData = await execDataStorage.get(data.keyId); + if (execData) + execData = JSON.parse(execData); + return execData; + } } export default TssPlugin; diff --git a/src/utils/tss/app-tss-key.ts b/src/utils/tss/app-tss-key.ts index 1a59aa02..abd203a9 100644 --- a/src/utils/tss/app-tss-key.ts +++ b/src/utils/tss/app-tss-key.ts @@ -16,7 +16,8 @@ export type AppTssKeyJson = { polynomial?: { t: number, Fx: string[] - } + }, + execData?: object } /** @@ -34,7 +35,7 @@ export default class AppTssKey { */ party: Party | null = null; - private distKey: DistKey; + public distKey: DistKey; private partnersPubKey = {} @@ -76,7 +77,8 @@ export default class AppTssKey { polynomial: !this.distKey.polynomial ? undefined : { t: this.distKey.polynomial.t, Fx: this.distKey.polynomial.Fx.map(p => p.encode('hex', true)) - } + }, + execData: this.distKey.execData } } @@ -91,6 +93,7 @@ export default class AppTssKey { address: tssModule.pub2addr(tssModule.keyFromPublic(key.publicKey)), partners: key.partners, polynomial: key.polynomial, + execData: key.execData, }) ) } diff --git a/src/utils/tss/exec-data-storage.ts b/src/utils/tss/exec-data-storage.ts new file mode 100644 index 00000000..91889e69 --- /dev/null +++ b/src/utils/tss/exec-data-storage.ts @@ -0,0 +1,27 @@ +import {createClient, RedisClient} from 'redis' +import redisConfig from '../../common/redis-config.js' +import {promisify} from "util" + +const redis = createClient(redisConfig); +const redisGet: (...args) => Promise = promisify(redis.get).bind(redis); +const redisSet: (...args) => Promise = promisify(redis.set).bind(redis); + +redis.on("error", function (error) { + console.error(`muon.utils.useOneTime error`, error); +}); + +/** + * Store execution data of a tss generation operation + * Execution data includes time that took to complete computation of generating the key + * and logs of networking times between nodes + */ +export async function set(key, val) { + val = JSON.stringify(val); + const expireMinutes = 30; + redisSet(key, val, 'EX', expireMinutes * 60); +} + +export async function get(key) { + let val = await redisGet(key); + return val; +}