UNPKG

origintrail-node

Version:

OriginTrail Node - Decentralized Knowledge Graph Node Library

504 lines (453 loc) 20.7 kB
import { kcTools } from 'assertion-tools'; import { setTimeout } from 'timers/promises'; import { PROOFING_INTERVAL, REORG_PROOFING_BUFFER, PRIVATE_HASH_SUBJECT_PREFIX, CHUNK_SIZE, OPERATION_ID_STATUS, TRIPLES_VISIBILITY, PROOFING_MAX_ATTEMPTS, } from '../constants/constants.js'; class ProofingService { constructor(ctx) { this.ctx = ctx; this.logger = ctx.logger; this.ualService = ctx.ualService; this.blockchainModuleManager = ctx.blockchainModuleManager; this.repositoryModuleManager = ctx.repositoryModuleManager; this.networkModuleManager = ctx.networkModuleManager; this.tripleStoreService = ctx.tripleStoreService; this.validationService = ctx.validationService; this.commandExecutor = ctx.commandExecutor; this.operationIdService = ctx.operationIdService; } async initialize() { this.logger.info('[PROOFING] Initializing ProofingService'); const promises = []; for (const blockchainId of this.blockchainModuleManager.getImplementationNames()) { this.logger.info( `[PROOFING] Initializing proofing service for blockchain ${blockchainId}`, ); promises.push(this.proofingMechanism(blockchainId)); } await Promise.all(promises); this.logger.info('[PROOFING] ProofingService initialization completed'); } async proofingMechanism(blockchainId) { this.logger.debug( `[PROOFING] Setting up proofing mechanism for blockchain ${blockchainId}`, ); // Flag to track if mechanism is running let isRunning = false; // Set up interval const interval = setInterval(async () => { // Skip if already running if (isRunning) { this.logger.debug( `[PROOFING] Proofing mechanism for ${blockchainId} still running, skipping this interval`, ); return; } try { isRunning = true; this.logger.debug( `[PROOFING] Starting proofing cycle for blockchain ${blockchainId}`, ); // Proofing logic await this.runProofing(blockchainId); this.logger.debug( `[PROOFING] Completed proofing cycle for blockchain ${blockchainId}`, ); } catch (error) { this.logger.error( `[PROOFING] Error in proofing mechanism for ${blockchainId}: ${error.message}, stack: ${error.stack}`, ); } finally { isRunning = false; } }, PROOFING_INTERVAL); // Store interval reference for cleanup this[`${blockchainId}Interval`] = interval; this.logger.info( `[PROOFING] Proofing mechanism initialized for blockchain ${blockchainId}`, ); } async runProofing(blockchainId) { this.logger.debug(`[PROOFING] Running proofing mechanism for ${blockchainId}`); const peerId = this.networkModuleManager.getPeerId().toB58String(); const isNodePartOfShard = await this.repositoryModuleManager.isNodePartOfShard( blockchainId, peerId, ); if (!isNodePartOfShard) { this.logger.debug( `[PROOFING] Skipping proofing. Node is not part of shard for blockchain: ${blockchainId}, peerId: ${peerId}`, ); return; } const identityId = await this.blockchainModuleManager.getIdentityId(blockchainId); // Check what is current proof period {isValid, activeProofPeriodStartBlock} const activeProofPeriodStatus = await this.blockchainModuleManager.getActiveProofPeriodStatus(blockchainId); const latestChallenge = await this.repositoryModuleManager.getLatestRandomSamplingChallengeRecordForBlockchainId( blockchainId, ); this.logger.debug( `[PROOFING] Checking proof period validity: isValid=${activeProofPeriodStatus.isValid}, activeProofPeriodStartBlock=${activeProofPeriodStatus.activeProofPeriodStartBlock}, latestChallengeBlock=${latestChallenge?.activeProofPeriodStartBlock}, sentSuccessfully=${latestChallenge?.sentSuccessfully}, blockchainId=${blockchainId}`, ); if ( activeProofPeriodStatus.isValid && latestChallenge?.activeProofPeriodStartBlock === activeProofPeriodStatus.activeProofPeriodStartBlock.toNumber() ) { if (latestChallenge.sentSuccessfully) { if (!latestChallenge.finalized) { this.logger.debug( `[PROOFING] Processing non-finalized challenge for blockchain: ${blockchainId}`, ); // We have latest challenge and we sent valid proof // Check onchain if it has score const score = await this.blockchainModuleManager.getNodeEpochProofPeriodScore( blockchainId, identityId, latestChallenge.epoch, latestChallenge.activeProofPeriodStartBlock, ); this.logger.debug( `[PROOFING] Retrieved node score for blockchain: ${blockchainId}, identityId: ${identityId}, score: ${score.toString()}`, ); // If score is greater than 0 than proof was sent and was valid // Ensure no reorgs happened by checking if it has score and enough time has passed and if possible mark it as finalized if (score.gt(0)) { // Sent more than minute ago check onchain confirm it finalized and it's good if ( latestChallenge.updatedAt.getTime() + REORG_PROOFING_BUFFER <= Date.now() ) { this.logger.info( `[PROOFING] Finalizing challenge for blockchainId: ${blockchainId}, challengeId: ${latestChallenge.id}`, ); latestChallenge.finalized = true; await this.repositoryModuleManager.setCompletedAndFinalizedRandomSamplingChallengeRecord( latestChallenge.id, true, true, ); this.operationIdService.emitChangeEvent( 'PROOF_CHALANGE_FINALIZED', this.generateOperationId( blockchainId, latestChallenge.epoch, latestChallenge.activeProofPeriodStartBlock, ), blockchainId, latestChallenge.epoch, latestChallenge.activeProofPeriodStartBlock, ); } else { this.logger.info( `[PROOFING] Waiting for reorg buffer to pass before finalizing for blockchain: ${blockchainId}, challengeId: ${latestChallenge.id}`, ); } } else { this.logger.warn( `[PROOFING] Zero score detected, resetting challenge status for blockchain: ${blockchainId}, challengeId: ${latestChallenge.id}`, ); latestChallenge.sentSuccessfully = false; latestChallenge.finalized = false; await this.repositoryModuleManager.setCompletedAndFinalizedRandomSamplingChallengeRecord( latestChallenge.id, latestChallenge.sentSuccessfully, latestChallenge.finalized, ); await this.prepareAndSendProof(blockchainId, identityId); } } } else { const ual = this.ualService.deriveUAL( blockchainId, latestChallenge.contractAddress, latestChallenge.knowledgeCollectionId, ); const data = await this.fetchAndProcessAssertion(blockchainId, ual); this.operationIdService.emitChangeEvent( 'PROOF_ASSERTION_FETCHED', this.generateOperationId( blockchainId, latestChallenge.epoch, latestChallenge.activeProofPeriodStartBlock, ), blockchainId, latestChallenge.epoch, latestChallenge.activeProofPeriodStartBlock, ); if (data.public.length === 0) { this.logger.warn( `[PROOFING] No assertions found for blockchain: ${blockchainId}, challengeId: ${latestChallenge.id}, ual: ${ual}`, ); return; } const proof = await this.calculateAndSubmitProof( data, latestChallenge, blockchainId, ); this.logger.info( `[PROOFING] Proof calculated and submitted successfully for blockchain: ${blockchainId}, challengeId: ${latestChallenge.id}`, ); return proof; } // If finalized is do nothing, wait for next proof } else { this.logger.info(`[PROOFING] Preparing new proof for blockchain: ${blockchainId}`); // Node needs to get new challenge or Node sent wrong proof await this.prepareAndSendProof(blockchainId, identityId); } } async prepareAndSendProof(blockchainId, identityId) { this.logger.debug(`[PROOFING] Starting proof preparation for blockchain: ${blockchainId}`); try { const newChallenge = await this.getAndPersistNewChallenge(blockchainId, identityId); const ual = this.ualService.deriveUAL( blockchainId, newChallenge.contractAddress, newChallenge.knowledgeCollectionId, ); this.logger.debug( `[PROOFING] New challenge created: challengeId=${newChallenge.id}, epoch=${newChallenge.epoch}, contractAddress=${newChallenge.contractAddress}, knowledgeCollectionId=${newChallenge.knowledgeCollectionId}`, ); const data = await this.fetchAndProcessAssertion(blockchainId, ual); this.operationIdService.emitChangeEvent( 'PROOF_ASSERTION_FETCHED', this.generateOperationId( blockchainId, newChallenge.epoch, newChallenge.activeProofPeriodStartBlock, ), blockchainId, newChallenge.epoch, newChallenge.activeProofPeriodStartBlock, ); if (data.public.length === 0) { throw new Error( `[PROOFING] No assertions found for blockchain: ${blockchainId}, ual: ${ual}`, ); } const proof = await this.calculateAndSubmitProof(data, newChallenge, blockchainId); this.logger.info( `[PROOFING] Proof calculated and submitted successfully for blockchain: ${blockchainId}, challengeId: ${newChallenge.id}`, ); return proof; } catch (error) { this.logger.error( `[PROOFING] Failed to prepare and send proof for blockchain: ${blockchainId}. Error: ${error.message}, stack: ${error.stack}`, ); throw error; } } async getAndPersistNewChallenge(blockchainId, identityId) { // Node has challenge for previous period need to get new one // Get new challenge const createChallengeResult = await this.blockchainModuleManager.createChallenge( blockchainId, ); if ( !createChallengeResult.success && !createChallengeResult?.error?.message?.includes( 'An unsolved challenge already exists for this node in the current proof period', ) ) { // Throw an error only if it's not the expected "already exists" error throw new Error(createChallengeResult.error); } const newChallenge = await this.blockchainModuleManager.getNodeChallenge( blockchainId, identityId, ); if (createChallengeResult.success) { // Only emit the event if a new challenge was actually generated this.operationIdService.emitChangeEvent( 'PROOF_NEW_CHALANGE_GENERATED', this.generateOperationId( blockchainId, newChallenge.epoch.toNumber(), newChallenge.activeProofPeriodStartBlock.toNumber(), ), blockchainId, newChallenge.epoch.toNumber(), newChallenge.activeProofPeriodStartBlock.toNumber(), ); } const newChallengeRecord = { blockchainId, epoch: newChallenge.epoch.toNumber(), activeProofPeriodStartBlock: newChallenge.activeProofPeriodStartBlock.toNumber(), contractAddress: newChallenge.knowledgeCollectionStorageContract.toLowerCase(), knowledgeCollectionId: newChallenge.knowledgeCollectionId.toNumber(), chunkNumber: newChallenge.chunkId.toNumber(), sentSuccessfully: false, finalized: false, }; const newRecord = await this.repositoryModuleManager.createRandomSamplingChallengeRecord( newChallengeRecord, ); this.operationIdService.emitChangeEvent( 'PROOF_NEW_CHALANGE_PERSISTED', this.generateOperationId( blockchainId, newChallenge.epoch.toNumber(), newChallenge.activeProofPeriodStartBlock.toNumber(), ), blockchainId, newChallenge.epoch.toNumber(), newChallenge.activeProofPeriodStartBlock.toNumber(), ); return newRecord; } async fetchAndProcessAssertion(blockchainId, ual) { let attempt = 0; let getResult; const getOperationId = await this.operationIdService.generateOperationId( OPERATION_ID_STATUS.GET.GET_START, ); this.operationIdService.emitChangeEvent( 'PROOFING_GET_STARTED', getOperationId, blockchainId, ); this.logger.debug( `[PROOFING] Proofing GET started for blockchain: ${blockchainId}, operationId: ${getOperationId}`, ); const { contract, knowledgeCollectionId } = this.ualService.resolveUAL(ual); await this.commandExecutor.add({ name: 'getCommand', sequence: [], delay: 0, data: { operationId: getOperationId, blockchain: blockchainId, contract, knowledgeCollectionId, state: 0, ual, contentType: TRIPLES_VISIBILITY.PUBLIC, }, transactional: false, }); do { // eslint-disable-next-line no-await-in-loop await setTimeout(500); // eslint-disable-next-line no-await-in-loop getResult = await this.operationIdService.getOperationIdRecord(getOperationId); attempt += 1; } while ( attempt < PROOFING_MAX_ATTEMPTS && getResult?.status !== OPERATION_ID_STATUS.FAILED && getResult?.status !== OPERATION_ID_STATUS.COMPLETED ); if (getResult?.status !== OPERATION_ID_STATUS.COMPLETED) { // We need to stop here and retry later throw new Error( `[PROOFING] Unable to Proofing GET Knowledge Collection for proof Id: ${knowledgeCollectionId}, for contract: ${contract}, blockchain: ${blockchainId}, GET result: ${JSON.stringify( getResult, )}`, ); } const { assertion } = await this.operationIdService.getCachedOperationIdData( getOperationId, ); this.logger.debug( `[PROOFING] Proofing GET: ${assertion.public.length} nquads found for asset with ual: ${ual}`, ); return assertion; } async calculateAndSubmitProof(data, challenge, blockchainId) { const publicAssertion = data.public; const filteredPublic = []; const privateHashTriples = []; publicAssertion.forEach((triple) => { if (triple.startsWith(`<${PRIVATE_HASH_SUBJECT_PREFIX}`)) { privateHashTriples.push(triple); } else { filteredPublic.push(triple); } }); let publicKnowledgeAssetsTriplesGrouped = kcTools.groupNquadsBySubject( filteredPublic, true, ); publicKnowledgeAssetsTriplesGrouped.push( ...kcTools.groupNquadsBySubject(privateHashTriples, true), ); publicKnowledgeAssetsTriplesGrouped = publicKnowledgeAssetsTriplesGrouped .map((t) => t.sort()) .flat(); // Calculate proof const proof = kcTools.calculateMerkleProof( publicKnowledgeAssetsTriplesGrouped, CHUNK_SIZE, challenge.chunkNumber, ); // Submit proof // How to validate result? (we do it in next iteration) const chunks = kcTools.splitIntoChunks(publicKnowledgeAssetsTriplesGrouped); const chunk = chunks[challenge.chunkNumber]; await this.blockchainModuleManager.submitProof(blockchainId, chunk, proof.proof); this.operationIdService.emitChangeEvent( 'PROOF_SUBMITTED', this.generateOperationId( blockchainId, challenge.epoch, challenge.activeProofPeriodStartBlock, ), blockchainId, null, null, ); const score = await this.blockchainModuleManager.getNodeEpochProofPeriodScore( blockchainId, await this.blockchainModuleManager.getIdentityId(blockchainId), challenge.epoch, challenge.activeProofPeriodStartBlock, ); if (score.gt(0)) { // Move score persistence to finalization await this.repositoryModuleManager.setCompletedAndScoreRandomSamplingChallengeRecord( challenge.id, true, BigInt(score.toString()), // eslint-disable-line no-undef ); this.operationIdService.emitChangeEvent( 'PROOF_SUBMITTED_SUCCESSFULLY', this.generateOperationId( blockchainId, challenge.epoch, challenge.activeProofPeriodStartBlock, ), blockchainId, null, null, ); } return proof; } generateOperationId(blockchainId, epoch, activeProofPeriodStartBlock) { return `${blockchainId}-${epoch}-${activeProofPeriodStartBlock}`; } // Add cleanup method to stop intervals cleanup() { this.logger.info('[PROOFING] Starting ProofingService cleanup'); for (const blockchainId of this.blockchainModuleManager.getImplementationNames()) { const intervalKey = `${blockchainId}Interval`; if (this[intervalKey]) { this.logger.debug(`Clearing interval for blockchain ${blockchainId}`); clearInterval(this[intervalKey]); this[intervalKey] = null; } } this.logger.info('[PROOFING] ProofingService cleanup completed'); } } export default ProofingService;