Skip to content

feat: balance pre-checks for L2PS transactions (rebased onto stabilisation)#937

Merged
tcsenpai merged 4 commits into
stabilisationfrom
fix-l2ps-balance-check-on-stabilisation
Jun 19, 2026
Merged

feat: balance pre-checks for L2PS transactions (rebased onto stabilisation)#937
tcsenpai merged 4 commits into
stabilisationfrom
fix-l2ps-balance-check-on-stabilisation

Conversation

@Shitikyan

@Shitikyan Shitikyan commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Rebased follow-up to #680 (which was opened in Feb against the old testnet base and has been sitting cold for 4 months). The two original feat + review commits are cherry-picked onto current stabilisation; a third commit re-aligns the typing where stabilisation moved on (tx.content.amount widened to number | string, GCR.getGCRNativeBalance renamed to getAccountBalance).

Closes #680.

What this brings up

  • Native-amount balance check in confirmTransactiontx.content.amount > 0 paths now reject senders without sufficient funds before signing ValidityData. Adjusted to BigInt comparisons for post-fork OS magnitudes.
  • L2PS encrypted-tx balance check in confirmTransaction — decrypts the inner tx, verifies sender balance, fee added only on native/send per executor parity. Fail-closed: any "cannot verify" outcome (missing l2ps_uid, L2PS not loaded, decryption error) returns a precise error instead of silently passing.
  • checkSenderBalance in handleL2PS — same fee-scoping; serialised per-sender via in-process withSenderLock to close the TOCTOU window between balance read and executor debit.
  • Safe failure-log in manageExecutionJSON.stringify of returnValue.extra could throw on circular refs / BigInts and abort the client response. Replaced with a guarded safeStringifyExtra that survives both.

Review history baked in

This PR ships #680's content with the bot-review fixes already applied:

  • Fee scoping (qodo bug Splitting transactions from blocks #2, CodeRabbit handleL2PS.ts:105 / validateTransaction.ts:180) — L2PS_TX_FEE only added when the inner tx is native/send.
  • Fail-closed validate path (CodeRabbit validateTransaction.ts:164) — every "cannot verify" branch now returns a precise [BALANCE ERROR] string instead of null.
  • TOCTOU lock (CodeRabbit handleL2PS.ts:151 Critical) — per-sender in-process serialisation around the (check + insert + execute) sequence.
  • Safe-stringify failure log (qodo bug Fees development #1, CodeRabbit manageExecution.ts:84) — JSON.stringify replaced with safeStringifyExtra that handles circular refs and BigInt.

Type-check delta

errors
baseline origin/stabilisation 20 pre-existing
this branch 20
net new 0

Test plan

  • Run native send with insufficient balance → expect BALANCE ERROR before signing
  • Run L2PS native/send with insufficient balance → expect same gate
  • Run L2PS non-send tx (web2 / crosschain / gcr_edits-only) → expect fee gate NOT applied
  • Run two concurrent L2PS sends from same sender that together exceed balance → expect second to be rejected at the gate (sender lock)
  • Force L2PS network not loaded and submit l2psEncryptedTx via confirmTransaction → expect explicit BALANCE ERROR (fail-closed)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a shared pre-validation step to ensure senders (including nested L2PS transactions) have sufficient funds before transactions are accepted.
    • Introduced per-sender serialized processing for L2PS transactions to reduce concurrency-related issues.
  • Bug Fixes
    • Strengthened fail-closed handling for invalid amounts and insufficient balances, with clearer client-facing 400 responses.
    • Improved error logging to safely stringify complex values (including circular references and bigints) without breaking error handling.
  • Refactor
    • Exposed the L2PS transaction fee constant for reuse across modules.

Shitikyan and others added 3 commits June 11, 2026 21:08
Correctness — fee-bearing scope
- handleL2PS.ts (checkSenderBalance) and validateTransaction.ts
  (checkL2PSBalance) both unconditionally added `L2PS_TX_FEE` to the
  required-balance check, but the executor only burns that fee inside
  `L2PSTransactionExecutor.handleNativeTransaction()` for
  `nativeOperation === "send"`. The pre-checks therefore rejected
  every non-`native/send` L2PS payload (web2, crosschain, gcr_edits-
  only) with a false-negative balance error. Now the fee is added
  only when the inner tx is genuinely fee-bearing, and
  `sendAmount` is shape-validated up front instead of being silently
  coerced to 0 on a malformed payload.

Correctness — fail-closed validate path
- validateTransaction.ts previously returned `null` on every "cannot
  verify" outcome (no l2ps_uid, L2PS not loaded, decryption error).
  `confirmTransaction` reads `null` as "no balance error", which means
  the node signed and broadcast `ValidityData { valid: true }` for txs
  it never actually verified — a fail-open hole that turned every
  operational failure into a stamp of approval. Every previously-`null`
  failure path now returns a precise `[BALANCE ERROR]` string so
  `confirmTransaction` surfaces it instead of vouching for the tx.

Correctness — TOCTOU between balance check and executor debit
- handleL2PS.ts now funnels every `(check + insert + execute)`
  sequence through a per-sender in-process lock (`withSenderLock`).
  Without it, two concurrent broadcasts from the same wallet both
  read the same pre-debit balance and both pass the gate; the
  executor then debits twice from a wallet that had funds for one.
  In-process serialisation is sufficient because every L2PS tx for a
  given sender arrives through one node entry point; cross-node
  ordering is handled downstream by the mempool + consensus pipeline.
  The lock map drops entries once a sender's queue drains, so
  long-lived senders don't leak entries.

Reliability — safe failure-path logging
- manageExecution.ts:84 previously called `JSON.stringify(returnValue.extra)`
  in the broadcastTx failure-log branch. If `extra` carried a circular
  reference or a `BigInt`, stringify itself would throw, abort the
  error-handling path, and leave the client without any response.
  Replaced with a `safeStringifyExtra()` helper that serialises BigInts
  as `"123n"` and degrades to `<unserialisable: ...>` instead of
  throwing.

Net `tsc --noEmit --skipLibCheck` delta on this branch:
  baseline `origin/testnet`: 36 pre-existing errors
  this branch:               36 — 0 net new

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
After rebasing onto stabilisation, two type drifts surfaced in the
native-amount balance branch:

- `tx.content.amount` was widened from `number` to `number | string` by
  the v4 bigint-widening work; the literal `> 0` and `<` comparisons no
  longer type-check, and a plain `Number()` cast would round any
  post-fork OS magnitude. Coerce through `BigInt(... ?? 0)` so the
  comparison stays precise for the full balance range.
- `GCR.getGCRNativeBalance` was renamed to `getAccountBalance` on
  stabilisation and now returns `bigint` directly. Match the new
  signature.

Behaviour preserved: any tx with positive `amount` whose sender lacks
the required balance still gets rejected with the same
`[BALANCE ERROR]` message, only with bigint-precise numbers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@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 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Shitikyan, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 53 minutes and 32 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 36576cb5-57c4-447e-9736-028271fa9dce

📥 Commits

Reviewing files that changed from the base of the PR and between 4e8d616 and cc1c1a8.

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

Walkthrough

This PR implements pre-signing balance validation for Layer 2 Protocol Service (L2PS) encrypted transactions and standard transactions. The L2PS fee constant is exported, transaction validation gains GCR balance checks and L2PS-specific decryption+balance verification, and the L2PS handler introduces concurrent per-sender processing with upfront balance validation to prevent insufficient-fund errors.

Changes

L2PS Balance Validation

Layer / File(s) Summary
L2PS fee constant export
src/libs/l2ps/L2PSTransactionExecutor.ts
L2PS_TX_FEE is exported to allow balance-check logic in other modules to compute required sender funds accurately.
L2PS balance check implementation
src/libs/l2ps/balanceCheck.ts
New module implementing checkInnerTxBalance to validate L2PS nested transaction sender balances, canonicalizing send amount and L2PS_TX_FEE into OS units, validating sender and amount formats, and returning fee-bearing status or balance insufficiency errors.
Pre-signing transaction balance validation
src/libs/blockchain/routines/validateTransaction.ts
Extended confirmTransaction to verify sender balance after signature verification: standard transactions use GCR.getAccountBalance, and l2psEncryptedTx use a new checkL2PSBalance helper that decrypts the inner transaction, validates the decrypted sender address, and delegates to checkInnerTxBalance for amount+fee validation before marking the transaction as valid.
Safe error logging infrastructure
src/libs/network/manageExecution.ts
Added safeStringifyExtra utility that serializes values for logging without throwing on bigint or circular references; integrated into broadcastTx error paths to safely log failed transaction details.
L2PS handler concurrency and balance pre-check
src/libs/network/routines/transactions/handleL2PS.ts
Integrated per-sender serialization via withSenderLock and balance pre-validation before processing: requires decrypted sender address, runs balance check via checkInnerTxBalance inside the sender lock, returning 400 on validation failure; ensures only one in-process handler execution per sender proceeds to mempool insertion and execution.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • kynesyslabs/node#926: Both PRs touch L2PS native send balance arithmetic, canonicalizing the inner amount and L2PS_TX_FEE into OS units; main PR exports the constant and adds upfront balance validation, while the retrieved PR adjusts executor-side canonicalization across fork settings.

Poem

🐰 A rabbit hops through balance-check lands,
With L2PS fees held firmly in hands,
No empty wallets shall pass the gate,
Per-sender locks ensure they arrive on-time freight!
Safe logging blooms, bigints no longer sting—
A protocol refined for each bouncing thing. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% 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 'feat: balance pre-checks for L2PS transactions (rebased onto stabilisation)' clearly and specifically describes the main feature being added: balance pre-checks for L2PS transactions.
Linked Issues check ✅ Passed The PR comprehensively addresses all objectives from issue #680: native-amount balance checks [680], L2PS encrypted-tx balance checks [680], fail-closed behavior [680], TOCTOU prevention [680], correct fee scoping [680], improved error reporting [680], and safe logging enhancements [680].
Out of Scope Changes check ✅ Passed All changes are directly scoped to balance pre-checking requirements: validateTransaction and L2PS balance validation, L2PS_TX_FEE export for fee logic, safe logging in manageExecution, and sender serialization in handleL2PS.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-l2ps-balance-check-on-stabilisation

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.

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds sender balance pre-checks at two enforcement points — confirmTransaction (for both native-send and L2PS encrypted txs) and handleL2PS (with a per-sender TOCTOU lock) — and extracts the shared unit-correct comparison into a new balanceCheck.ts helper. A safeStringifyExtra guard protects the failure-log path in manageExecution from BigInt / circular-ref throws.

  • balanceCheck.ts (new): single checkInnerTxBalance implementation that canonicalises amount and fee to OS units via canonicalizeAmountToOs before comparing against the OS-magnitude stored balance; used by both handleL2PS and validateTransaction to prevent the DEM-vs-OS no-op gate.
  • handleL2PS.ts: wraps the (check → insert → execute) sequence in a per-sender withSenderLock, correctly reusing a single chained const for both map storage and cleanup; closes the TOCTOU window for concurrent sends from the same wallet.
  • validateTransaction.ts: BigInt coercion is try/caught for fractional/malformed amounts; L2PS balance pre-check delegates to checkInnerTxBalance but does not verify the inner tx signature before signing ValidityData, creating an asymmetry with handleL2PS's decryptAndValidate.

Confidence Score: 4/5

Safe to merge for the balance-enforcement goal; the TOCTOU lock and OS-unit canonicalization are correctly implemented. The main rough edge is that confirmTransaction's L2PS path issues a signed valid certificate without verifying the inner tx signature, which handleL2PS does verify before execution.

The withSenderLock is correctly implemented (single chained const shared between map write and cleanup check), checkInnerTxBalance properly canonicalises both amount and fee to OS units, and the safe-stringify guard covers both extra fields. The one structural asymmetry — checkL2PSBalance in validateTransaction.ts skips inner tx signature verification that decryptAndValidate in handleL2PS.ts performs — means confirmTransaction can sign a ValidityData as valid for a tx whose inner signature is invalid. Execution still rejects such txs, so there is no fund risk, but the signed certificate is inaccurate.

src/libs/blockchain/routines/validateTransaction.ts — checkL2PSBalance decrypts without verifying the inner tx signature and duplicates the L2PS instance loading pattern already present in handleL2PS.ts.

Important Files Changed

Filename Overview
src/libs/blockchain/routines/validateTransaction.ts Adds native-amount balance check (BigInt-safe, try/caught) and L2PS balance pre-check via a new local checkL2PSBalance; L2PS loading logic is duplicated from handleL2PS.getL2PSInstance and the inner tx signature is not verified before signing ValidityData as valid.
src/libs/l2ps/balanceCheck.ts New shared helper that canonicalises L2PS amount + fee to OS units before comparing against the OS-magnitude stored balance; cleanly mirrors executor logic and is reused across both call sites.
src/libs/network/routines/transactions/handleL2PS.ts Adds per-sender serialisation via withSenderLock (correctly reuses a single chained const for both map storage and cleanup check) and delegates balance verification to the shared checkInnerTxBalance helper; TOCTOU and unit-mismatch issues from prior threads are addressed.
src/libs/network/manageExecution.ts Adds safeStringifyExtra to guard the failure-log path against BigInt/circular-ref throws; both returnValue.extra and result.response?.extra are now wrapped.
src/libs/l2ps/L2PSTransactionExecutor.ts Single-line change: exports L2PS_TX_FEE so balanceCheck.ts can import the constant without duplication.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant confirmTx as confirmTransaction
    participant handleL2PS as handleL2PS
    participant balanceCheck as checkInnerTxBalance
    participant Lock as withSenderLock
    participant Executor as L2PSTransactionExecutor

    Client->>confirmTx: l2psEncryptedTx (confirmTx)
    confirmTx->>confirmTx: verify outer tx signature
    confirmTx->>confirmTx: checkL2PSBalance(tx)
    Note over confirmTx: decryptTx (no inner sig verify)
    confirmTx->>balanceCheck: checkInnerTxBalance(decryptedTx)
    balanceCheck->>Executor: getBalance(sender)
    Executor-->>balanceCheck: balance (OS units)
    balanceCheck-->>confirmTx: "null | error"
    confirmTx-->>Client: signed ValidityData

    Client->>handleL2PS: l2psEncryptedTx (broadcastTx)
    handleL2PS->>handleL2PS: decryptAndValidate (+ inner sig verify)
    handleL2PS->>Lock: withSenderLock(sender)
    Lock->>balanceCheck: checkInnerTxBalance(decryptedTx)
    balanceCheck->>Executor: getBalance(sender)
    Executor-->>balanceCheck: balance (OS units)
    balanceCheck-->>Lock: "null | error"
    Lock->>Executor: execute (debit applied)
    Executor-->>Lock: result
    Lock-->>handleL2PS: result
    handleL2PS-->>Client: RPCResponse
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant confirmTx as confirmTransaction
    participant handleL2PS as handleL2PS
    participant balanceCheck as checkInnerTxBalance
    participant Lock as withSenderLock
    participant Executor as L2PSTransactionExecutor

    Client->>confirmTx: l2psEncryptedTx (confirmTx)
    confirmTx->>confirmTx: verify outer tx signature
    confirmTx->>confirmTx: checkL2PSBalance(tx)
    Note over confirmTx: decryptTx (no inner sig verify)
    confirmTx->>balanceCheck: checkInnerTxBalance(decryptedTx)
    balanceCheck->>Executor: getBalance(sender)
    Executor-->>balanceCheck: balance (OS units)
    balanceCheck-->>confirmTx: "null | error"
    confirmTx-->>Client: signed ValidityData

    Client->>handleL2PS: l2psEncryptedTx (broadcastTx)
    handleL2PS->>handleL2PS: decryptAndValidate (+ inner sig verify)
    handleL2PS->>Lock: withSenderLock(sender)
    Lock->>balanceCheck: checkInnerTxBalance(decryptedTx)
    balanceCheck->>Executor: getBalance(sender)
    Executor-->>balanceCheck: balance (OS units)
    balanceCheck-->>Lock: "null | error"
    Lock->>Executor: execute (debit applied)
    Executor-->>Lock: result
    Lock-->>handleL2PS: result
    handleL2PS-->>Client: RPCResponse
Loading

Reviews (3): Last reviewed commit: "review: address bot findings on PR #937" | Re-trigger Greptile

Comment thread src/libs/network/routines/transactions/handleL2PS.ts Outdated
Comment thread src/libs/network/routines/transactions/handleL2PS.ts Outdated
Comment thread src/libs/blockchain/routines/validateTransaction.ts Outdated
Comment thread src/libs/blockchain/routines/validateTransaction.ts Outdated
Comment thread src/libs/network/manageExecution.ts Outdated

@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: 4

🤖 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/validateTransaction.ts`:
- Around line 276-304: The balance-check logic duplicates validation and wrongly
assumes sendAmount is a number; extract a shared helper (e.g.,
L2PSTransactionExecutor.parseAmountOrThrow or a util parseL2PSAmount) that
accepts number|string, converts to BigInt safely (for numbers ensure
integer/non-negative, for strings allow large integers via BigInt), rejects
non-integer/negative values with a clear error, and returns BigInt; update
validateTransaction's check (currently in the block that reads
nativePayload.args and computes amount/fee) and handleL2PS.checkSenderBalance to
call this helper and compare BigInt(totalRequired) with the BigInt balance from
L2PSTransactionExecutor.getBalance to avoid type-mismatch and duplicated logic.
- Around line 276-304: The native send amount check currently rejects string
amounts; update the block that reads nativePayload.args so sendAmount can be
number|string and is converted using the same canonicalizer used in
L2PSTransactionExecutor.handleNativeTransaction (call
canonicalizeAmountToOs(sendAmount)) before validation and assignment to amount;
keep the checks for numeric/finite/non-negative after canonicalization, compute
fee/totalRequired as before, and then compare balance using
L2PSTransactionExecutor.getBalance(sender) as currently implemented (ensure
canonicalizeAmountToOs is imported/available).

In `@src/libs/network/routines/transactions/handleL2PS.ts`:
- Around line 141-158: handleL2PS.ts incorrectly assumes the inner tx amount is
a number (casts args to [string, number] and checks typeof sendAmount ===
"number"), which rejects valid post-fork string amounts and duplicates logic
from validateTransaction.ts; update the parsing in the fee-bearing branch of
handleL2PS (where feeBearing, sendAmount, amount are used) to accept string or
number (change the args typing to [string, string|number] or treat sendAmount as
string|number), normalize/parse string amounts into a numeric value (or BigInt
if required by L2PSTransactionExecutor.getBalance comparisons) with proper
validation (finite, non-negative), and factor this normalization into a shared
helper used by both handleL2PS and checkL2PSBalance in validateTransaction.ts to
avoid duplication and ensure consistent behavior.
- Around line 105-110: The cleanup check always fails because previous.then(()
=> next) creates a fresh Promise object so strict equality with the stored map
value never matches; fix by creating and storing the exact Promise instance you
later compare against: when you set senderLocks.set(sender, <promise>) (the
promise created from previous.then(() => next)), capture that promise in a local
variable (e.g., const expected = previous.then(() => next)) and use
senderLocks.set(sender, expected) and then compare senderLocks.get(sender) ===
expected before calling senderLocks.delete(sender); update the code around the
senderLocks set/get logic (symbols: senderLocks, sender, previous, next) so the
same Promise object is used for both storage and comparison.
🪄 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: 620b154f-f66b-4f45-8010-eb7fdeb76c99

📥 Commits

Reviewing files that changed from the base of the PR and between 4e8d616 and 0caa50a.

📒 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 thread src/libs/blockchain/routines/validateTransaction.ts Outdated
Comment thread src/libs/network/routines/transactions/handleL2PS.ts Outdated
Comment thread src/libs/network/routines/transactions/handleL2PS.ts Outdated
P1 — DEM/OS unit mismatch made the L2PS balance gate a silent no-op post-fork
  Both `checkSenderBalance` and `checkL2PSBalance` were adding
  `L2PS_TX_FEE` (DEM-unit `1`) and `sendAmount` (DEM units) but
  comparing against `L2PSTransactionExecutor.getBalance()` which returns
  the post-osDenomination OS-magnitude value (~10⁹× larger). A wallet
  with `0.5 DEM` passed the pre-check (`5×10⁸ >= 1`) and only failed at
  execution time, which is exactly what the gate was supposed to
  prevent. Hoisted both call sites onto a new shared helper
  `src/libs/l2ps/balanceCheck.ts::checkInnerTxBalance` that canonicalises
  amount + fee to OS via `canonicalizeAmountToOs` — identical to what
  the executor does for the actual debit — so the pre-check and the
  debit can never disagree on units again. Also handles `number | string`
  send-amount shapes (the v4 bigint widening surfaced strings here).

P1 — withSenderLock map leaked unboundedly
  The cleanup-time identity check compared `senderLocks.get(sender)`
  against a freshly-allocated `previous.then(() => next)`, but each
  `.then()` call produces a new promise object. The strict-equality
  check could never be true, so the `delete` line was dead code and the
  map grew one entry per sender forever. Stash the chained promise in
  a local const and reuse it on both the write and the cleanup-time
  check, so we compare the same object.

P1 — `BigInt(tx.content.amount)` could crash confirmTransaction
  An adversarial / misbehaving client shipping a fractional or
  non-numeric `amount` would hit `BigInt(1.5)` / `BigInt("1.5")` and
  throw an uncaught `RangeError`/`SyntaxError`, breaking the entire RPC
  validation pipeline instead of returning a signed-invalid
  ValidityData. Wrapped the BigInt cast in a try/catch that
  short-circuits with a clean `[AMOUNT ERROR]` reason.

P1 — `result.response?.extra` still went through implicit toString()
  The original safe-stringify fix in `manageExecution.ts:84` only
  guarded `returnValue.extra`. The same log line still interpolated
  `result.response?.extra` via a template literal — implicit `toString()`
  on a BigInt or circular-ref object would re-throw and abort the
  error-handling branch. Routed it through `safeStringifyExtra` too.

Refactor — collapse duplicated balance-check
  Both `checkSenderBalance` (handleL2PS) and `checkL2PSBalance`
  (validateTransaction) had near-identical logic that shipped the same
  unit-mismatch bug independently. They now share `checkInnerTxBalance`;
  the call sites just unwrap their respective decryption paths and
  delegate. Drops ~80 lines of duplication and keeps the invariant
  enforced in one place.

`tsc --noEmit --skipLibCheck` delta on this branch:
  baseline `origin/stabilisation`: 21 pre-existing errors
  this branch:                      21 — 0 net new

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Shitikyan

Copy link
Copy Markdown
Contributor Author

Bot review threads on this PR are stale relative to the latest commits — all findings have been addressed in code. Re-triggering the bots so the resolved threads reflect the actual state.

@coderabbitai review
@greptileai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

@Shitikyan Sure, I'll kick off a fresh review of the PR to reflect the latest commits!

✅ Action performed

Full review finished.

@tcsenpai
tcsenpai merged commit 8793c9d into stabilisation Jun 19, 2026
4 checks passed
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