Skip to content

[KLC-2309] Dispatch websocket account events without indexer#38

Merged
nickgs1337 merged 4 commits into
developfrom
KLC-2309-dispatch-accounts-events-without-indexer
May 20, 2026
Merged

[KLC-2309] Dispatch websocket account events without indexer#38
nickgs1337 merged 4 commits into
developfrom
KLC-2309-dispatch-accounts-events-without-indexer

Conversation

@phcarneirobc
Copy link
Copy Markdown
Contributor

@phcarneirobc phcarneirobc commented Apr 21, 2026

Problem

WebSocket account subscriptions only worked when the Elasticsearch indexer was active. Without it, account state changes were never dispatched
to subscribers — the entire account event path ran exclusively through the indexer pipeline.

Solution

Added processAccountEvents to the dispatchWebsocketEvents path inside eventsProcessor. When no indexer is active, it extracts the
affected addresses (senders + receipt "to" fields) from the block's transactions, loads each account's current state from AccountsDB, and
dispatches the account events directly to the WebSocket event queue.

Key Changes

  • New: processAccountEvents() in indexer/eventsProcessor.go — extracts affected addresses from transactions and dispatches ACCOUNTS
    events
  • New: buildAccountInfo() in indexer/eventsProcessor.go — builds *data.AccountInfo directly from UserAccountHandler
  • Updated: ArgEventsProcessor in indexer/dataIndexerArgs.go — added AccountsDB state.AccountsAdapter field
  • Updated: eventsProcessor struct — added accountsDB dependency injected at construction
  • Updated: cmd/node/startup.go — passes stateComponents.AccountsAdapter when creating eventsProcessor
  • Updated: indexer/eventsProcessor_test.go — 3 new test cases covering the new dispatch path

Testing

  • All existing tests pass (make tests)
  • New tests added for new functionality
    • TestEventsProcessor_SaveBlock_DispatchesAccountEvents — verifies ACCOUNTS event is dispatched when AccountsDB is present
    • TestEventsProcessor_SaveBlock_SkipsAccountEventsWhenNoAccountsDB — verifies no dispatch when AccountsDB is nil
    • TestEventsProcessor_SaveBlock_SkipsAccountEventsWhenIndexerActive — verifies no websocket events when indexer is handling dispatch
  • Manual testing performed:

Configuration Changes

None

Breaking Changes

None

This pull request ensures WebSocket subscribers receive account-state websocket ACCOUNTS events even when the Elasticsearch indexer is not running by dispatching account events directly during the SaveBlock/commit path.

Blockchain-Critical Components Affected

  • State management / account state access

    • eventsProcessor now depends on AccountsDB (state.AccountsAdapter) injected via ArgEventsProcessor and wired in cmd/node/startup.go.
    • New code extracts affected addresses from prepared transactions and receipts, loads each account from AccountsDB (read-only) using the UserAccountHandler, and builds data.AccountInfo payloads (nonce, name/root/code hashes, metadata, balances including unfrozen/frozen KDA buckets, allowance including pending rewards, permissions, timestamps).
    • buildAccountInfo centralizes account→data.AccountInfo construction and is reused by indexer/account and events paths.
  • Transaction processing / event dispatch

    • The SaveBlock / websocket dispatch path now uses prepared block data to emit USER_TRANSACTIONS/TRANSACTIONS and — when the indexer is not active and AccountsDB is present — dispatches ACCOUNTS events by loading current account state and sending to the WebSocket event queue.
    • The indexer path was refactored to accept optional prepared data (PreparedBlockData) so prepared work can be reused; elasticProcessor.SaveTransactions signature changed to accept a prepared any parameter and no longer enqueues USER_TRANSACTIONS/TRANSACTIONS itself.
    • prepareTransactionsForDatabase was documented/changed to be non-mutating (no longer deletes from the tx pool).

Node Stability, Data Integrity & Error Handling

  • No consensus logic, KVM execution, or state-mutation semantics are changed — changes are read-only and observability/eventing focused.
  • Per-address failures are isolated: address decoding failures, common.ErrAccNotFound, or individual buildAccountInfo errors are logged and skipped without aborting other event dispatches.
  • Existing non-blocking event-queue semantics (trySendEvent) are preserved; no new blocking concurrency primitives added in the event path.
  • Operational impact: synchronous AccountsDB reads occur on the commit/SaveBlock goroutine for affected addresses, which can increase I/O/CPU contention for blocks touching many accounts and may affect node latency under load.

Cross-Cutting / Dependency Changes

  • ArgEventsProcessor added AccountsDB (state.AccountsAdapter); eventsProcessor stores and receives this dependency via NewEventsProcessor; startup wiring updated to pass stateComponents.AccountsAdapter.
  • Introduced data.PreparedBlockData to carry prepared Txs, TxsMap, and Altered metadata so SaveBlock can prepare once and indexer worker can reuse prepared payload.
  • Elastic/indexer code moved account-info helpers out of elasticProcessor into shared helpers (indexer/accountInfo.go) and removed event enqueueing from elasticProcessor.SaveTransactions/SaveHeader to centralize websocket enqueueing in SaveBlock/commit.
  • work items and interfaces updated to propagate the prepared any parameter into indexer SaveTransactions.

Testing & Verification

  • Extensive test additions and updates (indexer/eventsProcessor_test.go, elasticProcessor_test.go, workItem tests) verifying:
    • ACCOUNTS events dispatch when AccountsDB present and indexer inactive.
    • ACCOUNTS events skipped when AccountsDB nil or when indexer active.
    • Address coverage for senders/receipt recipients and various receipt types.
    • Parity between websocket-derived address sets and indexer altered-account sets (excluding zero address).
    • SaveTransactions fallback behavior when prepared is nil.
  • Author reports all tests passing; new unit tests added to cover the new paths.

Risk Summary

  • Risk to consensus/data integrity: None — no state mutation changes.
  • Operational risk: Low-to-moderate — added synchronous account-state reads during SaveBlock may increase latency or lock contention for blocks that touch many accounts; reviewer recommended a follow-up to offload heavy trie reads to a buffered worker to mitigate contention.
  • Behavioral change: websocket ACCOUNTS/BLOCKS/TXS enqueueing now centralized in SaveBlock/commit path; when indexer enabled, indexer remains authoritative and the fallback is skipped so no duplicate events are emitted.

Reviewer Notes (design critique)

  • A reviewer recommended a larger refactor to (1) make SaveBlock the single orchestrator that prepares per-block data once, (2) enqueue websocket events from the commit goroutine, and (3) pass prepared data to the indexer worker so the indexer skips re-prep. The PR already moves toward this model (PreparedBlockData, central event dispatch) but reviewer flagged performance concerns (sync trie reads on commit) and proposed an 8-step refactor plan and additional parity tests; those design suggestions are relevant for follow-up work but are not required for correctness of this PR.

Copilot AI review requested due to automatic review settings April 21, 2026 14:48
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

<review_stack_artifact>

</review_stack_artifact>

🚥 Pre-merge checks | ✅ 3 | ❌ 5

❌ Failed checks (5 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning Title follows required format [KLC-XXXX] type: description, but 'Dispatch websocket account events without indexer' lacks an explicit type prefix (feat/fix/refactor/etc.). Add explicit type prefix after the JIRA key: KLC-2309 feat: Dispatch websocket account events without indexer (or choose appropriate type based on intent).
Docstring Coverage ⚠️ Warning Docstring coverage is 16.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Concurrency Safety ⚠️ Warning Two critical issues: (1) args.Prepared not reset—stale data can leak when websocket disabled but indexer enabled; (2) Blocking accountsDB calls on commit goroutine cause lock contention. Add args.Prepared = nil at SaveBlock start. Offload account DB reads to worker goroutine or implement non-blocking pattern.
Error Handling ⚠️ Warning Unchecked error return in eventsProcessor.go line 201. buildAlteredAccountInfo returns buildAccountInfo directly without checking the returned error, silently propagating failures. Check error from buildAccountInfo at line 201: info, err := buildAccountInfo(...); if err != nil { return nil, fmt.Errorf("build account info: %w", err) }; return info, nil
State Consistency ⚠️ Warning Stale prepared data vulnerability: args.Prepared not reset per call. When wsEnabled=false and indexerEnabled=true, prior block's prepared data passes to indexer, corrupting indexing. Add args.Prepared = nil at SaveBlock start to reset per-call state and prevent cross-block data reuse in indexer.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 KLC-2309-dispatch-accounts-events-without-indexer

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR ensures WebSocket account subscriptions work even when the Elasticsearch indexer is disabled by dispatching account events directly from node state during block processing.

Changes:

  • Added an accountsDB dependency to eventsProcessor (wired through ArgEventsProcessor and node startup).
  • Introduced processAccountEvents() + buildAccountInfo() to build and enqueue ACCOUNTS events without the indexer.
  • Added tests covering the new non-indexer dispatch path and skip conditions.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
indexer/eventsProcessor.go Adds AccountsDB-backed account-event dispatch in the non-indexer WebSocket path and refactors account-info building into a helper.
indexer/eventsProcessor_test.go Adds tests for dispatching/skipping account events depending on AccountsDB presence and indexer activity.
indexer/dataIndexerArgs.go Extends ArgEventsProcessor with an AccountsDB dependency.
cmd/node/startup.go Injects stateComponents.AccountsAdapter into eventsProcessor.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread indexer/eventsProcessor.go Outdated
Comment thread indexer/eventsProcessor_test.go Outdated
Copy link
Copy Markdown

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

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

Inline comments:
In `@indexer/eventsProcessor_test.go`:
- Around line 741-785: Modify the AccountsStub used in the test so
LoadAccountCalled inspects the incoming []byte address and returns distinct
state.AccountHandler (or data.AccountInfo entries) for the sender and the
receiver instead of always returning the same stub; then after calling
ep.SaveBlock(ArgsSaveBlockData{...}) drain events as before but assert that the
ACCOUNTS event's Message map contains both the sender and receiver keys (use the
same byte slices used when creating the transaction via
createTransactionHandlerMock and the TransferContract.ToAddress) to verify
fan-out to both accounts rather than merely non-empty presence.

In `@indexer/eventsProcessor.go`:
- Around line 175-180: The accountsMap is being keyed by the raw `address`
string while you construct a canonical address in `info` via `buildAccountInfo`,
which can produce duplicate/mismatched entries; change the insertion to use the
canonical `info.Address` as the map key (i.e., replace `accountsMap[address] =
info` with `accountsMap[info.Address] = info`) and, if applicable, update the
surrounding log (`processAccountEvents` warning) to reference `info.Address` so
all downstream code (including `SaveAccounts`) receives the normalized key.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: eb92e32f-71e4-44aa-8474-6a977ab8e4c6

📥 Commits

Reviewing files that changed from the base of the PR and between 4f92949 and dc87a11.

📒 Files selected for processing (4)
  • cmd/node/startup.go
  • indexer/dataIndexerArgs.go
  • indexer/eventsProcessor.go
  • indexer/eventsProcessor_test.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Agent
  • GitHub Check: setup-and-lint / setup-and-lint
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (Custom checks)

**/*.go: Verify that any new or modified concurrent code (goroutines, channels, mutexes, sync primitives) is free of race conditions. Check for: proper lock/unlock pairing, no goroutine leaks, correct channel lifecycle management, and proper context cancellation propagation.
Verify that errors are not silently discarded. Check for: unchecked error returns, error wrapping with context, proper error propagation up the call chain, and no bare panic() calls outside of init() functions.

Files:

  • cmd/node/startup.go
  • indexer/eventsProcessor.go
  • indexer/dataIndexerArgs.go
  • indexer/eventsProcessor_test.go
**/*_test.go

⚙️ CodeRabbit configuration file

**/*_test.go: Test files. Review for: - Adequate coverage of edge cases and error paths - Proper use of test helpers and assertions - Race condition coverage (tests should use -race flag patterns) - No hardcoded sleep for synchronization (use channels or sync primitives) - Test isolation (no shared mutable state between tests)

Files:

  • indexer/eventsProcessor_test.go

Comment thread indexer/eventsProcessor_test.go
Comment thread indexer/eventsProcessor.go Outdated
@phcarneirobc phcarneirobc marked this pull request as draft April 21, 2026 14:57
@phcarneirobc phcarneirobc marked this pull request as ready for review April 21, 2026 18:01
Copy link
Copy Markdown

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

♻️ Duplicate comments (1)
indexer/eventsProcessor_test.go (1)

741-785: ⚠️ Potential issue | 🟡 Minor

These account-event tests still allow false positives.

Both tests return the same UserAccountHandler for every lookup and then only assert NotEmpty or raw call count. A regression that emits one canonical account, or calls LoadAccount three times for the same address, would still pass. Make the stub distinguish sender/receipt addresses and assert those exact keys are present in the emitted ACCOUNTS map.

As per coding guidelines, **/*_test.go: Test files. Review for: - Adequate coverage of edge cases and error paths - Proper use of test helpers and assertions.

Also applies to: 791-837

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

In `@indexer/eventsProcessor_test.go` around lines 741 - 785, The tests currently
return the same UserAccountHandler for every LoadAccount lookup which permits
false positives; update the mock AccountsStub.LoadAccountCalled used by
createTestEventsProcessorWithAccountsDB to inspect the input address bytes and
return distinct account handlers (e.g., senderAccount vs receiverAccount) for
the sender and receiver addresses used in createTransactionHandlerMock, then
after calling ep.SaveBlock assert that the ACCOUNTS Event (Event.EvType ==
ACCOUNTS) contains exactly the expected keys (the sender and receiver address
strings) and that their values match the corresponding account handlers instead
of using require.NotEmpty or only call counts; apply the same change to the
duplicate test region that starts around the other failing block so both tests
verify distinct addresses are present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@indexer/eventsProcessor_test.go`:
- Line 753: The test currently discards the error return from
createTransactionHandlerMock; update each call site (e.g., where tx, _ :=
createTransactionHandlerMock(...) is used—also at the other occurrences
mentioned) to capture the error and immediately assert it with
require.NoError(t, err) (or t.Fatalf on failure) so fixture-construction
failures fail fast; locate createTransactionHandlerMock usages in
eventsProcessor_test.go and replace the ignored error with a checked error
variable and a require.NoError(t, err) after the call.

In `@indexer/eventsProcessor.go`:
- Around line 166-169: The call in processAccountEvents uses
accountsDB.LoadAccount which may materialize or create accounts; verify by
running the suggested grep scripts whether LoadAccount implementations persist
or allocate new accounts—if they do, change this call to the non-creating lookup
GetExistingAccount (i.e., replace accountsDB.LoadAccount(addrBytes) with
accountsDB.GetExistingAccount(addrBytes)) or, if LoadAccount is confirmed
read-only, explicitly handle a "not found" result (check the returned value/err
for a not-found case and skip rather than allow creation); reference
processAccountEvents, accountsDB.LoadAccount, and GetExistingAccount when making
the change.

---

Duplicate comments:
In `@indexer/eventsProcessor_test.go`:
- Around line 741-785: The tests currently return the same UserAccountHandler
for every LoadAccount lookup which permits false positives; update the mock
AccountsStub.LoadAccountCalled used by createTestEventsProcessorWithAccountsDB
to inspect the input address bytes and return distinct account handlers (e.g.,
senderAccount vs receiverAccount) for the sender and receiver addresses used in
createTransactionHandlerMock, then after calling ep.SaveBlock assert that the
ACCOUNTS Event (Event.EvType == ACCOUNTS) contains exactly the expected keys
(the sender and receiver address strings) and that their values match the
corresponding account handlers instead of using require.NotEmpty or only call
counts; apply the same change to the duplicate test region that starts around
the other failing block so both tests verify distinct addresses are present.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4a4577d3-a2b3-4699-a68d-12781b8c9e86

📥 Commits

Reviewing files that changed from the base of the PR and between dc87a11 and c87bb57.

📒 Files selected for processing (2)
  • indexer/eventsProcessor.go
  • indexer/eventsProcessor_test.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: setup-and-lint / setup-and-lint
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (Custom checks)

**/*.go: Verify that any new or modified concurrent code (goroutines, channels, mutexes, sync primitives) is free of race conditions. Check for: proper lock/unlock pairing, no goroutine leaks, correct channel lifecycle management, and proper context cancellation propagation.
Verify that errors are not silently discarded. Check for: unchecked error returns, error wrapping with context, proper error propagation up the call chain, and no bare panic() calls outside of init() functions.

Files:

  • indexer/eventsProcessor.go
  • indexer/eventsProcessor_test.go
**/*_test.go

⚙️ CodeRabbit configuration file

**/*_test.go: Test files. Review for: - Adequate coverage of edge cases and error paths - Proper use of test helpers and assertions - Race condition coverage (tests should use -race flag patterns) - No hardcoded sleep for synchronization (use channels or sync primitives) - Test isolation (no shared mutable state between tests)

Files:

  • indexer/eventsProcessor_test.go

Comment thread indexer/eventsProcessor_test.go Outdated
Comment thread indexer/eventsProcessor.go Outdated
Copy link
Copy Markdown

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

♻️ Duplicate comments (1)
indexer/eventsProcessor_test.go (1)

739-788: ⚠️ Potential issue | 🟡 Minor

Test does not verify both sender and receiver are dispatched.

The stub returns the same account for all lookups, and the assertion only checks require.NotEmpty(accountsMap). This test passes even if only the sender or only the recipient is processed.

Track which addresses are loaded and assert the map contains entries for both expected addresses.

💡 Suggested improvement
 func TestEventsProcessor_SaveBlock_DispatchesAccountEvents(t *testing.T) {
 	testQueue := saveAndRestoreEventQueue(t, true)

+	var loadedAddresses sync.Map
 	acc := createTestAccountStub()
 	accountsDB := &mock.AccountsStub{
-		GetExistingAccountCalled: func(_ []byte) (state.AccountHandler, error) {
+		GetExistingAccountCalled: func(addr []byte) (state.AccountHandler, error) {
+			loadedAddresses.Store(hex.EncodeToString(addr), true)
+			// Return stub with address set to the requested address
+			stub := createTestAccountStub()
+			stub.AddressBytesCalled = func() []byte { return addr }
 			return acc, nil
 		},
 	}
 	// ... rest of test ...

-	require.NotEmpty(t, accountsMap)
+	senderKey := hex.EncodeToString([]byte("sender"))
+	receiverKey := hex.EncodeToString([]byte("receiver"))
+	require.Contains(t, accountsMap, senderKey, "sender should be in accounts map")
+	require.Contains(t, accountsMap, receiverKey, "receiver should be in accounts map")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@indexer/eventsProcessor_test.go` around lines 739 - 788, The test
TestEventsProcessor_SaveBlock_DispatchesAccountEvents currently only checks that
accountsMap is non-empty and uses a stub that returns the same account for any
lookup; update the test to track which addresses are requested by replacing or
augmenting mock.AccountsStub.GetExistingAccountCalled to record the address
bytes passed (e.g., append to a slice or map of string keys), then after
ep.SaveBlock assert that the recorded lookups (and the keys of accountsMap from
the dispatched ACCOUNTS event) contain both the sender ("sender") and receiver
("receiver") addresses so you verify both accounts were processed and included
in accountsMap.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@indexer/eventsProcessor.go`:
- Around line 188-211: The AccountInfo returned by
eventsProcessor.buildAccountInfo omits the Foundation field causing
inconsistency with the elastic processor; update the struct literal returned by
buildAccountInfo (in eventsProcessor) to explicitly set Foundation: false
(matching the elasticProcessor's handling) so the field is present in code even
if omitted in JSON via omitempty.

---

Duplicate comments:
In `@indexer/eventsProcessor_test.go`:
- Around line 739-788: The test
TestEventsProcessor_SaveBlock_DispatchesAccountEvents currently only checks that
accountsMap is non-empty and uses a stub that returns the same account for any
lookup; update the test to track which addresses are requested by replacing or
augmenting mock.AccountsStub.GetExistingAccountCalled to record the address
bytes passed (e.g., append to a slice or map of string keys), then after
ep.SaveBlock assert that the recorded lookups (and the keys of accountsMap from
the dispatched ACCOUNTS event) contain both the sender ("sender") and receiver
("receiver") addresses so you verify both accounts were processed and included
in accountsMap.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b784361d-b22c-4506-905a-118b7cc3e5c9

📥 Commits

Reviewing files that changed from the base of the PR and between c87bb57 and edfd792.

📒 Files selected for processing (2)
  • indexer/eventsProcessor.go
  • indexer/eventsProcessor_test.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: setup-and-lint / setup-and-lint
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (Custom checks)

**/*.go: Verify that any new or modified concurrent code (goroutines, channels, mutexes, sync primitives) is free of race conditions. Check for: proper lock/unlock pairing, no goroutine leaks, correct channel lifecycle management, and proper context cancellation propagation.
Verify that errors are not silently discarded. Check for: unchecked error returns, error wrapping with context, proper error propagation up the call chain, and no bare panic() calls outside of init() functions.

Files:

  • indexer/eventsProcessor.go
  • indexer/eventsProcessor_test.go
**/*_test.go

⚙️ CodeRabbit configuration file

**/*_test.go: Test files. Review for: - Adequate coverage of edge cases and error paths - Proper use of test helpers and assertions - Race condition coverage (tests should use -race flag patterns) - No hardcoded sleep for synchronization (use channels or sync primitives) - Test isolation (no shared mutable state between tests)

Files:

  • indexer/eventsProcessor_test.go
🔇 Additional comments (10)
indexer/eventsProcessor.go (5)

17-23: LGTM — Optional accountsDB dependency added correctly.

The accountsDB field is optional (no nil check in checkArgEventsProcessor), and the code handles a nil adapter gracefully in processAccountEvents via the check.IfNil guard.

Also applies to: 45-45


76-89: LGTM — Clean conditional dispatch when indexer is inactive.

The guard at line 81 ensures websocket events (including accounts) are only dispatched when no active indexer is processing. Transaction preparation is done once and reused for both event types.


121-133: LGTM — Simple dispatch helper.

Early return on empty slice prevents unnecessary event dispatch.


135-186: Verified: Past review concerns addressed.

  • Uses GetExistingAccount (line 166) instead of LoadAccount, avoiding unintended account creation.
  • Map keyed by canonical info.Address (line 182), consistent with SaveAccounts.
  • Receipt field extraction (lines 146-150) defensively handles non-string values via type assertion with ok check.

264-282: LGTM — Refactored to share buildAccountInfo logic.

Consistent with processAccountEvents: both paths now use the same builder and key by canonical info.Address.

indexer/eventsProcessor_test.go (5)

264-265: Good fix — error handling added.

Fixture errors will now fail fast instead of producing confusing downstream failures.


726-737: LGTM — Consistent helper pattern.


790-839: Good coverage of receipt address extraction.

The load count assertion at line 822 validates that all three distinct addresses (sender, receipt from, receipt to) trigger account lookups. This complements the previous test by verifying the fan-out count.


841-875: LGTM — Validates nil accountsDB guard.

Confirms that the check.IfNil(ep.accountsDB) guard in processAccountEvents prevents account event dispatch.


877-922: LGTM — Validates indexer-active bypass.

Confirms that when an active indexer is configured, the websocket dispatch path (including account events) is skipped entirely, delegating to the indexer's own event handling.

Comment thread indexer/eventsProcessor.go Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 21, 2026
Beroni
Beroni previously approved these changes Apr 21, 2026
Copy link
Copy Markdown
Member

@fbsobreira fbsobreira left a comment

Choose a reason for hiding this comment

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

The intent is correct, but the current implementation does not match the account-change detection already used by the indexer path.

The main issue is that the new fallback derives affected accounts only from:

  • tx.Sender
  • receipt "from"
  • receipt "to"

That is narrower than the existing altered-account logic in the indexer code, so the websocket fallback will still miss valid account updates for several transaction and receipt types.

Findings

1. Account detection is inconsistent with the existing indexer logic

processAccountEvents() currently collects addresses from:

  • tx.Sender
  • receipt "from"
  • receipt "to"

This misses account mutations that are already handled by the indexer through AlteredData.Accounts, including receipt fields such as:

  • "id" for UpdateValidator
  • "address" for UpdateAccountPermission
  • "owner" for UpdateMetadata
  • "contract" for SCTrigger
  • "address" for SetAccountName

It also misses contract-driven account changes already captured via DecodeContract() and the altered-account flow.

Impact:

  • The PR fixes simple transfer-style scenarios.
  • It does not restore feature parity with the indexer-backed account event path.
  • Future changes will be easy to miss because the logic is now duplicated in two places.

2. The fallback can produce expected lookup failures and warning noise

The new code loads accounts for every collected address and warns on load failure. Some receipt flows use the zero address or otherwise refer to values that should not be treated as regular user accounts.

The existing indexer logic already handles zero-address flows specially during mint/burn-style operations. The new websocket fallback does not.

Impact:

  • unnecessary trie lookups
  • noisy warnings for expected cases
  • more work on the block save path than needed

The main concern is correctness and operational behavior:

  • synchronous AccountsDB.GetExistingAccount() calls are now added to the SaveBlock() path
  • blocks touching many accounts can add extra trie reads before the method returns
  • warning logs may increase significantly for normal edge cases if zero/missing accounts are not filtered

Recommended Approach

The better implementation is to reuse the existing altered-account extraction logic instead of creating a second account-detection heuristic inside eventsProcessor.

Proposed design

  1. Extract a shared helper from the current transaction/indexing path that analyzes the block transactions and returns:
    • prepared transactions
    • altered accounts
    • optionally the full AlteredData
  2. Use that same helper in both paths:
    • indexer path for persistence/indexing
    • websocket fallback path for account event dispatch
  3. In the websocket fallback:
    • iterate the altered account keys
    • skip ZeroAddressDecoded
    • load current account state from AccountsDB
    • ignore ErrAccNotFound without warning
    • build AccountInfo
    • dispatch one ACCOUNTS event with the resulting map

Why this is better

  • one source of truth for "which accounts changed"
  • avoids duplication and future drift
  • automatically covers all currently supported receipt and contract cases
  • keeps websocket behavior aligned with the indexer behavior

Practical Refactor Suggestion

  1. Extract a pure analysis helper, for example analyzeBlockTransactions(...)
  2. Make it return:
    • []*data.Transaction
    • *data.AlteredData
  3. Have the indexer/database flow call that helper and continue with persistence
  4. Have eventsProcessor call that helper and dispatch account websocket events from AlteredData.Accounts.GetAll()

Verification Checklist

To validate the better implementation, I would expect tests covering:

  • transfer updates dispatch sender and recipient account events
  • UpdateValidator dispatches account events for receipt "id"
  • UpdateAccountPermission dispatches account events for receipt "address"
  • UpdateMetadata dispatches account events for receipt "owner"
  • SCTrigger dispatches account events for receipt "contract"
  • SetAccountName dispatches account events for the updated address
  • zero-address flows are skipped without warning noise
  • missing accounts (ErrAccNotFound) are ignored quietly
  • no websocket account events are emitted when the indexer is active
  • websocket fallback and indexer-altered-account detection produce the same address set for the same block

@phcarneirobc phcarneirobc dismissed stale reviews from Beroni and coderabbitai[bot] via 814a15c May 5, 2026 11:19
Copy link
Copy Markdown

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

🤖 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 `@indexer/eventsProcessor.go`:
- Around line 138-170: The function dispatchAccountEventsFromAlteredAccounts
currently builds accountsMap and calls dispatchAccountEvents even when it is
empty; change it to check the populated accountsMap (built in
dispatchAccountEventsFromAlteredAccounts) and only call dispatchAccountEvents
when len(accountsMap) > 0 (otherwise return), so avoid emitting empty ACCOUNTS
events; update logic around accountsMap and the final dispatchAccountEvents call
accordingly.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c1e58db-ab4b-47c5-b3f9-4e941ffe0b66

📥 Commits

Reviewing files that changed from the base of the PR and between edfd792 and 814a15c.

📒 Files selected for processing (2)
  • indexer/eventsProcessor.go
  • indexer/eventsProcessor_test.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: notify
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (Custom checks)

**/*.go: Verify that any new or modified concurrent code (goroutines, channels, mutexes, sync primitives) is free of race conditions. Check for: proper lock/unlock pairing, no goroutine leaks, correct channel lifecycle management, and proper context cancellation propagation.
Verify that errors are not silently discarded. Check for: unchecked error returns, error wrapping with context, proper error propagation up the call chain, and no bare panic() calls outside of init() functions.

Files:

  • indexer/eventsProcessor_test.go
  • indexer/eventsProcessor.go
**/*_test.go

⚙️ CodeRabbit configuration file

**/*_test.go: Test files. Review for: - Adequate coverage of edge cases and error paths - Proper use of test helpers and assertions - Race condition coverage (tests should use -race flag patterns) - No hardcoded sleep for synchronization (use channels or sync primitives) - Test isolation (no shared mutable state between tests)

Files:

  • indexer/eventsProcessor_test.go
🧠 Learnings (1)
📚 Learning: 2026-04-21T20:12:22.959Z
Learnt from: phcarneirobc
Repo: klever-io/klever-go PR: 38
File: indexer/eventsProcessor.go:188-211
Timestamp: 2026-04-21T20:12:22.959Z
Learning: In Go structs that are JSON-marshaled, if a field is a `bool` and has the `json:"...,omitempty"` tag, then leaving that field at its zero value (`false`) is functionally equivalent (in the resulting JSON) to explicitly setting `Foundation: false`. Reviewers should not flag struct literals that omit such `bool` fields as an inconsistency; they will serialize identically because `omitempty` suppresses `false` values.

Applied to files:

  • indexer/eventsProcessor_test.go
  • indexer/eventsProcessor.go
🔇 Additional comments (1)
indexer/eventsProcessor_test.go (1)

742-788: Still not proving sender/recipient fan-out in the new account-event tests.

Both tests still use an AccountsStub that returns the same account for every lookup and then only assert event presence or lookup count. They will pass even if one affected address is dropped or the payload is keyed incorrectly. Make the stub return distinct accounts per requested address and assert that the ACCOUNTS payload contains each expected key.

As per coding guidelines, **/*_test.go: Test files. Review for: - Adequate coverage of edge cases and error paths - Proper use of test helpers and assertions.

Also applies to: 793-830

Comment thread indexer/eventsProcessor.go
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
indexer/eventsProcessor_test.go (1)

967-1008: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert emitted account keys, not just lookup count.

This test still passes if the websocket payload collapses all three altered accounts into one entry, because GetExistingAccountCalled always returns the same createTestAccountStub() and that stub always reports AddressBytes() == "testaddr". Make the stub echo the requested addrBytes and assert the ACCOUNTS event contains senderAddr, fromAddr, and toAddr.

Suggested direction
-	acc := createTestAccountStub()
 	accountsDB := &mock.AccountsStub{
-		GetExistingAccountCalled: func(_ []byte) (state.AccountHandler, error) {
+		GetExistingAccountCalled: func(addrBytes []byte) (state.AccountHandler, error) {
 			atomic.AddInt32(&loadCount, 1)
+			acc := createTestAccountStub()
+			acc.AddressBytesCalled = func() []byte { return append([]byte(nil), addrBytes...) }
 			return acc, nil
 		},
 	}
@@
-	_, ok := accountEvent.Message.(map[string]*data.AccountInfo)
-	require.True(t, ok)
+	accountsMap, ok := accountEvent.Message.(map[string]*data.AccountInfo)
+	require.True(t, ok)
+	require.Contains(t, accountsMap, senderAddr)
+	require.Contains(t, accountsMap, fromAddr)
+	require.Contains(t, accountsMap, toAddr)

As per coding guidelines, **/*_test.go: Test files. Review for: - Adequate coverage of edge cases and error paths - Proper use of test helpers and assertions.

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

In `@indexer/eventsProcessor_test.go` around lines 967 - 1008, The test currently
only asserts load count but not that the ACCOUNTS event contains the three
distinct keys; change the mock in
TestEventsProcessor_DispatchAccountEventsFromAlteredAccounts_IncludesAllAddresses
so GetExistingAccountCalled returns an account stub that echoes the requested
addrBytes (instead of always using createTestAccountStub()), e.g. construct a
state.AccountHandler whose AddressBytes() returns the incoming []byte parameter,
then call ep.dispatchAccountEventsFromAlteredAccounts and inspect the dequeued
ACCOUNTS event (from testQueue) to assert the event.Message map contains keys
senderAddr, fromAddr and toAddr (and fail if any are missing) while keeping the
existing load count assertion.
🤖 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.

Duplicate comments:
In `@indexer/eventsProcessor_test.go`:
- Around line 967-1008: The test currently only asserts load count but not that
the ACCOUNTS event contains the three distinct keys; change the mock in
TestEventsProcessor_DispatchAccountEventsFromAlteredAccounts_IncludesAllAddresses
so GetExistingAccountCalled returns an account stub that echoes the requested
addrBytes (instead of always using createTestAccountStub()), e.g. construct a
state.AccountHandler whose AddressBytes() returns the incoming []byte parameter,
then call ep.dispatchAccountEventsFromAlteredAccounts and inspect the dequeued
ACCOUNTS event (from testQueue) to assert the event.Message map contains keys
senderAddr, fromAddr and toAddr (and fail if any are missing) while keeping the
existing load count assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 06fb34f1-f8b1-4319-8ca3-05d24726c11b

📥 Commits

Reviewing files that changed from the base of the PR and between 814a15c and 78ecbd7.

📒 Files selected for processing (2)
  • indexer/eventsProcessor.go
  • indexer/eventsProcessor_test.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (Custom checks)

**/*.go: Verify that any new or modified concurrent code (goroutines, channels, mutexes, sync primitives) is free of race conditions. Check for: proper lock/unlock pairing, no goroutine leaks, correct channel lifecycle management, and proper context cancellation propagation.
Verify that errors are not silently discarded. Check for: unchecked error returns, error wrapping with context, proper error propagation up the call chain, and no bare panic() calls outside of init() functions.

Files:

  • indexer/eventsProcessor.go
  • indexer/eventsProcessor_test.go
**/*_test.go

⚙️ CodeRabbit configuration file

**/*_test.go: Test files. Review for: - Adequate coverage of edge cases and error paths - Proper use of test helpers and assertions - Race condition coverage (tests should use -race flag patterns) - No hardcoded sleep for synchronization (use channels or sync primitives) - Test isolation (no shared mutable state between tests)

Files:

  • indexer/eventsProcessor_test.go
🧠 Learnings (1)
📚 Learning: 2026-04-21T20:12:22.959Z
Learnt from: phcarneirobc
Repo: klever-io/klever-go PR: 38
File: indexer/eventsProcessor.go:188-211
Timestamp: 2026-04-21T20:12:22.959Z
Learning: In Go structs that are JSON-marshaled, if a field is a `bool` and has the `json:"...,omitempty"` tag, then leaving that field at its zero value (`false`) is functionally equivalent (in the resulting JSON) to explicitly setting `Foundation: false`. Reviewers should not flag struct literals that omit such `bool` fields as an inconsistency; they will serialize identically because `omitempty` suppresses `false` values.

Applied to files:

  • indexer/eventsProcessor.go
  • indexer/eventsProcessor_test.go
🔇 Additional comments (1)
indexer/eventsProcessor.go (1)

81-90: This concern does not apply—account state is committed before SaveBlock is invoked.

In CommitBlock (core/process/block/block.go line 589), mp.commitAll() persists all account changes to the database before mp.indexBlock() is called at line 618, which triggers eventsProcessor.SaveBlock(). The websocket account events in dispatchAccountEventsFromAlteredAccounts read via ep.accountsDB.GetExistingAccount(), retrieving post-commit state. No stale data risk exists here.

			> Likely an incorrect or invalid review comment.

@fbsobreira
Copy link
Copy Markdown
Member

fbsobreira commented May 6, 2026

Goal

Make eventsProcessor.SaveBlock the single orchestrator for per-block prep, then enqueue websocket events on the commit goroutine (delivery to subscribers stays async via EventQueue) and enqueue indexer work to the dispatcher queue — without changing indexed data, without recomputing, and without working when nothing is enabled.

Note on "sync": trySendEvent is a non-blocking enqueue to the in-memory EventQueue channel; a separate goroutine drains the queue and does the actual network send. Calling trySendEvent from the commit goroutine costs nanoseconds and is what already happens today on the indexer-disabled path. The indexer-enabled path currently calls trySendEvent from a worker goroutine after the work item is dequeued, which causes timing drift between BLOCKS (commit-thread) and TXS (worker-thread). Centralizing the enqueue on the commit goroutine eliminates that drift.

Constraints

  1. Behavior parity — websocket and indexer must see the same txs, the same ad.Accounts, the same field shape.
  2. Indexed data invariant — every record persisted to elastic stays byte-identical. Refactors that don't change the documents are fine.
  3. Skip work when nothing is enabled — if !UseEventQueue && indexer is nil/IsNilIndexer, no prep, no dispatch.
  4. Avoid duplicate prepprepareTransactionsForDatabase runs at most once per block.

Current (broken) flow

SaveBlock(args)
├── dispatchWebsocketEvents(args)             [SYNC]
│   └── only when indexer == nil:
│       ├── dispatchBlockEvent                            (BLOCKS)
│       └── prepareTransactionsForDatabase     ← #1
│           ├── dispatchTransactionEvents                 (TXS)
│           └── dispatchAccountEventsFromAlteredAccounts  (ACCOUNTS)
│
└── dispatchToIndexer(args)                   [enqueue]
    └── dataIndexer.SaveBlock → workItem (async worker goroutine):
        └── elasticProcessor.SaveTransactions
            └── prepareTransactionsForDatabase ← #2 (duplicate prep)
                ├── trySendEvent USER_TRANSACTIONS  (TXS, but ASYNC + delayed!)
                └── indexAlteredAccounts             (no websocket ACCOUNTS)

Two real bugs revealed by this trace:

  • Event timing inconsistency: when indexer is enabled, websocket TX events fire from a worker goroutine (delayed, possibly reordered relative to BLOCKS). When indexer is disabled, they fire synchronously. Same node, two timings.
  • Coverage gaps when indexer is enabled: no BLOCKS event, no ACCOUNTS event over websocket.

Target flow

eventsProcessor.SaveBlock(args)               [commit goroutine, single entry]
├── early-out if !UseEventQueue && indexer disabled
├── prepareTransactionsForDatabase  ← runs ONCE (or zero times if pool is empty)
│
├── Websocket enqueue [commit goroutine, non-blocking]   — when UseEventQueue
│   ├── dispatchBlockEvent              → trySendEvent(BLOCKS)
│   ├── dispatchTransactionEvents(txs)  → trySendEvent(USER_TRANSACTIONS, TRANSACTIONS)
│   └── dispatchAccountEventsFromAlteredAccounts(ad.Accounts) → trySendEvent(ACCOUNTS)
│       (EventQueue consumer goroutine drains and pushes to subscribers — unchanged)
│
└── Indexer dispatch [enqueue to dataIndexer.dispatcher] — when indexer enabled
    └── ep.indexer.SaveBlock(args, prepared)  ← carries pre-computed data
        └── elasticProcessor consumes prepared data, no re-prep
            ├── indexTransactions, indexAssets, …  (UNCHANGED elastic writes)
            └── (no internal trySendEvent — orchestrator already enqueued)

Concrete change set

1. Carry prepared data through the indexer interface

Add an opaque payload to ArgsSaveBlockData (no new method, no breaking signature change for non-elastic users):

// data/indexer/args.go (or wherever ArgsSaveBlockData lives)
type ArgsSaveBlockData struct {
    // … existing fields …
    Prepared *PreparedBlockData // optional; if non-nil, indexer skips its own prep
}

type PreparedBlockData struct {
    Txs     []*data.Transaction
    TxsMap  map[string]*data.Transaction
    Altered *data.AlteredData
}

If Prepared == nil is encountered downstream (legacy callers, tests), elasticProcessor.SaveTransactions falls back to the current prepareTransactionsForDatabase call — backward-compat preserved.

2. Refactor eventsProcessor.SaveBlock

func (ep *eventsProcessor) SaveBlock(args *indexerData.ArgsSaveBlockData) {
    wsEnabled := UseEventQueue
    indexerEnabled := !check.IfNil(ep.indexer) && !ep.indexer.IsNilIndexer()
    if !wsEnabled && !indexerEnabled {
        return // honor constraint #3 — nothing to compute
    }

    // Single prep. Skip when there's nothing to prepare.
    var prepared *indexerData.PreparedBlockData
    if args.TransactionsPool != nil && len(args.TransactionsPool.Txs) > 0 {
        txs, txsMap, ad, err := ep.prepareTransactionsForDatabase(args.Header, args.TransactionsPool)
        if err != nil {
            log.Warn("eventsProcessor.SaveBlock: prepare failed", "error", err)
            // fall through: still emit BLOCKS event below; skip TX/ACCOUNTS
        } else {
            prepared = &indexerData.PreparedBlockData{Txs: txs, TxsMap: txsMap, Altered: ad}
        }
    }

    // Websocket: enqueue on the commit goroutine (non-blocking trySendEvent).
    // Delivery to subscribers stays async via EventQueue.
    if wsEnabled {
        ep.dispatchBlockEvent(args)
        if prepared != nil {
            ep.dispatchTransactionEvents(prepared.Txs)
            ep.dispatchAccountEventsFromAlteredAccounts(args.Header.GetTimestamp(), prepared.Altered.Accounts)
        }
    }

    // Indexer: enqueue with prepared data
    if indexerEnabled {
        args.Prepared = prepared
        ep.indexer.SaveBlock(args)
    }
}

Delete the dispatchWebsocketEvents and dispatchToIndexer helpers — they collapse into the body above.

3. Adjust elasticProcessor.SaveTransactions to consume prepared data

func (ei *elasticProcessor) SaveTransactions(header nodeData.HeaderHandler, pool *indexer.Pool, prepared *indexer.PreparedBlockData) error {
    var (
        txs    []*data.Transaction
        txsMap map[string]*data.Transaction
        ad     *data.AlteredData
        err    error
    )
    if prepared != nil {
        txs, txsMap, ad = prepared.Txs, prepared.TxsMap, prepared.Altered
    } else {
        txs, txsMap, ad, err = ei.prepareTransactionsForDatabase(header, pool)
        if err != nil {
            return err
        }
    }

    // REMOVE the trySendEvent block here — orchestrator handles it now.
    // (lines 549-558 in elasticProcessor.go are deleted)

    // … existing index* calls unchanged …
}

The workItemBlock already holds *ArgsSaveBlockData; just thread prepared through:

// indexer/workItems/workItemBlock.go:61
err = wib.indexer.SaveTransactions(
    wib.argsSaveBlock.Header,
    wib.argsSaveBlock.TransactionsPool,
    wib.argsSaveBlock.Prepared,
)

And update the saveBlockIndexer interface in workItems/interface.go to match.

4. Align ACCOUNTS dispatch with indexer semantics (parity)

To honor constraint #1 (same data on both paths), dispatchAccountEventsFromAlteredAccounts must mirror elasticProcessor.indexAlteredAccounts exactly:

Aspect Indexer path This PR's websocket path Action
Zero address skip none skips Remove the skip at eventsProcessor.go:146
Account loader LoadAccount (creates on miss) GetExistingAccount (errors) Switch to LoadAccount
Type-assert miss log.Warn silent continue Add log.Warn
Kapp accounts uses kappsDB.LoadAccount (not loaded) Add kappsDB field, mirror elasticProcessor.go:1414-1421

This also closes findings #4, #5, #6 from the multi-agent review.

5. Mirror the same pattern in SaveAccounts

SaveAccounts at eventsProcessor.go:237 has the same asymmetric guard (UseEventQueue && … && (indexer == nil)). Apply the same fix: dispatch websocket ACCOUNTS unconditionally when UseEventQueue, then forward to indexer when configured.

6. Pool mutation containment

Now that there's a single caller of prepareTransactionsForDatabase per block, the delete(txPool.Txs, hash) inside it has only one source. Document it (one-line comment on the function) or — preferred — move the delete out and have the orchestrator decide. With prepared-data plumbing, the indexer never needs the original pool entries anyway (txsMap carries everything).

Indexed data invariant — explicitly preserved

The following stays identical:

  • BuildTransaction is unchanged (same data.Transaction shape, same SearchOrder assignment via setTransactionSearchOrder).
  • DecodeContract, indexReceipt, indexAlteredAccounts, indexTransactions, indexKDAPools, indexAssets, indexProposals, indexITOs, indexMarketplaces, indexOrders, prepareAndIndexLogs, indexScDeploys — all called with the same arguments.
  • The ad (altered data) object is the same instance the indexer already used; just constructed one level higher.
  • Elastic JSON documents — byte-identical.

What does change behaviorally for elastic-enabled deployments:

  • USER_TRANSACTIONS / TRANSACTIONS websocket events are now enqueued on the commit goroutine (matched in order with BLOCKS), instead of being enqueued later from the elastic worker goroutine. Subscribers see them in consistent order with no extra latency hop. This is a fix, not a regression.
  • BLOCKS and ACCOUNTS websocket events now also fire when indexer is enabled (previously didn't).

Worth a one-line note in the PR description so operators can update consumers if any rely on the old async timing.

Test plan

Replace the no-indexer-only tests with a 2×2 matrix:

UseEventQueue indexer enabled Expected outcome
false false prepareTransactionsForDatabase not called; no events
true false All websocket events fire; no indexer call
false true No websocket events; indexer receives Prepared != nil
true true All websocket events fire and indexer receives Prepared != nil (single prep)

Plus a parity test that asserts the same account set hits the websocket queue as ad.Accounts keys (no zero-address filter, no ErrAccNotFound skip). The existing TestEventsProcessor_WebsocketAndIndexerProduceSameAddressSet already exists — just remove the if addr != ZeroAddressDecoded filter on line 1168.

Performance note (from review F13)

accountsDB.GetExistingAccount / LoadAccount take mutOp.Lock() (write lock). Calling them synchronously from the block-commit goroutine for every altered account is a new cost on indexer-disabled nodes. Out of scope for this replay, but worth a follow-up: dispatch the trie reads to a buffered worker so commit isn't blocked by trie I/O. Track separately.

Suggested PR title rewrite

KLC-2309 Centralize block prep in events processor; dispatch websocket and indexer independently

Order of work

  1. Extend ArgsSaveBlockData with Prepared *PreparedBlockData (pure additive).
  2. Update Indexer / saveBlockIndexer interfaces + workItemBlock to thread it through.
  3. Rewrite eventsProcessor.SaveBlock per §2; delete now-dead helpers.
  4. Update elasticProcessor.SaveTransactions per §3; remove its trySendEvent block.
  5. Align dispatchAccountEventsFromAlteredAccounts per §4; add kappsDB.
  6. Mirror in SaveAccounts per §5.
  7. Update tests for the 2×2 matrix and the parity test.
  8. Run the existing indexer integration test (if any) to confirm elastic JSON is unchanged.

Copy link
Copy Markdown
Member

@fbsobreira fbsobreira left a comment

Choose a reason for hiding this comment

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

check comment

@fbsobreira fbsobreira marked this pull request as draft May 13, 2026 17:35
@fbsobreira fbsobreira force-pushed the KLC-2309-dispatch-accounts-events-without-indexer branch 2 times, most recently from 1ba18b1 to db5a529 Compare May 14, 2026 13:38
@fbsobreira fbsobreira force-pushed the KLC-2309-dispatch-accounts-events-without-indexer branch from db5a529 to dc512ca Compare May 14, 2026 13:43
@fbsobreira fbsobreira added the WIP work in progress label May 14, 2026
@fbsobreira fbsobreira self-assigned this May 19, 2026
@fbsobreira fbsobreira marked this pull request as ready for review May 19, 2026 12:57
…ndexer

Promote eventsProcessor to a per-block orchestrator so websocket subscribers
receive BLOCKS / USER_TRANSACTIONS / TRANSACTIONS / ACCOUNTS events whether
or not the elasticsearch indexer is configured. The orchestrator runs
prepareTransactionsForDatabase once on the commit goroutine and forwards
the prepared payload to the elastic worker via ArgsSaveBlockData.Prepared,
guaranteeing websocket and elastic see the same data with no duplicate prep.

Key changes:

- eventsProcessor.SaveBlock is now the single per-block dispatch site for
  websocket events. dispatchWebsocketEvents / dispatchToIndexer / the lighter
  prepareTransactionsForEvents shadow path are removed.

- ACCOUNTS events are derived from AlteredData.Accounts (the same set the
  indexer would index), each address resolved via AccountsDB.GetExistingAccount
  so ghost-account payloads (e.g. ZeroAddress for mint/burn) are not emitted.
  AccountsDB is wired through ArgEventsProcessor / startup.go.

- A single buildAccountInfo (plus convertPermissions /
  getAllowanceWithPendingRewards / calculateUnfrozenBalance) lives in
  indexer/accountInfo.go and is the source of truth for AccountInfo on both
  the elastic and websocket paths — no more parallel implementations.

- prepareTransactionsForDatabase no longer mutates pool.Txs. With the
  orchestrator running prep before the work item, the stale delete drained
  pool.Txs and caused workItems.ComputeSizeOfTxs and dispatchBlockEvent to
  see an empty pool, producing SizeTxs=0 in both the indexed block and the
  websocket BLOCKS payload.

- dispatchBlockEvent now uses workItems.ComputeSizeOfTxs (bytes) for SizeTxs,
  matching the elastic-write path. The count is already carried by the
  separate TxCount field, so no information is lost.

- elasticProcessor.SaveTransactions accepts a *data.PreparedBlockData and
  reuses it when present; the fallback re-prep is kept for direct test
  calls and as a safety net.

- elasticProcessor no longer emits BLOCKS / TRANSACTIONS / USER_TRANSACTIONS /
  ACCOUNTS events; those are owned by the orchestrator. Centralization
  invariants are locked in by tests.

- Per-block prep failure is logged at Error (not Warn) with the block nonce.

Tests:
- Behavioral matrix (ws on/off × indexer on/off) for SaveBlock.
- Per-receipt-type fan-out tests for sender + recipient extraction
  (transfer, validator, account-permission, metadata, SC-trigger,
  set-account-name).
- Regression test asserting SaveBlock does not drain TransactionsPool.Txs.
- Fallback-path test for SaveTransactions(prepared=nil, non-empty pool).
- Parity test confirming websocket and indexer query the same address set.
- BenchmarkEventsProcessor_SaveBlock (0 / 50 / 500 tx, ws-only / ws+indexer)
  to anchor commit-thread cost.
…hread prep for indexer-only nodes

- gate prepareTransactionsForDatabase on wsEnabled in SaveBlock; indexer-only
  nodes let the elastic worker re-prep on its own goroutine via the fallback
  in elasticProcessor.SaveTransactions, restoring pre-PR commit-thread cost
- extract prepare() helper and indexerEnabled() method; unify 6 inconsistent
  nil-checks on ep.indexer
- collapse 4 log.Warn calls in dispatchAccountEventsFromAlteredAccounts into
  buildAlteredAccountInfo helper; drop unreachable UserAccountHandler cast
- delete narrative/change-history comments across production + tests
- add BenchmarkEventsProcessor_SaveBlock_*_IndexerOnly variants to anchor the
  F1 gate (~30x cheaper at 500 tx/block vs WSOnly path)
…e complexity

Extract resolvePreparedBlockData and indexBlockArtifacts helpers so the
top-level SaveTransactions stays under the cognitive-complexity ceiling.
Behavior is unchanged: same prep fallback, same sequential index steps,
same first-error short-circuit.
@fbsobreira fbsobreira force-pushed the KLC-2309-dispatch-accounts-events-without-indexer branch from f115ccc to ee93104 Compare May 19, 2026 13:27
@fbsobreira fbsobreira removed performance WIP work in progress labels May 19, 2026
@fbsobreira
Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

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

🤖 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 `@indexer/eventsProcessor_test.go`:
- Around line 406-407: Replace the blocking receive "ev := <-testQueue" used
before asserting ev.EvType == BLOCKS with a non-blocking select that waits for
the event or fails fast on timeout; for example, use select { case ev :=
<-testQueue: require.Equal(t, BLOCKS, ev.EvType) case
<-time.After(1*time.Second): t.Fatalf("timed out waiting for %v event on
testQueue", BLOCKS) } (or use context.WithTimeout and require.FailNow) to avoid
deadlocks — apply this change where testQueue is read and BLOCKS is asserted
(functions/tests referencing testQueue, ev, and require.Equal).

In `@indexer/eventsProcessor.go`:
- Around line 75-99: In eventsProcessor.SaveBlock, ensure args.Prepared is
cleared each call to avoid reusing stale prepared payloads: at the start of
SaveBlock (before checking UseEventQueue/indexerEnabled) set args.Prepared =
nil, or explicitly set args.Prepared = nil in the non-websocket branch so that
prepare()’s result is only used when freshly produced; update references to
eventsProcessor.prepare, UseEventQueue, indexerEnabled and args.Prepared
accordingly.

In `@indexer/workItems/workItemBlock_test.go`:
- Around line 55-56: The tests set SaveTransactionsCalled mocks but never verify
the forwarded prepared parameter — update each mock (the SaveTransactionsCalled
closures) to capture/compare the incoming prepared value against the expected
prepared object: define an expectedPrepared variable in the test, have the mock
assert require.Equal(t, expectedPrepared, prepared) (or use reflect.DeepEqual +
t.Fatal) and return the same error paths as before; do this for both occurrences
of SaveTransactionsCalled so the forwarding contract for prepared is explicitly
asserted.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b64d699-9d33-4eee-9a41-331c252822b1

📥 Commits

Reviewing files that changed from the base of the PR and between 814a15c and ee93104.

📒 Files selected for processing (15)
  • cmd/node/startup.go
  • data/indexer/dtos.go
  • indexer/accountInfo.go
  • indexer/data/prepared.go
  • indexer/dataIndexerArgs.go
  • indexer/elasticProcessor.go
  • indexer/elasticProcessor_test.go
  • indexer/eventsProcessor.go
  • indexer/eventsProcessor_test.go
  • indexer/interface.go
  • indexer/mock/elasticProcessorStub.go
  • indexer/processTransactions.go
  • indexer/workItems/interface.go
  • indexer/workItems/workItemBlock.go
  • indexer/workItems/workItemBlock_test.go
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.go

📄 CodeRabbit inference engine (Custom checks)

**/*.go: Verify that any new or modified concurrent code (goroutines, channels, mutexes, sync primitives) is free of race conditions. Check for: proper lock/unlock pairing, no goroutine leaks, correct channel lifecycle management, and proper context cancellation propagation.
Verify that errors are not silently discarded. Check for: unchecked error returns, error wrapping with context, proper error propagation up the call chain, and no bare panic() calls outside of init() functions.

Files:

  • indexer/workItems/workItemBlock.go
  • data/indexer/dtos.go
  • indexer/interface.go
  • indexer/dataIndexerArgs.go
  • indexer/workItems/workItemBlock_test.go
  • cmd/node/startup.go
  • indexer/data/prepared.go
  • indexer/workItems/interface.go
  • indexer/mock/elasticProcessorStub.go
  • indexer/accountInfo.go
  • indexer/processTransactions.go
  • indexer/elasticProcessor_test.go
  • indexer/elasticProcessor.go
  • indexer/eventsProcessor_test.go
  • indexer/eventsProcessor.go
data/**

⚙️ CodeRabbit configuration file

data/**: Core data structures and state management. - Verify protobuf serialization/deserialization is correct and backwards-compatible - Check for proper nil/empty checks on decoded data - Ensure trie operations maintain data integrity - Validate state transitions are deterministic

Files:

  • data/indexer/dtos.go
**/*_test.go

⚙️ CodeRabbit configuration file

**/*_test.go: Test files. Review for: - Adequate coverage of edge cases and error paths - Proper use of test helpers and assertions - Race condition coverage (tests should use -race flag patterns) - No hardcoded sleep for synchronization (use channels or sync primitives) - Test isolation (no shared mutable state between tests)

Files:

  • indexer/workItems/workItemBlock_test.go
  • indexer/elasticProcessor_test.go
  • indexer/eventsProcessor_test.go
🧠 Learnings (1)
📚 Learning: 2026-04-21T20:12:22.959Z
Learnt from: phcarneirobc
Repo: klever-io/klever-go PR: 38
File: indexer/eventsProcessor.go:188-211
Timestamp: 2026-04-21T20:12:22.959Z
Learning: In Go structs that are JSON-marshaled, if a field is a `bool` and has the `json:"...,omitempty"` tag, then leaving that field at its zero value (`false`) is functionally equivalent (in the resulting JSON) to explicitly setting `Foundation: false`. Reviewers should not flag struct literals that omit such `bool` fields as an inconsistency; they will serialize identically because `omitempty` suppresses `false` values.

Applied to files:

  • indexer/workItems/workItemBlock.go
  • data/indexer/dtos.go
  • indexer/interface.go
  • indexer/dataIndexerArgs.go
  • indexer/workItems/workItemBlock_test.go
  • cmd/node/startup.go
  • indexer/data/prepared.go
  • indexer/workItems/interface.go
  • indexer/mock/elasticProcessorStub.go
  • indexer/accountInfo.go
  • indexer/processTransactions.go
  • indexer/elasticProcessor_test.go
  • indexer/elasticProcessor.go
  • indexer/eventsProcessor_test.go
  • indexer/eventsProcessor.go
🔇 Additional comments (12)
indexer/workItems/workItemBlock.go (1)

61-61: LGTM!

data/indexer/dtos.go (1)

14-17: LGTM!

indexer/interface.go (1)

34-34: LGTM!

indexer/dataIndexerArgs.go (1)

57-57: LGTM!

cmd/node/startup.go (1)

732-732: LGTM!

indexer/data/prepared.go (1)

1-9: LGTM!

indexer/workItems/interface.go (1)

19-19: LGTM!

indexer/mock/elasticProcessorStub.go (1)

21-22: LGTM!

Also applies to: 88-90

indexer/accountInfo.go (1)

17-44: LGTM!

Also applies to: 46-103

indexer/processTransactions.go (1)

52-54: LGTM!

indexer/elasticProcessor_test.go (1)

170-201: LGTM!

Also applies to: 238-239, 306-307, 320-321, 360-361, 1365-1393, 1395-1437, 1490-1592

indexer/elasticProcessor.go (1)

527-600: LGTM!

Also applies to: 1591-1592

Comment thread indexer/eventsProcessor_test.go
Comment thread indexer/eventsProcessor.go
Comment thread indexer/workItems/workItemBlock_test.go
@nickgs1337 nickgs1337 merged commit 06e3382 into develop May 20, 2026
12 checks passed
@nickgs1337 nickgs1337 deleted the KLC-2309-dispatch-accounts-events-without-indexer branch May 20, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants