diff --git a/src/libs/blockchain/routines/validateTransaction.ts b/src/libs/blockchain/routines/validateTransaction.ts index 4d127211..cd95637a 100644 --- a/src/libs/blockchain/routines/validateTransaction.ts +++ b/src/libs/blockchain/routines/validateTransaction.ts @@ -20,9 +20,12 @@ import Hashing from "src/libs/crypto/hashing" import { getSharedState } from "src/utilities/sharedState" import log from "src/utilities/logger" import { Operation, ValidityData } from "@kynesyslabs/demosdk/types" +import type { INativePayload } from "@kynesyslabs/demosdk/types" import { forgeToHex } from "src/libs/crypto/forgeUtils" import _ from "lodash" import { ucrypto, uint8ArrayToHex } from "@kynesyslabs/demosdk/encryption" +import ParallelNetworks from "@/libs/l2ps/parallelNetworks" +import L2PSTransactionExecutor, { L2PS_TX_FEE } from "@/libs/l2ps/L2PSTransactionExecutor" // INFO Cryptographically validate a transaction and calculate gas // REVIEW is it overkill to write an interface for the return value? @@ -98,6 +101,36 @@ export async function confirmTransaction( } log.debug("[TX] confirmTransaction - Transaction validity verified, compiling ValidityData") + + // Check sender balance covers the transfer amount + if (tx.content.amount > 0) { + const from = typeof tx.content.from === "string" ? tx.content.from : forgeToHex(tx.content.from) + let fromBalance = 0 + try { + fromBalance = await GCR.getGCRNativeBalance(from) + } catch { + // Address not in GCR — balance is 0 + } + if (fromBalance < tx.content.amount) { + validityData.data.message = + `[Tx Validation] [BALANCE ERROR] Insufficient balance: need ${tx.content.amount} but have ${fromBalance}\n` + validityData.data.valid = false + validityData = await signValidityData(validityData) + return validityData + } + } + + // For L2PS encrypted transactions, decrypt inner tx and check balance + if (tx.content.type === "l2psEncryptedTx") { + const l2psBalanceError = await checkL2PSBalance(tx) + if (l2psBalanceError) { + validityData.data.message = l2psBalanceError + validityData.data.valid = false + validityData = await signValidityData(validityData) + return validityData + } + } + validityData.data.message = "[Tx Validation] Transaction signature verified\n" validityData.data.valid = true @@ -105,6 +138,49 @@ export async function confirmTransaction( return validityData } +/** + * Decrypt L2PS encrypted tx and check inner tx balance before mempool. + * Returns error message string if insufficient, null if OK. + */ +async function checkL2PSBalance(tx: Transaction): Promise { + try { + const l2psPayload = (tx.content?.data as any)?.[1] + const l2psUid = l2psPayload?.l2ps_uid as string | undefined + 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 + + const sender = decryptedTx.content.from as string + + 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` + } + } catch (error) { + log.error(`[confirmTransaction] L2PS balance pre-check error: ${error instanceof Error ? error.message : error}`) + // Don't block on decryption errors — let handleL2PS deal with it + } + return null +} + async function signValidityData(data: ValidityData): Promise { const hash = Hashing.sha256(JSON.stringify(data.data)) // return data diff --git a/src/libs/l2ps/L2PSTransactionExecutor.ts b/src/libs/l2ps/L2PSTransactionExecutor.ts index 982c6d3c..2d093701 100644 --- a/src/libs/l2ps/L2PSTransactionExecutor.ts +++ b/src/libs/l2ps/L2PSTransactionExecutor.ts @@ -30,7 +30,7 @@ import { getErrorMessage } from "@/utilities/errorMessage" * L2PS Transaction Fee (in DEM) * This fee is burned (removed from sender, not added anywhere) */ -const L2PS_TX_FEE = 1 +export const L2PS_TX_FEE = 1 /** * Result of executing an L2PS transaction diff --git a/src/libs/network/manageExecution.ts b/src/libs/network/manageExecution.ts index 201eaf05..e72a5ab1 100644 --- a/src/libs/network/manageExecution.ts +++ b/src/libs/network/manageExecution.ts @@ -79,6 +79,9 @@ export async function manageExecution( returnValue.response = result.response returnValue.require_reply = result.require_reply returnValue.extra = result.extra + if (!result.success) { + log.error(`[SERVER] broadcastTx FAILED — returning to client: result=${returnValue.result}, extra=${JSON.stringify(returnValue.extra)}, response.extra=${result.response?.extra}`) + } break } catch (error) { const errorMessage = diff --git a/src/libs/network/routines/transactions/handleL2PS.ts b/src/libs/network/routines/transactions/handleL2PS.ts index 567c109a..ace6818e 100644 --- a/src/libs/network/routines/transactions/handleL2PS.ts +++ b/src/libs/network/routines/transactions/handleL2PS.ts @@ -1,4 +1,4 @@ -import type { BlockContent, L2PSTransaction, RPCResponse } from "@kynesyslabs/demosdk/types" +import type { BlockContent, L2PSTransaction, RPCResponse, INativePayload } from "@kynesyslabs/demosdk/types" import Chain from "src/libs/blockchain/chain" import Transaction from "src/libs/blockchain/transaction" import { emptyResponse } from "../../server_rpc" @@ -6,7 +6,7 @@ import { emptyResponse } from "../../server_rpc" import { L2PS, L2PSEncryptedPayload } from "@kynesyslabs/demosdk/l2ps" import ParallelNetworks from "@/libs/l2ps/parallelNetworks" import L2PSMempool from "@/libs/blockchain/l2ps_mempool" -import L2PSTransactionExecutor from "@/libs/l2ps/L2PSTransactionExecutor" +import L2PSTransactionExecutor, { L2PS_TX_FEE } from "@/libs/l2ps/L2PSTransactionExecutor" import log from "@/utilities/logger" /** @@ -72,6 +72,38 @@ async function decryptAndValidate( } + +/** + * Check sender balance before mempool insertion. + * Returns an error message if balance is insufficient, null if OK. + */ +async function checkSenderBalance(decryptedTx: Transaction): Promise { + 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 +} + export default async function handleL2PS( l2psTx: L2PSTransaction, ): Promise { @@ -111,6 +143,13 @@ export default async function handleL2PS( return createErrorResponse(response, 400, `Decrypted transaction hash mismatch: expected ${originalHash}, got ${decryptedTx.hash}`) } + // 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) + } + // Process Valid Transaction return await processValidL2PSTransaction(response, l2psUid, l2psTx, decryptedTx, originalHash) }