diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4597d442..406f9fb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ on: branches: - dev - main + - mainnet-* issue_comment: inputs: workflowBranch: diff --git a/package-lock.json b/package-lock.json index 67242347..516b0171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,22 @@ { "name": "@shardeum-foundation/archiver", - "version": "3.6.0-prerelease.0", + "version": "3.7.0-prerelease.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@shardeum-foundation/archiver", - "version": "3.6.0-prerelease.0", + "version": "3.7.0-prerelease.1", "license": "ISC", "dependencies": { "@ethereumjs/tx": "5.0.0", "@ethereumjs/util": "9.0.0", "@fastify/cors": "8.5.0", "@fastify/rate-limit": "7.6.0", - "@shardeum-foundation/lib-archiver-discovery": "1.2.0-prerelease.0", - "@shardeum-foundation/lib-crypto-utils": "4.2.0-prerelease.0", - "@shardeum-foundation/lib-net": "1.5.0-prerelease.0", - "@shardeum-foundation/lib-types": "1.3.0-prerelease.0", + "@shardeum-foundation/lib-archiver-discovery": "1.3.0-prerelease.0", + "@shardeum-foundation/lib-crypto-utils": "4.3.0-prerelease.0", + "@shardeum-foundation/lib-net": "1.6.0-prerelease.0", + "@shardeum-foundation/lib-types": "1.4.0-prerelease.1", "deepmerge": "4.3.1", "ethers": "6.13.4", "fastify": "4.12.0", @@ -1750,21 +1750,21 @@ } }, "node_modules/@shardeum-foundation/lib-archiver-discovery": { - "version": "1.2.0-prerelease.0", - "resolved": "https://registry.npmjs.org/@shardeum-foundation/lib-archiver-discovery/-/lib-archiver-discovery-1.2.0-prerelease.0.tgz", - "integrity": "sha512-O4picAV8rYay2MvtXTFRvk7pqlTFPi/l8t0dluBwBJTusftiQ0QzkbnhIKjp4VnU8aR2WqHlByKLFVhsy6m0rw==", + "version": "1.3.0-prerelease.0", + "resolved": "https://registry.npmjs.org/@shardeum-foundation/lib-archiver-discovery/-/lib-archiver-discovery-1.3.0-prerelease.0.tgz", + "integrity": "sha512-SzexKcEgHje76eXOSY52hYOLx+ZljtCWrzkvpaXCJCXVGkZ5da8sQBhxE0AY0QocDoUBhX90LFqoSc0jqIaF5g==", "dependencies": { - "@shardeum-foundation/lib-crypto-utils": "4.2.0-prerelease.0", + "@shardeum-foundation/lib-crypto-utils": "4.3.0-prerelease.0", "axios": "1.6.1", "gts": "3.1.1" } }, "node_modules/@shardeum-foundation/lib-crypto-utils": { - "version": "4.2.0-prerelease.0", - "resolved": "https://registry.npmjs.org/@shardeum-foundation/lib-crypto-utils/-/lib-crypto-utils-4.2.0-prerelease.0.tgz", - "integrity": "sha512-3uu/VVmFJjUlvhA9p7cjBYBNKVu6R0zl3c7xduFrLZpA/lSBMHYgNPG0GSDafK1QCYAYaZVbQ2eX8Kb7qJEcgQ==", + "version": "4.3.0-prerelease.0", + "resolved": "https://registry.npmjs.org/@shardeum-foundation/lib-crypto-utils/-/lib-crypto-utils-4.3.0-prerelease.0.tgz", + "integrity": "sha512-2h8nB6vyZ9LL7tBlXuZj/anFhQszGK3jIhD7bw18GN+TYupRBma4H1E/Qt6G7L5UXysnWoPQcDs1otfkKxZaow==", "dependencies": { - "@shardeum-foundation/lib-types": "1.3.0-prerelease.0", + "@shardeum-foundation/lib-types": "1.4.0-prerelease.0", "buffer-xor": "2.0.2", "fast-stable-stringify": "1.0.0", "sodium-native": "4.3.1" @@ -1773,10 +1773,15 @@ "node": "18.19.1" } }, + "node_modules/@shardeum-foundation/lib-crypto-utils/node_modules/@shardeum-foundation/lib-types": { + "version": "1.4.0-prerelease.0", + "resolved": "https://registry.npmjs.org/@shardeum-foundation/lib-types/-/lib-types-1.4.0-prerelease.0.tgz", + "integrity": "sha512-6x8iT6Fyi/s2RRr/O4KI6082KRaPNT1+KDT7C1sAaws+njCpM4r9iypeBpbMB9jb8Xo1/6sh6iVuex2gWx4LMQ==" + }, "node_modules/@shardeum-foundation/lib-net": { - "version": "1.5.0-prerelease.0", - "resolved": "https://registry.npmjs.org/@shardeum-foundation/lib-net/-/lib-net-1.5.0-prerelease.0.tgz", - "integrity": "sha512-IrFUMfE+1O9mhNbRWJATT68UzEzlLCQZ6JpSOcxShqy02v1C8J4zunfXirywRORuO9bFApKJjt50AeCZXaP3LQ==", + "version": "1.6.0-prerelease.0", + "resolved": "https://registry.npmjs.org/@shardeum-foundation/lib-net/-/lib-net-1.6.0-prerelease.0.tgz", + "integrity": "sha512-ooUVcABN1oWvdD0KLAKL7qjvSnWiJz1KmVGV8HTjeBXZ89rOcsSZp65ooy7VsY93NKaLn1doPAy0sHqZ2UvZ1A==", "hasInstallScript": true, "dependencies": { "cargo-cp-artifact": "0.1", @@ -1796,9 +1801,9 @@ } }, "node_modules/@shardeum-foundation/lib-types": { - "version": "1.3.0-prerelease.0", - "resolved": "https://registry.npmjs.org/@shardeum-foundation/lib-types/-/lib-types-1.3.0-prerelease.0.tgz", - "integrity": "sha512-tIudohI5u5ldcwHdeg2/IMYbtCPCv0/SMQv3NUQCo6cUsn0swXYRiI2q0ClVrPnkDAzALSYbUZrENP76SQT9Yw==" + "version": "1.4.0-prerelease.1", + "resolved": "https://registry.npmjs.org/@shardeum-foundation/lib-types/-/lib-types-1.4.0-prerelease.1.tgz", + "integrity": "sha512-sHFnZcEtRuC2+hT/yiWLwhkpDPv4suFSJJccq/NxbALpnDibOcSfiLGnxtMoEeDTDEqySDMB5Ubnqyj7z+psVw==" }, "node_modules/@sinclair/typebox": { "version": "0.27.8", diff --git a/package.json b/package.json index 276ed7a9..88d8a803 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shardeum-foundation/archiver", - "version": "3.6.0-prerelease.0", + "version": "3.7.0-prerelease.1", "engines": { "node": "18.19.1" }, @@ -89,10 +89,10 @@ "typescript-json-schema": "0.51.0" }, "dependencies": { - "@shardeum-foundation/lib-archiver-discovery": "1.2.0-prerelease.0", - "@shardeum-foundation/lib-crypto-utils": "4.2.0-prerelease.0", - "@shardeum-foundation/lib-net": "1.5.0-prerelease.0", - "@shardeum-foundation/lib-types": "1.3.0-prerelease.0", + "@shardeum-foundation/lib-archiver-discovery": "1.3.0-prerelease.0", + "@shardeum-foundation/lib-crypto-utils": "4.3.0-prerelease.0", + "@shardeum-foundation/lib-net": "1.6.0-prerelease.0", + "@shardeum-foundation/lib-types": "1.4.0-prerelease.1", "@ethereumjs/tx": "5.0.0", "@ethereumjs/util": "9.0.0", "@fastify/cors": "8.5.0", diff --git a/scripts/archiver_data_patcher.ts b/scripts/archiver_data_patcher.ts index 957c8826..1006840c 100644 --- a/scripts/archiver_data_patcher.ts +++ b/scripts/archiver_data_patcher.ts @@ -166,7 +166,7 @@ const runProgram = async (): Promise => { } } - const totalCycles = await CycleDB.queryCyleCount() + const totalCycles = await CycleDB.queryCycleCount() const totalAccounts = await AccountDB.queryAccountCount() const totalTransactions = await TransactionDB.queryTransactionCount() const totalReceipts = await ReceiptDB.queryReceiptCount() diff --git a/scripts/repair_missing_cycle.ts b/scripts/repair_missing_cycle.ts index d386cb58..d28d8c43 100644 --- a/scripts/repair_missing_cycle.ts +++ b/scripts/repair_missing_cycle.ts @@ -38,7 +38,7 @@ const start = async (): Promise => { } await dbstore.initializeDB(config) - const lastStoredCycleCount = await CycleDB.queryCyleCount() + const lastStoredCycleCount = await CycleDB.queryCycleCount() const lastStoredCycle = (await CycleDB.queryLatestCycleRecords(1))[0] console.log('lastStoredCycleCount', lastStoredCycleCount, 'lastStoredCycleCounter', lastStoredCycle.counter) diff --git a/src/API.ts b/src/API.ts index 4cfb5ca8..adb79e56 100644 --- a/src/API.ts +++ b/src/API.ts @@ -906,7 +906,7 @@ export function registerRoutes(server: FastifyInstance { diff --git a/src/Data/AccountDataProvider.ts b/src/Data/AccountDataProvider.ts index 8b91083e..ad2bb578 100644 --- a/src/Data/AccountDataProvider.ts +++ b/src/Data/AccountDataProvider.ts @@ -13,7 +13,7 @@ interface WrappedData { accountId: string /** hash of the data blob */ stateId: string - /** data blob opaqe */ + /** data blob opaque */ data: unknown /** Timestamp */ timestamp: number @@ -177,7 +177,7 @@ export const validateGlobalAccountReportRequest = ( } /** * - * This function is contructed to provide data in similar way as the `getAccountDataByRangeSmart` function in the validator + * This function is constructed to provide data in similar way as the `getAccountDataByRangeSmart` function in the validator * @param payload * @returns GetAccountDataByRangeSmart */ diff --git a/src/Data/Collector.ts b/src/Data/Collector.ts index 610e0df6..964c6252 100644 --- a/src/Data/Collector.ts +++ b/src/Data/Collector.ts @@ -1,5 +1,4 @@ import { P2P as P2PTypes } from '@shardeum-foundation/lib-types' -import { isDeepStrictEqual } from 'util' import * as Account from '../dbstore/accounts' import * as Transaction from '../dbstore/transactions' import * as Receipt from '../dbstore/receipts' @@ -10,7 +9,6 @@ import { clearCombinedAccountsData, combineAccountsData, collectCycleData, - nodesPerConsensusGroup, } from './Data' import { config } from '../Config' import * as Logger from '../Logger' @@ -26,7 +24,6 @@ import { globalAccountsMap, setGlobalNetworkAccount } from '../GlobalAccount' import { CycleLogWriter, ReceiptLogWriter, OriginalTxDataLogWriter } from '../Data/DataLogWriter' import * as OriginalTxDB from '../dbstore/originalTxsData' import ShardFunction from '../ShardFunctions' -import { ConsensusNodeInfo } from '../NodeList' import { accountSpecificHash, verifyAccountHash } from '../shardeum/calculateAccountHash' import { verifyAppReceiptData } from '../shardeum/verifyAppReceiptData' import { Cycle as DbCycle } from '../dbstore/types' @@ -34,6 +31,8 @@ import { Utils as StringUtils } from '@shardeum-foundation/lib-types' import { verifyPayload } from '../types/ajv/Helpers' import { AJVSchemaEnum } from '../types/enum/AJVSchemaEnum' import {verifyTransaction} from "../services/transactionVerification"; +import { CycleShardData } from '@shardeum-foundation/lib-types/build/src/state-manager/shardFunctionTypes' +import { generateTxId } from '../Utils' export let storingAccountData = false const processedReceiptsMap: Map = new Map() @@ -41,9 +40,9 @@ const receiptsInValidationMap: Map = new Map() const processedOriginalTxsMap: Map = new Map() const originalTxsInValidationMap: Map = new Map() const missingReceiptsMap: Map = new Map() -const missingOriginalTxsMap: Map = new Map() +// const missingOriginalTxsMap: Map = new Map() const collectingMissingReceiptsMap: Map = new Map() -const collectingMissingOriginalTxsMap: Map = new Map() +// const collectingMissingOriginalTxsMap: Map = new Map() interface MissingTx { txTimestamp: number @@ -63,417 +62,336 @@ export interface ReceiptVerificationResult { nestedCounterMessages?: string[] } -const verifyReceiptMajority = async ( - receipt: Receipt.Receipt | Receipt.ArchiverReceipt, - executionGroupNodes: ConsensusNodeInfo[], - minConfirmations: number = config.RECEIPT_CONFIRMATIONS -): Promise<{ success: boolean; newReceipt?: Receipt.ArchiverReceipt | Receipt.Receipt }> => { - /** - * Note: - * Currently, only the non-global receipt flow is implemented in `verifyReceiptMajority`, - * `verifyReceiptOffline`, and `verifyNonGlobalTxReceiptWithValidators`. In the future, - * global receipt methods can be added to maintain consistency. As of now, only offline - * verification for global receipts is available, and `verifyGlobalTxReceiptWithValidators` - * is not yet implemented. - */ - - // If robustQuery is disabled, do offline verification - if (!config.useRobustQueryForReceipt) { - return verifyReceiptOffline(receipt, executionGroupNodes, minConfirmations) - } - return verifyReceiptWithValidators(receipt, executionGroupNodes, minConfirmations) -} - -// Offline receipt verification /** - * Note: - * The `verifyReceiptOffline` function is responsible for verifying receipts - * without querying external validators, which is useful when the robust query - * feature is disabled. It currently supports only non-global receipt verification. - * Future enhancements should include validation logic for global receipts to ensure - * comprehensive receipt verification. This will help maintain consistency and reliability - * across all types of receipts processed by the system. - * - * The function delegates the verification process to `verifyNonGlobalTxReceiptOffline` - * based on the whether the receipt is global or not. + * Fetches authorized signers that belong to the execution shard from the provided signatures. + * @param {Crypto.core.Signature[] | P2PTypes.P2PTypes.Signature[]} signs - The array of signatures to verify. + * @param {CycleShardData} cycleShardData - The cycle shard data containing node information. + * @param {number} homePartition - The home partition number. + * @param {string} txId - The transaction ID. + * @param {number} timestamp - The timestamp of the transaction. + * @param {number} cycle - The cycle number. + * @returns {Set<[number, Crypto.core.Signature]> | Set<[number, P2PTypes.P2PTypes.Signature]>} - A set of tuples containing the index and signature of authorized signers. */ -const verifyReceiptOffline = async ( - receipt: Receipt.Receipt | Receipt.ArchiverReceipt, - executionGroupNodes: ConsensusNodeInfo[], - minConfirmations: number -): Promise<{ success: boolean; newReceipt?: Receipt.ArchiverReceipt | Receipt.Receipt }> => { - return verifyNonGlobalTxReceiptOffline(receipt, executionGroupNodes, minConfirmations) -} +function fetchAuthorizedSigners( + signaturePack: Crypto.core.Signature[] | P2PTypes.P2PTypes.Signature[], + cycleShardData: CycleShardData, + homePartition: number, + txId: string, + timestamp: number, + cycle: number +): Map { + const nodeMap = new Map() + // Fill the map with nodes keyed by their public keys + cycleShardData.nodes.forEach((node) => { + if (node.publicKey && node.publicKey.length == 64) { + nodeMap.set(node.publicKey.toLowerCase(), node) + } + }) -/** - * Note: - * The `verifyReceiptWithValidators` function currently supports only - * non-global receipt verification. Future enhancements should include - * validation logic for global receipts to ensure comprehensive receipt - * verification. This will help in maintaining consistency and reliability - * across all types of receipts processed by the system. - */ -const verifyReceiptWithValidators = async ( - receipt: Receipt.Receipt | Receipt.ArchiverReceipt, - executionGroupNodes: ConsensusNodeInfo[], - minConfirmations: number = config.RECEIPT_CONFIRMATIONS -): Promise<{ success: boolean; newReceipt?: Receipt.ArchiverReceipt | Receipt.Receipt }> => { - return verifyNonGlobalTxReceiptWithValidators(receipt, executionGroupNodes, minConfirmations) + // Create a set to store acceptable signers + // in case of GlobalTxReceipt, the signatures are of type : P2PTypes.P2PTypes.Signature + // in case of NonGlobalTxReceipt, signatures are of type : Crypto.core.Signature + const acceptableSigners = new Map< + string, + { index: number; sign: Crypto.core.Signature | P2PTypes.P2PTypes.Signature } + >() + for (const [index, sign] of signaturePack.entries()) { + const { owner: nodePubKey } = sign + + // Get the node id from the public key + const node = nodeMap.get(nodePubKey.toLowerCase()) + + // If the node is not found in the active nodes list, log an error and continue + if (node == null) { + Logger.mainLogger.error( + `The node with public key ${nodePubKey} of the receipt ${txId} with ${timestamp} is not in the active nodesList of cycle ${cycle}` + ) + if (nestedCountersInstance) + nestedCountersInstance.countEvent('receipt', 'sign_owner_not_in_active_nodesList') + continue + } + + // Check if the node is in the execution group + if (!cycleShardData.parititionShardDataMap.get(homePartition).coveredBy[node.id]) { + Logger.mainLogger.error( + `The node with public key ${nodePubKey} of the receipt ${txId} with ${timestamp} is not in the execution group of the tx` + ) + if (nestedCountersInstance) + nestedCountersInstance.countEvent('receipt', 'node_not_in_execution_group_of_tx') + continue + } + + acceptableSigners.set(nodePubKey.toLowerCase(), { index, sign }) + } + return acceptableSigners } /** - * Calls the /get-tx-receipt endpoint of the nodes in the execution group of the receipt to verify the receipt. If "RECEIPT_CONFIRMATIONS" number of nodes return the same receipt, the receipt is deemed valid. - * @param receipt - * @param executionGroupNodes - * @param minConfirmations - * @returns boolean + * Verifies the global transaction receipt. + * + * @param receipt - The receipt to verify. It can be either a `Receipt.Receipt` or `Receipt.ArchiverReceipt`. + * @returns A promise that resolves to an object indicating whether the verification was successful. + * + * The function performs the following checks: + * 1. Validates the transaction ID by comparing the generated transaction ID with the incoming transaction ID. + * 2. Ensures the voting group count does not exceed the number of nodes. + * 3. Checks if the number of signatures meets the required majority votes percentage. + * 4. Fetches the authorized signers and verifies if the valid signatures meet the required majority votes percentage. + * 5. Verifies the signatures and ensures the number of valid signatures meets the required threshold. + * + * If any of the checks fail, the function logs the appropriate error and returns `{ success: false }`. + * If all checks pass, the function returns `{ success: true }`. */ -const verifyNonGlobalTxReceiptWithValidators = async ( - receipt: Receipt.Receipt | Receipt.ArchiverReceipt, - executionGroupNodes: ConsensusNodeInfo[], - minConfirmations: number = config.RECEIPT_CONFIRMATIONS -): Promise<{ success: boolean; newReceipt?: Receipt.ArchiverReceipt | Receipt.Receipt }> => { +const verifyGlobalTxreceipt = async ( + receipt: Receipt.Receipt | Receipt.ArchiverReceipt +): Promise<{ success: boolean }> => { + const appliedReceipt = receipt.signedReceipt as P2PTypes.GlobalAccountsTypes.GlobalTxReceipt + const { signs, tx } = appliedReceipt const result = { success: false } - // Created signedData with full_receipt = false outside of queryReceipt to avoid signing the same data multiple times - let signedData = Crypto.sign({ - txId: receipt.tx.txId, - timestamp: receipt.tx.timestamp, - full_receipt: false, - }) - const queryReceipt = async (node: ConsensusNodeInfo): Promise => { - const QUERY_RECEIPT_TIMEOUT_SECOND = 2 - try { - return (await postJson( - `http://${node.ip}:${node.port}/get-tx-receipt`, - signedData, - QUERY_RECEIPT_TIMEOUT_SECOND - )) as GET_TX_RECEIPT_RESPONSE - } catch (error) { - Logger.mainLogger.error('Error in /get-tx-receipt:', error) - if (nestedCountersInstance) nestedCountersInstance.countEvent('receipt', 'Error_in_get-tx-receipt') - return null - } + + const { timestamp } = receipt.tx + const txId = appliedReceipt.tx.txId + const generatedTxId = generateTxId(appliedReceipt.tx.value) + if (generatedTxId != txId) { + if (nestedCountersInstance) nestedCountersInstance.countEvent('receipt', 'txId_mismatch') + Logger.mainLogger.error( + `VerifyGlobalTxreceipt : Transaction ID mismatch detected. Incoming txId: ${txId}, Generated txId: ${generatedTxId}` + ) + return result } - // Use only random 5 x Receipt Confirmations number of nodes from the execution group to reduce the number of nodes to query in large execution groups - const filteredExecutionGroupNodes = Utils.getRandomItemFromArr( - executionGroupNodes, - 0, - 5 * config.RECEIPT_CONFIRMATIONS - ) - const isReceiptEqual = (receipt1: any, receipt2: any): boolean => { - if (!receipt1 || !receipt2) return false - const r1 = Utils.deepCopy(receipt1) - const r2 = Utils.deepCopy(receipt2) - - // The confirmOrChallenge node could be different in the two receipts, so we need to remove it before comparing - delete r1?.confirmOrChallenge?.nodeId - delete r1?.confirmOrChallenge?.sign - delete r2?.confirmOrChallenge?.nodeId - delete r2?.confirmOrChallenge?.sign - - const equivalent = isDeepStrictEqual(r1, r2) - return equivalent + + const { cycle } = receipt + const executionShardKey = tx.source // while finding the home partition/node on the setGlobal side, the source string is used + const cycleShardData = shardValuesByCycle.get(cycle) + const { homePartition } = ShardFunction.addressToPartition(cycleShardData.shardGlobals, executionShardKey) + // Refer to https://github.com/shardeum/shardus-core/blob/7d8877b7e1a5b18140f898a64b932182d8a35298/src/p2p/GlobalAccounts.ts#L397 + let votingGroupCount = cycleShardData.nodes.length + + /** + * Workaround: Early cycles may lack enough nodes for receipts to pass verification. + * Relaxing the signature requirement for the first x cycles + * could prevent unnecessary rejections. Needs tuning for larger networks. + **/ + if (cycleShardData.cycleNumber > config.formingNetworkCycleThreshold) { + votingGroupCount = Math.max(votingGroupCount, cycleShardData.shardGlobals.nodesPerConsenusGroup) + } else { + if (nestedCountersInstance) nestedCountersInstance.countEvent('receipt', 'cycle_less_than_formingNetworkCycleThreshold') + Logger.mainLogger.log('cycle_less_than_formingNetworkCycleThreshold', votingGroupCount, cycleShardData.nodes.length) } - const robustQuery = await Utils.robustQuery( - filteredExecutionGroupNodes, - (execNode) => queryReceipt(execNode), - (rec1: GET_TX_RECEIPT_RESPONSE, rec2: GET_TX_RECEIPT_RESPONSE) => - isReceiptEqual(rec1?.receipt, rec2?.receipt), - minConfirmations, - false, // set shuffleNodes to false, - 500, // Add 500 ms delay - true - ) - if (config.VERBOSE) Logger.mainLogger.debug('robustQuery', receipt.tx.txId, robustQuery) - if (!robustQuery || !robustQuery.value || !(robustQuery.value as any).receipt) { + + let isReceiptMajority = signs.length / votingGroupCount >= config.requiredMajorityVotesPercentage + if (!isReceiptMajority) { Logger.mainLogger.error( - `❌ 'null' response from all nodes in receipt-validation for txId: ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp} - }` + `Invalid receipt globalModification signs count is less than ${config.requiredMajorityVotesPercentage}% of the votingGroupCount, ${signs.length}, ${votingGroupCount}` ) if (nestedCountersInstance) - nestedCountersInstance.countEvent('receipt', 'null_response_from_all_nodes_in_receipt-validation') + nestedCountersInstance.countEvent( + 'receipt', + `Invalid_receipt_globalModification_signs_count_less_than_${config.requiredMajorityVotesPercentage}%` + ) return result } - const robustQueryReceipt = (robustQuery.value as any).receipt as Receipt.SignedReceipt - - if (robustQuery.count < minConfirmations) { - // Wait for 500ms and try fetching the receipt from the nodes that did not respond in the robustQuery - await Utils.sleep(500) - let requiredConfirmations = minConfirmations - robustQuery.count - let nodesToQuery = executionGroupNodes.filter( - (node) => !robustQuery.nodes.some((n) => n.publicKey === node.publicKey) + const acceptableSigners = fetchAuthorizedSigners( + signs, + cycleShardData, + homePartition, + txId, + timestamp, + cycle + ) as Map + isReceiptMajority = acceptableSigners.size / votingGroupCount >= config.requiredMajorityVotesPercentage + if (!isReceiptMajority) { + Logger.mainLogger.error( + `Invalid receipt globalModification valid signs count is less than votingGroupCount ${acceptableSigners.size}, ${votingGroupCount}` ) - let retryCount = 5 // Retry 5 times - while (requiredConfirmations > 0) { - if (nodesToQuery.length === 0) { - if (retryCount === 0) break - // Wait for 500ms and try again - await Utils.sleep(500) - nodesToQuery = executionGroupNodes.filter( - (node) => !robustQuery.nodes.some((n) => n.publicKey === node.publicKey) + if (nestedCountersInstance) + nestedCountersInstance.countEvent( + 'receipt', + 'Invalid_receipt_globalModification_valid_signs_count_less_than_votingGroupCount' + ) + return result + } + const requiredSignatures = Math.floor(votingGroupCount * config.requiredMajorityVotesPercentage) + + const goodSignatures = new Map() + for (const [nodePubKey, signature] of acceptableSigners) { + if (Crypto.verify({ ...tx, sign: signature.sign })) { + goodSignatures.set(nodePubKey, signature) + // Break the loop if the required number of good signatures are found + if (goodSignatures.size >= requiredSignatures) break + } else { + if (nestedCountersInstance) + nestedCountersInstance.countEvent( + 'receipt', + 'VerifyGlobalTxReceipt_Found_invalid_signature_in_receipt_signedReceipt' ) - retryCount-- - if (nodesToQuery.length === 0) break - } - const node = nodesToQuery[0] - nodesToQuery.splice(0, 1) - const receiptResult: any = await queryReceipt(node) - if (!receiptResult || !receiptResult.receipt) continue - if (isReceiptEqual(robustQueryReceipt, receiptResult.receipt)) { - requiredConfirmations-- - robustQuery.nodes.push(node) - } + Logger.mainLogger.error( + `VerifyGlobalTxReceipt : Found invalid signature in receipt signedReceipt ${txId}, ${nodePubKey}, ${signature.index}` + ) } } - // Check if the robustQueryReceipt is the same as our receipt - const sameReceipt = isReceiptEqual(receipt.signedReceipt, robustQueryReceipt) - - if (!sameReceipt) { - Logger.mainLogger.debug( - `Found different receipt in robustQuery ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp}` - ) + if (goodSignatures.size < requiredSignatures) { if (nestedCountersInstance) - nestedCountersInstance.countEvent('receipt', 'Found_different_receipt_in_robustQuery') - if (config.VERBOSE) Logger.mainLogger.debug(receipt.signedReceipt) - if (config.VERBOSE) Logger.mainLogger.debug(robustQueryReceipt) - // update signedData with full_receipt = true - signedData = Crypto.sign({ txId: receipt.tx.txId, timestamp: receipt.tx.timestamp, full_receipt: true }) - for (const node of robustQuery.nodes) { - const fullReceiptResult: GET_TX_RECEIPT_RESPONSE = await queryReceipt(node) - if (config.VERBOSE) - Logger.mainLogger.debug( - `'fullReceiptResult ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp}`, - fullReceiptResult - ) - if (!fullReceiptResult || !fullReceiptResult.receipt) { - Logger.mainLogger.error( - `Got fullReceiptResult null from robustQuery node for ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp}` - ) - continue - } - const fullReceipt = fullReceiptResult.receipt as Receipt.ArchiverReceipt | Receipt.Receipt - if ( - isReceiptEqual(fullReceipt.signedReceipt, robustQueryReceipt) && - validateReceiptType(fullReceipt) - ) { - if (config.verifyAppReceiptData) { - if (profilerInstance) profilerInstance.profileSectionStart('Verify_app_receipt_data') - if (nestedCountersInstance) nestedCountersInstance.countEvent('receipt', 'Verify_app_receipt_data') - const { valid, needToSave } = await verifyAppReceiptData(receipt) - if (profilerInstance) profilerInstance.profileSectionEnd('Verify_app_receipt_data') - if (!valid) { - Logger.mainLogger.error( - `The app receipt verification failed from robustQuery nodes ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp}` - ) - continue - } - if (!needToSave) { - Logger.mainLogger.debug( - `Found valid full receipt in robustQuery ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp}` - ) - Logger.mainLogger.error( - `Found valid receipt from robustQuery: but no need to save ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp}` - ) - return { success: false } - } - } - if (config.verifyAccountData && receipt.globalModification === false) { - if (profilerInstance) profilerInstance.profileSectionStart('Verify_receipt_account_data') - if (nestedCountersInstance) - nestedCountersInstance.countEvent('receipt', 'Verify_receipt_account_data') - const result = verifyAccountHash(fullReceipt) - if (profilerInstance) profilerInstance.profileSectionEnd('Verify_receipt_account_data') - if (!result) { - Logger.mainLogger.error( - `The account verification failed from robustQuery nodes ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp}` - ) - continue - } - } - if (config.verifyReceiptData) { - if (profilerInstance) profilerInstance.profileSectionStart('Verify_app_receipt_data') - if (nestedCountersInstance) nestedCountersInstance.countEvent('receipt', 'Verify_app_receipt_data') - const { success } = await verifyReceiptData(fullReceipt, false) - if (profilerInstance) profilerInstance.profileSectionEnd('Verify_app_receipt_data') - if (!success) { - Logger.mainLogger.error( - `The receipt validation failed from robustQuery nodes ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp}` - ) - continue - } - } - Logger.mainLogger.debug( - `Found valid full receipt in robustQuery ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp}` - ) - if (nestedCountersInstance) - nestedCountersInstance.countEvent('receipt', 'Found_valid_full_receipt_in_robustQuery') - return { success: true, newReceipt: fullReceipt } - } else { - Logger.mainLogger.error( - `The receipt validation failed from robustQuery nodes ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp}` - ) - Logger.mainLogger.error( - StringUtils.safeStringify(robustQueryReceipt), - StringUtils.safeStringify(fullReceipt) - ) - } - } + nestedCountersInstance.countEvent( + 'receipt', + 'VerifyGlobalTxReceipt_Invalid_receipt_signedReceipt_valid_signatures_count_less_than_requiredSignatures' + ) Logger.mainLogger.error( - `No valid full receipt found in robustQuery ${receipt.tx.txId} , ${receipt.cycle}, ${receipt.tx.timestamp}` + `VerifyGlobalTxReceipt : Invalid receipt signedReceipt valid signatures count is less than requiredSignatures ${txId}, ${goodSignatures.size}, ${requiredSignatures}` ) - if (nestedCountersInstance) - nestedCountersInstance.countEvent('receipt', 'No_valid_full_receipt_found_in_robustQuery') - return { success: false } + + return result } + return { success: true } } -// Offline global receipt verification -const verifyGlobalTxreceiptOffline = async ( +/** + * Verifies a non-global transaction receipt. + * + * @param receipt - The receipt to verify, which can be either a `Receipt.Receipt` or `Receipt.ArchiverReceipt`. + * @returns A promise that resolves to an object indicating the success status of the verification. + * + * The function performs the following steps: + * 1. Extracts the necessary variables from the signed receipt. + * 2. Determines the home partition index of the primary account (`executionShardKey`). + * 3. Calculates the voting group count and ensures it does not exceed the number of nodes. + * 4. Checks if the receipt has a majority of signatures based on the required percentage. + * 5. Calculates the vote hash and fetches the authorized signers. + * 6. Verifies if the number of valid signatures meets the required majority. + * 7. Uses a map to store valid signatures and ensures there are no duplicates. + * 8. Verifies each signature and counts valid ones until the required number is met. + * 9. Logs errors and counts events for various failure conditions. + */ +const verifyNonGlobalTxReceipt = async ( receipt: Receipt.Receipt | Receipt.ArchiverReceipt -): Promise<{ success: boolean; requiredSignatures?: number }> => { - const appliedReceipt = receipt.signedReceipt as P2PTypes.GlobalAccountsTypes.GlobalTxReceipt +): Promise<{ success: boolean }> => { const result = { success: false } - const { txId, timestamp } = receipt.tx - const { executionShardKey, cycle } = receipt + const { cycle } = receipt + const { txId: txid, timestamp, originalTxData } = receipt.tx const cycleShardData = shardValuesByCycle.get(cycle) + const { signaturePack, proposal, voteOffsets } = receipt.signedReceipt as Receipt.SignedReceipt + // verify tx id + const generatedTxId = generateTxId((originalTxData as any)?.tx) + if (generatedTxId != proposal.txid) { + if (nestedCountersInstance) nestedCountersInstance.countEvent('receipt', 'txId_mismatch') + Logger.mainLogger.error( + `VerifyNonGlobalTxReceipt : Transaction ID mismatch detected. Incoming txId: ${txid}, Generated txId: ${generatedTxId}` + ) + return result + } + + // shardKey extraction + const { executionShardKey } = proposal + // Determine the home partition index of the primary account (executionShardKey) const { homePartition } = ShardFunction.addressToPartition(cycleShardData.shardGlobals, executionShardKey) - const { signs } = appliedReceipt - // Refer to https://github.com/shardeum/shardus-core/blob/7d8877b7e1a5b18140f898a64b932182d8a35298/src/p2p/GlobalAccounts.ts#L397 + let votingGroupCount = cycleShardData.shardGlobals.nodesPerConsenusGroup - if (votingGroupCount > cycleShardData.nodes.length) { + if (votingGroupCount < cycleShardData.nodes.length) { if (nestedCountersInstance) - nestedCountersInstance.countEvent('receipt', 'votingGroupCount_greater_than_nodes_length') + nestedCountersInstance.countEvent('receipt', 'votingGroupCount_lesser_than_nodes_length') Logger.mainLogger.error( - 'votingGroupCount_greater_than_nodes_length', + 'verifyNonGlobalTxReceipt : votingGroupCount_lesser_than_nodes_length', votingGroupCount, cycleShardData.nodes.length ) votingGroupCount = cycleShardData.nodes.length } - let isReceiptMajority = (signs.length / votingGroupCount) * 100 >= config.requiredMajorityVotesPercentage + + let isReceiptMajority = signaturePack.length / votingGroupCount >= config.requiredMajorityVotesPercentage if (!isReceiptMajority) { Logger.mainLogger.error( - `Invalid receipt globalModification signs count is less than ${config.requiredMajorityVotesPercentage}% of the votingGroupCount, ${signs.length}, ${votingGroupCount}` + `VerifyNonGlobalTxReceipt : Invalid receipt globalModification signs count is less than ${config.requiredMajorityVotesPercentage}% of the votingGroupCount, ${signaturePack.length}, ${votingGroupCount}` ) if (nestedCountersInstance) nestedCountersInstance.countEvent( 'receipt', - `Invalid_receipt_globalModification_signs_count_less_than_${config.requiredMajorityVotesPercentage}%` + `VerifyNonGlobalTxReceipt_Invalid_receipt_globalModification_signs_count_less_than_${config.requiredMajorityVotesPercentage}%` ) return result } - const nodeMap = new Map() - // Fill the map with nodes keyed by their public keys - cycleShardData.nodes.forEach((node) => { - if (node.publicKey) { - nodeMap.set(node.publicKey, node) - } - }) - // Using a set to store the unique signers to avoid duplicates - const uniqueSigners = new Set() - for (const sign of signs) { - const { owner: nodePubKey } = sign - // Get the node id from the public key - const node = nodeMap.get(nodePubKey) - if (node == null) { - Logger.mainLogger.error( - `The node with public key ${nodePubKey} of the receipt ${txId} with ${timestamp} is not in the active nodesList of cycle ${cycle}` - ) - if (nestedCountersInstance) - nestedCountersInstance.countEvent('receipt', 'globalModification_sign_owner_not_in_active_nodesList') - continue - } - // Check if the node is in the execution group - if (!cycleShardData.parititionShardDataMap.get(homePartition).coveredBy[node.id]) { - Logger.mainLogger.error( - `The node with public key ${nodePubKey} of the receipt ${txId} with ${timestamp} is not in the execution group of the tx` - ) - if (nestedCountersInstance) - nestedCountersInstance.countEvent( - 'receipt', - 'globalModification_sign_node_not_in_execution_group_of_tx' - ) - continue - } - - uniqueSigners.add(nodePubKey) - } - isReceiptMajority = (uniqueSigners.size / votingGroupCount) * 100 >= config.requiredMajorityVotesPercentage + const voteHash = calculateVoteHash(proposal) + const acceptableSigners = fetchAuthorizedSigners( + signaturePack, + cycleShardData, + homePartition, + txid, + timestamp, + cycle + ) as Map + isReceiptMajority = acceptableSigners.size / votingGroupCount >= config.requiredMajorityVotesPercentage if (!isReceiptMajority) { Logger.mainLogger.error( - `Invalid receipt globalModification valid signs count is less than votingGroupCount ${uniqueSigners.size}, ${votingGroupCount}` + `VerifyNonGlobalTxReceipt : Invalid receipt valid signs count is less than votingGroupCount ${acceptableSigners.size}, ${votingGroupCount}` ) if (nestedCountersInstance) nestedCountersInstance.countEvent( 'receipt', - 'Invalid_receipt_globalModification_valid_signs_count_less_than_votingGroupCount' + 'VerifyNonGlobalTxReceipt_Invalid_receipt_valid_signs_count_less_than_votingGroupCount' ) return result } - const requiredSignatures = Math.floor(votingGroupCount * (config.requiredMajorityVotesPercentage / 100)) - return { success: true, requiredSignatures } -} - -const verifyNonGlobalTxReceiptOffline = async ( - receipt: Receipt.Receipt | Receipt.ArchiverReceipt, - executionGroupNodes: ConsensusNodeInfo[], - minConfirmations: number -): Promise<{ success: boolean; newReceipt?: Receipt.ArchiverReceipt | Receipt.Receipt }> => { - // Code for normal receipts verification - const normalReceipt = receipt.signedReceipt as Receipt.SignedReceipt - const validSigners = new Set() - - if (!normalReceipt.signaturePack || !Array.isArray(normalReceipt.signaturePack)) { - return { success: false } - } - - for (const signature of normalReceipt.signaturePack) { - if (!signature || !signature.owner) continue - const node = executionGroupNodes.find((n) => n.publicKey.toLowerCase() === signature.owner.toLowerCase()) - if (!node) continue + const requiredSignatures = Math.floor(votingGroupCount * config.requiredMajorityVotesPercentage) - try { - const voteHash = calculateVoteHash(normalReceipt.proposal) - const appliedVoteHash = { - txid: receipt.tx.txId, - voteHash - } - - if (Crypto.verify({ ...appliedVoteHash, sign: signature })) { - validSigners.add(signature.owner) - } - } catch (error) { - console.error('Error verifying signature:', error) - continue + // Using a map to store the good signatures to avoid duplicates + const goodSignatures = new Map() + for (const [nodePublicKey, signature] of acceptableSigners) { + if (Crypto.verify({ txid, voteHash, sign: signature.sign, voteTime: voteOffsets.at(signature.index) })) { + goodSignatures.set(nodePublicKey, signature) + // Break the loop if the required number of good signatures are found + if (goodSignatures.size >= requiredSignatures) break + } else { + if (nestedCountersInstance) + nestedCountersInstance.countEvent( + 'receipt', + 'VerifyNonGlobalTxReceipt_Found_invalid_signature_in_receipt_signedReceipt' + ) + Logger.mainLogger.error( + `VerifyNonGlobalTxReceipt : Found invalid signature in receipt signedReceipt ${txid}, ${nodePublicKey}, ${signature.index} | voteHash: ${voteHash} | voteTime: ${voteOffsets.at(signature.index)}` + ) } } + if (goodSignatures.size < requiredSignatures) { + if (nestedCountersInstance) + nestedCountersInstance.countEvent( + 'receipt', + 'VerifyNonGlobalTxReceipt_Invalid_receipt_signedReceipt_valid_signatures_count_less_than_requiredSignatures' + ) + Logger.mainLogger.error( + `VerifyNonGlobalTxReceipt : Invalid receipt signedReceipt valid signatures count is less than requiredSignatures ${txid}, ${goodSignatures.size}, ${requiredSignatures}` + ) - return { - success: validSigners.size >= minConfirmations + return result } + + return { success: true } } + /** * Validate type and field existence of the receipt data before processing it further * @param receipt * @returns boolean */ export const validateReceiptType = (receipt: Receipt.Receipt | Receipt.ArchiverReceipt): boolean => { + if ((receipt as any).executionShardKey) { + delete (receipt as any).executionShardKey + } + // Validate against the Receipt schema will come when archiver is syncing from another archiver - const errors_validation_receipt = verifyPayload(AJVSchemaEnum.Receipt, receipt); + const errors_validation_receipt = verifyPayload(AJVSchemaEnum.Receipt, receipt) if (!errors_validation_receipt) { - return true; // Valid Receipt + return true // Valid Receipt } // Validate against the ArchiverReceipt schema this will be used when receipt object is getting received from validator - const errors_validation_archiver_receipt = verifyPayload(AJVSchemaEnum.ArchiverReceipt, receipt); + const errors_validation_archiver_receipt = verifyPayload(AJVSchemaEnum.ArchiverReceipt, receipt) if (!errors_validation_archiver_receipt) { - return true; // Valid ArchiverReceipt + return true // Valid ArchiverReceipt } // If neither validation passes, log the errors and return false @@ -481,28 +399,42 @@ export const validateReceiptType = (receipt: Receipt.Receipt | Receipt.ArchiverR 'Invalid Receipt', { receiptType: errors_validation_receipt ? 'ArchiverReceipt' : 'Receipt', - receiptErrors: errors_validation_receipt || errors_validation_archiver_receipt, + receiptErrors: [errors_validation_receipt, errors_validation_archiver_receipt], }, 'where receipt was', StringUtils.safeStringify(receipt) - ); - - return false; // Invalid receipt -}; + ) + return false // Invalid receipt +} +/** + * Verifies the receipt data to ensure its integrity and validity. + * + * @param receipt - The receipt object which can be either a `Receipt.Receipt` or `Receipt.ArchiverReceipt`. + * @returns A promise that resolves to an object containing a `success` boolean indicating the verification result. + * + * The function performs the following checks: + * 1. Validates the transaction ID by comparing the generated transaction ID with the incoming transaction ID. + * 2. Logs the time taken between the receipt timestamp and the current time if verbose logging is enabled. + * 3. Checks if the receipt cycle is older than 2 cycles and logs an error if true. + * 4. Retrieves the cycle shard data and logs an error if not found. + * 5. If the receipt is a global modification receipt, validates it using AJV and returns the result. + * 6. If the receipt is a non-global transaction receipt, validates it and returns the result. + * + * The function logs appropriate errors and counts events using `nestedCountersInstance` for various failure scenarios. + */ export const verifyReceiptData = async ( - receipt: Receipt.Receipt | Receipt.ArchiverReceipt, - checkReceiptRobust = true + receipt: Receipt.Receipt | Receipt.ArchiverReceipt ): Promise<{ success: boolean - requiredSignatures?: number - newReceipt?: Receipt.ArchiverReceipt | Receipt.Receipt }> => { const result = { success: false } // Check the signed nodes are part of the execution group nodes of the tx - const { executionShardKey, cycle, globalModification } = receipt + const { cycle, globalModification } = receipt + const { txId, timestamp } = receipt.tx + if (config.VERBOSE) { const currentTimestamp = Date.now() // Console log the timetaken between the receipt timestamp and the current time ( both in ms and s) @@ -524,332 +456,60 @@ export const verifyReceiptData = async ( if (nestedCountersInstance) nestedCountersInstance.countEvent('receipt', 'Cycle_shard_data_not_found') return result } - // Determine the home partition index of the primary account (executionShardKey) - const { homePartition } = ShardFunction.addressToPartition(cycleShardData.shardGlobals, executionShardKey) - - let globalReceiptValidationErrors - try { - // Validate if receipt is a global modification receipt using AJV - globalReceiptValidationErrors = verifyPayload(AJVSchemaEnum.GlobalTxReceipt, receipt?.signedReceipt) - } catch (error) { - globalReceiptValidationErrors = true - if (nestedCountersInstance) - nestedCountersInstance.countEvent( - 'receipt', - `Failed to validate receipt schema txId: ${txId}, cycle: ${cycle}, timestamp: ${timestamp}, error: ${error}` - ) - Logger.mainLogger.error( - `Failed to validate receipt schema txId: ${txId}, cycle: ${cycle}, timestamp: ${timestamp}, error: ${error}` - ) - return result - } - // If the receipt is a global modification receipt, validate the receipt - if (!globalReceiptValidationErrors) { - return verifyGlobalTxreceiptOffline(receipt) - } - const { signaturePack } = receipt.signedReceipt as Receipt.SignedReceipt - if (config.newPOQReceipt === false) { - // Refer to https://github.com/shardeum/shardus-core/blob/f7000c36faa0cd1e0832aa1e5e3b1414d32dcf66/src/state-manager/TransactionConsensus.ts#L1406 - let votingGroupCount = cycleShardData.shardGlobals.nodesPerConsenusGroup - if (votingGroupCount > cycleShardData.nodes.length) { - votingGroupCount = cycleShardData.nodes.length - } - const requiredSignatures = - config.usePOQo === true - ? Math.ceil(votingGroupCount * config.requiredVotesPercentage) - : Math.round(votingGroupCount * config.requiredVotesPercentage) - if (signaturePack.length < requiredSignatures) { - Logger.mainLogger.error( - `Invalid receipt appliedReceipt signatures count is less than requiredSignatures, ${signaturePack.length}, ${requiredSignatures}` - ) - if (nestedCountersInstance) - nestedCountersInstance.countEvent( - 'receipt', - 'Invalid_receipt_appliedReceipt_signatures_count_less_than_requiredSignatures' - ) - return result - } - // Using a set to store the unique signatures to avoid duplicates - const uniqueSigners = new Set() - for (const signature of signaturePack) { - const { owner: nodePubKey } = signature - // Get the node id from the public key - const node = cycleShardData.nodes.find((node) => node.publicKey === nodePubKey) - if (node == null) { - Logger.mainLogger.error( - `The node with public key ${nodePubKey} of the receipt ${txId}} with ${timestamp} is not in the active nodesList of cycle ${cycle}` - ) + let globalReceiptValidationErrors + if (globalModification) { + try { + // Validate if receipt is a global modification receipt using AJV + globalReceiptValidationErrors = verifyPayload(AJVSchemaEnum.GlobalTxReceipt, receipt?.signedReceipt) + // If the receipt is a global modification receipt, validate the receipt + if (!globalReceiptValidationErrors) { + return verifyGlobalTxreceipt(receipt) + } else { + Logger.mainLogger.error('VerifyReceiptData : globalReceiptValidationErrors have occured') if (nestedCountersInstance) - nestedCountersInstance.countEvent( - 'receipt', - 'appliedReceipt_signature_owner_not_in_active_nodesList' - ) - continue + nestedCountersInstance.countEvent('receipt', 'globalReceiptValidationErrors') } - // Check if the node is in the execution group - if (!cycleShardData.parititionShardDataMap.get(homePartition).coveredBy[node.id]) { - Logger.mainLogger.error( - `The node with public key ${nodePubKey} of the receipt ${txId} with ${timestamp} is not in the execution group of the tx` - ) - if (nestedCountersInstance) - nestedCountersInstance.countEvent( - 'receipt', - 'appliedReceipt_signature_node_not_in_execution_group_of_tx' - ) - continue - } - uniqueSigners.add(nodePubKey) - } - if (uniqueSigners.size < requiredSignatures) { - Logger.mainLogger.error( - `Invalid receipt appliedReceipt valid signatures count is less than requiredSignatures ${uniqueSigners.size}, ${requiredSignatures}` - ) + return result + } catch (error) { + globalReceiptValidationErrors = true if (nestedCountersInstance) nestedCountersInstance.countEvent( 'receipt', - 'Invalid_receipt_appliedReceipt_valid_signatures_count_less_than_requiredSignatures' + `Failed to validate receipt schema txId: ${txId}, cycle: ${cycle}, timestamp: ${timestamp}, error: ${error}` ) - return result - } - return { success: true, requiredSignatures } - } - - // const { confirmOrChallenge } = appliedReceipt as Receipt.AppliedReceipt2 - // // Check if the appliedVote node is in the execution group - // if (!cycleShardData.nodeShardDataMap.has(appliedVote.node_id)) { - // Logger.mainLogger.error('Invalid receipt appliedReceipt appliedVote node is not in the active nodesList') - // if (nestedCountersInstance) - // nestedCountersInstance.countEvent('receipt', 'Invalid_receipt_appliedVote_node_not_in_active_nodesList') - // return result - // } - // if (appliedVote.sign.owner !== cycleShardData.nodeShardDataMap.get(appliedVote.node_id).node.publicKey) { - // Logger.mainLogger.error( - // 'Invalid receipt appliedReceipt appliedVote node signature owner and node public key does not match' - // ) - // if (nestedCountersInstance) - // nestedCountersInstance.countEvent( - // 'receipt', - // 'Invalid_receipt_appliedVote_node_signature_owner_and_node_public_key_does_not_match' - // ) - // return result - // } - // if (!cycleShardData.parititionShardDataMap.get(homePartition).coveredBy[appliedVote.node_id]) { - // Logger.mainLogger.error( - // 'Invalid receipt appliedReceipt appliedVote node is not in the execution group of the tx' - // ) - // if (nestedCountersInstance) - // nestedCountersInstance.countEvent( - // 'receipt', - // 'Invalid_receipt_appliedVote_node_not_in_execution_group_of_tx' - // ) - // return result - // } - // if (!Crypto.verify(appliedVote)) { - // Logger.mainLogger.error('Invalid receipt appliedReceipt appliedVote signature verification failed') - // if (nestedCountersInstance) - // nestedCountersInstance.countEvent( - // 'receipt', - // 'Invalid_receipt_appliedVote_signature_verification_failed' - // ) - // return result - // } - - // // Check if the confirmOrChallenge node is in the execution group - // if (!cycleShardData.nodeShardDataMap.has(confirmOrChallenge.nodeId)) { - // Logger.mainLogger.error( - // 'Invalid receipt appliedReceipt confirmOrChallenge node is not in the active nodesList' - // ) - // if (nestedCountersInstance) - // nestedCountersInstance.countEvent( - // 'receipt', - // 'Invalid_receipt_confirmOrChallenge_node_not_in_active_nodesList' - // ) - // return result - // } - // if ( - // confirmOrChallenge.sign.owner !== - // cycleShardData.nodeShardDataMap.get(confirmOrChallenge.nodeId).node.publicKey - // ) { - // Logger.mainLogger.error( - // 'Invalid receipt appliedReceipt confirmOrChallenge node signature owner and node public key does not match' - // ) - // if (nestedCountersInstance) - // nestedCountersInstance.countEvent( - // 'receipt', - // 'Invalid_receipt_confirmOrChallenge_signature_owner_and_node_public_key_does_not_match' - // ) - // return result - // } - // if (!cycleShardData.parititionShardDataMap.get(homePartition).coveredBy[confirmOrChallenge.nodeId]) { - // Logger.mainLogger.error( - // 'Invalid receipt appliedReceipt confirmOrChallenge node is not in the execution group of the tx' - // ) - // if (nestedCountersInstance) - // nestedCountersInstance.countEvent( - // 'receipt', - // 'Invalid_receipt_confirmOrChallenge_node_not_in_execution_group_of_tx' - // ) - // return result - // } - // if (!Crypto.verify(confirmOrChallenge)) { - // Logger.mainLogger.error('Invalid receipt appliedReceipt confirmOrChallenge signature verification failed') - // if (nestedCountersInstance) - // nestedCountersInstance.countEvent( - // 'receipt', - // 'Invalid_receipt_confirmOrChallenge_signature_verification_failed' - // ) - // return result - // } - - if (!checkReceiptRobust) return { success: true } - // List the execution group nodes of the tx, Use them to robustQuery to verify the receipt - const executionGroupNodes = Object.values( - cycleShardData.parititionShardDataMap.get(homePartition).coveredBy - ) as unknown as ConsensusNodeInfo[] - if (config.VERBOSE) Logger.mainLogger.debug('executionGroupNodes', receipt.tx.txId, executionGroupNodes) - const minConfirmations = - nodesPerConsensusGroup > config.RECEIPT_CONFIRMATIONS - ? config.RECEIPT_CONFIRMATIONS - : Math.ceil(config.RECEIPT_CONFIRMATIONS / 2) // 3 out of 5 nodes - const { success, newReceipt } = await verifyReceiptMajority(receipt, executionGroupNodes, minConfirmations) - if (!success) { - Logger.mainLogger.error('Invalid receipt: Robust check failed') - if (nestedCountersInstance) - nestedCountersInstance.countEvent('receipt', 'Invalid_receipt_robust_check_failed') - return result - } - if (newReceipt) return { success: true, requiredSignatures: 0, newReceipt } - return { success: true } -} - -const verifyAppliedReceiptSignatures = ( - receipt: Receipt.ArchiverReceipt | Receipt.Receipt, - requiredSignatures: number, - failedReasons = [], - nestedCounterMessages = [] -): { success: boolean } => { - const result = { success: false, failedReasons, nestedCounterMessages } - const { globalModification, cycle, executionShardKey } = receipt - const { txId: txid, timestamp } = receipt.tx - let globalReceiptValidationErrors // This is used to store the validation errors of the globalTxReceipt - - try { - globalReceiptValidationErrors = verifyPayload(AJVSchemaEnum.GlobalTxReceipt, receipt?.signedReceipt) - } catch (error) { - globalReceiptValidationErrors = true - failedReasons.push( - `Invalid Global Tx Receipt error: ${error}. txId ${receipt.tx.txId} , cycle ${receipt.cycle} , timestamp ${receipt.tx.timestamp}` - ) - nestedCounterMessages.push( - `Invalid Global Tx Receipt error: ${error}. txId ${receipt.tx.txId} , cycle ${receipt.cycle} , timestamp ${receipt.tx.timestamp}` - ) - return result - } - // If the globalReceiptValidationErrors is null, then the receipt is a globalModification receipt - if (!globalReceiptValidationErrors) { - const appliedReceipt = receipt.signedReceipt as P2PTypes.GlobalAccountsTypes.GlobalTxReceipt - // Refer to https://github.com/shardeum/shardus-core/blob/7d8877b7e1a5b18140f898a64b932182d8a35298/src/p2p/GlobalAccounts.ts#L294 - - const { signs, tx } = appliedReceipt - const cycleShardData = shardValuesByCycle.get(cycle) - const { homePartition } = ShardFunction.addressToPartition(cycleShardData.shardGlobals, executionShardKey) - const nodeMap = new Map() - // Fill the map with nodes keyed by their public keys - cycleShardData.nodes.forEach((node) => { - if (node.publicKey) { - nodeMap.set(node.publicKey, node) - } - }) - const acceptableSigners = new Set() - for (const sign of signs) { - const { owner: nodePubKey } = sign - // Get the node id from the public key - const node = nodeMap.get(nodePubKey) - if (node == null) { - Logger.mainLogger.error( - `The node with public key ${nodePubKey} of the receipt ${txid} with ${timestamp} is not in the active nodesList of cycle ${cycle}` - ) - if (nestedCountersInstance) - nestedCountersInstance.countEvent( - 'receipt', - 'globalModification_sign_owner_not_in_active_nodesList' - ) - continue - } - // Check if the node is in the execution group - if (!cycleShardData.parititionShardDataMap.get(homePartition).coveredBy[node.id]) { - Logger.mainLogger.error( - `The node with public key ${nodePubKey} of the receipt ${txid} with ${timestamp} is not in the execution group of the tx` - ) - if (nestedCountersInstance) - nestedCountersInstance.countEvent( - 'receipt', - 'globalModification_sign_node_not_in_execution_group_of_tx' - ) - continue - } - acceptableSigners.add(sign) - } - // Using a map to store the good signatures to avoid duplicates - const goodSignatures = new Map() - for (const sign of acceptableSigners) { - if (Crypto.verify({ ...tx, sign: sign })) { - goodSignatures.set(sign.owner, sign) - // Break the loop if the required number of good signatures are found - if (goodSignatures.size >= requiredSignatures) break - } - } - if (goodSignatures.size < requiredSignatures) { - failedReasons.push( - `Invalid receipt globalModification valid signs count is less than requiredSignatures ${txid}, ${goodSignatures.size}, ${requiredSignatures}` - ) - nestedCounterMessages.push( - 'Invalid_receipt_globalModification_valid_signs_count_less_than_requiredSignatures' + Logger.mainLogger.error( + `Failed to validate receipt schema txId: ${txId}, cycle: ${cycle}, timestamp: ${timestamp}, error: ${error}` ) return result } - return { success: true } - } - const { proposal, signaturePack, voteOffsets } = receipt.signedReceipt as Receipt.SignedReceipt - // Refer to https://github.com/shardeum/shardus-core/blob/50b6d00f53a35996cd69210ea817bee068a893d6/src/state-manager/TransactionConsensus.ts#L2799 - const voteHash = calculateVoteHash(proposal, failedReasons, nestedCounterMessages) - // Refer to https://github.com/shardeum/shardus-core/blob/50b6d00f53a35996cd69210ea817bee068a893d6/src/state-manager/TransactionConsensus.ts#L2663 - const appliedVoteHash = { - txid, - voteHash, - } - // Using a map to store the good signatures to avoid duplicates - const goodSignatures = new Map() - for (const [index, signature] of signaturePack.entries()) { - if (Crypto.verify({ ...appliedVoteHash, sign: signature, voteTime: voteOffsets.at(index) })) { - goodSignatures.set(signature.owner, signature) - // Break the loop if the required number of good signatures are found - if (goodSignatures.size >= requiredSignatures) break - } else { - failedReasons.push( - `Found invalid signature in receipt signedReceipt ${txid}, ${signature.owner}, ${index}` - ) - nestedCounterMessages.push('Found_invalid_signature_in_receipt_signedReceipt') - } - } - if (goodSignatures.size < requiredSignatures) { - failedReasons.push( - `Invalid receipt signedReceipt valid signatures count is less than requiredSignatures ${txid}, ${goodSignatures.size}, ${requiredSignatures}` - ) - nestedCounterMessages.push( - 'Invalid_receipt_signedReceipt_valid_signatures_count_less_than_requiredSignatures' - ) - return result } - return { success: true } + + // If the receipt is a non global transaction receipt, validate the receipt + return verifyNonGlobalTxReceipt(receipt) } -const calculateVoteHash = ( - vote: Receipt.AppliedVote | Receipt.Proposal, - failedReasons = [], - nestedCounterMessages = [] -): string => { +/** + * Calculates a hash for a given vote, which can be either an `AppliedVote` or a `Proposal`. + * The hash calculation varies based on the type of vote and the configuration settings. + * + * @param vote - The vote object, which can be of type `Receipt.AppliedVote` or `Receipt.Proposal`. + * @returns The calculated hash as a string. If an error occurs during the calculation, an empty string is returned. + * + * The function performs the following steps: + * 1. If `config.usePOQo` is true and the vote is a `Proposal`: + * - Extracts the `applied` and `cant_preApply` properties from the proposal. + * - Calculates a hash of the account IDs, before state hashes, and after state hashes. + * - Combines the apply status hash, accounts hash, and app receipt data hash to generate the proposal hash. + * 2. If `config.usePOQo` is true and the vote is an `AppliedVote`: + * - Extracts the `transaction_result` and `cant_apply` properties from the applied vote. + * - Extracts the account state hashes and app data hash. + * - Combines the hashes of the applied hash, state hash, and app data hash to generate the applied vote hash. + * 3. If neither condition is met, it hashes the vote object with an additional `node_id` property set to an empty string. + * + * If an error occurs during the hash calculation, it logs the error and increments a nested counter event. + */ +const calculateVoteHash = (vote: Receipt.AppliedVote | Receipt.Proposal): string => { try { if (config.usePOQo === true && (vote as Receipt.Proposal).applied !== undefined) { const proposal = vote as Receipt.Proposal @@ -863,7 +523,7 @@ const calculateVoteHash = ( Crypto.hashObj(proposal.afterStateHashes) ) const proposalHash = Crypto.hash( - Crypto.hashObj(applyStatus) + accountsHash + proposal.appReceiptDataHash + Crypto.hashObj(applyStatus) + accountsHash + proposal.appReceiptDataHash + proposal.executionShardKey ) return proposalHash } else if (config.usePOQo === true) { @@ -889,15 +549,29 @@ const calculateVoteHash = ( } return Crypto.hashObj({ ...vote, node_id: '' }) } catch { - failedReasons.push('Error in calculateVoteHash', vote) - nestedCounterMessages.push('Error_in_calculateVoteHash') + Logger.mainLogger.error('Error in calculateVoteHash', vote) + if (nestedCountersInstance) nestedCountersInstance.countEvent('receipt', 'Error_in_calculateVoteHash') return '' } } +/** + * Verifies the given archiver receipt. + * + * @param {Receipt.ArchiverReceipt | Receipt.Receipt} receipt - The receipt to verify. It can be either an ArchiverReceipt or a generic Receipt. + * @returns {Promise} A promise that resolves to a ReceiptVerificationResult object indicating the success or failure of the verification process. + * + * The function performs the following verifications: + * 1. If `config.verifyAppReceiptData` is enabled, it verifies the application receipt data. + * - If the verification fails, it returns a failure result with the reasons. + * - If the receipt is valid but does not need to be saved, it returns a failure result with the reasons. + * 2. If `config.verifyAccountData` is enabled, it verifies the account data. + * - If the verification fails, it returns a failure result with the reasons. + * + * The function returns a success result if all enabled verifications pass. + */ export const verifyArchiverReceipt = async ( - receipt: Receipt.ArchiverReceipt | Receipt.Receipt, - requiredSignatures: number + receipt: Receipt.ArchiverReceipt | Receipt.Receipt ): Promise => { const { txId, timestamp } = receipt.tx const existingReceipt = await Receipt.queryReceiptByReceiptId(txId) @@ -939,25 +613,32 @@ export const verifyArchiverReceipt = async ( return { success: false, failedReasons, nestedCounterMessages } } } - if (config.verifyReceiptSignaturesSeparately) { - // if (profilerInstance) profilerInstance.profileSectionStart('Verify_receipt_signatures_data') - // if (nestedCountersInstance) nestedCountersInstance.countEvent('receipt', 'Verify_receipt_signatures_data') - const { success } = verifyAppliedReceiptSignatures( - receipt, - requiredSignatures, - failedReasons, - nestedCounterMessages - ) - // if (profilerInstance) profilerInstance.profileSectionEnd('Verify_receipt_signatures_data') - if (!success) { - failedReasons.push(`Invalid receipt: Verification failed ${txId}, ${receipt.cycle}, ${timestamp}`) - nestedCounterMessages.push('Invalid_receipt_verification_failed') - return { success: false, failedReasons, nestedCounterMessages } - } - } return { success: true, failedReasons, nestedCounterMessages } } +/** + * Stores receipt data in the database. + * + * @param {Receipt.Receipt[] | Receipt.ArchiverReceipt[]} receipts - Array of receipts to be stored. + * @param {string} [senderInfo=''] - Optional sender information. + * @param {boolean} [verifyData=false] - Flag to indicate if the receipt data should be verified. + * @param {boolean} [saveOnlyGossipData=false] - Flag to indicate if only gossip data should be saved. + * @returns {Promise} - A promise that resolves when the operation is complete. + * + * @remarks + * This function processes and stores receipt data. It performs validation and verification of receipts, + * updates account and transaction data, and handles bulk insertion of data into the database. + * + * The function skips processing if the receipts array is empty or invalid. It also skips receipts that + * have already been processed or are in the validation map. + * + * If `verifyData` is true, the function verifies the receipt data and handles any verification failures. + * + * The function processes each receipt, updating account and transaction data, and performs bulk insertion + * when the number of receipts, accounts, transactions, or processed transactions reaches a specified bucket size. + * + * If the archiver is not active and the processed receipts map exceeds 2000 entries, the map is cleared. + */ export const storeReceiptData = async ( receipts: Receipt.Receipt[] | Receipt.ArchiverReceipt[], senderInfo = '', @@ -968,12 +649,22 @@ export const storeReceiptData = async ( const bucketSize = 1000 let combineReceipts = [] let combineAccounts = [] + let combineOriginalTxsData = [] let combineTransactions = [] let combineProcessedTxs = [] let txDataList: TxData[] = [] + let originalTxDataList: TxData[] = [] // this is kind of duplicate of 'txDataList' but have created to avoid confusion if (saveOnlyGossipData) return for (let receipt of receipts) { - const txId = receipt?.tx?.txId + let txId: string + if (receipt.globalModification) { + const appliedReceipt = receipt.signedReceipt as P2PTypes.GlobalAccountsTypes.GlobalTxReceipt + txId = appliedReceipt.tx.txId + } else { + const { proposal } = receipt.signedReceipt as Receipt.SignedReceipt + txId = proposal.txid + } + const timestamp = receipt?.tx?.timestamp if (!txId || !timestamp) continue if ( @@ -1023,7 +714,7 @@ export const storeReceiptData = async ( // } if (config.verifyReceiptData) { - const { success, requiredSignatures, newReceipt } = await verifyReceiptData(receipt) + const { success } = await verifyReceiptData(receipt) if (!success) { Logger.mainLogger.error('Invalid receipt: Verification failed', txId, receipt.cycle, timestamp) receiptsInValidationMap.delete(txId) @@ -1032,7 +723,6 @@ export const storeReceiptData = async ( if (profilerInstance) profilerInstance.profileSectionEnd('Validate_receipt') continue } - if (newReceipt) receipt = newReceipt if (profilerInstance) profilerInstance.profileSectionStart('Verify_archiver_receipt') if (nestedCountersInstance) nestedCountersInstance.countEvent('receipt', 'Verify_archiver_receipt') @@ -1041,7 +731,7 @@ export const storeReceiptData = async ( // const result = await offloadReceipt(txId, timestamp, requiredSignatures, receipt) let result try { - result = await verifyArchiverReceipt(receipt, requiredSignatures) + result = await verifyArchiverReceipt(receipt) } catch (error) { receiptsInValidationMap.delete(txId) if (nestedCountersInstance) @@ -1082,26 +772,37 @@ export const storeReceiptData = async ( const medianOffset = sortedVoteOffsets[Math.floor(sortedVoteOffsets.length / 2)] ?? 0 const applyTimestamp = tx.timestamp + medianOffset * 1000 if (config.VERBOSE) console.log('RECEIPT', 'Save', txId, timestamp, senderInfo) - processedReceiptsMap.set(tx.txId, tx.timestamp) - receiptsInValidationMap.delete(tx.txId) - if (missingReceiptsMap.has(tx.txId)) missingReceiptsMap.delete(tx.txId) + processedReceiptsMap.set(txId, tx.timestamp) + receiptsInValidationMap.delete(txId) + if (missingReceiptsMap.has(txId)) missingReceiptsMap.delete(txId) receipt.beforeStates = globalModification || config.storeReceiptBeforeStates ? receipt.beforeStates : [] // Store beforeStates for globalModification tx, or if config.storeReceiptBeforeStates is true + let executionShardKey: string + if (globalModification) { + const appliedReceipt = receipt.signedReceipt as P2PTypes.GlobalAccountsTypes.GlobalTxReceipt + executionShardKey = appliedReceipt.tx.source + } else { + const appliedReceipt = receipt.signedReceipt as Receipt.SignedReceipt + executionShardKey = appliedReceipt.proposal.executionShardKey + } + combineReceipts.push({ ...receipt, - receiptId: tx.txId, + receiptId: txId, timestamp: tx.timestamp, applyTimestamp, + executionShardKey, }) if (config.dataLogWrite && ReceiptLogWriter) ReceiptLogWriter.writeToLog( `${StringUtils.safeStringify({ ...receipt, - receiptId: tx.txId, + receiptId: txId, timestamp: tx.timestamp, applyTimestamp, })}\n` ) txDataList.push({ txId, timestamp }) + originalTxDataList.push({ txId, timestamp }) // If the receipt is a challenge, then skip updating its accounts data or transaction data // if ( // config.newPOQReceipt === true && @@ -1162,9 +863,20 @@ export const storeReceiptData = async ( // combineAccounts.push(accObj) // } // } + + const originalTxData: OriginalTxsData.OriginalTxData = { + txId: txId, + timestamp: tx.timestamp, + cycle: cycle, + originalTxData: tx.originalTxData, + } + if (config.dataLogWrite && OriginalTxDataLogWriter) { + OriginalTxDataLogWriter.writeToLog(`${StringUtils.safeStringify(originalTxData)}\n`) + } + const txObj: Transaction.Transaction = { - txId: tx.txId, - appReceiptId: appReceiptData ? appReceiptData.accountId : tx.txId, // Set txId if appReceiptData lacks appReceiptId + txId: txId, + appReceiptId: appReceiptData ? appReceiptData.accountId : txId, // Set txId if appReceiptData lacks appReceiptId timestamp: tx.timestamp, cycleNumber: cycle, data: appReceiptData ? appReceiptData.data : {}, @@ -1172,13 +884,14 @@ export const storeReceiptData = async ( } const processedTx: ProcessedTransaction.ProcessedTransaction = { - txId: tx.txId, + txId: txId, cycle: cycle, txTimestamp: tx.timestamp, applyTimestamp, } // await Transaction.insertTransaction(txObj) + combineOriginalTxsData.push(originalTxData) combineTransactions.push(txObj) combineProcessedTxs.push(processedTx) // Receipts size can be big, better to save per 100 @@ -1188,13 +901,19 @@ export const storeReceiptData = async ( combineReceipts = [] txDataList = [] } + + if (combineOriginalTxsData.length >= bucketSize) { + await OriginalTxsData.bulkInsertOriginalTxsData(combineOriginalTxsData) + combineOriginalTxsData = [] + originalTxDataList = [] + } + if (combineAccounts.length >= bucketSize) { await Account.bulkInsertAccounts(combineAccounts) combineAccounts = [] } if (combineTransactions.length >= bucketSize) { await Transaction.bulkInsertTransactions(combineTransactions) - combineTransactions = [] } if (combineProcessedTxs.length >= bucketSize) { @@ -1207,6 +926,11 @@ export const storeReceiptData = async ( await Receipt.bulkInsertReceipts(combineReceipts) if (State.isActive) sendDataToAdjacentArchivers(DataType.RECEIPT, txDataList) } + + if (combineOriginalTxsData.length > 0) { + await OriginalTxsData.bulkInsertOriginalTxsData(combineOriginalTxsData) + } + if (combineAccounts.length > 0) await Account.bulkInsertAccounts(combineAccounts) if (combineTransactions.length > 0) await Transaction.bulkInsertTransactions(combineTransactions) if (combineProcessedTxs.length > 0) await ProcessedTransaction.bulkInsertProcessedTxs(combineProcessedTxs) @@ -1431,7 +1155,7 @@ export const storeOriginalTxData = async ( if (config.VERBOSE) console.log('ORIGINAL_TX_DATA', 'Save', txId, timestamp, senderInfo) processedOriginalTxsMap.set(txId, timestamp) originalTxsInValidationMap.delete(txId) - if (missingOriginalTxsMap.has(txId)) missingOriginalTxsMap.delete(txId) + // if (missingOriginalTxsMap.has(txId)) missingOriginalTxsMap.delete(txId) if (config.dataLogWrite && OriginalTxDataLogWriter) OriginalTxDataLogWriter.writeToLog(`${StringUtils.safeStringify(originalTxData)}\n`) @@ -1546,43 +1270,45 @@ export const processGossipData = (gossipdata: GossipData): void => { } } } - if (dataType === DataType.ORIGINAL_TX_DATA) { - for (const { txId, timestamp } of data as TxData[]) { - if ( - (processedOriginalTxsMap.has(txId) && processedOriginalTxsMap.get(txId) === timestamp) || - (originalTxsInValidationMap.has(txId) && originalTxsInValidationMap.get(txId) === timestamp) || - (collectingMissingOriginalTxsMap.has(txId) && collectingMissingOriginalTxsMap.get(txId) === timestamp) - ) { - // console.log('GOSSIP', 'ORIGINAL_TX_DATA', 'SKIP', txId, 'sender', sign.owner) - continue - } else { - if (missingOriginalTxsMap.has(txId)) { - if ( - missingOriginalTxsMap.get(txId).txTimestamp === timestamp && - !missingOriginalTxsMap.get(txId).senders.some((sender) => sender === sign.owner) - ) - missingOriginalTxsMap.get(txId).senders.push(sign.owner) - else { - // Not expected to happen, but log error if it happens <-- could be malicious act of the sender - if (missingOriginalTxsMap.get(txId).txTimestamp !== timestamp) - Logger.mainLogger.error( - `Received gossip for originalTxData ${txId} with different timestamp ${timestamp} from archiver ${sign.owner}` - ) - if (missingOriginalTxsMap.get(txId).senders.some((sender) => sender === sign.owner)) - Logger.mainLogger.error( - `Received gossip for originalTxData ${txId} from the same sender ${sign.owner}` - ) - } - } else - missingOriginalTxsMap.set(txId, { - txTimestamp: timestamp, - receivedTimestamp, - senders: [sign.owner], - }) - // console.log('GOSSIP', 'ORIGINAL_TX_DATA', 'MISS', txId, 'sender', sign.owner) - } - } - } + + // if (dataType === DataType.ORIGINAL_TX_DATA) { + // for (const { txId, timestamp } of data as TxData[]) { + // if ( + // (processedOriginalTxsMap.has(txId) && processedOriginalTxsMap.get(txId) === timestamp) || + // (originalTxsInValidationMap.has(txId) && originalTxsInValidationMap.get(txId) === timestamp) || + // (collectingMissingOriginalTxsMap.has(txId) && collectingMissingOriginalTxsMap.get(txId) === timestamp) + // ) { + // // console.log('GOSSIP', 'ORIGINAL_TX_DATA', 'SKIP', txId, 'sender', sign.owner) + // continue + // } else { + // if (missingOriginalTxsMap.has(txId)) { + // if ( + // missingOriginalTxsMap.get(txId).txTimestamp === timestamp && + // !missingOriginalTxsMap.get(txId).senders.some((sender) => sender === sign.owner) + // ) + // missingOriginalTxsMap.get(txId).senders.push(sign.owner) + // else { + // // Not expected to happen, but log error if it happens <-- could be malicious act of the sender + // if (missingOriginalTxsMap.get(txId).txTimestamp !== timestamp) + // Logger.mainLogger.error( + // `Received gossip for originalTxData ${txId} with different timestamp ${timestamp} from archiver ${sign.owner}` + // ) + // if (missingOriginalTxsMap.get(txId).senders.some((sender) => sender === sign.owner)) + // Logger.mainLogger.error( + // `Received gossip for originalTxData ${txId} from the same sender ${sign.owner}` + // ) + // } + // } else + // missingOriginalTxsMap.set(txId, { + // txTimestamp: timestamp, + // receivedTimestamp, + // senders: [sign.owner], + // }) + // // console.log('GOSSIP', 'ORIGINAL_TX_DATA', 'MISS', txId, 'sender', sign.owner) + // } + // } + // } + if (dataType === DataType.CYCLE) { collectCycleData( data as P2PTypes.CycleCreatorTypes.CycleData[], @@ -1609,22 +1335,23 @@ export const collectMissingTxDataFromArchivers = async (): Promise => { } cloneMissingReceiptsMap.clear() } - if (missingOriginalTxsMap.size > 0) { - const cloneMissingOriginalTxsMap: Map> = new Map() - for (const [txId, { txTimestamp, receivedTimestamp, senders }] of missingOriginalTxsMap) { - if (currentTimestamp - receivedTimestamp > config.waitingTimeForMissingTxData) { - cloneMissingOriginalTxsMap.set(txId, { txTimestamp, senders }) - collectingMissingOriginalTxsMap.set(txId, txTimestamp) - missingOriginalTxsMap.delete(txId) - } - } - if (cloneMissingOriginalTxsMap.size > 0) - Logger.mainLogger.debug('Collecting missing originalTxsData', cloneMissingOriginalTxsMap.size) - for (const [txId, { txTimestamp, senders }] of cloneMissingOriginalTxsMap) { - collectMissingOriginalTxsData(senders, txId, txTimestamp) - } - cloneMissingOriginalTxsMap.clear() - } + + // if (missingOriginalTxsMap.size > 0) { + // const cloneMissingOriginalTxsMap: Map> = new Map() + // for (const [txId, { txTimestamp, receivedTimestamp, senders }] of missingOriginalTxsMap) { + // if (currentTimestamp - receivedTimestamp > config.waitingTimeForMissingTxData) { + // cloneMissingOriginalTxsMap.set(txId, { txTimestamp, senders }) + // collectingMissingOriginalTxsMap.set(txId, txTimestamp) + // missingOriginalTxsMap.delete(txId) + // } + // } + // if (cloneMissingOriginalTxsMap.size > 0) + // Logger.mainLogger.debug('Collecting missing originalTxsData', cloneMissingOriginalTxsMap.size) + // for (const [txId, { txTimestamp, senders }] of cloneMissingOriginalTxsMap) { + // collectMissingOriginalTxsData(senders, txId, txTimestamp) + // } + // cloneMissingOriginalTxsMap.clear() + // } } export const collectMissingReceipts = async ( @@ -1720,7 +1447,7 @@ const collectMissingOriginalTxsData = async ( `Failed to collect originalTxData for txId ${txId} with timestamp ${txTimestamp} from archivers ${senders}` ) } - collectingMissingOriginalTxsMap.delete(txId) + // collectingMissingOriginalTxsMap.delete(txId) if (profilerInstance) profilerInstance.profileSectionEnd('Collect_missing_originalTxData') } diff --git a/src/Data/Data.ts b/src/Data/Data.ts index 4bed7587..34ffa444 100644 --- a/src/Data/Data.ts +++ b/src/Data/Data.ts @@ -261,11 +261,12 @@ export function initSocketClient(node: NodeList.ConsensusNodeInfo): void { sender.nodeInfo.port, newData.responses.ORIGINAL_TX_DATA.length ) - storeOriginalTxData( - newData.responses.ORIGINAL_TX_DATA, - sender.nodeInfo.ip + ':' + sender.nodeInfo.port, - config.saveOnlyGossipData - ) + // gracefully ignoring since it is now coupled with the receipt flow + // storeOriginalTxData( + // newData.responses.ORIGINAL_TX_DATA, + // sender.nodeInfo.ip + ':' + sender.nodeInfo.port, + // config.saveOnlyGossipData + // ) } if (newData.responses && newData.responses.RECEIPT) { if (config.VERBOSE) @@ -1823,56 +1824,65 @@ export const syncCyclesAndTxsData = async ( if (!response || response.totalCycles < 0 || response.totalReceipts < 0) { return } - const { totalCycles, totalReceipts, totalOriginalTxs } = response + const { totalCycles, totalReceipts } = response Logger.mainLogger.debug('totalCycles', totalCycles, 'lastStoredCycleCount', lastStoredCycleCount) Logger.mainLogger.debug('totalReceipts', totalReceipts, 'lastStoredReceiptCount', lastStoredReceiptCount) - Logger.mainLogger.debug( - 'totalOriginalTxs', - totalOriginalTxs, - 'lastStoredOriginalTxCount', - lastStoredOriginalTxCount - ) + // Logger.mainLogger.debug( + // 'totalOriginalTxs', + // totalOriginalTxs, + // 'lastStoredOriginalTxCount', + // lastStoredOriginalTxCount + // ) if ( totalCycles === lastStoredCycleCount && - totalReceipts === lastStoredReceiptCount && - totalOriginalTxs === lastStoredOriginalTxCount + totalReceipts === lastStoredReceiptCount + // && totalOriginalTxs === lastStoredOriginalTxCount ) { Logger.mainLogger.debug('The archiver has synced the lastest cycle ,receipts and originalTxs data!') return } let totalReceiptsToSync = totalReceipts - let totalOriginalTxsToSync = totalOriginalTxs + // let totalOriginalTxsToSync = totalOriginalTxs let totalCyclesToSync = totalCycles let completeForReceipt = false - let completeForOriginalTx = false + // let completeForOriginalTx = false let completeForCycle = false let startReceipt = lastStoredReceiptCount - let startOriginalTx = lastStoredOriginalTxCount + // let startOriginalTx = lastStoredOriginalTxCount let startCycle = lastStoredCycleCount let endReceipt = startReceipt + MAX_RECEIPTS_PER_REQUEST - let endOriginalTx = startOriginalTx + MAX_ORIGINAL_TXS_PER_REQUEST + // let endOriginalTx = startOriginalTx + MAX_ORIGINAL_TXS_PER_REQUEST let endCycle = startCycle + MAX_CYCLES_PER_REQUEST if (totalCycles === lastStoredCycleCount) completeForCycle = true if (totalReceipts === lastStoredReceiptCount) completeForReceipt = true - if (totalOriginalTxs === lastStoredOriginalTxCount) completeForOriginalTx = true + // if (totalOriginalTxs === lastStoredOriginalTxCount) completeForOriginalTx = true - while (!completeForReceipt || !completeForCycle || !completeForOriginalTx) { + while ( + !completeForReceipt || + !completeForCycle + // || !completeForOriginalTx + ) { if ( endReceipt >= totalReceiptsToSync || - endCycle >= totalCyclesToSync || - endOriginalTx >= totalOriginalTxsToSync + endCycle >= totalCyclesToSync + // || endOriginalTx >= totalOriginalTxsToSync ) { response = await getTotalDataFromArchivers() - if (response && response.totalReceipts && response.totalCycles && response.totalOriginalTxs) { + if ( + response && + response.totalReceipts && + response.totalCycles + // && response.totalOriginalTxs + ) { if (response.totalReceipts !== totalReceiptsToSync) { completeForReceipt = false totalReceiptsToSync = response.totalReceipts } - if (response.totalOriginalTxs !== totalOriginalTxsToSync) { - completeForOriginalTx = false - totalOriginalTxsToSync = response.totalOriginalTxs - } + // if (response.totalOriginalTxs !== totalOriginalTxsToSync) { + // completeForOriginalTx = false + // totalOriginalTxsToSync = response.totalOriginalTxs + // } if (response.totalCycles !== totalCyclesToSync) { completeForCycle = false totalCyclesToSync = response.totalCycles @@ -1880,17 +1890,17 @@ export const syncCyclesAndTxsData = async ( if (totalReceiptsToSync < startReceipt) { completeForReceipt = true } - if (totalOriginalTxsToSync < startOriginalTx) { - completeForOriginalTx = true - } + // if (totalOriginalTxsToSync < startOriginalTx) { + // completeForOriginalTx = true + // } if (totalCyclesToSync < startCycle) { completeForCycle = true } Logger.mainLogger.debug( 'totalReceiptsToSync', totalReceiptsToSync, - 'totalOriginalTxsToSync', - totalOriginalTxsToSync, + // 'totalOriginalTxsToSync', + // totalOriginalTxsToSync, 'totalCyclesToSync', totalCyclesToSync ) @@ -1921,31 +1931,31 @@ export const syncCyclesAndTxsData = async ( startReceipt = endReceipt + 1 endReceipt += MAX_ORIGINAL_TXS_PER_REQUEST } - if (!completeForOriginalTx) { - Logger.mainLogger.debug(`Downloading Original-Txs from ${startOriginalTx} to ${endOriginalTx}`) - const res = (await queryFromArchivers( - RequestDataType.ORIGINALTX, - { - start: startOriginalTx, - end: endOriginalTx, - }, - QUERY_TIMEOUT_MAX - )) as ArchiverOriginalTxResponse - if (res && res.originalTxs) { - const downloadedOriginalTxs = res.originalTxs as OriginalTxDB.OriginalTxData[] - Logger.mainLogger.debug(`Downloaded Original-Txs: `, downloadedOriginalTxs.length) - await storeOriginalTxData(downloadedOriginalTxs) - if (downloadedOriginalTxs.length < MAX_ORIGINAL_TXS_PER_REQUEST) { - startOriginalTx += downloadedOriginalTxs.length + 1 - endOriginalTx += downloadedOriginalTxs.length + MAX_ORIGINAL_TXS_PER_REQUEST - continue - } - } else { - Logger.mainLogger.debug('Invalid Original-Tx download response') - } - startOriginalTx = endOriginalTx + 1 - endOriginalTx += MAX_ORIGINAL_TXS_PER_REQUEST - } + // if (!completeForOriginalTx) { + // Logger.mainLogger.debug(`Downloading Original-Txs from ${startOriginalTx} to ${endOriginalTx}`) + // const res = (await queryFromArchivers( + // RequestDataType.ORIGINALTX, + // { + // start: startOriginalTx, + // end: endOriginalTx, + // }, + // QUERY_TIMEOUT_MAX + // )) as ArchiverOriginalTxResponse + // if (res && res.originalTxs) { + // const downloadedOriginalTxs = res.originalTxs as OriginalTxDB.OriginalTxData[] + // Logger.mainLogger.debug(`Downloaded Original-Txs: `, downloadedOriginalTxs.length) + // await storeOriginalTxData(downloadedOriginalTxs) + // if (downloadedOriginalTxs.length < MAX_ORIGINAL_TXS_PER_REQUEST) { + // startOriginalTx += downloadedOriginalTxs.length + 1 + // endOriginalTx += downloadedOriginalTxs.length + MAX_ORIGINAL_TXS_PER_REQUEST + // continue + // } + // } else { + // Logger.mainLogger.debug('Invalid Original-Tx download response') + // } + // startOriginalTx = endOriginalTx + 1 + // endOriginalTx += MAX_ORIGINAL_TXS_PER_REQUEST + // } if (!completeForCycle) { Logger.mainLogger.debug(`Downloading cycles from ${startCycle} to ${endCycle}`) const res = (await queryFromArchivers( @@ -1990,7 +2000,7 @@ export const syncCyclesAndTxsDataBetweenCycles = async ( ) await syncCyclesBetweenCycles(lastStoredCycle, cycleToSyncTo) await syncReceiptsByCycle(lastStoredCycle, cycleToSyncTo) - await syncOriginalTxsByCycle(lastStoredCycle, cycleToSyncTo) + // await syncOriginalTxsByCycle(lastStoredCycle, cycleToSyncTo) } // // simple method to validate old data; it's not good when there are multiple archivers, the receipts saving order may not be the same diff --git a/src/Utils.ts b/src/Utils.ts index 4b69241c..90bc1929 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -6,6 +6,7 @@ import * as Logger from './Logger' import {Sign} from "./types/internalTxType"; import {DevSecurityLevel} from "./types/security"; import {ethers} from "ethers"; +import * as crypto from '@shardeum-foundation/lib-crypto-utils' export interface CountSchema { count: string @@ -618,4 +619,37 @@ export function verifyMultiSigs( isValid: validSigs >= minSigRequired, validCount: validSigs } +} + +/** + * Generates a hash for the given object. If the object has a `sign` property, + * it will generate a signed hash; otherwise, it will generate a regular hash. + * + * @param obj - The object to be hashed. + * @returns The resulting hash as a string. + */ +function hashSignedObj(obj: any): string { + if (!obj.sign) { + return crypto.hashObj(obj) + } + return crypto.hashObj(obj, true) +} + +/** + * Generates a transaction ID (txId) for the given transaction object. + * + * @param tx - The transaction object. It can be an EVM transaction with a `raw` property or any other transaction object. + * @returns The generated transaction ID as a string. + * + * @remarks + * - If the transaction object has a `raw` property (indicating it is an EVM transaction), the `raw` property is used for the txId calculation. + * - For other transaction objects, the entire object is used for the txId calculation. + */ +export function generateTxId(tx: any): string { + if (tx.raw) { + // if it is an evm tx, do not involve attached timestamp in txId calculation + return hashSignedObj({ raw: tx.raw }) + } + + return hashSignedObj(tx) } \ No newline at end of file diff --git a/src/dbstore/cycles.ts b/src/dbstore/cycles.ts index 77ebfdec..5f44076c 100644 --- a/src/dbstore/cycles.ts +++ b/src/dbstore/cycles.ts @@ -161,7 +161,7 @@ export async function queryCycleRecordsBetween( } } -export async function queryCyleCount(): Promise { +export async function queryCycleCount(): Promise { let cycles try { const sql = `SELECT COUNT(*) FROM cycles` diff --git a/src/dbstore/receipts.ts b/src/dbstore/receipts.ts index 2dedd8a4..28c53f45 100644 --- a/src/dbstore/receipts.ts +++ b/src/dbstore/receipts.ts @@ -16,6 +16,7 @@ export type Proposal = { afterStateHashes: string[] appReceiptDataHash: string txid: string + executionShardKey?: string } export type SignedReceipt = { @@ -40,7 +41,6 @@ export interface ArchiverReceipt { afterStates?: AccountsCopy[] beforeStates?: AccountsCopy[] appReceiptData:object & { accountId?: string; data: object; [key: string]: any } - executionShardKey: string globalModification: boolean } diff --git a/src/server.ts b/src/server.ts index 2130e254..6cf053c3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -223,8 +223,8 @@ async function syncAndStartServer(): Promise { let lastStoredReceiptCount = await ReceiptDB.queryReceiptCount() // Retrieve the count of cycles currently stored in the database - let lastStoredCycleCount = await CycleDB.queryCyleCount() - let lastStoredOriginalTxCount = await OriginalTxDB.queryOriginalTxDataCount() + let lastStoredCycleCount = await CycleDB.queryCycleCount() + // let lastStoredOriginalTxCount = await OriginalTxDB.queryOriginalTxDataCount() // Query the latest cycle record from the database let lastStoredCycleInfo = (await CycleDB.queryLatestCycleRecords(1))[0] @@ -287,8 +287,8 @@ async function syncAndStartServer(): Promise { lastStoredCycleCount, 'lastStoredReceiptCount', lastStoredReceiptCount, - 'lastStoredOriginalTxCount', - lastStoredOriginalTxCount + // 'lastStoredOriginalTxCount', + // lastStoredOriginalTxCount ) // If your not the first archiver node, get a nodelist from the others @@ -364,19 +364,19 @@ async function syncAndStartServer(): Promise { lastStoredReceiptCycle = receiptResult.matchedCycle } - if (lastStoredOriginalTxCount > 0) { - Logger.mainLogger.debug('Validating old Original Txs data!') - const lastStoredOriginalTxInfo = await OriginalTxDB.queryLatestOriginalTxs(1) - if (lastStoredOriginalTxInfo && lastStoredOriginalTxInfo.length > 0) - lastStoredOriginalTxCycle = lastStoredOriginalTxInfo[0].cycle - const txResult = await Data.compareWithOldOriginalTxsData(lastStoredOriginalTxCycle) - if (!txResult.success) { - throw Error( - 'The saved Original-Txs of last 10 cycles data do not match with the archiver data! Clear the DB and start the server again!' - ) - } - lastStoredOriginalTxCycle = txResult.matchedCycle - } + // if (lastStoredOriginalTxCount > 0) { + // Logger.mainLogger.debug('Validating old Original Txs data!') + // const lastStoredOriginalTxInfo = await OriginalTxDB.queryLatestOriginalTxs(1) + // if (lastStoredOriginalTxInfo && lastStoredOriginalTxInfo.length > 0) + // lastStoredOriginalTxCycle = lastStoredOriginalTxInfo[0].cycle + // const txResult = await Data.compareWithOldOriginalTxsData(lastStoredOriginalTxCycle) + // if (!txResult.success) { + // throw Error( + // 'The saved Original-Txs of last 10 cycles data do not match with the archiver data! Clear the DB and start the server again!' + // ) + // } + // lastStoredOriginalTxCycle = txResult.matchedCycle + // } // Synchronize Genesis accounts and transactions from the network archivers await Data.syncGenesisAccountsFromArchiver() // Sync Genesis Accounts that the network start with. @@ -400,17 +400,17 @@ async function syncAndStartServer(): Promise { await Data.syncReceiptsByCycle(lastStoredReceiptCycle) } - if (lastStoredOriginalTxCount === 0) await Data.syncOriginalTxs() - else { - Logger.mainLogger.debug('lastStoredOriginalTxCycle', lastStoredOriginalTxCycle) - await Data.syncOriginalTxsByCycle(lastStoredOriginalTxCycle) - } + // if (lastStoredOriginalTxCount === 0) await Data.syncOriginalTxs() + // else { + // Logger.mainLogger.debug('lastStoredOriginalTxCycle', lastStoredOriginalTxCycle) + // await Data.syncOriginalTxsByCycle(lastStoredOriginalTxCycle) + // } // After receipt data syncing completes, check cycle and receipt again to be sure it's not missing any data // Query for the cycle and receipt counts lastStoredReceiptCount = await ReceiptDB.queryReceiptCount() - lastStoredOriginalTxCount = await OriginalTxDB.queryOriginalTxDataCount() - lastStoredCycleCount = await CycleDB.queryCyleCount() + // lastStoredOriginalTxCount = await OriginalTxDB.queryOriginalTxDataCount() + lastStoredCycleCount = await CycleDB.queryCycleCount() lastStoredCycleInfo = (await CycleDB.queryLatestCycleRecords(1))[0] // Check for any missing data and perform syncing if necessary @@ -419,7 +419,7 @@ async function syncAndStartServer(): Promise { `The archiver has ${lastStoredCycleCount} and the latest stored cycle is ${lastStoredCycleInfo.counter}` ) } - await Data.syncCyclesAndTxsData(lastStoredCycleCount, lastStoredReceiptCount, lastStoredOriginalTxCount) + await Data.syncCyclesAndTxsData(lastStoredCycleCount, lastStoredReceiptCount) // , lastStoredOriginalTxCount) } else { // Sync all state metadata until no older data is fetched from other archivers await syncStateMetaData(State.activeArchivers) diff --git a/src/types/ajv/Receipts.ts b/src/types/ajv/Receipts.ts index dbb6412a..58b08d8a 100644 --- a/src/types/ajv/Receipts.ts +++ b/src/types/ajv/Receipts.ts @@ -15,7 +15,8 @@ const schemaProposal = { beforeStateHashes: { type: 'array', items: { type: 'string' } }, afterStateHashes: { type: 'array', items: { type: 'string' } }, appReceiptDataHash: { type: 'string' }, - txid: { type: 'string' } + txid: { type: 'string' }, + executionShardKey : { type: 'string' } }, required: ['applied', 'cant_preApply', 'accountIDs', 'beforeStateHashes', 'afterStateHashes', 'appReceiptDataHash', 'txid'], additionalProperties: false @@ -65,9 +66,10 @@ const schemaGlobalTxReceipt = { addressHash: { type: 'string' }, value: {}, when: { type: 'integer' }, - source: { type: 'string' } + source: { type: 'string' }, + txId: { type: 'string' } }, - required: ['address', 'addressHash', 'value', 'when', 'source'], + required: ['address', 'addressHash', 'value', 'when', 'source', 'txId'], additionalProperties: false }, txGroupCycle: { type: 'integer', minimum: 0 } @@ -88,15 +90,19 @@ const schemaAppReceiptData = { }; const schemaTx = { - type: 'object', - properties: { - originalTxData: { type: 'object', additionalProperties: true }, - txId: { type: 'string' }, - timestamp: { type: 'integer', minimum: 0 } + type: 'object', + properties: { + originalTxData: { + type: 'object', + items: { $ref: AJVSchemaEnum.OriginalTxData }, // receipt is now coupled with OriginalTxData during validation + additionalProperties: true, // TODO[1892] : should we remove or set this to false or leave it as it is }, - required: ['originalTxData', 'txId', 'timestamp'], - additionalProperties: false -}; + txId: { type: 'string' }, + timestamp: { type: 'integer', minimum: 0 }, + }, + required: ['originalTxData', 'txId', 'timestamp'], + additionalProperties: false, +} // Define the main ArchiverReceipt schema const schemaArchiverReceipt = { @@ -108,10 +114,9 @@ const schemaArchiverReceipt = { afterStates: { type: 'array', items: { $ref: AJVSchemaEnum.AccountsCopy } }, // Using imported schema beforeStates: { type: 'array', items: { $ref: AJVSchemaEnum.AccountsCopy } }, // Using imported schema appReceiptData: schemaAppReceiptData, - executionShardKey: { type: 'string' }, globalModification: { type: 'boolean' } }, - required: ['tx', 'cycle', 'signedReceipt', 'appReceiptData', 'executionShardKey', 'globalModification'], + required: ['tx', 'cycle', 'signedReceipt', 'appReceiptData', 'globalModification'], additionalProperties: false }; diff --git a/src/worker-process/index.ts b/src/worker-process/index.ts index e5eb6ea8..28a67cd7 100644 --- a/src/worker-process/index.ts +++ b/src/worker-process/index.ts @@ -29,7 +29,8 @@ export const initWorkerProcess = async (): Promise => { } try { // console.log(`Worker process ${process.pid} is verifying receipt`, receipt.tx.txId, receipt.tx.timestamp) - verificationResult = await verifyArchiverReceipt(receipt, data.requiredSignatures) + // this code is not being used anymore, worker threads need to be enabled. Ignore for now. + verificationResult = await verifyArchiverReceipt(receipt) } catch (error) { console.error(`Error in Worker ${process.pid} while verifying receipt`, error) verificationResult.failedReasons.push('Error in Worker while verifying receipt') diff --git a/test/unit/src/shardeum/verifyGlobalTxReceipt.test.ts b/test/unit/src/shardeum/verifyGlobalTxReceipt.test.ts index 4712b954..2862d88c 100644 --- a/test/unit/src/shardeum/verifyGlobalTxReceipt.test.ts +++ b/test/unit/src/shardeum/verifyGlobalTxReceipt.test.ts @@ -251,7 +251,6 @@ describe('verifyGlobalTxAccountChange', () => { stateId: '344fd275ba5fa8460a8164168725bd61f2b27dbf7f0a6e38434b3e9f35f39258', timestamp: 1730101530472, }, - executionShardKey: 'fromacc', globalModification: true, } failedReasons = []