Skip to content

Fix blockchain state mismatch across RPCs for the same head#950

Open
cwilvx wants to merge 69 commits into
stabilisationfrom
mempool-mismatch
Open

Fix blockchain state mismatch across RPCs for the same head#950
cwilvx wants to merge 69 commits into
stabilisationfrom
mempool-mismatch

Conversation

@cwilvx

@cwilvx cwilvx commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

This pull request fixes the state mismatch across RPCs for a multinode network. The mismatch occured when the node had more nodes than the ones participating in the consensus, and the non-participants synced the gcr via the sync routine.

What this PR introduces the following behavioral changes to the nodes and the network:

  1. Failed transactions will now be included in a block, with their status set to FAILED and the reason for failure provided as transaction metadata in tx.attrs.code and tx.attrs.message. The user can fetch the transaction by hash and check if the tx was confirmed or it failed.
  2. We now have a .attrs object on both transactions and blocks. The field is currently used to track the reference block and failure reasons for transactions, and applied transactions order for blocks. The field
  3. A block is only valid if forged by 2/3 + 1 of shard and 2/3 + 1 of the shard size. With a shard size of 4, you require at least 3 validator nodes to be online for the network to forge new blocks. This currently fixes our forking problem.
  4. Clients are allowed to submit transactions with a nonce > expected current nonce. If the nonce is less than expected during the next consensus round, the tx will fail. If greater, the tx will be kept in the mempool. Eventually the tx will be finalized or will expire due to the aging reference_block.
  5. A block is now verified against the existing head before insert. If anything in the hashes or signatures doesn't align, the node does not accept the block.
  6. Transactions are now sorted deterministically in order of nonce in the consensus.
  7. Some Mempool.addTransaction calls are gated with a mutex to prevent tampering with the mempool during merging.
  8. I've left process.exit code blocks with a comment NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE) all over the main sync and consensus source files. The code guards against behavior that causes state drift across RPCs. Please do not remove for now.

Summary by CodeRabbit

  • New Features
    • Added support for storing per-block and per-transaction metadata (“attrs”) and expanded transaction status handling (confirmed/failed/pending).
  • Bug Fixes
    • Strengthened validation across block, transaction, and consensus flows, tightening signature/quorum checks and improving coherence/edit verification.
    • Reduced duplicate processing by serializing critical mempool/relay/sync paths with lock-based admission.
    • Updated snapshot data (chain state, block hashes, checksums, and validator set).
    • Adjusted runtime script defaults to disable TLSNotary and monitoring unless overridden.

cwilvx added 3 commits June 29, 2026 18:30
+ add comment: NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE) to find the code in next debug session
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds attrs and status schema support for transactions and blocks, changes transaction validation and hashing rules, serializes mempool admission with a mutex, rewires consensus around structured transaction tracking, and strengthens block sync and verification. It also updates snapshot data and runtime defaults.

Changes

Consensus Stability and Blockchain Correctness

Layer / File(s) Summary
Status, attrs schema
src/utilities/constants.ts, src/errors/codes.ts, src/libs/blockchain/block.ts, src/libs/blockchain/transaction.ts, src/model/entities/Blocks.ts, src/model/entities/Transactions.ts, src/model/entities/Mempool.ts, src/migrations/...
Adds TRANSACTION_STATUS and TransactionStatus, new transaction error codes, nullable attrs fields on block and transaction models, and migrations for both tables. Widens transaction status typing in entity models.
Validation and hash canonicalization
src/libs/blockchain/transaction.ts, src/libs/blockchain/validation/txValidator.ts, src/libs/blockchain/validation/verifyGcrEdits.ts, src/libs/blockchain/routines/validateTransaction.ts, src/libs/network/endpointValidation.ts, src/libs/blockchain/gcr/gcr_routines/GCRNonceRoutines.ts, src/libs/network/handlers/identityHandlers.ts
Removes expectedPrior stripping from coherence and GCR edit hashing paths, adds early transaction checks in validation, changes confirmTx failure output, updates nonce helpers, and changes address nonce lookup to use GCR.getAccountNonce.
Mempool admission and relayed transaction locking
src/libs/blockchain/mempool.ts, src/libs/network/endpointExecution.ts, src/libs/network/dtr/dtrmanager.ts, src/libs/network/handlers/miscHandlers.ts, src/libs/blockchain/gcr/gcr.ts
Adds Mempool.lock and addTransactionWithLock, updates duplicate and nonce revalidation, and routes execute, relay, and award-point paths through the locked insertion flow.
Block and transaction persistence
src/libs/blockchain/chainDb.ts, src/libs/blockchain/chain.ts, src/libs/blockchain/chainBlocks.ts, src/libs/blockchain/chainTransactions.ts, src/libs/blockchain/chainGenesis.ts, src/libs/blockchain/gcr/handleGCR.ts, src/libs/blockchain/genesis/restoreSnapshot.ts, src/libs/l2ps/L2PSConsensus.ts
Changes block/transaction insertion APIs, adds getBlockHash, persists block attrs, inserts genesis with genesisTx, removes status arguments from raw transaction conversion, switches chunk sizing to maxRowsPerInsert, and updates L2PS batch insertion.
Consensus, block insertion, and sync
src/libs/consensus/v2/PoRBFT.ts, src/libs/blockchain/routines/Sync.ts, src/libs/blockchain/validation/verifyBlock.ts, src/libs/communications/broadcastManager.ts, src/libs/network/manageGCRRoutines.ts, src/libs/consensus/v2/routines/*, src/libs/network/*, src/utilities/sharedState.ts, src/libs/peer/PeerManager.ts
Reworks consensus tx tracking with txMap and structured failures, adds verifyBlockAttrs and syncLock, tightens block verification and peer selection, and updates related logging, sync, and peer discovery flows.
Snapshot data update and script defaults
scripts/run, data/snapshot/manifest.json, data/snapshot/validators.jsonl, data/snapshot/identity_commitments.jsonl
Flips default service flags, refreshes snapshot metadata, adds validator records, and updates identity commitment timestamps.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

Possibly related PRs

  • kynesyslabs/node#404: Both PRs modify src/utilities/sharedState.ts, including rate-limit configuration and getInfo() payload fields.
  • kynesyslabs/node#476: Both PRs change consensus flow in src/libs/consensus/v2/PoRBFT.ts.
  • kynesyslabs/node#641: Both PRs modify block/transaction sync batching in src/libs/blockchain/routines/Sync.ts.

Suggested labels

Review effort 4/5

🐇 I hopped through attrs and keys,
Through locks and hashes in the breeze.
The block got tagged, the txs aligned,
The sync stayed strict, the shards stayed lined.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.60% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the core bug fix: resolving blockchain state mismatches across RPCs for the same head.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mempool-mismatch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR changes block and transaction handling to keep synced nodes aligned on the same chain head. The main changes are:

  • Adds attrs metadata to blocks and transactions.
  • Includes failed transactions in blocks with failure metadata.
  • Adds deterministic transaction ordering for consensus.
  • Strengthens block verification, quorum checks, and sync validation.
  • Adds locking around selected mempool admission and sync paths.

Confidence Score: 4/5

The transaction failure-reporting path needs correction before merge because persisted failed transactions are exposed to clients with the wrong lifecycle state.

The changed consensus behavior is broad, but the identified issue is focused and was confirmed with a runtime harness exercising the real status helper behavior.

src/libs/blockchain/chainTransactions.ts

T-Rex T-Rex Logs

What T-Rex did

  • Reproduced a failed transaction status mismatch using a focused Jest harness that exercises getTxByHash and getTransactionStatus with a mocked repository; observed stored status as failed and getTransactionStatus as {"state":"included"}.
  • Compared attrs persistence between before and after runs, noting missing attrs initially and nullable jsonb attrs after, using trex-artifacts/attrs-persistence-check.cjs.
  • Observed nonce-flow regression: head commit produced blockOrderedTransactions ["0xlow_nonce_hash"], persisted 0xlow_nonce_hash with status "FAILED" and attrs code "TX_NONCE_INVALID_LOW" with a reference block of 42, and retained 0xhigh_nonce_hash in mempool; repro harness captured in trex-artifacts/failed_tx_nonce_flow_repro.ts.
  • Verified verifyBlock improvements by showing the head now rejects pro<3 for shardSize=4, with deterministic ordering, and noted that the previousHash mismatch path exits early via the process.Exit pathway after the change.
  • Validated mempool mutex optimization: after the change, addTransactionWithLock is used and access is serialized with maxConcurrentAddTransaction=1; both runs exit 0 with successful mocked admissions.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (2)

  1. src/libs/blockchain/chainTransactions.ts, line 99-105 (link)

    P1 Report failed status
    getTransactionStatus now reports every persisted transaction as included, even when consensus stores it with TRANSACTION_STATUS.FAILED. With this PR, failed transactions are inserted into transactions, so clients polling this status helper receive the wrong lifecycle state for the new failure path.

    Artifacts

    Repro: focused Jest harness for persisted failed transaction status

    • Contains supporting evidence from the run (text/typescript; charset=utf-8).

    Repro: failing Jest output showing failed storage status and included helper result

    • Keeps the command output available without making the summary code-heavy.

    View artifacts

    T-Rex Ran code and verified through T-Rex

  2. General comment

    P1 verifyBlock terminates the process instead of returning a malformed previousHash rejection

    • Bug
      • The validation objective asks to show verifyBlock rejects a non-genesis synced block whose previousHash does not match the existing head. In head, the mismatch branch logs an error and calls process.exit(1) before the structured { valid: false, reason: ... } return, making that return unreachable. A malformed synced block can therefore kill the process rather than being rejected and handled by the sync caller.
    • Cause
      • src/libs/blockchain/validation/verifyBlock.ts lines 47-59 place process.exit(1) inside the previousHash mismatch branch before returning the invalid verdict.
    • Fix
      • Remove the process.exit(1) from verifyBlock and return { valid: false, reason: ... }; let the sync caller decide whether to disconnect, retry, quarantine the peer, or perform any fatal shutdown policy outside this pure verification function.

    T-Rex Ran code and verified through T-Rex

Reviews (7): Last reviewed commit: "add latest data snapshot" | Re-trigger Greptile

Comment thread src/libs/consensus/v2/PoRBFT.ts
Comment thread src/libs/blockchain/routines/Sync.ts
Comment on lines 44 to +55
}
}

// 2. Resolve the eligible signer set for this block.
const signatures = block.validation_data?.signatures
if (!signatures || typeof signatures !== "object") {
return { valid: false, reason: "block has no validation_data.signatures" }
// Verify last block hash is the same as this block's previous hash
const lastBlockHash = await Chain.getLastBlockHash()
if (lastBlockHash !== block.content.previousHash) {
// NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE):
log.error(
`last block hash mismatch: last block hash ${lastBlockHash}, block ${block.number}'s previous hash ${block.content.previousHash}`,
)
process.exit(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dead return after process.exit(1)

The return { valid: false, ... } block is unreachable — process.exit(1) terminates the process before it can execute. Any caller of verifyBlock that expects a graceful { valid: false } for a chain-continuity failure will instead receive a crashed node. During any scenario where a block with a non-matching previousHash reaches syncBlock (fork detection, out-of-order delivery, race with another sync path), the entire node process dies rather than the block being cleanly rejected.

Comment on lines 789 to +800
const confirmed = await Chain.getExistingTransactionHashes(
finalTxs.map(t => t.hash),
)

if (confirmed.size > 0) {
log.error("Found confirmed txs during tx application")
// NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE):
// fetch confirmed txs from db
const confirmedTxs = await Chain.getTransactionsFromHashes(Array.from(confirmed))
log.error("Confirmed txs: " + JSON.stringify(Array.from(confirmed), null, 2))
log.error("Confirmed txs full: " + JSON.stringify(confirmedTxs.map(t => t.hash), null, 2))
process.exit(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dead filtering code after process.exit(1)

The finalTxs.filter(...) block that removes already-confirmed transactions from the apply set is unreachable — process.exit(1) terminates the process before it executes. Previously this was a graceful defensive filter; now it is a hard crash. Any legitimate race where a tx lands in both the mempool apply set and the transactions table (e.g., a block was inserted by the broadcast path just before consensus reaches the apply step) will bring the node down permanently instead of skipping the duplicate.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/libs/blockchain/block.ts (1)

27-54: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Initialize attrs in the constructor.

attrs is typed as Record<string, any> | null, but new Block() instances leave it undefined, so freshly forged/broadcast blocks may not satisfy the new metadata contract.

Proposed fix
     constructor() {
         this.number = null
         this.hash = null // Calculated on the content
         this.status = null
+        this.attrs = null
         this.next_proposer = ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/block.ts` around lines 27 - 54, Initialize the attrs
field in the Block constructor so new Block() instances always have a defined
metadata object. The current Block class sets number, hash, status, content,
proposer, and validation_data but leaves attrs undefined despite the
Record<string, any> | null type, so assign an explicit initial value there.
Update the constructor in Block to include attrs alongside the other default
fields and keep the initialization consistent with the block metadata contract.
src/libs/blockchain/validation/verifyBlock.ts (1)

62-122: 🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Verify signatures against the expected shard, not every active validator.

This accepts any threshold active validators from GCR.getGCRValidatorsAtBlock(...); it does not prove those signers were selected for the block’s shard. A non-shard validator quorum could produce a block that sync accepts. Recompute/load the expected shard for block.number and derive both validatorIdentities and threshold from that shard membership.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/validation/verifyBlock.ts` around lines 62 - 122, The
verification in verifyBlock currently accepts signatures from the full validator
set returned by GCR.getGCRValidatorsAtBlock, which can allow a quorum outside
the block’s intended shard. Update verifyBlock to load or recompute the expected
shard for block.number, derive validatorIdentities from that shard membership,
and base the signature threshold on the shard size rather than
getSharedState.shardSize so only the correct shard’s signers can satisfy
validation.
src/libs/blockchain/routines/Sync.ts (2)

474-493: 🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Make block, tx, and GCR sync atomic.

Both sync paths persist the block before fetching/verifying txs and applying GCR state. If tx fetch, attrs verification, GCR application, or tx insertion fails afterward, the node can retain an advanced block head with missing transaction/GCR state. Verify/fetch first, then persist block+transactions+GCR effects as one unit or rollback on failure.

Also applies to: 731-741

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/routines/Sync.ts` around lines 474 - 493, The fast sync
flow in Sync should not persist the block before tx/GCR work succeeds;
`askTxsForBlock`, `verifyBlockAttrs`, `syncGCRTables`, and the later tx
insertion path must be treated as one atomic unit. Refactor the logic around the
block insertion/verification sequence so you fetch and validate first, then
commit the block, transactions, and GCR state together, or roll back/abort on
any failure. Use the existing `mergePeerlist`, `askTxsForBlock`,
`verifyBlockAttrs`, and `syncGCRTables` flow as the anchor points, and apply the
same atomicity change to the other sync block referenced by the comment.

1130-1144: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Keep fastSyncAborted asserted until the timed-out sync finishes. Promise.race returns early here, but syncLock.runExclusive(...) keeps running in the background, and the finally clears fastSyncAborted before that work exits. That lets the in-flight sync continue mutating state after timeout. Hold the abort flag until the lock settles, or await the locked task after signaling abort.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/routines/Sync.ts` around lines 1130 - 1144, The fast sync
timeout handling in Sync.ts clears fastSyncAborted too early because
Promise.race returns before syncLock.runExclusive(async () =>
fastSyncRoutine(peers)) has finished. Update the fastSync path so the abort flag
stays set until the locked sync task fully settles, either by awaiting the
runExclusive task after setting the timeout state or by deferring the flag reset
until the background work completes. Keep the fix localized around
fastSyncRoutine, syncLock.runExclusive, and the finally cleanup that currently
resets getSharedState.fastSyncAborted.
src/libs/l2ps/L2PSConsensus.ts (1)

466-526: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Build a full transaction shape before insertion.

Chain.insertTransaction now serializes fields from the transaction object itself. Here status is nested under content, blockNumber is missing, and content.transaction_fee is missing, so the batch tx is either persisted with null metadata or fails in Transaction.toRawTransaction.

🐛 Proposed fix
             const l1BatchTx = {
                 type: "l2psBatch",
                 hash: `0x${batchHash}`,
+                status: TRANSACTION_STATUS.CONFIRMED,
+                blockNumber,
                 signature: {
                     type: "ed25519",
                     data: "", // System-generated, no actual signature needed
                 },
                 content: {
                     type: "l2psBatch",
                     from: "l2ps:consensus", // System sender for L2PS batch
+                    from_ed25519_address: "l2ps:consensus",
                     to: "l2ps:batch",
                     amount: 0,
                     nonce: blockNumber,
                     timestamp: Date.now(),
-                    status: TRANSACTION_STATUS.CONFIRMED,
+                    transaction_fee: {
+                        network_fee: 0,
+                        rpc_fee: 0,
+                        additional_fee: 0,
+                        rpc_address: null,
+                    },
                     data: [
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/l2ps/L2PSConsensus.ts` around lines 466 - 526, The batch transaction
object passed to Chain.insertTransaction is incomplete because status is nested
inside content, while blockNumber and content.transaction_fee are missing, which
can lead to null metadata or serialization failures in
Transaction.toRawTransaction. Update the l1BatchTx shape in L2PSConsensus so all
fields required by the raw transaction serializer are present at the top level
and inside content as expected, including status, blockNumber, and
transaction_fee. Verify the transaction built in this block matches the full
shape used by Chain.insertTransaction before calling it.
src/libs/blockchain/routines/validateTransaction.ts (1)

485-485: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove stale assignNonce/expectedPrior comments
Update the outdated docstrings in src/libs/blockchain/mempool.ts, src/forks/forkConfig.ts, and src/libs/blockchain/validation/verifyGcrEdits.ts so they match the current nonce flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/routines/validateTransaction.ts` at line 485, Remove the
outdated assignNonce/expectedPrior docstrings so they match the current nonce
handling flow. Update the stale comments in the relevant logic within
mempool.ts, forkConfig.ts, and verifyGcrEdits.ts to describe the current
behavior, and ensure any references to nonce assignment or expected prior state
are aligned with the actual implementation in the affected functions/classes.
🧹 Nitpick comments (6)
src/model/entities/Mempool.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow MempoolTx.status to TransactionStatus

string | TransactionStatus still collapses to string, so this field doesn’t enforce the canonical status set. If this entity should only persist pending | confirmed | failed, make the property TransactionStatus and keep any legacy/raw coercion at the deserialization boundary instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/model/entities/Mempool.ts` at line 1, MempoolTx.status is still typed too
loosely, so it doesn’t enforce the canonical transaction status set; update the
Mempool entity to use TransactionStatus directly for the status field. Keep any
raw string-to-status normalization at the deserialization boundary in the
Mempool model rather than widening the property type, and verify any
constructors/parsers that assign status still map legacy values before they
reach MempoolTx.

Source: Linters/SAST tools

src/libs/blockchain/transaction.ts (1)

51-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep status narrowed to TransactionStatus | null. string | TransactionStatus is just string, so the field loses its status constraint; normalize any legacy values before assignment instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/transaction.ts` around lines 51 - 53, The Transaction
model’s status field is too broad because `TransactionStatus` is already a
string type, so `string | TransactionStatus` collapses the constraint and allows
invalid values. Update the `status` declaration in `Transaction` to
`TransactionStatus | null` only, and normalize any legacy string inputs before
assigning them to `status` in the constructor or related assignment path so only
valid transaction statuses are stored.

Source: Linters/SAST tools

src/libs/blockchain/routines/Sync.ts (1)

321-443: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split verifyBlockAttrs to satisfy the complexity gate.

Sonar reports this function over the allowed cognitive complexity. Extract the count, block-number, deterministic-order, and attrs checks into small helpers while addressing the sync-integrity checks above.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/routines/Sync.ts` around lines 321 - 443,
`verifyBlockAttrs` is too complex and needs to be split into smaller units to
pass the cognitive complexity gate. Extract the major checks into dedicated
helpers from `Sync.ts`—for example the transaction-count validation,
block-number validation, deterministic order comparison, and attribute integrity
checks—then have `verifyBlockAttrs` orchestrate them and return the applied txs.
Keep the existing `log.error` and `process.exit(1)` behavior inside the helpers
so the sync-integrity logic remains unchanged, and use the existing symbols
`verifyBlockAttrs`, `orderDeterministically`, `Hashing.sha256`, and
`TRANSACTION_STATUS.CONFIRMED` to guide the refactor.

Source: Linters/SAST tools

src/libs/blockchain/gcr/gcr.ts (2)

154-171: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

getAccountNonces is O(n·m); build a lookup map once.

accounts.find(...) inside the per-pubkey reduce makes this quadratic in batch size. A single Map keeps it linear.

⚡ Proposed fix
-        // return a map of pubkey to nonce, 0 for missing accounts
-        return pubkeys.reduce((acc, pubkey) => {
-            acc[pubkey] =
-                accounts.find(account => account.pubkey === pubkey)?.nonce ?? 0
-            return acc
-        }, {})
+        // return a map of pubkey to nonce, 0 for missing accounts
+        const nonceByPubkey = new Map(
+            accounts.map(account => [account.pubkey, account.nonce]),
+        )
+        return pubkeys.reduce<Record<string, number>>((acc, pubkey) => {
+            acc[pubkey] = nonceByPubkey.get(pubkey) ?? 0
+            return acc
+        }, {})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/gcr/gcr.ts` around lines 154 - 171, getAccountNonces
currently does a linear accounts.find() for every pubkey inside the reduce,
making the method quadratic for large batches. Update the GCR.getAccountNonces
flow to build a single lookup map from the queried accounts first, then use that
map during the pubkeys.reduce() so each pubkey resolves in constant time while
preserving the existing 0 default for missing accounts.

143-152: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Select only nonce to avoid loading the full account row.

getAccountNonce runs on the per-transaction validation path (confirmTransaction) yet fetches the entire GCRMain row — including the heavy identities, points, and referralInfo jsonb columns — just to read a single integer. getAccountBalance above already uses select for the same reason.

⚡ Proposed fix
         const account = await gcrRepository.findOne({
             where: { pubkey },
+            select: ["nonce"],
         })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/gcr/gcr.ts` around lines 143 - 152, `GCR.getAccountNonce`
is fetching the full `GCRMain` entity when only `nonce` is needed in the
`confirmTransaction` path. Update the repository query in `getAccountNonce` to
select only the `nonce` field, following the same pattern used by
`getAccountBalance`, and keep the fallback behavior returning 0 when no row is
found.
src/libs/blockchain/mempool.ts (1)

309-328: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Nonce recheck logic is fine, but the rationale comment above is now stale.

The new rule (txNonce <= accountNonce → reject) no longer queries pending mempool count, and GCRNonceRoutines no longer carries the expectedPrior check. The large comment block preceding this transaction (re: account.nonce + 1 + pendingCount and "consensus-side expectedPrior check in GCRNonceRoutines (PR 3)") now misdescribes the safety model on a consensus-critical path. Please update it so future readers don't rely on a guard that was removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/mempool.ts` around lines 309 - 328, Update the stale
rationale comment near the nonce validation in mempool addTransaction logic so
it matches the current behavior. The comment should describe the actual guard
used by Mempool.addTransaction and GCRNonceRoutines now that the pending mempool
count and expectedPrior check are gone, and should not mention the removed
`account.nonce + 1 + pendingCount` rule or the old consensus-side safeguard.
Keep the explanation aligned with the `txNonce <= accountNonce` rejection path
and the returned invalid nonce error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/libs/blockchain/chainBlocks.ts`:
- Around line 271-274: The transaction status set in chainBlocks.ts currently
includes FAILED, which is fine for ingestion but not for confirmed-only side
effects. Update the downstream projection logic in the affected handlers
(including persistConfirmedTransactionProjection and the IdentityCommitment
blockNumber update path) so they only operate on transactions whose status is
CONFIRMED, while keeping FAILED rows available elsewhere. Use the existing
transactionEntities/status handling in chainBlocks.ts to filter before applying
these confirmed-only updates.
- Around line 248-292: The block transaction validation in chainBlocks.ts is too
permissive because it only runs when both arrays are non-empty and accepts any
subset of ordered_transactions. Update the transaction check around the
blockTxs/ordered_transactions handling so saving a non-empty block requires an
exact hash match before proceeding, or ensure the sync flow fetches and verifies
transactions before insertBlock is called. Use the existing blockTxs,
block.content.ordered_transactions, and transactionEntities logic to enforce the
full set match before setting blockNumber or continuing.

In `@src/libs/blockchain/gcr/handleGCR.ts`:
- Around line 170-172: The exported normalizer normalizePubkey currently assumes
a non-null string or Uint8Array and can fall through to forgeToHex on undefined
when callers use from_ed25519_address directly. Update normalizePubkey to guard
nullish input explicitly by rejecting or safely handling it before calling
forgeToHex, or adjust the deterministic-order caller to always pass
from_ed25519_address || from so the helper never receives undefined.
- Around line 793-800: The confirmed-transaction guard in handleGCR should not
terminate the process with process.exit(1); instead, make this branch fail the
block-apply flow by returning a typed failure or throwing a domain-specific
error that sync/consensus can catch. Update the logic around confirmed.size,
Chain.getTransactionsFromHashes, and the surrounding tx application path so the
duplicate-confirmed-tx condition aborts application cleanly without killing the
node.

In `@src/libs/blockchain/mempool.ts`:
- Line 39: The shared Mutex on Mempool.lock is never reassigned, so update the
Mempool class to make lock readonly and keep the existing static mutex instance
intact. Use the Mempool.lock declaration as the only place to change, ensuring
runExclusive callers continue to use the same mutual exclusion guard and
accidental reassignment is prevented.

In `@src/libs/blockchain/routines/Sync.ts`:
- Around line 375-418: The GCR sync logic in Sync.ts is trusting peer-controlled
block.attrs and tx.status to decide which transactions are applied, so make
local execution authoritative in the fastSync path and in
syncGCRTables/HandleGCR.applyTransactions comparisons. Use the result of
applying transactions locally as the source of truth, then compare the
successful hashes against block.attrs["gcrAppliedTxsHash"] and related metadata,
or otherwise move the relevant attrs into the signed block content; keep the
mismatch checks and logging around the existing applied/sorted transaction flow.
- Around line 378-418: Guard all reads of block.attrs in the fast sync check
inside Sync.ts before indexing into it, because it can be null for pre-attrs or
malformed blocks. Update the mismatch handling around the gcrAppliedTxCount and
gcrAppliedTxsHash comparisons to early-exit or reject when attrs is missing
instead of dereferencing it directly. Use the existing fastSync block-validation
logic in Sync.ts as the place to add the null check, and keep the current error
logging and process.exit paths for the valid attrs case.

In `@src/libs/blockchain/routines/validateTransaction.ts`:
- Around line 89-104: The nonce validation in validateTransaction is using
from_ed25519_address, while Mempool.addTransaction relies on content.from, so
the same transaction can be evaluated against different sender identities.
Update validateTransaction to read the nonce using the same canonical account
key used by the mempool path, and make sure the chosen identifier is normalized
consistently before calling GCR.getAccountNonce. Keep the nonce comparison and
error handling unchanged, but ensure both validation paths reference the same
sender field.

In `@src/libs/blockchain/validation/verifyBlock.ts`:
- Around line 47-59: In verifyBlock, the lastBlockHash mismatch path should
reject the block instead of terminating the process. Remove the process.exit(1)
call in the Chain.getLastBlockHash / block.content.previousHash check, and
return the existing invalid result object with the mismatch reason so
peer-supplied blocks are handled as a normal validation failure.

In `@src/libs/communications/broadcastManager.ts`:
- Line 10: Remove the unused Mutex import from the broadcastManager module since
it is not referenced anywhere in the file and is triggering the lint/Sonar
warning. Update the import list at the top of the file and keep the rest of the
broadcastManager implementation unchanged.

In `@src/libs/consensus/v2/PoRBFT.ts`:
- Around line 713-763: Reject multi-nonce transactions before the nonce-edit
loop in PoRBFT’s transaction validation path mutates validTxs. In the tx
processing logic around the nonceEdits handling, first detect if
tx.content.gcr_edits contains more than one edit of type "nonce" and mark the
transaction failed/rejected immediately instead of iterating per edit. This
ensures each tx is validated once, prevents duplicate pushes on the low-nonce
path, and avoids accepting a tx after only the first matching nonce edit while
skipping the rest.
- Around line 453-491: The final transaction sanity check in PoRBFT still runs
after a handled AbortConsensusError and can incorrectly exit the process during
a rollback. Update the consensus flow around updateValidatorPhase(), the
catch/rollback path, and the final txs/blockTxs verification so that this
post-check is skipped or guarded when exitReason indicates a handled abort like
blockTimestampNotReceived or voteError. Keep the mismatch logging in the same
transaction validation block, but ensure process.exit(1) only happens for truly
unrecoverable cases.

In `@src/libs/network/endpointValidation.ts`:
- Around line 105-111: The mismatch-dump error handling in
handleValidateTransaction is losing useful diagnostics by converting non-Error
values with String(dumpErr), which turns plain objects into [object Object].
Update the catch block in endpointValidation.ts to preserve object details when
logging mismatch dump failures, using the existing log.error call and the
dumpErr symbol so the message includes structured information instead of
collapsing it to a generic string.

In `@src/libs/network/handlers/identityHandlers.ts`:
- Around line 28-30: The RPC error payload in identityHandlers should not
stringify non-Error thrown values, because that turns object errors into
"[object Object]" and hides details. Update the error handling around
response.extra to preserve the original object when error is not an Error, and
only use error.message for real Error instances. Keep the fix localized to the
identityHandlers response building logic so the payload retains the full thrown
value for downstream consumers.

In `@src/libs/network/manageGCRRoutines.ts`:
- Around line 265-268: The fast-path dedupe branch in manageGCRRoutines should
return the same “already processed” response shape as
BroadcastManager.handleNewBlock, not a plain string. Update the block.number <=
getSharedState.lastBlockNumber path to populate the structured payload including
syncData so peers receive the same contract regardless of which dedupe path
handles the block. Make the change in the logic around
getSharedState.lastBlockNumber and keep the response consistent with the
existing already-processed handling.

In `@src/model/entities/Blocks.ts`:
- Around line 33-34: The overwrite path in chainBlocks should also persist
attrs, since the existingBlock && position branch currently only copies
block.attrs into newBlock and leaves the stored row stale. Update the block
rewrite logic in chainBlocks so the existing block entity’s attrs field is
assigned from the incoming block before saving, keeping it consistent with the
Blocks entity’s attrs column.

In `@src/utilities/sharedState.ts`:
- Around line 495-508: Make consensusInfo best-effort in sharedState’s /info
path: the getCommonValidatorSeed() call and secman.secretary.identity access can
throw when consensus or secretary state is unavailable. Update the consensusInfo
block to safely read SecretaryManager data and wrap or guard the common
validator seed lookup so the rest of the /info response still succeeds, leaving
optional fields unset when unavailable.

---

Outside diff comments:
In `@src/libs/blockchain/block.ts`:
- Around line 27-54: Initialize the attrs field in the Block constructor so new
Block() instances always have a defined metadata object. The current Block class
sets number, hash, status, content, proposer, and validation_data but leaves
attrs undefined despite the Record<string, any> | null type, so assign an
explicit initial value there. Update the constructor in Block to include attrs
alongside the other default fields and keep the initialization consistent with
the block metadata contract.

In `@src/libs/blockchain/routines/Sync.ts`:
- Around line 474-493: The fast sync flow in Sync should not persist the block
before tx/GCR work succeeds; `askTxsForBlock`, `verifyBlockAttrs`,
`syncGCRTables`, and the later tx insertion path must be treated as one atomic
unit. Refactor the logic around the block insertion/verification sequence so you
fetch and validate first, then commit the block, transactions, and GCR state
together, or roll back/abort on any failure. Use the existing `mergePeerlist`,
`askTxsForBlock`, `verifyBlockAttrs`, and `syncGCRTables` flow as the anchor
points, and apply the same atomicity change to the other sync block referenced
by the comment.
- Around line 1130-1144: The fast sync timeout handling in Sync.ts clears
fastSyncAborted too early because Promise.race returns before
syncLock.runExclusive(async () => fastSyncRoutine(peers)) has finished. Update
the fastSync path so the abort flag stays set until the locked sync task fully
settles, either by awaiting the runExclusive task after setting the timeout
state or by deferring the flag reset until the background work completes. Keep
the fix localized around fastSyncRoutine, syncLock.runExclusive, and the finally
cleanup that currently resets getSharedState.fastSyncAborted.

In `@src/libs/blockchain/routines/validateTransaction.ts`:
- Line 485: Remove the outdated assignNonce/expectedPrior docstrings so they
match the current nonce handling flow. Update the stale comments in the relevant
logic within mempool.ts, forkConfig.ts, and verifyGcrEdits.ts to describe the
current behavior, and ensure any references to nonce assignment or expected
prior state are aligned with the actual implementation in the affected
functions/classes.

In `@src/libs/blockchain/validation/verifyBlock.ts`:
- Around line 62-122: The verification in verifyBlock currently accepts
signatures from the full validator set returned by GCR.getGCRValidatorsAtBlock,
which can allow a quorum outside the block’s intended shard. Update verifyBlock
to load or recompute the expected shard for block.number, derive
validatorIdentities from that shard membership, and base the signature threshold
on the shard size rather than getSharedState.shardSize so only the correct
shard’s signers can satisfy validation.

In `@src/libs/l2ps/L2PSConsensus.ts`:
- Around line 466-526: The batch transaction object passed to
Chain.insertTransaction is incomplete because status is nested inside content,
while blockNumber and content.transaction_fee are missing, which can lead to
null metadata or serialization failures in Transaction.toRawTransaction. Update
the l1BatchTx shape in L2PSConsensus so all fields required by the raw
transaction serializer are present at the top level and inside content as
expected, including status, blockNumber, and transaction_fee. Verify the
transaction built in this block matches the full shape used by
Chain.insertTransaction before calling it.

---

Nitpick comments:
In `@src/libs/blockchain/gcr/gcr.ts`:
- Around line 154-171: getAccountNonces currently does a linear accounts.find()
for every pubkey inside the reduce, making the method quadratic for large
batches. Update the GCR.getAccountNonces flow to build a single lookup map from
the queried accounts first, then use that map during the pubkeys.reduce() so
each pubkey resolves in constant time while preserving the existing 0 default
for missing accounts.
- Around line 143-152: `GCR.getAccountNonce` is fetching the full `GCRMain`
entity when only `nonce` is needed in the `confirmTransaction` path. Update the
repository query in `getAccountNonce` to select only the `nonce` field,
following the same pattern used by `getAccountBalance`, and keep the fallback
behavior returning 0 when no row is found.

In `@src/libs/blockchain/mempool.ts`:
- Around line 309-328: Update the stale rationale comment near the nonce
validation in mempool addTransaction logic so it matches the current behavior.
The comment should describe the actual guard used by Mempool.addTransaction and
GCRNonceRoutines now that the pending mempool count and expectedPrior check are
gone, and should not mention the removed `account.nonce + 1 + pendingCount` rule
or the old consensus-side safeguard. Keep the explanation aligned with the
`txNonce <= accountNonce` rejection path and the returned invalid nonce error.

In `@src/libs/blockchain/routines/Sync.ts`:
- Around line 321-443: `verifyBlockAttrs` is too complex and needs to be split
into smaller units to pass the cognitive complexity gate. Extract the major
checks into dedicated helpers from `Sync.ts`—for example the transaction-count
validation, block-number validation, deterministic order comparison, and
attribute integrity checks—then have `verifyBlockAttrs` orchestrate them and
return the applied txs. Keep the existing `log.error` and `process.exit(1)`
behavior inside the helpers so the sync-integrity logic remains unchanged, and
use the existing symbols `verifyBlockAttrs`, `orderDeterministically`,
`Hashing.sha256`, and `TRANSACTION_STATUS.CONFIRMED` to guide the refactor.

In `@src/libs/blockchain/transaction.ts`:
- Around line 51-53: The Transaction model’s status field is too broad because
`TransactionStatus` is already a string type, so `string | TransactionStatus`
collapses the constraint and allows invalid values. Update the `status`
declaration in `Transaction` to `TransactionStatus | null` only, and normalize
any legacy string inputs before assigning them to `status` in the constructor or
related assignment path so only valid transaction statuses are stored.

In `@src/model/entities/Mempool.ts`:
- Line 1: MempoolTx.status is still typed too loosely, so it doesn’t enforce the
canonical transaction status set; update the Mempool entity to use
TransactionStatus directly for the status field. Keep any raw string-to-status
normalization at the deserialization boundary in the Mempool model rather than
widening the property type, and verify any constructors/parsers that assign
status still map legacy values before they reach MempoolTx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 259cdeca-3d61-4946-8b99-67cae1090f07

📥 Commits

Reviewing files that changed from the base of the PR and between 01ef0a0 and f8746ad.

📒 Files selected for processing (41)
  • data/snapshot/manifest.json
  • data/snapshot/validators.jsonl
  • scripts/run
  • src/config/defaults.ts
  • src/errors/codes.ts
  • src/libs/blockchain/block.ts
  • src/libs/blockchain/chain.ts
  • src/libs/blockchain/chainBlocks.ts
  • src/libs/blockchain/chainGenesis.ts
  • src/libs/blockchain/chainTransactions.ts
  • src/libs/blockchain/gcr/gcr.ts
  • src/libs/blockchain/gcr/gcr_routines/GCRNonceRoutines.ts
  • src/libs/blockchain/gcr/handleGCR.ts
  • src/libs/blockchain/mempool.ts
  • src/libs/blockchain/routines/Sync.ts
  • src/libs/blockchain/routines/validateTransaction.ts
  • src/libs/blockchain/transaction.ts
  • src/libs/blockchain/validation/txValidator.ts
  • src/libs/blockchain/validation/verifyBlock.ts
  • src/libs/blockchain/validation/verifyGcrEdits.ts
  • src/libs/communications/broadcastManager.ts
  • src/libs/consensus/v2/PoRBFT.ts
  • src/libs/consensus/v2/routines/broadcastBlockHash.ts
  • src/libs/consensus/v2/routines/deterministicOrder.ts
  • src/libs/consensus/v2/routines/getCommonValidatorSeed.ts
  • src/libs/consensus/v2/routines/getShard.ts
  • src/libs/consensus/v2/routines/manageProposeBlockHash.ts
  • src/libs/l2ps/L2PSConsensus.ts
  • src/libs/network/dtr/dtrmanager.ts
  • src/libs/network/endpointExecution.ts
  • src/libs/network/endpointValidation.ts
  • src/libs/network/handlers/identityHandlers.ts
  • src/libs/network/handlers/miscHandlers.ts
  • src/libs/network/manageGCRRoutines.ts
  • src/migrations/1782679232721-AddAttrsToTransactions.ts
  • src/migrations/1782679300000-AddAttrsToBlocks.ts
  • src/model/entities/Blocks.ts
  • src/model/entities/Mempool.ts
  • src/model/entities/Transactions.ts
  • src/utilities/constants.ts
  • src/utilities/sharedState.ts

Comment on lines +248 to +292
if (blockTxs.length > 0 && block.content.ordered_transactions.length > 0) {
// NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE):
// confirm array contains all the txs in the block
const blockTxsHashes = blockTxs.map(tx => tx.hash)
const blockOrderedTransactionsHashes =
block.content.ordered_transactions

if (
blockTxsHashes.every(hash =>
blockOrderedTransactionsHashes.includes(hash),
)
) {
transactionEntities = blockTxs
}

// NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE):
else {
log.error(
"Block transactions mismatch with block ordered transactions",
)
process.exit(1)
}

const status = new Set([
TRANSACTION_STATUS.CONFIRMED,
TRANSACTION_STATUS.FAILED,
])

// DEBUG: Confirm all transactions' blockNumber == block.number
// if (transactionEntities.some(tx => tx.blockNumber !== block.number)) {
// log.error(
// `[insertBlock] Transaction blockNumber mismatch: ${transactionEntities.map(tx => tx.blockNumber).join(", ")}`,
// )
// process.exit(1)
// }
// confirm all txs have a status set
const txsWithoutStatus = transactionEntities.filter(
tx => !status.has(tx.status),
)
if (txsWithoutStatus.length > 0) {
log.error(
"Transactions without status: " +
JSON.stringify(txsWithoutStatus, null, 2),
)
process.exit(1)
}

transactionEntities = transactionEntities.map(tx => ({
...tx,
blockNumber: block.number,
}))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Require an exact transaction set before saving non-empty blocks.

Line 248 skips validation when blockTxs is empty, and Lines 255-260 accept any subset of ordered_transactions. Since sync callers insert blocks with [] before fetching/verifying txs, a later tx/attrs/GCR failure can leave the head advanced while tx state is incomplete. Require an exact hash match before saving, or move sync tx fetching before insertBlock.

Proposed guard
-    if (blockTxs.length > 0 && block.content.ordered_transactions.length > 0) {
+    if (block.content.ordered_transactions.length > 0) {
         // NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE):
         // confirm array contains all the txs in the block
         const blockTxsHashes = blockTxs.map(tx => tx.hash)
         const blockOrderedTransactionsHashes =
             block.content.ordered_transactions
+        const blockTxByHash = new Map(blockTxs.map(tx => [tx.hash, tx]))
+        const orderedHashSet = new Set(blockOrderedTransactionsHashes)

         if (
-            blockTxsHashes.every(hash =>
-                blockOrderedTransactionsHashes.includes(hash),
-            )
+            blockTxs.length === blockOrderedTransactionsHashes.length &&
+            blockTxByHash.size === blockTxs.length &&
+            orderedHashSet.size === blockOrderedTransactionsHashes.length &&
+            blockOrderedTransactionsHashes.every(hash => blockTxByHash.has(hash))
         ) {
-            transactionEntities = blockTxs
+            transactionEntities = blockOrderedTransactionsHashes.map(hash =>
+                blockTxByHash.get(hash),
+            )
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/chainBlocks.ts` around lines 248 - 292, The block
transaction validation in chainBlocks.ts is too permissive because it only runs
when both arrays are non-empty and accepts any subset of ordered_transactions.
Update the transaction check around the blockTxs/ordered_transactions handling
so saving a non-empty block requires an exact hash match before proceeding, or
ensure the sync flow fetches and verifies transactions before insertBlock is
called. Use the existing blockTxs, block.content.ordered_transactions, and
transactionEntities logic to enforce the full set match before setting
blockNumber or continuing.

Comment on lines +271 to +274
const status = new Set([
TRANSACTION_STATUS.CONFIRMED,
TRANSACTION_STATUS.FAILED,
])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep failed transactions out of confirmed-only side effects.

Allowing FAILED rows is correct, but downstream side effects still use all transactionEntities: failed L2PS txs can reach persistConfirmedTransactionProjection, and failed identity tx hashes can update IdentityCommitment.blockNumber. Filter these projections to confirmed txs only.

Proposed filter
+                    const confirmedTransactionEntities =
+                        transactionEntities.filter(
+                            tx => tx.status === TRANSACTION_STATUS.CONFIRMED,
+                        )
+
-                    const l2psTxs = transactionEntities.filter(
+                    const l2psTxs = confirmedTransactionEntities.filter(
                         tx => tx.content?.type === "l2ps_hash_update",
                     )
-                const committedTxHashes = transactionEntities.map(tx => tx.hash)
+                const committedTxHashes = confirmedTransactionEntities.map(
+                    tx => tx.hash,
+                )

Also applies to: 401-419

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/chainBlocks.ts` around lines 271 - 274, The transaction
status set in chainBlocks.ts currently includes FAILED, which is fine for
ingestion but not for confirmed-only side effects. Update the downstream
projection logic in the affected handlers (including
persistConfirmedTransactionProjection and the IdentityCommitment blockNumber
update path) so they only operate on transactions whose status is CONFIRMED,
while keeping FAILED rows available elsewhere. Use the existing
transactionEntities/status handling in chainBlocks.ts to filter before applying
these confirmed-only updates.

Comment on lines +170 to 172
export function normalizePubkey(account: string | Uint8Array): string {
return typeof account === "string" ? account : forgeToHex(account)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the exported normalizer’s input contract.

The new deterministic-order consumer can pass from_ed25519_address directly; when that field is absent, this falls through to forgeToHex(undefined) and can crash ordering. Either make the helper reject/handle nullish input explicitly, or update callers to pass from_ed25519_address || from.

Safer caller pattern
-const sa = normalizePubkey(txa.content.from_ed25519_address)
-const sb = normalizePubkey(txb.content.from_ed25519_address)
+const sa = normalizePubkey(txa.content.from_ed25519_address || txa.content.from)
+const sb = normalizePubkey(txb.content.from_ed25519_address || txb.content.from)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/gcr/handleGCR.ts` around lines 170 - 172, The exported
normalizer normalizePubkey currently assumes a non-null string or Uint8Array and
can fall through to forgeToHex on undefined when callers use
from_ed25519_address directly. Update normalizePubkey to guard nullish input
explicitly by rejecting or safely handling it before calling forgeToHex, or
adjust the deterministic-order caller to always pass from_ed25519_address ||
from so the helper never receives undefined.

Comment on lines 793 to +800
if (confirmed.size > 0) {
log.error("Found confirmed txs during tx application")
// NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE):
// fetch confirmed txs from db
const confirmedTxs = await Chain.getTransactionsFromHashes(Array.from(confirmed))
log.error("Confirmed txs: " + JSON.stringify(Array.from(confirmed), null, 2))
log.error("Confirmed txs full: " + JSON.stringify(confirmedTxs.map(t => t.hash), null, 2))
process.exit(1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don’t terminate the node from this guard.

A duplicate confirmed tx should reject/abort the block-apply path, not call process.exit(1) from inside transaction application. Return a typed failure or throw a domain error so sync/consensus can handle it without killing the process.

Minimal safer direction
                 const confirmedTxs = await Chain.getTransactionsFromHashes(Array.from(confirmed))
                 log.error("Confirmed txs: " + JSON.stringify(Array.from(confirmed), null, 2))
                 log.error("Confirmed txs full: " + JSON.stringify(confirmedTxs.map(t => t.hash), null, 2))
-                process.exit(1)
+                throw new Error(
+                    "Confirmed transactions encountered during tx application: " +
+                        JSON.stringify(Array.from(confirmed)),
+                )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (confirmed.size > 0) {
log.error("Found confirmed txs during tx application")
// NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE):
// fetch confirmed txs from db
const confirmedTxs = await Chain.getTransactionsFromHashes(Array.from(confirmed))
log.error("Confirmed txs: " + JSON.stringify(Array.from(confirmed), null, 2))
log.error("Confirmed txs full: " + JSON.stringify(confirmedTxs.map(t => t.hash), null, 2))
process.exit(1)
if (confirmed.size > 0) {
log.error("Found confirmed txs during tx application")
// NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE):
// fetch confirmed txs from db
const confirmedTxs = await Chain.getTransactionsFromHashes(Array.from(confirmed))
log.error("Confirmed txs: " + JSON.stringify(Array.from(confirmed), null, 2))
log.error("Confirmed txs full: " + JSON.stringify(confirmedTxs.map(t => t.hash), null, 2))
throw new Error(
"Confirmed transactions encountered during tx application: " +
JSON.stringify(Array.from(confirmed)),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/gcr/handleGCR.ts` around lines 793 - 800, The
confirmed-transaction guard in handleGCR should not terminate the process with
process.exit(1); instead, make this branch fail the block-apply flow by
returning a typed failure or throwing a domain-specific error that
sync/consensus can catch. Update the logic around confirmed.size,
Chain.getTransactionsFromHashes, and the surrounding tx application path so the
duplicate-confirmed-tx condition aborts application cleanly without killing the
node.

const SYSTEM_RELAY_TX_TYPES = new Set<string>(["l2psBatch"])

export default class Mempool {
public static lock = new Mutex()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Mark the lock readonly.

lock is never reassigned; flagging it readonly matches the SonarCloud finding and prevents an accidental reassignment from silently dropping mutual exclusion for every runExclusive caller.

Proposed fix
-    public static lock = new Mutex()
+    public static readonly lock = new Mutex()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static lock = new Mutex()
public static readonly lock = new Mutex()
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 39-39: Make this public static property readonly.

See more on https://sonarcloud.io/project/issues?id=kynesyslabs_node&issues=AZ8UKzBQlrQJlDPZQO9R&open=AZ8UKzBQlrQJlDPZQO9R&pullRequest=950

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/mempool.ts` at line 39, The shared Mutex on Mempool.lock
is never reassigned, so update the Mempool class to make lock readonly and keep
the existing static mutex instance intact. Use the Mempool.lock declaration as
the only place to change, ensuring runExclusive callers continue to use the same
mutual exclusion guard and accidental reassignment is prevented.

Source: Linters/SAST tools

Comment on lines +28 to +30
response.extra = {
message: error instanceof Error ? error.message : String(error),
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Preserve object errors in the RPC payload.

Line 29 falls back to String(error), so non-Error throws degrade to "[object Object]" and you lose the real failure details.

Suggested fix
             response.extra = {
-                message: error instanceof Error ? error.message : String(error),
+                message:
+                    error instanceof Error
+                        ? error.message
+                        : typeof error === "object" && error !== null
+                          ? JSON.stringify(error)
+                          : String(error),
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
response.extra = {
message: error instanceof Error ? error.message : String(error),
}
response.extra = {
message:
error instanceof Error
? error.message
: typeof error === "object" && error !== null
? JSON.stringify(error)
: String(error),
}
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 29-29: 'error' will use Object's default stringification format ('[object Object]') when stringified.

See more on https://sonarcloud.io/project/issues?id=kynesyslabs_node&issues=AZ8UKy_nlrQJlDPZQO9P&open=AZ8UKy_nlrQJlDPZQO9P&pullRequest=950

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/network/handlers/identityHandlers.ts` around lines 28 - 30, The RPC
error payload in identityHandlers should not stringify non-Error thrown values,
because that turns object errors into "[object Object]" and hides details.
Update the error handling around response.extra to preserve the original object
when error is not an Error, and only use error.message for real Error instances.
Keep the fix localized to the identityHandlers response building logic so the
payload retains the full thrown value for downstream consumers.

Source: Linters/SAST tools

Comment on lines +236 to +242
const isSafeError = safeEthosErrors.some(safe =>
errorMsg.includes(safe),
)
response.extra = {
error: isSafeError ? errorMsg : "Failed to fetch Ethos score",
error: isSafeError
? errorMsg
: "Failed to fetch Ethos score",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Use exact matches for safe Ethos errors.

includes reflects any message containing a safe phrase, including appended internal details. Keep the sanitizer strict by returning only exact allow-listed messages.

Proposed fix
-                const isSafeError = safeEthosErrors.some(safe =>
-                    errorMsg.includes(safe),
-                )
+                const isSafeError = safeEthosErrors.includes(errorMsg)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isSafeError = safeEthosErrors.some(safe =>
errorMsg.includes(safe),
)
response.extra = {
error: isSafeError ? errorMsg : "Failed to fetch Ethos score",
error: isSafeError
? errorMsg
: "Failed to fetch Ethos score",
const isSafeError = safeEthosErrors.includes(errorMsg)
response.extra = {
error: isSafeError
? errorMsg
: "Failed to fetch Ethos score",

Comment on lines +265 to +268
if (block.number <= getSharedState.lastBlockNumber) {
response.result = 200
response.response = "Block is already processed"
break

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the dedupe response contract.

This fast path returns a plain string, while BroadcastManager.handleNewBlock returns the structured “already processed” payload with syncData. Peers hitting only this path can miss sync metadata.

Proposed fix
             if (block.number <= getSharedState.lastBlockNumber) {
-                response.result = 200
-                response.response = "Block is already processed"
+                response.response = await BroadcastManager.handleNewBlock(
+                    sender,
+                    block,
+                )
                 break
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (block.number <= getSharedState.lastBlockNumber) {
response.result = 200
response.response = "Block is already processed"
break
if (block.number <= getSharedState.lastBlockNumber) {
response.response = await BroadcastManager.handleNewBlock(
sender,
block,
)
break
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/network/manageGCRRoutines.ts` around lines 265 - 268, The fast-path
dedupe branch in manageGCRRoutines should return the same “already processed”
response shape as BroadcastManager.handleNewBlock, not a plain string. Update
the block.number <= getSharedState.lastBlockNumber path to populate the
structured payload including syncData so peers receive the same contract
regardless of which dedupe path handles the block. Make the change in the logic
around getSharedState.lastBlockNumber and keep the response consistent with the
existing already-processed handling.

Comment on lines +33 to +34
@Column("jsonb", { name: "attrs", nullable: true })
attrs: Record<string, any> | null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Persist attrs on the overwrite path too.

src/libs/blockchain/chainBlocks.ts:195-491 copies block.attrs into newBlock, but the existingBlock && position branch still never updates existingBlock.attrs. Any rewrite of an existing block row will keep stale metadata even though this entity now exposes the column.

Minimal fix
         existingBlock.status = block.status
         existingBlock.proposer = block.proposer
         existingBlock.validation_data = block.validation_data
+        existingBlock.attrs = block.attrs
         log.info("about to save block")
         return await blocksRepo.save(existingBlock)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/model/entities/Blocks.ts` around lines 33 - 34, The overwrite path in
chainBlocks should also persist attrs, since the existingBlock && position
branch currently only copies block.attrs into newBlock and leaves the stored row
stale. Update the block rewrite logic in chainBlocks so the existing block
entity’s attrs field is assigned from the incoming block before saving, keeping
it consistent with the Blocks entity’s attrs column.

Comment on lines +495 to +508
const consensusInfo: Record<string, any> = {}
const secman = SecretaryManager.getInstance()

if (secman && secman.shard && (secman.shard.members || []).length > 0) {
const cvsa = await getCommonValidatorSeed()
consensusInfo.blockRef = secman.shard.blockRef
consensusInfo.cvsa = cvsa.commonValidatorSeed
consensusInfo.cvsaBlockRef = cvsa.lastBlockNumber
consensusInfo.secretary = secman.secretary.identity
consensusInfo.shard = secman.shard.members.map(m => ({
identity: m.identity,
url: m.connection.string + "/info",
}))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Make consensusInfo best-effort.

getCommonValidatorSeed() can throw when the chain head/genesis is unavailable, and secman.secretary.identity assumes secretary initialization. Don’t let optional consensus metadata fail /info.

Proposed guard
-        if (secman && secman.shard && (secman.shard.members || []).length > 0) {
-            const cvsa = await getCommonValidatorSeed()
-            consensusInfo.blockRef = secman.shard.blockRef
-            consensusInfo.cvsa = cvsa.commonValidatorSeed
-            consensusInfo.cvsaBlockRef = cvsa.lastBlockNumber
-            consensusInfo.secretary = secman.secretary.identity
-            consensusInfo.shard = secman.shard.members.map(m => ({
-                identity: m.identity,
-                url: m.connection.string + "/info",
-            }))
+        try {
+            if (secman?.shard?.members?.length) {
+                const cvsa = await getCommonValidatorSeed()
+                consensusInfo.blockRef = secman.shard.blockRef
+                consensusInfo.cvsa = cvsa.commonValidatorSeed
+                consensusInfo.cvsaBlockRef = cvsa.lastBlockNumber
+                consensusInfo.secretary = secman.secretary?.identity ?? null
+                consensusInfo.shard = secman.shard.members.map(m => ({
+                    identity: m.identity,
+                    url: m.connection.string + "/info",
+                }))
+            }
+        } catch (error) {
+            log.debug(
+                `[SharedState.getInfo] consensusInfo unavailable: ${
+                    error instanceof Error ? error.message : String(error)
+                }`,
+            )
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const consensusInfo: Record<string, any> = {}
const secman = SecretaryManager.getInstance()
if (secman && secman.shard && (secman.shard.members || []).length > 0) {
const cvsa = await getCommonValidatorSeed()
consensusInfo.blockRef = secman.shard.blockRef
consensusInfo.cvsa = cvsa.commonValidatorSeed
consensusInfo.cvsaBlockRef = cvsa.lastBlockNumber
consensusInfo.secretary = secman.secretary.identity
consensusInfo.shard = secman.shard.members.map(m => ({
identity: m.identity,
url: m.connection.string + "/info",
}))
}
const consensusInfo: Record<string, any> = {}
const secman = SecretaryManager.getInstance()
try {
if (secman?.shard?.members?.length) {
const cvsa = await getCommonValidatorSeed()
consensusInfo.blockRef = secman.shard.blockRef
consensusInfo.cvsa = cvsa.commonValidatorSeed
consensusInfo.cvsaBlockRef = cvsa.lastBlockNumber
consensusInfo.secretary = secman.secretary?.identity ?? null
consensusInfo.shard = secman.shard.members.map(m => ({
identity: m.identity,
url: m.connection.string + "/info",
}))
}
} catch (error) {
log.debug(
`[SharedState.getInfo] consensusInfo unavailable: ${
error instanceof Error ? error.message : String(error)
}`,
)
}
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 498-498: Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=kynesyslabs_node&issues=AZ8UKzIRlrQJlDPZQO9p&open=AZ8UKzIRlrQJlDPZQO9p&pullRequest=950

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utilities/sharedState.ts` around lines 495 - 508, Make consensusInfo
best-effort in sharedState’s /info path: the getCommonValidatorSeed() call and
secman.secretary.identity access can throw when consensus or secretary state is
unavailable. Update the consensusInfo block to safely read SecretaryManager data
and wrap or guard the common validator seed lookup so the rest of the /info
response still succeeds, leaving optional fields unset when unavailable.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/libs/peer/PeerManager.ts (1)

398-415: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Gate recursive discovery before creating promises.

With waitForDiscovery=false, callers still start recursive hello RPCs and return before those promises settle. Also, the true path waits only one level because child calls omit waitForDiscovery.

Proposed fix
-        if (newPeersUnfiltered.length === 0) {
+        if (newPeersUnfiltered.length === 0 || !waitForDiscovery) {
             return
         }

         // INFO: Recursively say hello to the new peers
         const peerManager = PeerManager.getInstance()
         const newPeers = newPeersUnfiltered.filter(
             ({ publicKey }) => !peerManager.getPeer(publicKey),
         )

-        const promise = newPeers.map(peer =>
-            PeerManager.sayHelloToPeer(new Peer(peer.url, peer.publicKey)),
-        )
-
-        if (waitForDiscovery) {
-            await Promise.all(promise)
-        }
+        await Promise.all(
+            newPeers.map(peer =>
+                PeerManager.sayHelloToPeer(
+                    new Peer(peer.url, peer.publicKey),
+                    true,
+                ),
+            ),
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/peer/PeerManager.ts` around lines 398 - 415, Gate the recursive
discovery flow in PeerManager’s peer-hello logic so recursion only happens when
waitForDiscovery is true; currently the Promise list is created in all cases,
which still fires nested hello RPCs even when callers opted out. Update the code
around the newPeers filtering and PeerManager.sayHelloToPeer calls to
build/await those recursive promises only inside the waitForDiscovery branch,
and ensure the recursive invocation propagates waitForDiscovery so the true path
waits through child discoveries instead of stopping after one level.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/libs/peer/PeerManager.ts`:
- Around line 398-415: Gate the recursive discovery flow in PeerManager’s
peer-hello logic so recursion only happens when waitForDiscovery is true;
currently the Promise list is created in all cases, which still fires nested
hello RPCs even when callers opted out. Update the code around the newPeers
filtering and PeerManager.sayHelloToPeer calls to build/await those recursive
promises only inside the waitForDiscovery branch, and ensure the recursive
invocation propagates waitForDiscovery so the true path waits through child
discoveries instead of stopping after one level.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d8f8a5c6-3359-453c-a357-1023e4316d33

📥 Commits

Reviewing files that changed from the base of the PR and between f8746ad and a53b34e.

📒 Files selected for processing (3)
  • src/libs/consensus/v2/routines/getShard.ts
  • src/libs/network/manageHelloPeer.ts
  • src/libs/peer/PeerManager.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/libs/consensus/v2/routines/getShard.ts

Comment on lines +158 to +173
for (const hash of txHashes) {
const tx = consensusTxMap.get(hash)

if (tx) {
mempoolTxs.push(tx)

// NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE):
if (!status.has(tx.status)) {
log.error(
"Transaction status mismatch: " +
hash +
" " +
tx.status,
)
process.exit(1)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 process.exit(1) fires on any RPC call during the consensus voting window

The getBlockTransactions function checks !status.has(tx.status) and calls process.exit(1) if a transaction's status is not CONFIRMED or FAILED. However, txMap is populated (in consensusRoutine) before forgeBlock sets candidateBlock, and tx statuses are only assigned later — after the block receives enough votes and applyGCREditsFromMergedMempool runs. During the entire voting window between forgeBlock and GCR application, any valid client that calls getBlockTransactions(candidateBlock.hash) will find entries in txMap with null status and immediately crash the node via process.exit(1). This is labeled "NODE_CRITICAL_DEBUG (DO NOT REMOVE COMMENTED OUT CODE)" but the check is live, not commented out.

Comment on lines +23 to +24
const sa = normalizePubkey(txa.content.from_ed25519_address)
const sb = normalizePubkey(txb.content.from_ed25519_address)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 normalizePubkey receives undefined whenever from_ed25519_address is not set. The type guard inside normalizePubkey treats anything that is not a string as a Uint8Array and calls forgeToHex on it, which throws on null/undefined. The old implementation used tx.content?.from with a full defensive coercion that handled every possible buffer shape. handleGCR.ts still uses the fallback from_ed25519_address || from, so the pattern for safe access is already established — apply the same guard here to avoid a crash in consensus or sync for any transaction where the ed25519 address field is absent.

Suggested change
const sa = normalizePubkey(txa.content.from_ed25519_address)
const sb = normalizePubkey(txb.content.from_ed25519_address)
const sa = normalizePubkey(txa.content.from_ed25519_address || txa.content.from)
const sb = normalizePubkey(txb.content.from_ed25519_address || txb.content.from)

Comment on lines +116 to +122
const threshold = Math.floor((getSharedState.shardSize * 2) / 3) + 1
if (verifiedSigners.size < threshold) {
return {
valid: false,
reason: `insufficient verified signatures: ${verifiedSigners.size}/${getSharedState.shardSize} (need ${threshold})`,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Quorum threshold uses current shardSize instead of actual validator count

The threshold is computed against getSharedState.shardSize (a static runtime config value) rather than validatorIdentities.size (the height-stable resolved eligible-validator count). This was explicitly disabled in the previous revision with the comment "BECAUSE WE CAN'T VERIFY THE VALIDATORS WERE ONLINE TO FORGE THIS BLOCK — THAT CAUSES REJECTION FOR ALL BLOCKS FORGED IN THE EARLY PHASES OF THE NETWORK". Re-enabling the check without adjusting for historical blocks means any node syncing from genesis that encounters a block forged during an era when fewer validators than Math.floor(shardSize * 2 / 3) + 1 were registered will hit { valid: false } at line 117-122, which upstream in syncBlock returns false (sync failure), permanently stalling that node at that height. The correct base for the threshold is validatorIdentities.size — the same set already resolved height-stably above — which is what the old commented-out threshold code used.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/libs/blockchain/mempool.ts (1)

309-315: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve duplicate-nonce rejection under the future-nonce policy.

The locked re-check now only rejects txNonce <= accountNonce, so two transactions from the same sender with the same future nonce can both pass after serializing through the advisory lock. Keep accepting nonce gaps, but also reject an existing pending transaction for the same sender and nonce before save.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/mempool.ts` around lines 309 - 315, The nonce validation
in mempool handling now only checks against the on-chain account nonce, so
duplicate future nonces can slip through after the advisory lock. In the same
flow around the account lookup in mempool.ts, add an additional
pending-transaction check for the sender and txNonce before calling save, while
still allowing nonce gaps greater than accountNonce. Use the existing
senderFrom, txNonce, and GCRMain/repository logic to reject duplicates
consistently under the future-nonce policy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/libs/blockchain/mempool.ts`:
- Around line 309-315: The nonce validation in mempool handling now only checks
against the on-chain account nonce, so duplicate future nonces can slip through
after the advisory lock. In the same flow around the account lookup in
mempool.ts, add an additional pending-transaction check for the sender and
txNonce before calling save, while still allowing nonce gaps greater than
accountNonce. Use the existing senderFrom, txNonce, and GCRMain/repository logic
to reject duplicates consistently under the future-nonce policy.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59ce8656-4575-4af6-be9a-7c9903e48a26

📥 Commits

Reviewing files that changed from the base of the PR and between ba44dec and dc0046f.

📒 Files selected for processing (12)
  • data/snapshot/gcr_main.jsonl
  • data/snapshot/gcr_storageprogram.jsonl
  • data/snapshot/identity_commitments.jsonl
  • data/snapshot/manifest.json
  • data/snapshot/validators.jsonl
  • src/libs/blockchain/chainBlocks.ts
  • src/libs/blockchain/chainDb.ts
  • src/libs/blockchain/chainTransactions.ts
  • src/libs/blockchain/gcr/handleGCR.ts
  • src/libs/blockchain/genesis/restoreSnapshot.ts
  • src/libs/blockchain/mempool.ts
  • src/libs/consensus/v2/PoRBFT.ts
✅ Files skipped from review due to trivial changes (1)
  • data/snapshot/validators.jsonl
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/libs/blockchain/gcr/handleGCR.ts
  • src/libs/blockchain/chainTransactions.ts
  • src/libs/blockchain/chainBlocks.ts
  • src/libs/consensus/v2/PoRBFT.ts

+ try: shuffle txs list before sort to test deterministic sort

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/libs/blockchain/routines/Sync.ts (2)

333-341: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Compute missing transactions by hash, not by list length.

A peer can return the right count with the wrong hashes or extra transactions, skipping the signer fallback and driving verifyBlockAttrs into a fatal mismatch. Build the expected-hash set first, discard extras, then fetch any missing hashes.

🐛 Proposed fix
-    if (txs.length < block.content.ordered_transactions.length) {
+    const masterSet = new Set(block.content.ordered_transactions)
+    const txByHash = new Map(
+        txs.filter(tx => masterSet.has(tx.hash)).map(tx => [tx.hash, tx]),
+    )
+    const missingTxs = new Set(
+        [...masterSet].filter(hash => !txByHash.has(hash)),
+    )
+
+    if (missingTxs.size > 0) {
         log.error(
             "[fastSync] No transactions received for block: " + block.hash,
         )
         log.debug("Trying to get transaction from signers")
-
-        const masterSet = new Set(block.content.ordered_transactions)
-        const missingTxs = masterSet.difference(new Set(txs.map(tx => tx.hash)))
             for (const tx of res) {
                 if (missingTxs.has(tx.hash)) {
-                    txs.push(tx)
+                    txByHash.set(tx.hash, tx)
                     missingTxs.delete(tx.hash)
                 }
             }
         if (missingTxs.size > 0) {
             ...
             process.exit(1)
         }
     }
+
+    txs = block.content.ordered_transactions.map(hash => txByHash.get(hash)!)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/routines/Sync.ts` around lines 333 - 341, The fast-sync
fallback in Sync.ts is currently gated only on tx count, which can miss cases
where the peer returns the wrong hashes or extra transactions. Update the
transaction reconciliation logic in the block-processing path to build the
expected hash set from block.content.ordered_transactions first, compare against
the returned tx hashes, discard extras, and compute missingTxs strictly by hash
before deciding whether to query signers. Keep the existing logging and signer
fallback flow in the same area, but trigger it based on hash mismatches rather
than txs.length.

522-538: 🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Verify attrs and transactions before inserting the block.

Chain.insertBlock(...) runs before askTxsForBlock(...) and verifyBlockAttrs(...). If attrs verification exits or fails afterward, the node can restart with the bad block already persisted and skip it via the existence check.

🐛 Safer flow outline
-    await Chain.insertBlock(block, [])
-    log.debug("Block inserted successfully")
+    const txs = await askTxsForBlock(block, peer)
+    const applied = await verifyBlockAttrs(block, txs)
+
+    await Chain.insertBlock(block, [])
+    log.debug("Block inserted successfully")

Apply the same ordering in batchDownloadBlocks: fetch/resolve block transactions, run verifyBlockAttrs, then insert the block. Ideally, commit block, tx, and GCR updates in one storage transaction or rollback on any later failure.

Also applies to: 785-793

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/routines/Sync.ts` around lines 522 - 538, Move the block
persistence step in Sync's fast sync flow so `Chain.insertBlock(...)` happens
only after `askTxsForBlock(...)` and `verifyBlockAttrs(...)` succeed, using the
same ordering already expected in `batchDownloadBlocks`. Locate the sequence
around the fastSync block handling in `Sync.ts` and reorder it to fetch
transactions, validate attributes, then insert the block; keep the peer merge
and logging around the validated flow, and prefer a single storage transaction
or rollback-safe path if later updates fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/libs/blockchain/routines/Sync.ts`:
- Around line 515-520: The duplicate-block recheck in Sync should not be treated
as a failure when another path inserts the same block during verification. In
the Sync routine’s second existence check around Chain.getBlockByNumber and the
“Block already exists, skipping ...” branch, change the return path so the
concurrent duplicate insert is treated as a successful no-op instead of a sync
failure, and apply the same handling in the other matching duplicate-check
block.
- Around line 48-55: Remove the test-only shuffle from the production sync path
by eliminating the use of shuffleArray in Sync and restoring deterministic
ordering in the block verification flow. Update the relevant sync routine in
Sync.ts, especially the code around the block verification sequence that
currently depends on Math.random(), so it processes items in a stable order
instead of shuffling them. Ensure any calls or helpers tied to shuffleArray are
removed or replaced with deterministic logic.

---

Outside diff comments:
In `@src/libs/blockchain/routines/Sync.ts`:
- Around line 333-341: The fast-sync fallback in Sync.ts is currently gated only
on tx count, which can miss cases where the peer returns the wrong hashes or
extra transactions. Update the transaction reconciliation logic in the
block-processing path to build the expected hash set from
block.content.ordered_transactions first, compare against the returned tx
hashes, discard extras, and compute missingTxs strictly by hash before deciding
whether to query signers. Keep the existing logging and signer fallback flow in
the same area, but trigger it based on hash mismatches rather than txs.length.
- Around line 522-538: Move the block persistence step in Sync's fast sync flow
so `Chain.insertBlock(...)` happens only after `askTxsForBlock(...)` and
`verifyBlockAttrs(...)` succeed, using the same ordering already expected in
`batchDownloadBlocks`. Locate the sequence around the fastSync block handling in
`Sync.ts` and reorder it to fetch transactions, validate attributes, then insert
the block; keep the peer merge and logging around the validated flow, and prefer
a single storage transaction or rollback-safe path if later updates fail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5cf37eb8-965a-40ae-af5e-082de7886b06

📥 Commits

Reviewing files that changed from the base of the PR and between dc0046f and 637f27a.

📒 Files selected for processing (5)
  • src/libs/blockchain/chain.ts
  • src/libs/blockchain/chainBlocks.ts
  • src/libs/blockchain/routines/Sync.ts
  • src/libs/blockchain/validation/verifyBlock.ts
  • src/libs/consensus/v2/routines/deterministicOrder.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/libs/blockchain/chain.ts
  • src/libs/blockchain/validation/verifyBlock.ts
  • src/libs/blockchain/chainBlocks.ts

Comment on lines +48 to +55
function shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array]
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
}
return shuffled
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove the test shuffle from the production sync path.

Line 388 explicitly says this must be removed before merging. Keeping Math.random() in block verification can make valid blocks fail nondeterministically if deterministic ordering has any tie case.

🧹 Proposed cleanup
-function shuffleArray<T>(array: T[]): T[] {
-    const shuffled = [...array]
-    for (let i = shuffled.length - 1; i > 0; i--) {
-        const j = Math.floor(Math.random() * (i + 1))
-        ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
-    }
-    return shuffled
-}
-    // for testing, shuffle list
-    // TODO: Remove this before merging
-    txs = shuffleArray(txs)
     const sorted = orderDeterministically(txs)

Also applies to: 387-389

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 51-51: Make sure that using this pseudorandom number generator is safe here.

See more on https://sonarcloud.io/project/issues?id=kynesyslabs_node&issues=AZ8ZUSGdv-TeQgfFr-Lu&open=AZ8ZUSGdv-TeQgfFr-Lu&pullRequest=950

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/routines/Sync.ts` around lines 48 - 55, Remove the
test-only shuffle from the production sync path by eliminating the use of
shuffleArray in Sync and restoring deterministic ordering in the block
verification flow. Update the relevant sync routine in Sync.ts, especially the
code around the block verification sequence that currently depends on
Math.random(), so it processes items in a stable order instead of shuffling
them. Ensure any calls or helpers tied to shuffleArray are removed or replaced
with deterministic logic.

Source: Linters/SAST tools

Comment on lines +515 to +520
// check exists again
exists = await Chain.getBlockByNumber(block.number)
if (exists) {
log.error("Block already exists, skipping ...")
return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat a concurrent duplicate insert as success.

The first existence check returns true, but the second check returns false for the same final state. If another guarded path inserts the block during verification, this reports a sync failure even though the chain already has the block.

🐛 Proposed fix
     if (exists) {
-        log.error("Block already exists, skipping ...")
-        return false
+        log.debug("Block already exists, skipping ...")
+        return true
     }

Also applies to: 778-783

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/routines/Sync.ts` around lines 515 - 520, The
duplicate-block recheck in Sync should not be treated as a failure when another
path inserts the same block during verification. In the Sync routine’s second
existence check around Chain.getBlockByNumber and the “Block already exists,
skipping ...” branch, change the return path so the concurrent duplicate insert
is treated as a successful no-op instead of a sync failure, and apply the same
handling in the other matching duplicate-check block.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants