Skip to content

feat: implement confirmation blocks logic for event listening#17

Closed
calj wants to merge 3 commits intomasterfrom
feat/confirmation-blocks
Closed

feat: implement confirmation blocks logic for event listening#17
calj wants to merge 3 commits intomasterfrom
feat/confirmation-blocks

Conversation

@calj
Copy link
Contributor

@calj calj commented Mar 5, 2026

Summary by CodeRabbit

  • New Features

    • Added configurable confirmation block depth. Users can now specify the number of blockchain confirmations required before transaction finality.
  • Improvements

    • Enhanced blockchain event listening with confirmation-aware block synchronization for more reliable transaction monitoring.

@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

Walkthrough

These changes introduce blockchain confirmation block depth configuration throughout the custody listener system. The system now accepts and propagates a confirmationBlocks parameter from configuration to event listening logic, replacing historical log channels with a head subscription-driven reconciliation approach.

Changes

Cohort / File(s) Summary
Configuration
config/config.go
Added ConfirmationBlocks uint64 field with YAML tag confirmation_blocks to BlockchainConfig struct.
Listener Core Refactoring
custody/listener.go
Introduced confirmationBlocks field to Listener struct. Updated NewListener constructor to accept confirmationBlocks parameter. Reworked listenEvents to use head subscription (SubscribeNewHead) instead of historical/current log channels. Modified block reconciliation to trigger based on new head events with target block calculated as current head minus confirmationBlocks. Updated reconcileBlockRange signature and watch method propagation.
Test Infrastructure
service/integration_test.go
Added SubscribeNewHead method to simBackendClient to support head subscription via polling with ticker-based block detection and proper quit signal handling. Added go-ethereum event package import.
Service Integration
service/service.go
Updated NewListener call in Withdraw listener construction to pass conf.Blockchain.ConfirmationBlocks parameter.

Sequence Diagram(s)

sequenceDiagram
    participant Service as Service
    participant Listener as Listener
    participant Backend as Backend (Client)
    participant Handler as Handler/Reconciliation
    
    Service->>Listener: NewListener(confirmationBlocks=N)
    Listener->>Listener: Store confirmationBlocks field
    
    Listener->>Backend: SubscribeNewHead(ctx, headerCh)
    Backend-->>Listener: Subscription + header channel
    
    loop On New Block Header
        Backend->>Listener: Send header on channel
        Listener->>Listener: Calculate targetBlock = currentHead - N
        Listener->>Handler: reconcileBlockRange(targetBlock)
        Handler->>Handler: Process logs up to targetBlock
        Listener->>Listener: Update lastBlock & lastIndex
    end
    
    Listener->>Listener: Handle subscription errors with backoff
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A new head subscription hops into view,
Confirmation blocks guide what we pursue,
No more historical channels delay the flow,
Just head-driven reconciliation, steady and slow! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing confirmation blocks logic for event listening across multiple files (config, listener, service).

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/confirmation-blocks

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

Copy link

@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

🧹 Nitpick comments (2)
service/integration_test.go (1)

125-126: Remove stale inline TODO-style comment in send path.

These lines imply missing quit/context handling, but the select directly below already handles both. Keeping this comment can mislead future edits.

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

In `@service/integration_test.go` around lines 125 - 126, Remove the stale inline
TODO-style comment that suggests missing quit/context handling in the send path;
the select immediately below already handles both channel send and quit, so
delete the two comment lines near the send logic (the lines referencing "Check
if channel is closed or full?" and "Ideally we should select on quit as well
while sending") to avoid misleading future readers and keep the send path in the
function where the select is implemented (the send/send path surrounding the
select block).
custody/listener.go (1)

229-232: Extract target-block calculation to one helper.

The same currentHead -> targetBlock logic appears twice; pulling it into a helper reduces drift risk and makes confirmation semantics easier to audit.

Also applies to: 259-262

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

In `@custody/listener.go` around lines 229 - 232, Extract the repeated calculation
of targetBlock into a small helper (e.g., computeTargetBlock or targetBlockFor)
that takes currentHead and confirmationBlocks (both uint64) and returns uint64:
if currentHead > confirmationBlocks return currentHead - confirmationBlocks else
return 0; replace both occurrences that set targetBlock from currentHead and
confirmationBlocks (the blocks that currently assign targetBlock using
currentHead/confirmationBlocks) with a call to this helper so confirmation
semantics live in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config/config.go`:
- Around line 23-27: BlockchainConfig.Validate() currently doesn't check
ConfirmationBlocks, so a missing/zero value silently disables confirmation
depth; update the BlockchainConfig.Validate() method to reject zero (and
negative if applicable) by returning an error when ConfirmationBlocks == 0 (or <
1), and include a clear error message referencing ConfirmationBlocks and
expected minimum (e.g., "ConfirmationBlocks must be >= 1"); ensure any callers
that load/parse config propagate/handle that validation error.

In `@custody/listener.go`:
- Around line 247-249: The bug is that after a full reconciliation the code
resets lastIndex to 0 which causes logs in the same lastBlock to be reprocessed;
instead, when advancing the cursor (around variables lastBlock, lastIndex and
targetBlock) set lastIndex to the position after the last processed log in that
block (e.g., lastIndex = lastProcessedIndex + 1 or lastIndex = targetIndex + 1)
so the next pass skips already-delivered entries; apply the same fix to the
analogous block at the other occurrence (the section around lines 277-279) so
both advancement paths move the index forward rather than back to 0.

---

Nitpick comments:
In `@custody/listener.go`:
- Around line 229-232: Extract the repeated calculation of targetBlock into a
small helper (e.g., computeTargetBlock or targetBlockFor) that takes currentHead
and confirmationBlocks (both uint64) and returns uint64: if currentHead >
confirmationBlocks return currentHead - confirmationBlocks else return 0;
replace both occurrences that set targetBlock from currentHead and
confirmationBlocks (the blocks that currently assign targetBlock using
currentHead/confirmationBlocks) with a call to this helper so confirmation
semantics live in one place.

In `@service/integration_test.go`:
- Around line 125-126: Remove the stale inline TODO-style comment that suggests
missing quit/context handling in the send path; the select immediately below
already handles both channel send and quit, so delete the two comment lines near
the send logic (the lines referencing "Check if channel is closed or full?" and
"Ideally we should select on quit as well while sending") to avoid misleading
future readers and keep the send path in the function where the select is
implemented (the send/send path surrounding the select block).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e276ef93-6918-4781-9ef7-c98941ea913b

📥 Commits

Reviewing files that changed from the base of the PR and between 89a593b and 62f3ac4.

📒 Files selected for processing (4)
  • config/config.go
  • custody/listener.go
  • service/integration_test.go
  • service/service.go

Comment on lines +23 to 27
RPCURL string `yaml:"rpc_url"`
ContractAddr string `yaml:"contract_address"`
PrivateKey string `yaml:"private_key"`
ConfirmationBlocks uint64 `yaml:"confirmation_blocks"`
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate confirmation_blocks so zero doesn’t silently disable confirmation depth.

Line 26 introduces ConfirmationBlocks, but BlockchainConfig.Validate() does not guard it. With an omitted config value, this defaults to 0 and the listener processes unconfirmed head blocks.

🔧 Suggested fix
func (c BlockchainConfig) Validate() error {
	if c.RPCURL == "" {
		return errors.New("missing blockchain RPC URL")
	}
	...
	if c.PrivateKey == "" {
		return errors.New("missing private key")
	}
+	if c.ConfirmationBlocks == 0 {
+		return errors.New("confirmation_blocks must be greater than 0")
+	}
	return nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@config/config.go` around lines 23 - 27, BlockchainConfig.Validate() currently
doesn't check ConfirmationBlocks, so a missing/zero value silently disables
confirmation depth; update the BlockchainConfig.Validate() method to reject zero
(and negative if applicable) by returning an error when ConfirmationBlocks == 0
(or < 1), and include a clear error message referencing ConfirmationBlocks and
expected minimum (e.g., "ConfirmationBlocks must be >= 1"); ensure any callers
that load/parse config propagate/handle that validation error.

Comment on lines +247 to 249
lastBlock = targetBlock
lastIndex = 0
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Cursor advancement replays logs from the previously reconciled block.

After a full reconciliation, resetting lastIndex to 0 means the next pass can re-handle same-block logs with index > 0, causing duplicate deliveries.

🔧 Suggested fix
-				lastBlock = targetBlock
-				lastIndex = 0
+				lastBlock = targetBlock
+				// Full block has been reconciled; skip it entirely on next pass.
+				lastIndex = ^uint32(0)
...
-				lastBlock = targetBlock
-				lastIndex = 0
+				lastBlock = targetBlock
+				// Full block has been reconciled; skip it entirely on next pass.
+				lastIndex = ^uint32(0)

Also applies to: 277-279

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

In `@custody/listener.go` around lines 247 - 249, The bug is that after a full
reconciliation the code resets lastIndex to 0 which causes logs in the same
lastBlock to be reprocessed; instead, when advancing the cursor (around
variables lastBlock, lastIndex and targetBlock) set lastIndex to the position
after the last processed log in that block (e.g., lastIndex = lastProcessedIndex
+ 1 or lastIndex = targetIndex + 1) so the next pass skips already-delivered
entries; apply the same fix to the analogous block at the other occurrence (the
section around lines 277-279) so both advancement paths move the index forward
rather than back to 0.

calj added 3 commits March 6, 2026 10:42
Rework the event listener to subscribe to new block headers
(SubscribeNewHead) instead of SubscribeFilterLogs, then reconcile
logs per block range. This makes the listener resilient to WebSocket
disconnections (e.g. Infura error 1006).

The service goroutine now loops: on first run it uses the existing
client; on subsequent iterations it re-dials via RPCURL, re-binds
the contract, and resumes from the persisted cursor. When RPCURL is
empty (simulated backend in tests) it exits cleanly instead of
attempting to dial.

Also fixes two bugs in waitForBackOffTimeout:
- return false (not true) when max retries exceeded
- use 1<<n (bit-shift) instead of 2^n (XOR) for exponential backoff
@calj calj force-pushed the feat/confirmation-blocks branch from 62f3ac4 to d15b864 Compare March 6, 2026 09:54
@calj calj closed this Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant