Skip to content

feat: Implement balance checks for L2PS transactions to ensure suffic…#680

Open
Shitikyan wants to merge 2 commits intotestnetfrom
fix-l2ps-balance-check
Open

feat: Implement balance checks for L2PS transactions to ensure suffic…#680
Shitikyan wants to merge 2 commits intotestnetfrom
fix-l2ps-balance-check

Conversation

@Shitikyan
Copy link
Contributor

@Shitikyan Shitikyan commented Feb 19, 2026

…ient funds before processing

Summary by CodeRabbit

Release Notes

  • New Features

    • Transactions now include balance validation to verify sufficient funds before finalization
    • Added balance verification for Layer 2 protocol encrypted transactions
  • Bug Fixes

    • Improved error reporting for insufficient balance scenarios
    • Enhanced error logging for failed transaction broadcasts

@qodo-code-review
Copy link
Contributor

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Review Summary by Qodo

Implement balance checks for L2PS and regular transactions
✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Add balance validation for L2PS encrypted transactions before mempool insertion
• Implement sender balance checks for regular transactions to prevent insufficient funds
• Export L2PS_TX_FEE constant for reuse across modules
• Add error logging for failed broadcast transactions

Grey Divider

File Changes

1. src/libs/blockchain/routines/validateTransaction.ts ✨ Enhancement +76/-0

Add balance validation for transactions

• Added balance check for regular transactions before processing
• Implemented checkL2PSBalance() function to decrypt and validate L2PS transaction balances
• Added imports for L2PS-related modules and types
• Returns validation error if sender has insufficient funds

src/libs/blockchain/routines/validateTransaction.ts


2. src/libs/l2ps/L2PSTransactionExecutor.ts ✨ Enhancement +1/-1

Export L2PS transaction fee constant

• Exported L2PS_TX_FEE constant to make it accessible to other modules

src/libs/l2ps/L2PSTransactionExecutor.ts


3. src/libs/network/manageExecution.ts Error handling +3/-0

Add error logging for failed broadcasts

• Added error logging when broadcastTx fails to help with debugging
• Logs result code, extra data, and response details on failure

src/libs/network/manageExecution.ts


View more (1)
4. src/libs/network/routines/transactions/handleL2PS.ts ✨ Enhancement +41/-2

Add balance pre-check for L2PS transactions

• Added import for INativePayload type
• Imported L2PS_TX_FEE from L2PSTransactionExecutor
• Implemented checkSenderBalance() function to validate balance before mempool insertion
• Added pre-check balance validation call before processing valid L2PS transactions

src/libs/network/routines/transactions/handleL2PS.ts


Grey Divider

Qodo Logo

@sonarqubecloud
Copy link

@qodo-code-review
Copy link
Contributor

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Logging can throw on failure 🐞 Bug ⛯ Reliability
Description
In broadcastTx failure handling, manageExecution JSON.stringifies returnValue.extra; if extra is
a non-serializable object (e.g., contains circular refs or BigInt), the log statement can throw and
break the error-handling path (client may not receive the intended error response).
Code

src/libs/network/manageExecution.ts[R82-84]

+                if (!result.success) {
+                    log.error(`[SERVER] broadcastTx FAILED — returning to client: result=${returnValue.result}, extra=${JSON.stringify(returnValue.extra)}, response.extra=${result.response?.extra}`)
+                }
Evidence
manageExecution performs JSON.stringify(returnValue.extra) in the failure branch.
returnValue.extra is assigned from result.extra, and handleExecuteTransaction sometimes sets
result.extra to an object, meaning it is not guaranteed to be safely JSON-serializable.

src/libs/network/manageExecution.ts[69-85]
src/libs/network/endpointHandlers.ts[419-443]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`manageExecution` logs `broadcastTx` failures using `JSON.stringify(returnValue.extra)`. Because `extra` can be an object (and potentially non-JSON-serializable), the logging line can throw and break the error path.

### Issue Context
`returnValue.extra` is populated from `result.extra`, and `handleExecuteTransaction` assigns objects to `result.extra` in multiple branches.

### Fix Focus Areas
- src/libs/network/manageExecution.ts[69-85]

### Suggested fix
- Wrap the stringify in a try/catch and fall back to a safe representation.
- Optionally implement a small `safeStringify` helper that:
 - converts `bigint` to string
 - handles circular references (or falls back to `util.inspect`)
 - truncates overly large outputs

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. L2PS fee/balance check inconsistent 🐞 Bug ✓ Correctness
Description
The new L2PS balance pre-checks always add L2PS_TX_FEE (even when the executor does not charge any
fee for that tx type), and they don’t validate sendAmount like the executor does. This can
incorrectly reject L2PS transactions that would otherwise execute successfully (e.g., non-send
native ops, or non-native txs with only gcr_edits) and can also produce misleading errors/behavior
when the amount is malformed.
Code

src/libs/network/routines/transactions/handleL2PS.ts[R80-102]

+async function checkSenderBalance(decryptedTx: Transaction): Promise<string | null> {
+    const sender = decryptedTx.content.from as string
+    if (!sender) return "Missing sender address in decrypted transaction"
+
+    // Extract amount from native payload
+    let amount = 0
+    if (decryptedTx.content.type === "native" && Array.isArray(decryptedTx.content.data)) {
+        const nativePayload = decryptedTx.content.data[1] as INativePayload
+        if (nativePayload?.nativeOperation === "send") {
+            const [, sendAmount] = nativePayload.args as [string, number]
+            amount = sendAmount || 0
+        }
+    }
+
+    const totalRequired = amount + L2PS_TX_FEE
+    try {
+        const balance = await L2PSTransactionExecutor.getBalance(sender)
+        if (balance < BigInt(totalRequired)) {
+            return `Insufficient balance: need ${totalRequired} (${amount} + ${L2PS_TX_FEE} fee) but have ${balance}`
+        }
+    } catch (error) {
+        return `Balance check failed: ${error instanceof Error ? error.message : "Unknown error"}`
+    }
Evidence
checkSenderBalance (and the similar checkL2PSBalance in confirmTransaction) always requires
amount + L2PS_TX_FEE even when the executor’s logic only burns/checks the fee for `nativeOperation
=== "send". The executor also validates amount` as a finite integer > 0, but the new pre-check
does not. Therefore the pre-check and executor can disagree: pre-check may reject while executor
would accept, or pre-check may crash/skip in edge cases.

src/libs/network/routines/transactions/handleL2PS.ts[80-102]
src/libs/blockchain/routines/validateTransaction.ts[163-176]
src/libs/l2ps/L2PSTransactionExecutor.ts[207-220]
src/libs/l2ps/L2PSTransactionExecutor.ts[158-189]
src/libs/l2ps/L2PSTransactionExecutor.ts[260-266]
src/libs/l2ps/L2PSTransactionExecutor.ts[435-438]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`checkSenderBalance` / `checkL2PSBalance` currently require `L2PS_TX_FEE` for all decrypted L2PS transactions, but the executor only checks/burns the fee for `nativeOperation === &quot;send&quot;` and may accept other tx types without fees. This mismatch can cause false rejections. Additionally, the pre-check doesn’t validate the extracted `sendAmount` like the executor does.

### Issue Context
- Executor fee+amount checks are implemented in `handleNativeTransaction` for `nativeOperation === &quot;send&quot;`.
- Other tx types (non-native with `gcr_edits`, or unknown native operations) do not burn/check a fee.

### Fix Focus Areas
- src/libs/network/routines/transactions/handleL2PS.ts[80-102]
- src/libs/blockchain/routines/validateTransaction.ts[145-176]
- src/libs/l2ps/L2PSTransactionExecutor.ts[158-220]

### Suggested fix
1. Decide on policy:
  - **Option A (match current executor):** Only add `L2PS_TX_FEE` to `totalRequired` for txs where the executor actually burns/charges the fee (currently native `send`).
  - **Option B (enforce fee for all L2PS txs):** Update `L2PSTransactionExecutor.generateGCREdits`/handlers to also burn/check the fee for other tx categories, then keep pre-check consistent.
2. Validate amount exactly as executor does before BigInt conversion:
  - `typeof sendAmount === &#x27;number&#x27; &amp;&amp; Number.isFinite(sendAmount) &amp;&amp; Number.isInteger(sendAmount) &amp;&amp; sendAmount &gt; 0`
3. Compute required balance using BigInt arithmetic:
  - `const required = BigInt(sendAmount) + BigInt(L2PS_TX_FEE)`
  - Avoid `BigInt(amount + fee)`.
4. Consider extracting a shared helper (e.g., in `L2PSTransactionExecutor`) to compute required balance so pre-check and executor cannot drift.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Double L2PS decrypt work 🐞 Bug ➹ Performance
Description
confirmTransaction now decrypts L2PS encrypted transactions for balance pre-check, but handleL2PS
decrypts again during processing. This duplicates expensive cryptographic work and config loading
for clients using confirmTx + broadcastTx flows, increasing latency and DoS surface.
Code

src/libs/blockchain/routines/validateTransaction.ts[R151-160]

+        const parallelNetworks = ParallelNetworks.getInstance()
+        let l2psInstance = await parallelNetworks.getL2PS(l2psUid)
+        if (!l2psInstance) {
+            l2psInstance = await parallelNetworks.loadL2PS(l2psUid)
+        }
+        if (!l2psInstance) return null // No L2PS config, let handleL2PS catch it
+
+        const decryptedTx = await l2psInstance.decryptTx(tx as any)
+        if (!decryptedTx?.content?.from) return null
+
Evidence
The newly-added checkL2PSBalance decrypts the transaction during confirmTransaction. The L2PS
handling path also decrypts the same encrypted transaction in decryptAndValidate, meaning the
system may decrypt twice for the same tx lifecycle.

src/libs/blockchain/routines/validateTransaction.ts[151-160]
src/libs/network/routines/transactions/handleL2PS.ts[47-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`confirmTransaction` decrypts L2PS encrypted transactions to perform a balance pre-check, but `handleL2PS` also decrypts the same payload later. This duplicates CPU/IO work and can be abused to amplify node load.

### Issue Context
- `checkL2PSBalance()` calls `l2psInstance.decryptTx(...)` during confirmTransaction.
- `decryptAndValidate()` calls `l2psInstance.decryptTx(...)` again during actual L2PS handling.

### Fix Focus Areas
- src/libs/blockchain/routines/validateTransaction.ts[141-182]
- src/libs/network/routines/transactions/handleL2PS.ts[44-70]

### Suggested fix
- Prefer doing the balance check in one place (typically `handleL2PS` right before mempool insertion/execution).
- If confirmTx must pre-check, consider:
 - caching the decrypted transaction temporarily keyed by encrypted tx hash, then reusing it in `handleL2PS`, or
 - returning a token/reference from confirmTx that allows reuse.
- At minimum, add a guard so confirmTransaction only decrypts when strictly necessary and the node has the L2PS config loaded.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +82 to +84
if (!result.success) {
log.error(`[SERVER] broadcastTx FAILED — returning to client: result=${returnValue.result}, extra=${JSON.stringify(returnValue.extra)}, response.extra=${result.response?.extra}`)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Action required

1. Logging can throw on failure 🐞 Bug ⛯ Reliability

In broadcastTx failure handling, manageExecution JSON.stringifies returnValue.extra; if extra is
a non-serializable object (e.g., contains circular refs or BigInt), the log statement can throw and
break the error-handling path (client may not receive the intended error response).
Agent Prompt
### Issue description
`manageExecution` logs `broadcastTx` failures using `JSON.stringify(returnValue.extra)`. Because `extra` can be an object (and potentially non-JSON-serializable), the logging line can throw and break the error path.

### Issue Context
`returnValue.extra` is populated from `result.extra`, and `handleExecuteTransaction` assigns objects to `result.extra` in multiple branches.

### Fix Focus Areas
- src/libs/network/manageExecution.ts[69-85]

### Suggested fix
- Wrap the stringify in a try/catch and fall back to a safe representation.
- Optionally implement a small `safeStringify` helper that:
  - converts `bigint` to string
  - handles circular references (or falls back to `util.inspect`)
  - truncates overly large outputs

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +80 to +102
async function checkSenderBalance(decryptedTx: Transaction): Promise<string | null> {
const sender = decryptedTx.content.from as string
if (!sender) return "Missing sender address in decrypted transaction"

// Extract amount from native payload
let amount = 0
if (decryptedTx.content.type === "native" && Array.isArray(decryptedTx.content.data)) {
const nativePayload = decryptedTx.content.data[1] as INativePayload
if (nativePayload?.nativeOperation === "send") {
const [, sendAmount] = nativePayload.args as [string, number]
amount = sendAmount || 0
}
}

const totalRequired = amount + L2PS_TX_FEE
try {
const balance = await L2PSTransactionExecutor.getBalance(sender)
if (balance < BigInt(totalRequired)) {
return `Insufficient balance: need ${totalRequired} (${amount} + ${L2PS_TX_FEE} fee) but have ${balance}`
}
} catch (error) {
return `Balance check failed: ${error instanceof Error ? error.message : "Unknown error"}`
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Action required

2. L2ps fee/balance check inconsistent 🐞 Bug ✓ Correctness

The new L2PS balance pre-checks always add L2PS_TX_FEE (even when the executor does not charge any
fee for that tx type), and they don’t validate sendAmount like the executor does. This can
incorrectly reject L2PS transactions that would otherwise execute successfully (e.g., non-send
native ops, or non-native txs with only gcr_edits) and can also produce misleading errors/behavior
when the amount is malformed.
Agent Prompt
### Issue description
`checkSenderBalance` / `checkL2PSBalance` currently require `L2PS_TX_FEE` for all decrypted L2PS transactions, but the executor only checks/burns the fee for `nativeOperation === "send"` and may accept other tx types without fees. This mismatch can cause false rejections. Additionally, the pre-check doesn’t validate the extracted `sendAmount` like the executor does.

### Issue Context
- Executor fee+amount checks are implemented in `handleNativeTransaction` for `nativeOperation === "send"`.
- Other tx types (non-native with `gcr_edits`, or unknown native operations) do not burn/check a fee.

### Fix Focus Areas
- src/libs/network/routines/transactions/handleL2PS.ts[80-102]
- src/libs/blockchain/routines/validateTransaction.ts[145-176]
- src/libs/l2ps/L2PSTransactionExecutor.ts[158-220]

### Suggested fix
1. Decide on policy:
   - **Option A (match current executor):** Only add `L2PS_TX_FEE` to `totalRequired` for txs where the executor actually burns/charges the fee (currently native `send`).
   - **Option B (enforce fee for all L2PS txs):** Update `L2PSTransactionExecutor.generateGCREdits`/handlers to also burn/check the fee for other tx categories, then keep pre-check consistent.
2. Validate amount exactly as executor does before BigInt conversion:
   - `typeof sendAmount === 'number' && Number.isFinite(sendAmount) && Number.isInteger(sendAmount) && sendAmount > 0`
3. Compute required balance using BigInt arithmetic:
   - `const required = BigInt(sendAmount) + BigInt(L2PS_TX_FEE)`
   - Avoid `BigInt(amount + fee)`.
4. Consider extracting a shared helper (e.g., in `L2PSTransactionExecutor`) to compute required balance so pre-check and executor cannot drift.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 24, 2026

Walkthrough

The changes add sender balance validation checks at transaction processing layers. A constant is exported for use across modules, balance verification helpers are introduced in both blockchain and network layers, and error logging is enhanced for broadcast failures.

Changes

Cohort / File(s) Summary
Balance validation in blockchain layer
src/libs/blockchain/routines/validateTransaction.ts
Added checkL2PSBalance helper and balance pre-checks before transaction finalization. Validates native balance against transaction amount, with special decryption and fee computation for L2PS encrypted transactions. Returns early with BALANCE ERROR marking if insufficient funds detected.
L2PS fee constant export
src/libs/l2ps/L2PSTransactionExecutor.ts
Exported L2PS_TX_FEE constant to make the fee value accessible to other modules for balance calculations.
Balance validation in L2PS network handler
src/libs/network/routines/transactions/handleL2PS.ts
Added checkSenderBalance helper that validates sender has sufficient funds (amount + L2PS_TX_FEE) before mempool insertion. Inserted balance pre-check gate immediately after decrypted-hash verification, rejecting with HTTP 400-style error on insufficiency.
Error logging enhancement
src/libs/network/manageExecution.ts
Enhanced error logging for failed broadcastTx execution to include result, extra, and response details.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ValidateTransaction
    participant GCR
    participant L2PS as ParallelNetworks<br/>& L2PSExecutor
    participant Mempool

    rect rgba(100, 150, 200, 0.5)
    Note over ValidateTransaction,GCR: Regular Transaction Validation
    Client->>ValidateTransaction: confirmTransaction(tx)
    ValidateTransaction->>GCR: getGCRNativeBalance(sender)
    GCR-->>ValidateTransaction: balance
    alt balance < amount
        ValidateTransaction-->>Client: InvalidTransaction (BALANCE ERROR)
    else sufficient balance
        ValidateTransaction-->>Client: ValidTransaction
    end
    end

    rect rgba(200, 150, 100, 0.5)
    Note over ValidateTransaction,L2PS: L2PS Encrypted Transaction Validation
    Client->>ValidateTransaction: confirmTransaction(L2PSEncryptedTx)
    ValidateTransaction->>L2PS: checkL2PSBalance(tx)
    L2PS->>L2PS: decrypt inner transaction
    L2PS->>L2PS: extract nativeAmount from decrypted payload
    L2PS->>L2PSExecutor: getBalance(sender)
    L2PSExecutor-->>L2PS: L2PS balance
    alt (nativeAmount + L2PS_TX_FEE) > L2PS balance
        L2PS-->>ValidateTransaction: null
        ValidateTransaction-->>Client: InvalidTransaction (BALANCE ERROR)
    else sufficient L2PS balance
        L2PS-->>ValidateTransaction: success
        ValidateTransaction-->>Client: ValidTransaction
    end
    end

    rect rgba(150, 200, 100, 0.5)
    Note over Client,Mempool: L2PS Network Handler Pre-Check
    Client->>Mempool: submitL2PSTx(decryptedTx)
    Mempool->>Mempool: checkSenderBalance(decryptedTx)
    alt balance check fails
        Mempool-->>Client: HTTP 400 Error
    else balance sufficient
        Mempool->>Mempool: processValidL2PSTransaction()
        Mempool-->>Client: Accepted
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

Review effort 3/5


🐰 Balance checks now guard the gates,
Before transactions seal their fates,
L2PS and native, both assured,
No empty wallets get procured,
Smart validation hops along the way! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing balance checks for L2PS transactions to ensure sufficient funds, which is the primary objective across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix-l2ps-balance-check

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 and usage tips.

@sonarqubecloud
Copy link

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/libs/blockchain/routines/validateTransaction.ts`:
- Around line 154-164: The current guards in validateTransaction (checks around
l2psUid, ParallelNetworks.getInstance().getL2PS/loadL2PS, l2psInstance.decryptTx
and decryptedTx?.content?.from) return null on operational failures which
confirmTransaction treats as "no balance error" and allows validity to pass;
change these paths to surface failures instead of failing open: replace the null
returns with thrown errors or a distinct failure result (e.g., an explicit
BalanceVerificationFailure/throw) so the caller (confirmTransaction) can treat
inability to inspect as a verification failure; update both the block around
getL2PS/loadL2PS/decryptTx and the similar logic at lines 182-186 to propagate
errors (via throw or explicit error return) rather than returning null, and
ensure confirmTransaction handles that signal as a balance verification error.
- Around line 168-180: The balance check is charging L2PS_TX_FEE even for
non-native/send payloads because amount defaults to 0; change the logic to only
add L2PS_TX_FEE when the decryptedTx is a native send. Concretely, detect native
send (use the existing decryptedTx.content.type === "native" and
nativePayload?.nativeOperation === "send" check), set amount from
nativePayload.args only in that branch, compute totalRequired = amount +
(isNativeSend ? L2PS_TX_FEE : 0), then call
L2PSTransactionExecutor.getBalance(sender) and compare balance <
BigInt(totalRequired) as before so non-send payloads are not charged the fee.

In `@src/libs/network/manageExecution.ts`:
- Around line 82-84: The log line in manageExecution.ts that logs on broadcastTx
failure uses JSON.stringify on returnValue.extra and can throw for BigInt or
circular structures; change the logging to first produce a safe serializedExtra
by performing defensive serialization (e.g., JSON.stringify wrapped in a
try/catch or a serializer with a replacer that converts BigInt to string and
handles circular refs via a seen Set) and then use that serializedExtra in
log.error; update the failure branch around variables result, returnValue and
the log.error call so any serialization errors are caught and do not mask the
original failure.

In `@src/libs/network/routines/transactions/handleL2PS.ts`:
- Around line 80-105: checkSenderBalance currently always adds L2PS_TX_FEE and
queries getBalance even for non-native-send transactions; restrict the pre-check
so it only runs for decrypted native send payloads: detect
decryptedTx.content.type === "native" and nativePayload?.nativeOperation ===
"send" (as you already parse) and if not a native send, return null immediately;
otherwise compute amount, include L2PS_TX_FEE in totalRequired, call
L2PSTransactionExecutor.getBalance(sender) and compare as before. Ensure
references to L2PS_TX_FEE, checkSenderBalance, decryptedTx.content.type/data,
nativePayload.nativeOperation, and L2PSTransactionExecutor.getBalance are used
to locate and update the logic.
- Around line 146-151: The pre-check in handleL2PS uses checkSenderBalance but
is vulnerable to TOCTOU because concurrent sends can both pass before
L2PSTransactionExecutor.execute applies debits; fix by implementing
sender-scoped pending-debit tracking or locking: when handleL2PS accepts a tx,
reserve the sender's amount in the mempool (add per-sender pendingDebit field)
or acquire a sender-level lock (e.g., by sender ID) before running
checkSenderBalance and before calling L2PSTransactionExecutor.execute to
serialize processing; ensure L2PSTransactionExecutor.execute consults and
updates the same reservation (or requires the lock) and mempool updates remove
or adjust the pendingDebit on commit/rollback so concurrent requests correctly
see reserved amounts.
🪄 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: fbb2f0b1-36aa-45cf-a92c-1479c5785343

📥 Commits

Reviewing files that changed from the base of the PR and between 50ab546 and 3188069.

📒 Files selected for processing (4)
  • src/libs/blockchain/routines/validateTransaction.ts
  • src/libs/l2ps/L2PSTransactionExecutor.ts
  • src/libs/network/manageExecution.ts
  • src/libs/network/routines/transactions/handleL2PS.ts

Comment on lines +154 to +164
if (!l2psUid) return null // Can't check without UID, let handleL2PS catch it

const parallelNetworks = ParallelNetworks.getInstance()
let l2psInstance = await parallelNetworks.getL2PS(l2psUid)
if (!l2psInstance) {
l2psInstance = await parallelNetworks.loadL2PS(l2psUid)
}
if (!l2psInstance) return null // No L2PS config, let handleL2PS catch it

const decryptedTx = await l2psInstance.decryptTx(tx as any)
if (!decryptedTx?.content?.from) return null
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't fail open when the L2PS balance cannot be verified.

Lines 154-164 and 182-186 return null whenever the inner tx cannot be inspected (!l2psUid, missing/unloaded L2PS, missing from, or any thrown error). confirmTransaction() interprets null as “no balance error”, so this guard silently disappears under operational failures and can still produce signed ValidityData with valid = true.

Also applies to: 182-186

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libs/blockchain/routines/validateTransaction.ts` around lines 154 - 164,
The current guards in validateTransaction (checks around l2psUid,
ParallelNetworks.getInstance().getL2PS/loadL2PS, l2psInstance.decryptTx and
decryptedTx?.content?.from) return null on operational failures which
confirmTransaction treats as "no balance error" and allows validity to pass;
change these paths to surface failures instead of failing open: replace the null
returns with thrown errors or a distinct failure result (e.g., an explicit
BalanceVerificationFailure/throw) so the caller (confirmTransaction) can treat
inability to inspect as a verification failure; update both the block around
getL2PS/loadL2PS/decryptTx and the similar logic at lines 182-186 to propagate
errors (via throw or explicit error return) rather than returning null, and
ensure confirmTransaction handles that signal as a balance verification error.

Comment on lines +168 to +180
let amount = 0
if (decryptedTx.content.type === "native" && Array.isArray(decryptedTx.content.data)) {
const nativePayload = decryptedTx.content.data[1] as INativePayload
if (nativePayload?.nativeOperation === "send") {
const [, sendAmount] = nativePayload.args as [string, number]
amount = sendAmount || 0
}
}

const totalRequired = amount + L2PS_TX_FEE
const balance = await L2PSTransactionExecutor.getBalance(sender)
if (balance < BigInt(totalRequired)) {
return `[Tx Validation] [BALANCE ERROR] Insufficient balance: need ${totalRequired} but have ${balance}\n`
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Apply the L2PS fee only to inner native/send transactions.

Line 177 charges L2PS_TX_FEE even when the decrypted payload is not a native send, because amount falls back to 0. That marks non-send L2PS payloads invalid here even though src/libs/l2ps/L2PSTransactionExecutor.ts only burns the fee on native/send.

Suggested fix
-        let amount = 0
-        if (decryptedTx.content.type === "native" && Array.isArray(decryptedTx.content.data)) {
-            const nativePayload = decryptedTx.content.data[1] as INativePayload
-            if (nativePayload?.nativeOperation === "send") {
-                const [, sendAmount] = nativePayload.args as [string, number]
-                amount = sendAmount || 0
-            }
-        }
-
-        const totalRequired = amount + L2PS_TX_FEE
+        if (decryptedTx.content.type !== "native" || !Array.isArray(decryptedTx.content.data)) {
+            return null
+        }
+        const nativePayload = decryptedTx.content.data[1] as INativePayload
+        if (nativePayload?.nativeOperation !== "send") {
+            return null
+        }
+        const [, sendAmount] = nativePayload.args as [string, number]
+        if (!Number.isInteger(sendAmount) || sendAmount <= 0) {
+            return "[Tx Validation] [BALANCE ERROR] Invalid amount: must be a positive integer\n"
+        }
+
+        const totalRequired = BigInt(sendAmount) + BigInt(L2PS_TX_FEE)
         const balance = await L2PSTransactionExecutor.getBalance(sender)
-        if (balance < BigInt(totalRequired)) {
+        if (balance < totalRequired) {
             return `[Tx Validation] [BALANCE ERROR] Insufficient balance: need ${totalRequired} but have ${balance}\n`
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libs/blockchain/routines/validateTransaction.ts` around lines 168 - 180,
The balance check is charging L2PS_TX_FEE even for non-native/send payloads
because amount defaults to 0; change the logic to only add L2PS_TX_FEE when the
decryptedTx is a native send. Concretely, detect native send (use the existing
decryptedTx.content.type === "native" and nativePayload?.nativeOperation ===
"send" check), set amount from nativePayload.args only in that branch, compute
totalRequired = amount + (isNativeSend ? L2PS_TX_FEE : 0), then call
L2PSTransactionExecutor.getBalance(sender) and compare balance <
BigInt(totalRequired) as before so non-send payloads are not charged the fee.

Comment on lines +82 to +84
if (!result.success) {
log.error(`[SERVER] broadcastTx FAILED — returning to client: result=${returnValue.result}, extra=${JSON.stringify(returnValue.extra)}, response.extra=${result.response?.extra}`)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l src/libs/network/manageExecution.ts

Repository: kynesyslabs/node

Length of output: 99


🏁 Script executed:

cat -n src/libs/network/manageExecution.ts | sed -n '70,100p'

Repository: kynesyslabs/node

Length of output: 1830


🏁 Script executed:

cat -n src/libs/network/manageExecution.ts | sed -n '1,85p'

Repository: kynesyslabs/node

Length of output: 4616


🏁 Script executed:

cat -n src/libs/network/manageExecution.ts | head -121

Repository: kynesyslabs/node

Length of output: 6196


🏁 Script executed:

rg "handleExecuteTransaction" -A 5 --type ts

Repository: kynesyslabs/node

Length of output: 9676


🏁 Script executed:

fd -name "endpointHandlers*" -o -name "*types*"

Repository: kynesyslabs/node

Length of output: 290


🏁 Script executed:

rg "type ExecutionResult|interface ExecutionResult" -A 10 --type ts

Repository: kynesyslabs/node

Length of output: 42


🏁 Script executed:

rg "result\.extra\s*=" src/libs/network/endpointHandlers.ts | head -20

Repository: kynesyslabs/node

Length of output: 530


🏁 Script executed:

rg "result\.extra\s*=" src/libs/network/endpointHandlers.ts -A 2 -B 1

Repository: kynesyslabs/node

Length of output: 1751


🏁 Script executed:

rg "type|interface" src/libs/network/endpointHandlers.ts | grep -i execution

Repository: kynesyslabs/node

Length of output: 42


🏁 Script executed:

rg "ExecutionResult" src/libs/network/endpointHandlers.ts -B 5 -A 5 | head -50

Repository: kynesyslabs/node

Length of output: 1129


🏁 Script executed:

find . -name "*.ts" -type f | xargs rg "type ExecutionResult|interface ExecutionResult" 2>/dev/null

Repository: kynesyslabs/node

Length of output: 44


🏁 Script executed:

rg "ExecutionResult" --type ts | grep "import\|type\|interface"

Repository: kynesyslabs/node

Length of output: 139


🏁 Script executed:

cat -n src/libs/network/endpointHandlers.ts | sed -n '1,20p'

Repository: kynesyslabs/node

Length of output: 936


🏁 Script executed:

rg "from.*demosdk" src/libs/network/endpointHandlers.ts

Repository: kynesyslabs/node

Length of output: 510


🏁 Script executed:

find . -path ./node_modules -prune -o -name "types.ts" -o -name "types.d.ts" | grep -v node_modules

Repository: kynesyslabs/node

Length of output: 245


🏁 Script executed:

rg "ExecutionResult" src/libs/network/endpointHandlers.ts -B 10 | head -30

Repository: kynesyslabs/node

Length of output: 1372


🏁 Script executed:

cat -n src/libs/network/endpointHandlers.ts | sed -n '1,50p'

Repository: kynesyslabs/node

Length of output: 2466


🏁 Script executed:

rg "BigInt" src/libs/network --type ts | head -20

Repository: kynesyslabs/node

Length of output: 490


🏁 Script executed:

rg "BigInt" . --type ts | grep -v node_modules | head -20

Repository: kynesyslabs/node

Length of output: 2452


🏁 Script executed:

rg "JSON\.stringify.*extra" src/libs/network --type ts

Repository: kynesyslabs/node

Length of output: 294


🏁 Script executed:

rg "toJSON|replacer" src/libs/network --type ts | head -20

Repository: kynesyslabs/node

Length of output: 152


Serialize result.extra defensively to avoid masking the actual failure.

Line 83 calls JSON.stringify on returnValue.extra inside the try block. If result.extra ever contains a BigInt or circular reference, JSON.stringify will throw, and the catch block replaces the real failure response with a generic error message. Use a custom replacer function, wrap the serialization in a try-catch, or use defensive serialization to prevent the original failure context from being lost.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libs/network/manageExecution.ts` around lines 82 - 84, The log line in
manageExecution.ts that logs on broadcastTx failure uses JSON.stringify on
returnValue.extra and can throw for BigInt or circular structures; change the
logging to first produce a safe serializedExtra by performing defensive
serialization (e.g., JSON.stringify wrapped in a try/catch or a serializer with
a replacer that converts BigInt to string and handles circular refs via a seen
Set) and then use that serializedExtra in log.error; update the failure branch
around variables result, returnValue and the log.error call so any serialization
errors are caught and do not mask the original failure.

Comment on lines +80 to +105
async function checkSenderBalance(decryptedTx: Transaction): Promise<string | null> {
const sender = decryptedTx.content.from as string
if (!sender) return "Missing sender address in decrypted transaction"

// Extract amount from native payload
let amount = 0
if (decryptedTx.content.type === "native" && Array.isArray(decryptedTx.content.data)) {
const nativePayload = decryptedTx.content.data[1] as INativePayload
if (nativePayload?.nativeOperation === "send") {
const [, sendAmount] = nativePayload.args as [string, number]
amount = sendAmount || 0
}
}

const totalRequired = amount + L2PS_TX_FEE
try {
const balance = await L2PSTransactionExecutor.getBalance(sender)
if (balance < BigInt(totalRequired)) {
return `Insufficient balance: need ${totalRequired} (${amount} + ${L2PS_TX_FEE} fee) but have ${balance}`
}
} catch (error) {
return `Balance check failed: ${error instanceof Error ? error.message : "Unknown error"}`
}

return null
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Only charge this pre-check on decrypted native/send payloads.

Line 94 adds L2PS_TX_FEE even when the decrypted tx is not a native send, because amount stays 0 and the helper still queries the balance. src/libs/l2ps/L2PSTransactionExecutor.ts only burns that fee in handleNativeTransaction() for nativeOperation === "send", so this gate now rejects valid non-send L2PS transactions.

Suggested fix
 async function checkSenderBalance(decryptedTx: Transaction): Promise<string | null> {
     const sender = decryptedTx.content.from as string
     if (!sender) return "Missing sender address in decrypted transaction"
 
-    // Extract amount from native payload
-    let amount = 0
-    if (decryptedTx.content.type === "native" && Array.isArray(decryptedTx.content.data)) {
-        const nativePayload = decryptedTx.content.data[1] as INativePayload
-        if (nativePayload?.nativeOperation === "send") {
-            const [, sendAmount] = nativePayload.args as [string, number]
-            amount = sendAmount || 0
-        }
-    }
-
-    const totalRequired = amount + L2PS_TX_FEE
+    if (decryptedTx.content.type !== "native" || !Array.isArray(decryptedTx.content.data)) {
+        return null
+    }
+
+    const nativePayload = decryptedTx.content.data[1] as INativePayload
+    if (nativePayload?.nativeOperation !== "send") {
+        return null
+    }
+
+    const [, sendAmount] = nativePayload.args as [string, number]
+    if (!Number.isInteger(sendAmount) || sendAmount <= 0) {
+        return "Invalid amount: must be a positive integer"
+    }
+
+    const totalRequired = BigInt(sendAmount) + BigInt(L2PS_TX_FEE)
     try {
         const balance = await L2PSTransactionExecutor.getBalance(sender)
-        if (balance < BigInt(totalRequired)) {
-            return `Insufficient balance: need ${totalRequired} (${amount} + ${L2PS_TX_FEE} fee) but have ${balance}`
+        if (balance < totalRequired) {
+            return `Insufficient balance: need ${totalRequired} (${sendAmount} + ${L2PS_TX_FEE} fee) but have ${balance}`
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libs/network/routines/transactions/handleL2PS.ts` around lines 80 - 105,
checkSenderBalance currently always adds L2PS_TX_FEE and queries getBalance even
for non-native-send transactions; restrict the pre-check so it only runs for
decrypted native send payloads: detect decryptedTx.content.type === "native" and
nativePayload?.nativeOperation === "send" (as you already parse) and if not a
native send, return null immediately; otherwise compute amount, include
L2PS_TX_FEE in totalRequired, call L2PSTransactionExecutor.getBalance(sender)
and compare as before. Ensure references to L2PS_TX_FEE, checkSenderBalance,
decryptedTx.content.type/data, nativePayload.nativeOperation, and
L2PSTransactionExecutor.getBalance are used to locate and update the logic.

Comment on lines +146 to +151
// Pre-check sender balance BEFORE mempool insertion
const balanceError = await checkSenderBalance(decryptedTx)
if (balanceError) {
log.error(`[handleL2PS] Balance pre-check failed: ${balanceError}`)
return createErrorResponse(response, 400, balanceError)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

mempool_file="$(fd 'l2ps_mempool\.ts$' | head -n1)"
test -n "$mempool_file"

echo "== $mempool_file =="
sed -n '1,260p' "$mempool_file"

echo
echo "== reservation-related matches =="
rg -n -C4 '\b(addTransaction|existsByOriginalHash|updateGCREdits|updateStatus)\s*\(' "$mempool_file"
rg -n -C4 '\b(balance|pending|sender|from_address|original_hash)\b' "$mempool_file"

Repository: kynesyslabs/node

Length of output: 15354


🏁 Script executed:

# Check if there are any sender/balance-related fields or methods in l2ps_mempool.ts beyond line 260
rg -n 'sender|balance|pending|debit|reserve|from_address' src/libs/blockchain/l2ps_mempool.ts

Repository: kynesyslabs/node

Length of output: 494


🏁 Script executed:

# Find and examine checkSenderBalance function
rg -n 'checkSenderBalance' --type ts -A 15 src/

Repository: kynesyslabs/node

Length of output: 3097


🏁 Script executed:

# Find processValidL2PSTransaction and L2PSTransactionExecutor
fd '.*\.(ts|tsx)$' src/ | xargs rg -l 'processValidL2PSTransaction|L2PSTransactionExecutor' | head -5

Repository: kynesyslabs/node

Length of output: 256


🏁 Script executed:

# Get full checkSenderBalance implementation
sed -n '80,120p' src/libs/network/routines/transactions/handleL2PS.ts

Repository: kynesyslabs/node

Length of output: 1573


🏁 Script executed:

# Get processValidL2PSTransaction implementation
sed -n '160,220p' src/libs/network/routines/transactions/handleL2PS.ts

Repository: kynesyslabs/node

Length of output: 2400


🏁 Script executed:

# Check L2PSTransactionExecutor for balance handling
rg -n 'class L2PSTransactionExecutor|execute.*function|balance|sender|debit' src/libs/l2ps/L2PSTransactionExecutor.ts -A 3 | head -80

Repository: kynesyslabs/node

Length of output: 3571


Concurrent sends from same sender can bypass the balance guard due to TOCTOU vulnerability.

The balance check at line 147 reads the current L1 state once, but between this check and actual balance deduction in L2PSTransactionExecutor.execute(), concurrent transactions from the same sender can both pass. The executor re-reads balance at line 215 of L2PSTransactionExecutor.ts, but without any sender-scoped reservation or lock, both concurrent txs see the same available balance. Additionally, the mempool stores no pending debit accounting per sender (confirmed: only tracks hash, l2ps_uid, sequence_number, status, etc., not sender balances). The GCR edits for balance changes are created but only applied during batch aggregation, not atomically at execution.

Implement sender-scoped pending debit tracking: either reserve amounts in the mempool as transactions are added, or introduce sender-level locking to serialize balance checks and execution for the same sender.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libs/network/routines/transactions/handleL2PS.ts` around lines 146 - 151,
The pre-check in handleL2PS uses checkSenderBalance but is vulnerable to TOCTOU
because concurrent sends can both pass before L2PSTransactionExecutor.execute
applies debits; fix by implementing sender-scoped pending-debit tracking or
locking: when handleL2PS accepts a tx, reserve the sender's amount in the
mempool (add per-sender pendingDebit field) or acquire a sender-level lock
(e.g., by sender ID) before running checkSenderBalance and before calling
L2PSTransactionExecutor.execute to serialize processing; ensure
L2PSTransactionExecutor.execute consults and updates the same reservation (or
requires the lock) and mempool updates remove or adjust the pendingDebit on
commit/rollback so concurrent requests correctly see reserved amounts.

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.

1 participant