Skip to content

fix: charge the node for ingest-decidable atomic-batch inner due-diligence failures - #26804

Open
ruslanvelkov-beep wants to merge 1 commit into
hiero-ledger:mainfrom
ruslanvelkov-beep:atomic-batch-inner-due-diligence
Open

fix: charge the node for ingest-decidable atomic-batch inner due-diligence failures#26804
ruslanvelkov-beep wants to merge 1 commit into
hiero-ledger:mainfrom
ruslanvelkov-beep:atomic-batch-inner-due-diligence

Conversation

@ruslanvelkov-beep

Copy link
Copy Markdown
Contributor

What

Inner transactions of an atomic batch bypassed node due-diligence charging. When an inner failed a node due-diligence check at pre-handle, DispatchProcessor#chargeCreator set the inner's status but returned without charging for BATCH_INNER dispatches — so the cost of letting the batch reach consensus fell on the batch payer instead of the submitting node. The same operation submitted top-level charges the node; wrapping it in a batch inverted who pays.

Fixes #26615.

How

chargeCreator now charges the submitting node for a BATCH_INNER due-diligence failure when the failure is ingest-decidable — determinable from the payer and transaction body alone (structural/pure-check failures, oversize/parse, invalid node account, invalid payer signature). The charge is routed through the batch's recorded fee-charging context rather than the fee accumulator directly, so it is captured by the batch's rollback-and-replay and survives the batch failing — exactly like the batch payer's fee. A direct fee-accumulator charge would be discarded with the inner's savepoint.

State-dependent failures the node could not have foreseen at ingest are left uncharged. These depend on mutable ledger state that can change after submission by any means (a separate transaction, another batch, or an earlier inner in the same batch):

STATE_DEPENDENT_DUE_DILIGENCE_CODES = { PAYER_ACCOUNT_NOT_FOUND, PAYER_ACCOUNT_DELETED, INSUFFICIENT_PAYER_BALANCE }

The batch's external resolution (INNER_TRANSACTION_FAILED) and the inner record statuses are unchanged — only who is charged shifts. The rationale for this code set is recorded in a comment on #26615.

Acceptance criteria

  • Ingest-decidable inner due-diligence failures charge the submitting node, consistent with top-level behavior.
  • State-dependent inner failures continue to not charge the node.
  • Batch resolution (INNER_TRANSACTION_FAILED) and inner record statuses remain externally consistent.

Testing

  • DispatchProcessorTest (unit): ingest-decidable inner → node charged via the recorded context; each state-dependent code (PAYER_ACCOUNT_DELETED, INSUFFICIENT_PAYER_BALANCE) → node not charged.
  • GovernanceTransactionsPostIngestTests (embedded): (a) an unbalanced inner → node charged, batch INNER_TRANSACTION_FAILED, inner records INVALID_ACCOUNT_AMOUNTS; (b) an inner drained below its network fee by an earlier inner → node not charged.
  • :app unit + embedded batch suites green locally; full subprocess batch suites left to CI.

…er due-diligence failures (hiero-ledger#26615)

Signed-off-by: Ruslan Velkov <ruslan.velkov@limechain.tech>
@ruslanvelkov-beep ruslanvelkov-beep self-assigned this Aug 12, 2026
@ruslanvelkov-beep
ruslanvelkov-beep requested review from a team as code owners August 12, 2026 12:54
@lfdt-bot

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Impacted file tree graph

@@             Coverage Diff              @@
##               main   #26804      +/-   ##
============================================
- Coverage     70.49%   70.48%   -0.02%     
- Complexity    11670    11671       +1     
============================================
  Files          2583     2583              
  Lines        108300   108310      +10     
  Branches      12109    12111       +2     
============================================
- Hits          76345    76340       -5     
- Misses        27977    27990      +13     
- Partials       3978     3980       +2     
Files with missing lines Coverage Δ Complexity Δ
...a/node/app/workflows/handle/DispatchProcessor.java 93.05% <100.00%> (+2.01%) 0.00 <0.00> (ø)

... and 19 files with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@testlens-app

testlens-app Bot commented Aug 12, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 16bacc3
▶️ Tests: 52085 executed
⚪️ Checks: 39/39 completed


Learn more about TestLens at testlens.app.

Comment on lines +251 to +252
private static final Set<ResponseCodeEnum> STATE_DEPENDENT_DUE_DILIGENCE_CODES =
EnumSet.of(PAYER_ACCOUNT_NOT_FOUND, PAYER_ACCOUNT_DELETED, INSUFFICIENT_PAYER_BALANCE);

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.

I don’t think we should skip charging the node for these exceptions. We charge the node for the same due-diligence failures in regular transactions, and batch transactions should behave consistently. Otherwise, a node could submit a transaction with an invalid payer or a payer with zero balance, and neither the node nor the payer would be charged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These are state-dependent, i.e. the Node couldn't foresee at ingest - the account ID might have been deleted after ingest, but before handle, by some other txn; or the account ID doesn't exist (ingest only checks about the valid shape, e.g. positive numbers for shard, realm, etc., existence is checked at handle, but a deleted account might have been garbage-collected - so the account ID passes ingest but is not found at handle); or the payer has insufficient balance - this could definitely happen because the same payer might have been charged multiple times by previous txns from the same batch, previous txns from a different batch, or previous top-level txns - at ingest we don't simulate how the same payer is going to be affected after multiple txns (and we can't because there might be smart contract payments involved), we just perform the check: for each individual txn in the batch see whether the payer can pay for that particular txn.

There was another case: txn expired for scheduled txns but we've got a time buffer at ingest of 10s by default, i.e. at ingest txns that are about to expire within the next 10s should not be passed to consensus (and 10s is a lot of time), so an expired txn at handle (in a batch or top-level) should be treated as a Node due diligence error. But for insufficient balance in a batch - not (this cannot be determined with 100% confidence at ingest, so it's not the Node's fault if such an error propagates at handle).

Comment on lines +273 to +278
dispatch.feeChargingOrElse(appFeeCharging)
.customized(dispatch)
.charge(
dispatch.creatorInfo().accountId(),
new Fees(0, dispatch.fees().networkFee(), 0),
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.

This only charges the node for the failing inner transaction. Fees from earlier inner transactions are still replayed and charged to their payers. If any inner has a due-diligence failure, the node should have rejected the entire batch at ingest. I think the whole batch should be treated as a node due-diligence failure: do not charge the inner payers, and charge the submitting node the applicable network fees.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Treating the whole batch as a Node Due Diligence changes the ticket (Acceptance criteria 3: Batch resolution (INNER_TRANSACTION_FAILED) and inner record statuses remain externally consistent). Also, not charging the earlier inner payers would run against the HIP-551 invariant that already-processed inners pay their fees even when the batch rolls back — they did valid work; only the failing inner is the node's fault. And per my previous reply, there are situations in which it wasn't even the node's fault.
So changing this behavior will require further discussion.

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.

Inner transactions of an atomic batch bypass node due-diligence charging

3 participants