Skip to content

YNU-941: apply final round audit findings#758

Merged
nksazonov merged 27 commits into
mainfrom
fix/audit-findings-final
May 13, 2026
Merged

YNU-941: apply final round audit findings#758
nksazonov merged 27 commits into
mainfrom
fix/audit-findings-final

Conversation

@philanton
Copy link
Copy Markdown
Contributor

@philanton philanton commented May 13, 2026

Summary by CodeRabbit

  • New Features

    • Session-key registrations now require and validate both wallet and session-key-holder signatures.
    • ValidatorRegistered event watcher added (SDK + tooling) for subscription/replay.
    • Key-state queries gain pagination and an include_inactive option; responses include metadata.
  • Improvements

    • New channel lifecycle state: closing.
    • WebSocket controls: max message size and per-connection byte-rate limits.
    • Per-user session-key registration limits enforced.
  • Security / Enforcement

    • Stricter escrow/msg.value and challenge-duration validations; purge events now list purged escrow IDs.

Review Change Stack

nksazonov and others added 23 commits May 8, 2026 15:56
## Summary
- Audit finding **MF-H01**: `channels.v1.get_last_key_states` and
`app_sessions.v1.get_last_key_states` returned every session-key state
for a wallet in a single response. A user who registers many keys for
their own wallet could repeatedly trigger expensive DB reads and large
payloads (DB / CPU / memory exhaustion).
- Add mandatory pagination with a hard maximum page size of **10** to
both endpoints. Default limit also **10**; values above the cap are
silently clamped.
- Store layer now returns `(states, totalCount, err)`, computed via a
`COUNT()` subquery against the same join. Handlers populate the standard
`PaginationMetadataV1` response field used elsewhere in the v1 API.
- `ORDER BY (created_at DESC, id ASC)` for stable pagination — `id` is
the primary key, used as a deterministic tiebreaker so concurrent
inserts in the same `created_at` tick do not cause overlapping or missed
rows across pages.

## Changes
- `pkg/rpc/api.go`: add `Pagination` to `*GetLastKeyStatesRequest`, add
`Metadata` to `*GetLastKeyStatesResponse` for both `channels.v1` and
`app_sessions.v1`.
- `nitronode/store/database/{channel,app_session}_session_key_state.go`:
new signature `(wallet, sessionKey, limit, offset) -> (states,
totalCount, err)`. Adds COUNT subquery + `LIMIT`/`OFFSET` + stable
order.
- `nitronode/store/database/interface.go`: signatures updated.
- `nitronode/api/{channel_v1,app_session_v1}/`: handler `Store`
interfaces, handlers, and mocks updated. Handlers parse pagination
params, clamp to default 10 / max 10, build metadata.
- `docs/api.yaml`: document new `pagination` request field and
`metadata` response field for both methods.
- DB store tests: bulk-updated existing call sites; added a new
pagination subtest (5 keys, page 2/2/1, no overlap, `totalCount == 5`).

## Test plan
- [x] `go build ./...`
- [x] `go vet ./...`
- [x] `go test ./...` — all packages pass, including new pagination
tests in `nitronode/store/database`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added pagination to get_last_key_states for channels and apps:
requests accept optional pagination params; responses include pagination
metadata (page, per-page, total count, page count). Maximum page size is
capped at 10.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Maharshi Mishra <ihsraham27@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ection (#723)

## Summary
- Cap inbound WebSocket frame size at the transport layer via
`gorilla.SetReadLimit` (default 128 KiB). Frames over the cap close the
connection with code 1009 before any allocation.
- Add a pluggable `FrameRateLimiter` interface
(`pkg/rpc/rate_limiter.go`) with a `ByteTokenBucket` implementation.
Per-connection byte budget (default 256 KiB/s steady, 1 MiB burst)
consulted before each frame is dispatched.
- Wire knobs through `WebsocketNodeConfig` and nitronode `runtime.go`
(`NITRONODE_WS_MAX_MESSAGE_SIZE`, `NITRONODE_WS_BYTES_PER_SEC`,
`NITRONODE_WS_BYTES_BURST`).
- Unit tests cover bucket burst/refill/cap/overflow + connection
behavior on oversized frame and limiter rejection.
- stress-v1 chart picks up the new envs and gains NGINX ingress
annotations (`limit-connections`, `limit-rps`, `limit-burst-multiplier`,
`proxy-body-size`). Chart README documents the layered defense
(Cloudflare → ingress-nginx → app).

## Why
Audit finding: the RPC server unmarshals incoming WebSocket frames
before any size or rate check. An unauthenticated attacker could send
oversized frames or sustained large traffic and exhaust memory/CPU
before the per-message rate-limit middleware ran (it executes inside
`processRequests`, after the first JSON unmarshal). The new caps stop
the work at the transport layer.

## Companion PR
Pairs with [layer-3/ops#15](layer-3/ops#15)
which preserves real client IPs through ingress-nginx so the per-IP
annotations on the WebSocket Ingress key on real clients.

## Notable
- Latent bug fixed: `readMessages` did not call `handleClosure` on the
queue-full path, so `Serve`'s `wg.Wait` could hang on that exit.
- Defaults sized for browser reload reconnect storms (auth + subscribe ≈
20 KB / tab) — they clear the bucket instantly.
- Disable the byte-rate cap with `NITRONODE_WS_BYTES_PER_SEC=-1` for
canary rollout. Frame size cap stays on regardless.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* WebSocket connection hardening with configurable message size limits
and per-connection byte-rate limiting to mitigate denial-of-service
scenarios.

* **Documentation**
* Added WebSocket DoS hardening guidance including WAF rate-limiting
recommendations and NGINX Ingress configuration examples.

* **Tests**
* Added comprehensive tests for WebSocket rate limiting and oversized
frame handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Plumb the on-chain user signature from the parsed `State` payload
through the reactor into `channelEvent` / `channelChallengedEvent`, then
forward it to the matching `EventHandlerService` handlers.
- New `ChannelHubEventHandlerStore.UpdateStateUserSigIfMissing`
populates `channel_states.user_sig` when NULL; idempotent on replay via
`user_sig IS NULL` guard.
- Closes the wedge where a unilateral on-chain checkpoint of a node-only
state (e.g. recipient submits the `TransferReceive` state directly)
leaves the local row partially signed, causing
`EnsureNoOngoingStateTransitions` to permanently mismatch versions and
block the channel until an on-chain challenge.

## Threat addressed
Recipient signs the node-issued `TransferReceive` state and submits it
on chain. `HomeChannelCheckpointed` bumps `channels.state_version` to
N+1, but `channel_states` row at N+1 still has `user_sig = NULL`. The
gate query in `EnsureNoOngoingStateTransitions` filters on both
signatures, picks the prior bilateral row at N, and reports a version
mismatch on every subsequent state transition. Channel wedged offchain;
recovery requires on-chain challenge — costly griefing vector with a
one-tx attack surface.

## Test plan
- [x] `go build ./...`
- [x] `go vet ./...`
- [x] `go test ./...`
- [x] New unit tests:
- `nitronode/event_handlers/service_test.go` — `BackfillsUserSig`,
`BackfillError`; existing handler tests updated to expect the backfill
mock call (empty sig).
- `nitronode/store/database/db_store_test.go` — wedge-resolution
end-to-end (gate fails → backfill → gate passes), idempotent replay
(existing sig preserved), empty-sig no-op, unknown-version no-op.
- `pkg/blockchain/evm/channel_hub_reactor_test.go` — reactor hex-encodes
`Candidate.UserSig` into the event.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)

## Summary
- `readMessages` returned without calling `handleClosure` when
`processSink` was full, leaving the `Serve` wait group stuck at one
outstanding `Done`. The parent closure callback never fired, the
WebSocket was never closed, and the outer HTTP handler stayed blocked —
`connHub.Remove` never ran. Unauthenticated clients could exhaust server
resources by flooding messages faster than the queue drained.
- Call `handleClosure(nil)` before returning from the queue-full branch
so the wait group invariant (every early return decrements exactly once)
holds.
- Add a regression test that fills `processSink` without a consumer and
asserts the parent closure fires and the underlying connection is
closed.

## Notes
- Same fix already exists on `fix/nitronode-mf-c01` (part of broader
frame-size + rate-limiter hardening). Three-way merge between this
branch and c01 is clean — identical line addition, non-overlapping
hunks.
- c01 also adds a parallel `frameRateLimiter.Admit` early-return path
with the same `handleClosure(nil)` shape.

## Test plan
- [x] `go test ./pkg/rpc/... -count=1` passes
- [x] New test fails without the one-line fix (`parent handleClosure not
invoked after processSink overflow; goroutine leak`)
- [x] `go vet ./pkg/rpc/...` clean
- [x] CI green

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…in materialization (#730)

## Summary

- Rename `CheckOpenChannel` → `CheckActiveChannel`. Return `(string,
*core.ChannelStatus, error)` instead of `(string, bool, error)` so
callers can distinguish `Void` (DB-only, awaiting onchain confirmation)
from `Open` (materialized onchain). Nil status pointer means no active
channel.
- Document why the permissive `Void+Open` filter is intentional:
non-escrow offchain transitions (transfers, etc.) are allowed before
onchain confirmation lands.
- Gate cross-chain escrow transitions (`MutualLock`, `EscrowLock`) on
`Status == Open` with an executable guard placed **before** the stub
`return rpc.Errorf("transition is not supported yet")`. Single-step
unstub later: drop the stub return; guard stays active. Today the guard
already runs — `Void` inputs get `"home channel is not materialized
onchain"`, `Open` inputs fall through to `"not supported yet"`.
- Add `TestSubmitState_MutualLock_VoidHomeChannel_Rejected` and
`TestSubmitState_EscrowLock_VoidHomeChannel_Rejected` to lock the
contract: future PRs that unstub these transitions without preserving
the guard get red CI.

## Why

Audit finding (MF-I06): `request_creation()` inserts the home channel
into the DB as `ChannelStatusVoid` before the channel is materialized
onchain. `CheckOpenChannel` treated `status <= Open` as usable, so a
future cross-chain `MutualLock` / `INITIATE_ESCROW_DEPOSIT` flow could
be co-signed before the prior creation/checkpoint state was submitted
via `createChannel()`. Onchain, `_isChannelHomeChain()` returns false
while status == VOID, so `initiateEscrowDeposit()` would not take the
home-chain path. The fix is to surface the actual status to callers and
gate cross-chain escrow transitions on `Status == Open`.

The escrow branches are currently stubbed, so the guard has no
live-traffic impact today (the stub `return` would fire regardless, and
`nodeSig` is never returned via `c.Succeed` on the error path). The
value of shipping the guard now is the CI contract: the Void-rejection
tests pin the behavior so an unstub-PR that forgets the guard cannot
land green.

## Test plan

- [x] `go build ./...`
- [x] `go vet ./...`
- [x] `go test ./nitronode/store/database/...
./nitronode/api/channel_v1/... ./nitronode/api/app_session_v1/...`
- [x] New Void-rejection tests pass; existing `_Success` tests (skipped)
unaffected.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved channel status validation to distinguish between in-progress
and on-chain channels across the application. The system now provides
more granular channel state information to ensure accurate eligibility
checks during deposit submissions and state transitions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Replace hardcoded `sslmode=disable` in `postgresqlDbUrl` with
configurable `NITRONODE_DATABASE_SSLMODE` (binary default: `require`,
validated against the libpq list).
- Honor `NITRONODE_DATABASE_URL` verbatim when set so operators can
supply a fully-qualified DSN; individual host/user/password/sslmode
fields are ignored in that case.
- Wire helm chart helper to propagate `config.database.sslmode` as
`NITRONODE_DATABASE_SSLMODE`. Chart default is `require` so helm and the
binary agree; deployments where Postgres is only reachable on a private
network (e.g. cluster-internal pgbouncer, VPC-only Cloud SQL) can opt
out by setting `sslmode: disable`. The stress-v1 profile does so
explicitly.

## Why
Audit finding MF-M05: every Postgres connection (schema create,
migrations, runtime) used `sslmode=disable`, so credentials and protocol
traffic could be observed or tampered with by anyone on the path. The
chart documented an `sslmode` value but never propagated it.

## Notes
- Binary default is `require` (TLS without cert verification). Operators
wanting strict cert checking should set `verify-full`.
- Local dev / testcontainers paths still use `sslmode=disable`
intentionally.
- Release note: direct binary users without TLS-capable Postgres must
set `NITRONODE_DATABASE_SSLMODE=disable` explicitly. Helm users on a
private-network Postgres must set `config.database.sslmode: disable` to
keep the previous behavior.

## Test plan
- [x] `go test ./nitronode/store/database/ -run TestPostgresqlDbUrl` —
covers default, explicit, invalid, schema append, URL precedence,
unsupported driver
- [x] `go vet ./nitronode/store/database/` clean
- [x] `go build ./nitronode/...` clean
- [x] Helm template renders correctly with `sslmode` set / unset
(default → `require`, override → `disable`)
- [x] Manual smoke against TLS-enabled Postgres

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Replace the `lastSignedState.EscrowChannelID != nil` guard in
`issueTransferReceiverState` (`channel_v1`) and
`issueReleaseReceiverState` (`app_session_v1`) with a new
`EnsureNoOngoingEscrowOperation(wallet, asset)` store method.
- Predicate keys on the latest signed transition type instead of the raw
`EscrowChannelID` field: `EscrowLock` / `MutualLock` always block;
`EscrowDeposit` / `EscrowWithdraw` block only when the on-chain escrow
channel `state_version` hasn't caught up; everything else allows.
- Adds DB-store tests covering all branches (no prior state, non-escrow,
lock-types, finalize synced/unsynced, unsigned).

## Background (audit MF-M07)
The previous guard rejected any receiver-side state whenever the
receiver's latest signed state had a non-nil `EscrowChannelID`. The
field is cleared by `State.NextState()` only when the previous
transition was `EscrowDeposit` or `EscrowWithdraw`, so the signed state
itself retains it after a finalized escrow flow. As a result, a user who
completed a deposit/withdrawal could be unable to receive transfers or
app-session releases until an unrelated off-chain action advanced past
the finalized state.

The replacement matches the existing `EnsureNoOngoingStateTransitions`
pattern (transition type + state.version vs channel.state_version) but
is scoped to escrow concerns only, so home-deposit / home-withdrawal
logic is untouched.

## Test plan
- [x] `go test ./nitronode/store/database/... -run
EnsureNoOngoingEscrowOperation -v`
- [x] `go test ./nitronode/...`
- [x] `go test ./pkg/...`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
- Enhanced escrow operation validation to detect and block state
transitions when escrow activities are in-flight, including locks,
deposits, and withdrawals for wallet-asset pairs.
- Strengthened operational safeguards by preventing state changes during
pending escrow operations not yet finalized on-chain.
- Improved consistency of escrow-related state management across payment
channel and application session handlers.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## summary

fixes l-04 by rejecting redundant native value on channel challenge and
escrow deposit paths that do not consume `msg.value`.

## root cause

`challengeChannel()` and the home-chain branch of
`initiateEscrowDeposit()` are payable, but some valid paths do not pull
user funds. those calls could accept native value that was not credited
to channel, escrow, node, or reclaim accounting.

## changes

- validate `msg.value` from computed transition effects before applying
channel or escrow deposit effects
- reject non-zero value for same-version challenges and home-chain
escrow deposit initiation
- keep exact native value support for real native user-pull paths
- tighten `_pullFunds()` so zero-amount pulls cannot bypass value
validation
- add unit tests for rejected surplus eth and valid exact native eth
flows

## validation

- `forge test --match-contract
'ChannelHubTest_(challenge|initiateEscrowDeposit)'` passed, 13 tests
- `forge test` passed, 282 tests
- `forge fmt --check src/ChannelHub.sol
test/ChannelHub_units/ChannelHub_challenge.t.sol
test/ChannelHub_units/ChannelHub_initiateEscrowDeposit.t.sol` passed
- `git diff --check` passed

- `slither . --filter-paths 'lib|test'` ran with Slither 0.11.5; it
reports 83 findings on this branch and the same 83 findings on
`fix/audit-findings-final`, so no new Slither finding was introduced by
this PR. The reported categories are existing project-level warnings:
arbitrary-send-eth, reentrancy-eth/events, return-bomb, timestamp,
assembly, solc-version, low-level-calls, naming-convention, and
too-many-digits.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Enhanced native ETH validation during channel challenges to ensure
correct payment amounts are provided
* Improved escrow deposit handling with stricter ETH value verification
for both home-chain and non-home-chain payment scenarios

* **Tests**
* Added comprehensive test coverage for ETH payment validation across
channel challenge and escrow deposit operations

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/layer-3/nitrolite/pull/741)

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: nksazonov <nsazonov@openware.com>
## Summary

- Block cross-wallet session-key shadow registration by adding `UNIQUE
(session_key, kind)` to `current_session_key_states_v1` and rejecting
collisions with a generic `invalid_session_key_state: session_key not
allowed` (no ownership leak).
- Require a `session_key_sig` co-signature from the session-key holder
on every submit, so nobody can register a key they do not control. App
side reuses the existing packed payload (already binds `user_address`);
channel side gets a new `PackChannelSessionKeyOwnershipV1` that adds
`user_address` to the ownership payload.
- Reject self-collisions (`user_address == session_key`).

### Why

Without these, an attacker could register the victim's session key under
the attacker's wallet, outpace the victim's version, and have
`GetAppSessionKeyOwner` (which resolved ownership by `MAX(version)` over
`session_key` alone) return the attacker for the victim's signatures.
Result: the victim's delegated quorum signatures got attributed to the
attacker or rejected as non-participant.

An attacker without the session-key private key never reaches the
pointer lookup — they fail at `session_key_sig does not match
session_key` and learn nothing about existing registrations. The generic
`session_key not allowed` response is reachable only when the submitter
can produce a valid possession proof, i.e. when they already control the
key in question; surfacing existence to that caller is acceptable.

### Migration

`20260508000000_session_key_ownership_constraints.sql` runs a pre-flight
`RAISE EXCEPTION` on existing `(session_key, kind)` duplicates before
adding the constraint. Any duplicates surface for ops review since they
are exploitation evidence under the old behavior.

### SDK + tooling

- `sdk/go`: `SignAppSessionKeyOwnership` /
`SignChannelSessionKeyOwnership` helpers.
- `sdk/ts`: `client.signAppSessionKeyOwnership` /
`client.signChannelSessionKeyOwnership` +
`packChannelSessionKeyOwnershipV1`.
- `sdk/ts-compat`: facade methods exposed.
- `cerebro` CLI: requires the session-key private key up-front and
refuses to register a key it does not control.
- `example-app`: enable + active-disable populate `session_key_sig`;
arbitrary-key revoke surfaces a friendly error since the example app
does not retain other keys' private keys.

### Known design implication

`session_key_sig` is required on every submit, including revokes. A
wallet that loses the session-key private key cannot revoke its own
delegation early — it has to wait for `expires_at`. Documented inline in
the SDK READMEs and example app.

## Test plan

- [x] `go build ./...`
- [x] `go vet ./...`
- [x] `go test ./...`
- [x] `cd sdk/ts && npm run typecheck && npm test && npm run lint`
- [x] `cd sdk/ts-compat && npm test && npm run lint`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Maharshi Mishra <ihsraham27@gmail.com>
## summary
- enforce the 7-day max challenge duration in `ChannelHub`
- fail NitroNode startup when configured challenge bounds are looser
than the contract
- update the checked-in TS ChannelHub ABI

## tests
- `cd contracts && forge test`
- `go test ./...`
- `cd sdk/ts && npm run typecheck && npm run build:ci`
…ion gauge (#745)

## Summary
- Replace the client-controlled `Origin` header label on
`rpc_connections_active` with `application_id`, sourced from the
`app_id` query parameter validated at WebSocket upgrade. Unauthenticated
remotes can no longer drive unbounded Prometheus label cardinality by
sending unique `Origin` headers.
- Track per-application connection counts inside `ConnectionHub` and
call `DeleteLabelValues` once a bucket reaches zero, so series for
disconnected applications are shed instead of lingering as zero-valued
gauges.
- Expose `ApplicationID()` on the `rpc.Connection` interface (alongside
the existing `Origin()`) so the hub can label without re-parsing the
request. The raw `Origin` field is retained for logging only.

## Test plan
- [x] `go build ./...`
- [x] `go vet ./...`
- [x] `go test ./pkg/rpc/... ./nitronode/metrics/...`
- [ ] Verify `rpc_connections_active{application_id="..."}` on a running
node and that the series disappears after the last connection for that
app closes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Session key pagination with 10-item page limit for improved query
performance.
  * Per-user session key limits (default 100) to prevent abuse.
* WebSocket DoS hardening with inbound frame size caps and
per-connection byte-rate limiting.
* Protocol drift validation in CI ensures RPC, ABI, and signing
consistency across code layers.

* **Documentation**
* Expanded token compatibility requirements; tokens without ERC-20
`decimals()` are rejected.
  * Clarified on-chain signature domain behavior (EIP-191 vs raw ECDSA).
  * Protocol drift guardrails and trust assumptions documented.

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/layer-3/nitrolite/pull/745)

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w finalize (#753)

## Summary
- When `EscrowDepositChallenged` fires without a newer fully-signed
`FINALIZE_ESCROW_DEPOSIT` available locally, the node now schedules a
`challengeChannel(...)` on the home blockchain using the
`INITIATE_ESCROW_DEPOSIT` state and a node-produced challenger
signature.
- Closes the cross-chain attack where a user lets the non-home escrow
challenge expire, recovers escrow-chain funds, and then enforces
FINALIZE on home against the node's locked allocation.
- Introduces generic `ActionTypeChallenge = 2` +
`ScheduleChallenge(stateID, chainID)`. `BlockchainWorker` packs via
`core.PackChallengeState`, signs with the node `ChannelSigner`, and
submits via `BlockchainClient.Challenge(state, sig,
ChannelParticipantNode)`.

## Behavior change in `HandleEscrowDepositChallenged`
- Branch A (newer signed FINALIZE exists) — unchanged, still schedules
`FinalizeEscrowDeposit` on the escrow chain.
- Branch B (no newer FINALIZE, or last signed state is the INITIATE
itself) — fetches the INITIATE state via
`GetStateByChannelIDAndVersion`, resolves its home channel, and
schedules `Challenge` on the home blockchain. Skips with a warning if
the INITIATE state is missing locally, has no `HomeChannelID`, or the
home channel is not `Open`.
- After the home challenge timer expires, operator runs
`closeChannel(...)` manually to recover the node allocation (existing
manual recovery path).

## Test plan
- [x] `go test ./nitronode/event_handlers/... -run
TestHandleEscrowDepositChallenged -count=1`
- [x] `go test ./nitronode/... ./pkg/... -count=1` (full suite green)
- [x] `go build ./...`
- [x] `go vet ./...`
- [ ] Manual integration: user withholds FINALIZE state, observe home
channel transitions to DISPUTED on-chain after escrow challenge.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added validator registration event monitoring with recovery support on
reconnection.
* Implemented pagination (default/max page size 10) for key state query
endpoints.
* Added WebSocket per-connection rate limiting and maximum message size
enforcement.

* **Documentation**
* Clarified signature domain compatibility between on-chain and
off-chain validators.
* Updated protocol enforcement rules for channel state submission
requirements.
* Added validator registration monitoring and ERC-20 approval security
guidance.

* **Configuration**
  * Added configurable WebSocket DoS hardening environment variables.
  * Made database SSL mode configurable (default: `require`).

* **Bug Fixes & Improvements**
  * Enhanced escrow operation locking and user signature persistence.
  * Enforced per-user session key caps for improved resource management.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/layer-3/nitrolite/pull/753)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- `CreateAppSession` raw-string nonce check (`Nonce == "" || Nonce ==
"0"`) had a dead branch and a bypass: `unmapAppDefinitionV1` errors out
on empty input before the check, and `strconv.ParseUint` accepts
zero-padded inputs (`"00"`, `"000"`, ...) that parse to `0` and skip the
`== "0"` comparison. Net result: an app session could be stored with
`Nonce = 0`.
- Replaced the raw-string check with `appDef.Nonce == 0` so the
validation runs against the parsed numeric value.
- Extended `TestCreateAppSession_ZeroNonce` into a table-driven test
covering `"0"`, `"00"`, `"000"`.

## Test plan
- [x] `go test ./nitronode/api/app_session_v1/...`
- [x] `go vet ./nitronode/api/app_session_v1/...`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y with include_inactive opt-in (#749)

## Summary

- `get_last_key_states` on both `channels.v1` and `app_sessions.v1`
returned the latest version per session key with no `expires_at` filter,
so expired latest states surfaced as "active" — contradicting endpoint
name and docs (auth itself was unaffected because
`GetAppSessionKeyOwner` / `ValidateChannelSessionKeyForAsset` apply
their own expiry filter).
- Add `include_inactive` request flag, defaulting `false`. With the
default the store filters `expires_at > now` on both the list and the
count using a single `now` binding so pagination stays consistent.
Setting it to `true` returns all latest states (expired or revoked) for
ops/debug.
- Cerebro version-detection caller opts in to `include_inactive=true` so
post-expiry rotation still observes the prior version and the
server-side monotonic pointer is not violated.

## Out of scope (follow-up PR)

True revocation semantics (allowing `expires_at` in the past at submit
time, freeing the per-user cap slot for revoked keys, log on revoke, doc
pass for re-activation) ship on a separate branch. This PR is purely the
API correctness fix; the flag is forward-compatible with that work.

## Test plan

- [x] `go vet ./...`
- [x] `go test ./...` (full suite green, incl. real-DB tests in
`nitronode/store/database`)
- [ ] CI: `test-go.yml`
- [ ] Manual: hit `channels.v1.get_last_key_states` /
`app_sessions.v1.get_last_key_states` with and without
`include_inactive` against a node carrying a mix of expired and active
keys

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added validator registration event monitoring for enhanced security
visibility
  * Added pagination and filtering support for session key state queries
* Enhanced WebSocket DoS hardening with frame-size limits and rate
limiting

* **Bug Fixes**
  * Fixed ERC20 transfer handling for oversized return data
  * Improved concurrent session key submission reliability
  * Fixed signature validation and state enforcement path validation

* **Documentation**
* Updated security model with trust assumptions and validator
responsibilities
  * Clarified on-chain protocol enforcement conditions and constraints
  * Added comprehensive validator monitoring guidance

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/layer-3/nitrolite/pull/749)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Do you want the full hidden review stack artifact (very large; can be split across multiple messages) or only the visible walkthrough, cohorts table, and metadata (no hidden artifact)?

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audit-findings-final

philanton and others added 2 commits May 13, 2026 19:37
Snapshot was stale on the branch — MF-C02 (38d00d3) inserted
ChannelStatus.Closing = 3 (shifting Closed to 4) but did not refresh
this snapshot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
contracts/test/mocks/MalformedReturningERC20.sol (3)

42-44: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add NatSpec documentation to public functions.

The mint function lacks NatSpec documentation. As per coding guidelines, all public/external functions in Solidity must include NatSpec comments.

📝 Suggested NatSpec addition
+    /// `@notice` Mints tokens to specified address for test setup
+    /// `@param` to Recipient address
+    /// `@param` amount Token amount to mint
     function mint(address to, uint256 amount) public {

As per coding guidelines, include NatSpec comments on all public/external functions in Solidity.

🤖 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 `@contracts/test/mocks/MalformedReturningERC20.sol` around lines 42 - 44, The
public function mint is missing NatSpec comments; add a NatSpec block
immediately above the mint function (function name: mint) including at least
`@notice` describing what the function does, `@dev` for any implementation notes,
and `@param` to document the "to" and "amount" parameters (and `@return` if you
later add returns). Ensure the comment block uses the triple-slash or /** */
NatSpec format and sits directly above the mint declaration so tooling and
linters pick it up.

16-27: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add NatSpec documentation to public functions.

The transfer function lacks NatSpec documentation. As per coding guidelines, all public/external functions in Solidity must include NatSpec comments documenting parameters, return values, and behavior.

📝 Suggested NatSpec addition
+    /// `@notice` Returns malformed 1-byte data without performing transfer
+    /// `@dev` Simulates non-compliant token that could cause abi.decode to revert
+    /// `@return` Always returns 1 byte (0x01) instead of standard 32-byte bool
     function transfer(address, uint256) public pure override returns (bool) {

As per coding guidelines, include NatSpec comments on all public/external functions in Solidity.

🤖 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 `@contracts/test/mocks/MalformedReturningERC20.sol` around lines 16 - 27, The
public function transfer in MalformedReturningERC20.sol is missing NatSpec
comments; add a Solidity NatSpec block above the transfer function (the public
pure override transfer(address, uint256) returns (bool) declaration) documenting
the purpose/behavior, each parameter (`@param` for the address and uint256), the
return value (`@return`) and any important notes about its malformed behavior
(that it returns a single byte and does not perform an actual transfer) so
consumers understand the intentionally abnormal behavior.

29-40: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add NatSpec documentation to public functions.

The transferFrom function lacks NatSpec documentation. As per coding guidelines, all public/external functions in Solidity must include NatSpec comments.

📝 Suggested NatSpec addition
+    /// `@notice` Returns malformed 1-byte data without performing transfer
+    /// `@dev` Simulates non-compliant token that could cause abi.decode to revert
+    /// `@return` Always returns 1 byte (0x01) instead of standard 32-byte bool
     function transferFrom(address, address, uint256) public pure override returns (bool) {

As per coding guidelines, include NatSpec comments on all public/external functions in Solidity.

🤖 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 `@contracts/test/mocks/MalformedReturningERC20.sol` around lines 29 - 40, The
public function transferFrom is missing NatSpec; add a Solidity NatSpec comment
block above the transferFrom function in MalformedReturningERC20.sol including
at minimum `@notice` describing the mock behavior, `@dev` explaining it returns a
single byte without transferring (malformed token simulation), `@param`
annotations for the address parameters and uint256, and `@return` describing the
malformed boolean-like return value so tooling and consumers can understand the
purpose and behavior of transferFrom.
pkg/rpc/node.go (1)

33-38: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add doc comment to exported ByteTokenBucket.

The exported struct ByteTokenBucket is missing a doc comment. As per coding guidelines, all exported names in Go must have doc comments.

📝 Suggested doc comment
+// ByteTokenBucket is a token bucket on bytes read. One bucket per connection.
+// Not safe for concurrent use; the connection's read goroutine is the sole
+// caller of Admit.
 type ByteTokenBucket struct {
 	bytesPerSec float64
 	burst       float64
 	tokens      float64
 	last        time.Time
 }

As per coding guidelines: "Follow gofmt formatting and add doc comments on all exported names in Go code".

🤖 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 `@pkg/rpc/node.go` around lines 33 - 38, Add a proper Go doc comment for the
exported struct ByteTokenBucket describing its purpose and usage (what it
represents, how tokens/bytes are consumed/refilled, and any concurrency
guarantees) immediately above the type declaration for ByteTokenBucket so it
satisfies Go exported-name documentation rules; ensure the comment starts with
"ByteTokenBucket" and is formatted as a full sentence.
🧹 Nitpick comments (11)
contracts/src/Utils.sol (1)

19-31: ⚡ Quick win

Verify struct layout invariant is maintained.

The getChannelId function uses hardcoded 0xC0 (192 bytes) based on ChannelDefinition having 6 fields. The warning comment in Types.sol is good, but consider adding a compile-time assertion or a test that verifies the struct size matches this assumption. If fields are added/removed in the future, the mismatch would silently produce wrong channel IDs.

🛡️ Consider adding a size verification test

Add a test in contracts/test/Utils.t.sol that verifies the memory layout assumption:

function test_channelDefinitionSizeInvariant() public {
    // Verify that ChannelDefinition memory layout matches Utils.getChannelId assumption
    ChannelDefinition memory def = ChannelDefinition({...});
    bytes memory encoded = abi.encode(def);
    // The encoded size should match 6 * 32 = 192 bytes for the struct data
    // (excluding the initial offset/length words that abi.encode adds)
}

This test would catch any struct modifications that break the assembly optimization.

🤖 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 `@contracts/src/Utils.sol` around lines 19 - 31, Add a verification that the
hardcoded 0xC0 size used in getChannelId matches the actual ChannelDefinition
layout: implement either a compile-time/assertion or a unit test that computes
the struct size for ChannelDefinition and fails if it differs from 6*32 (192) so
future edits can’t silently break getChannelId; reference the getChannelId
function and the ChannelDefinition struct when adding the check (e.g., a
solidity constant or assert in an initialization/test function that compares
abi.encode(def).length or a computed SIZE constant against 192).
contracts/test/mocks/OversizedReturnERC20.sol (1)

11-12: ⚡ Quick win

Clarify that the zero-return-failure pattern deviates from standard ERC20.

The comment states "a zero return value signals failure without a state change," but standard ERC20 tokens revert on failure rather than returning zero. Consider clarifying that this test mock intentionally deviates from standard behavior for testing purposes.

📝 Suggested clarification
- *      The actual transfer is performed only when firstWord != 0, matching normal token semantics
- *      (a zero return value signals failure without a state change).
+ *      The actual transfer is performed only when firstWord != 0. This deviates from standard
+ *      ERC20 behavior (which reverts on failure) to test edge-case handling of non-reverting failures.
🤖 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 `@contracts/test/mocks/OversizedReturnERC20.sol` around lines 11 - 12, Update
the comment in OversizedReturnERC20 (the test mock contract) to explicitly state
that the “a zero return value signals failure without a state change” pattern is
a deliberate deviation from the ERC20 standard used only for testing; reference
the contract name OversizedReturnERC20 and its transfer/transferFrom behavior so
readers know this mock intentionally returns 0 instead of reverting to simulate
legacy/non-standard token behavior for tests.
docs/api.yaml (2)

847-863: ⚡ Quick win

Clarify validation behavior for unsupported sort field.

Same issue as in the channels endpoint - the description states sort is not supported and must be omitted, but it's optional in the type definition. Clarify the validation behavior.

💡 Suggested clarification
-                  description: Pagination parameters (offset, limit). Default limit 10, max 10. The `sort` field is not supported by this endpoint and must be omitted.
+                  description: Pagination parameters (offset, limit). Default limit 10, max 10. The `sort` field is not supported and will be rejected if provided.
🤖 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 `@docs/api.yaml` around lines 847 - 863, The docs currently say the pagination
`sort` field "is not supported and must be omitted" but the type allows it;
update the endpoint description for the pagination field to explicitly state the
validation behavior: clarify whether the server will reject requests containing
`pagination.sort` (returning a 400 with a validation error like "pagination.sort
is not supported") or will silently ignore it; then make the text precise (e.g.,
"The server validates and will reject requests that include `pagination.sort`
with HTTP 400: 'pagination.sort is not supported'") and apply the same wording
to the `pagination` description under this endpoint so consumers know the exact
validation outcome.

648-664: ⚡ Quick win

Clarify validation behavior for unsupported sort field.

The description states "The sort field is not supported by this endpoint and must be omitted," but pagination_params includes sort as an optional field. Consider clarifying whether:

  • Requests with sort will be rejected with a validation error
  • Or if sort is silently ignored
💡 Suggested clarification
-                  description: Pagination parameters (offset, limit). Default limit 10, max 10. The `sort` field is not supported by this endpoint and must be omitted.
+                  description: Pagination parameters (offset, limit). Default limit 10, max 10. The `sort` field is not supported and will be rejected if provided.
🤖 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 `@docs/api.yaml` around lines 648 - 664, Clarify whether the unsupported `sort`
field in the `pagination_params` is rejected or ignored: update the endpoint
description for the `user_address`/`session_key` retrieval to state explicitly
whether requests containing `pagination.sort` will produce a validation error or
will be silently ignored, and ensure the documentation text references
`pagination_params` and the `sort` field and matches the actual API validation
behavior (or update validation to match the doc) so clients know to omit `sort`
when calling this endpoint.
nitronode/api/app_session_v1/submit_session_key_state.go (1)

89-122: 💤 Low value

Document the session key cap race condition trade-off.

The TODO comment at lines 107-113 documents that concurrent registration of different new keys for the same user can bypass the cap by at most (concurrent writers - 1) keys. This is acceptable as a soft DOS bound, but the documented mitigation (per-user advisory lock) should be tracked if a hard quota is ever required.

The implementation correctly prevents the cap check from blocking legitimate key rotation (existing keys can always be updated regardless of cap).

🤖 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 `@nitronode/api/app_session_v1/submit_session_key_state.go` around lines 89 -
122, The TODO about the race condition when registering different new session
keys needs to be expanded to document the trade-off and a tracked mitigation:
update the comment near LockSessionKeyState/CountSessionKeysForUser to state
that concurrent new-key writers can exceed h.maxSessionKeysPerUser by up to
(concurrent writers - 1), mark this as an intentional soft DOS bound, and add a
tracked follow-up (e.g., TODO/ISSUE) recommending using a per-user advisory lock
via pg_advisory_xact_lock(hashtext(user_address)) if a hard quota is later
required; reference the symbols LockSessionKeyState, CountSessionKeysForUser and
h.maxSessionKeysPerUser in the comment so maintainers can find the relevant code
and the suggested fix.
nitronode/api/channel_v1/submit_state.go (1)

178-186: 💤 Low value

Consider consolidating the home channel status checks.

The MutualLock branch duplicates the channelStatus != Open check from lines 73-81. Since the earlier check already rejects non-Open channels with "home channel is not materialized onchain," this second check at line 183 is unreachable when the transition is enabled.

When enabling MutualLock support, consider removing this redundant check to simplify the code path.

🤖 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 `@nitronode/api/channel_v1/submit_state.go` around lines 178 - 186, The
MutualLock case duplicates the earlier check for channelStatus !=
core.ChannelStatusOpen (which already returns "home channel is not materialized
onchain"); remove the redundant if-block inside the TransitionTypeMutualLock
branch so the code relies on the prior check, leaving only the
MutualLock-specific logic (or, if you are implementing MutualLock support now,
replace the duplicate guard with the actual MutualLock handling instead of
returning the duplicated error). Ensure you update/remove the duplicated return
rpc.Errorf("home channel is not materialized onchain") and keep the surrounding
comment about needing the home channel materialized for MutualLock.
nitronode/blockchain_worker.go (1)

49-57: ⚡ Quick win

Add an exported doc comment for NewBlockchainWorker.

NewBlockchainWorker is exported and was modified, but it still has no GoDoc comment.

As per coding guidelines, **/*.go: Follow gofmt formatting and include doc comments on all exported names in Go.

🤖 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 `@nitronode/blockchain_worker.go` around lines 49 - 57, Add a GoDoc comment for
the exported constructor NewBlockchainWorker: insert a comment immediately above
the NewBlockchainWorker function that starts with "NewBlockchainWorker" and
briefly describes what the constructor returns and its purpose (e.g., creates
and initializes a BlockchainWorker for the given blockchainID using the provided
client, store, channelSigner, assetStore, logger, and metrics exporter). Keep
the comment concise, follow GoDoc style (first word is the function name) and
ensure it reads as documentation for callers.
nitronode/runtime.go (1)

87-88: 💤 Low value

Doc/code mismatch on the byte-rate "disable" sentinel.

Line 87 comment says Set <0 to disable, but the gate at line 251 uses if bytesPerSec > 0, so 0 also disables byte limiting. Either tighten the gate to >= 0 or relax the doc to match actual behavior — operators following the doc and setting -1 will get the same result as 0, which is fine, but anyone parsing the doc literally as "must be >= 0 to enable" will be confused.

📝 Suggested doc fix
-	// WsBytesPerSec is the steady-state byte budget per connection. Set <0 to disable.
+	// WsBytesPerSec is the steady-state byte budget per connection. Set <=0 to disable.
 	WsBytesPerSec float64 `yaml:"ws_bytes_per_sec" env:"NITRONODE_WS_BYTES_PER_SEC" env-default:"262144"`

Also applies to: 251-251

🤖 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 `@nitronode/runtime.go` around lines 87 - 88, Doc/code mismatch: update the
WsBytesPerSec field comment to match the runtime gate that enables byte-limiting
only when bytesPerSec > 0. Change the comment on WsBytesPerSec (the struct field
named WsBytesPerSec) to state that values <= 0 disable byte limiting (or
explicitly "only values > 0 enable byte limiting"), so the docs and the check in
the runtime (the bytesPerSec > 0 gate) are consistent.
nitronode/config/migrations/postgres/20260513000000_add_channel_status_closing.sql (1)

14-14: ⚡ Quick win

Coordinate this enum remap with the application rollout.

This migration silently changes the on-disk encoding of Closed from 3 to 4. If the migration runs before the new application binary that knows Closed = 4 is fully rolled out (or vice versa), live rows will be read with the wrong status: any in-flight reads on the old binary that see status = 4 will misinterpret it, and new-binary writes that land before the UPDATE flips existing rows will leave a mix of encodings for Closed.

Worth confirming the deploy plan ensures all writers/readers are on the new enum before this migration runs, or that the service is briefly drained during apply.

🤖 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
`@nitronode/config/migrations/postgres/20260513000000_add_channel_status_closing.sql`
at line 14, This migration remaps the numeric enum for Closed from 3 to 4
(UPDATE channels SET status = 4 WHERE status = 3) and must be applied only when
all readers/writers understand the new encoding; change the rollout to a safe,
coordinated two-step migration: 1) deploy an app update that treats both 3 and 4
as Closed (make Channel status parsing in the app accept old and new values), 2)
run the SQL backfill (the existing UPDATE in
20260513000000_add_channel_status_closing.sql) to rewrite rows to 4, and 3)
deploy a final app that treats 4 as canonical and removes 3 support;
alternatively add a short drain/maintenance window or a runtime migration guard
that verifies all nodes are the new version before executing the UPDATE.
pkg/rpc/node.go (1)

135-139: 💤 Low value

Consider clarifying where the default is applied.

The doc comment states "Non-positive values fall back to the default" and documents "default: 128 KiB", but this function doesn't contain the default-assignment logic. Readers may expect to see validation or default handling here.

If the default is applied in NewWebsocketConnection, consider adding a cross-reference: "Non-positive values are normalized to 128 KiB in NewWebsocketConnection."

🤖 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 `@pkg/rpc/node.go` around lines 135 - 139, The comment for WsConnMaxMessageSize
is ambiguous about where the default is applied; update the docstring to say
that non-positive values are normalized to the 128 KiB default in
NewWebsocketConnection so readers know where default/validation logic lives,
e.g., mention "Non-positive values are normalized to 128 KiB in
NewWebsocketConnection"; reference WsConnMaxMessageSize and
NewWebsocketConnection to help locate the handling.
sdk/go/app_session.go (1)

417-429: 💤 Low value

Consider validating session key address matches signer.

The function accepts a sessionKeySigner but doesn't verify that sessionKeySigner.PublicKey().Address() matches state.SessionKey. While the doc comment states "The signer must be the holder of the session key," a runtime check would catch caller errors earlier.

🛡️ Optional validation
 func SignAppSessionKeyOwnership(state app.AppSessionKeyStateV1, sessionKeySigner *sign.EthereumMsgSigner) (string, error) {
+	signerAddr := sessionKeySigner.PublicKey().Address().String()
+	if signerAddr != state.SessionKey {
+		return "", fmt.Errorf("signer address %s does not match state.SessionKey %s", signerAddr, state.SessionKey)
+	}
 	packed, err := app.PackAppSessionKeyStateV1(state)
🤖 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 `@sdk/go/app_session.go` around lines 417 - 429, SignAppSessionKeyOwnership
currently signs without verifying the signer actually owns the session key; add
a runtime check inside SignAppSessionKeyOwnership that compares
sessionKeySigner.PublicKey().Address() (or the appropriate
PublicKey().Address()/AddressHex() accessor) to state.SessionKey and return a
descriptive error if they differ before packing/signing; keep the existing
packing and Sign call (use the existing sessionKeySigner and state types) so
callers get an immediate error when the signer does not match the declared
SessionKey.
🤖 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 `@contracts/test/mocks/OversizedReturnERC20.sol`:
- Around line 38-40: The public function mint in the OversizedReturnERC20
contract is missing NatSpec documentation; add a Solidity NatSpec comment block
(/// `@notice`, `@dev`, and `@param` tags at minimum) immediately above the mint
function declaration to describe its behavior, any important notes, and the
meaning of the parameters (e.g., to and amount), so tools and reviewers can
understand its intent.
- Around line 23-36: The public function transfer lacks NatSpec documentation;
add a NatSpec comment block above the transfer function describing its purpose,
parameters (address to, uint256 amount), return behavior (custom oversized
return data controlled by RETURN_DATA_SIZE and FIRST_WORD), and any side effects
(calls _transfer when FIRST_WORD != 0) so callers and static analyzers
understand the nonstandard return payload and state changes in transfer.

In `@nitronode/api/app_session_v1/get_last_key_states_test.go`:
- Around line 45-85: Both tests set expectations on mockStore but never assert
them; add mockStore.AssertExpectations(t) at the end of
TestGetLastKeyStates_DefaultsToPageOneOnEmptyResult and at the end of
TestGetLastKeyStates_PaginationMetadata_AlignedOffset so the mocked
GetLastAppSessionKeyStates call is verified (locate functions
TestGetLastKeyStates_DefaultsToPageOneOnEmptyResult and
TestGetLastKeyStates_PaginationMetadata_AlignedOffset and append
mockStore.AssertExpectations(t) after the existing asserts).

In `@nitronode/blockchain_worker.go`:
- Around line 216-227: submitChallenge currently dereferences w.assetStore and
w.channelSigner (and then calls w.client.Challenge) without guarding for nil,
causing runtime panics; add precondition checks at the top of submitChallenge to
verify w.assetStore != nil and w.channelSigner != nil (and optionally w.client
!= nil) and return a clear, wrapped error (e.g., "missing channelSigner" /
"missing assetStore") instead of proceeding to core.PackChallengeState or Sign;
update error messages to reference submitChallenge and the offending field to
make failures explicit.

In `@nitronode/event_handlers/service.go`:
- Around line 314-326: When lastSignedState exists with a newer Version but
lastSignedState.EscrowLedger == nil, the handler currently only logger.Warns and
skips auto-defense; change that branch to (a) log an error/emit a metric via
logger.Error and then (b) call
s.scheduleHomeChannelChallengeForEscrowDeposit(ctx, tx, chanID,
event.StateVersion) so the home-chain defense is scheduled (instead of falling
through), keeping the existing
tx.ScheduleFinalizeEscrowDeposit(lastSignedState.ID, ...) path for the non-nil
EscrowLedger case and preserving error returns on failures.

In `@pkg/blockchain/evm/validator_watcher.go`:
- Around line 71-83: The code advances the duplicate-skip watermark (headBlock)
before successfully fetching historical logs, so if client.FilterLogs fails the
live loop later drops events <= headBlock and they are lost; change the logic so
headBlock (and any fromBlock advancement) is only updated after FilterLogs
returns successfully and the returned histLogs have been processed (i.e., move
the headBlock = header.Number.Uint64() assignment and any fromBlock update into
the err == nil branch after processing histLogs), and ensure the live-check that
drops logs <= headBlock in the live loop respects this (do not advance headBlock
on FilterLogs error).

In `@sdk/go/channel.go`:
- Around line 986-1000: SignChannelSessionKeyOwnership currently dereferences
sessionKeySigner without checking for nil, which can panic; add an explicit nil
guard at the start of SignChannelSessionKeyOwnership that returns a clear error
(e.g., "nil sessionKeySigner") if sessionKeySigner == nil, then proceed with
computing metadataHash (core.GetChannelSessionKeyAuthMetadataHashV1), packing
(core.PackChannelKeyStateV1) and signing (sessionKeySigner.Sign) as before;
ensure the returned error wraps context so callers get actionable failure
information.

In `@sdk/go/validator_watcher.go`:
- Around line 45-48: The code currently calls ethclient.Dial(rpcURL) which
ignores the provided context; replace that call with ethclient.DialContext(ctx,
rpcURL) so the connection respects cancellation/deadlines from the caller's ctx,
keeping the rest of the error handling (returning fmt.Errorf with chainID)
intact and still assigning the result to ethCl.

In `@sdk/ts/src/client.ts`:
- Around line 1697-1712: The signChannelSessionKeyOwnership function currently
trusts the provided sessionKeySigner and can produce a bad signature if the
signer does not control state.session_key; update signChannelSessionKeyOwnership
to first derive or query the signer's address (using the EthereumMsgSigner API
on the signer instance) and compare it to state.session_key (normalized to the
same hex/address format), and if they differ throw a clear error (e.g.,
"sessionKeySigner does not match state.session_key") so callers fail fast; apply
the same check to the analogous signing routine around the 1794–1800 block.
- Around line 1892-1900: createEVMClients currently always builds the
publicClient with http(rpcUrl), which fails for ws:// or wss:// RPC endpoints
used by watchValidatorRegistered; update createEVMClients to detect the rpcUrl
scheme and use webSocket({ url: rpcUrl }) when the URL starts with "ws://" or
"wss://", otherwise continue using http({ url: rpcUrl }), ensure the returned
publicClient (and any other client created there) uses the chosen transport, and
import/use viem's webSocket helper alongside http so WebSocket endpoints
properly support push-based delivery to watchValidatorRegistered.

---

Outside diff comments:
In `@contracts/test/mocks/MalformedReturningERC20.sol`:
- Around line 42-44: The public function mint is missing NatSpec comments; add a
NatSpec block immediately above the mint function (function name: mint)
including at least `@notice` describing what the function does, `@dev` for any
implementation notes, and `@param` to document the "to" and "amount" parameters
(and `@return` if you later add returns). Ensure the comment block uses the
triple-slash or /** */ NatSpec format and sits directly above the mint
declaration so tooling and linters pick it up.
- Around line 16-27: The public function transfer in MalformedReturningERC20.sol
is missing NatSpec comments; add a Solidity NatSpec block above the transfer
function (the public pure override transfer(address, uint256) returns (bool)
declaration) documenting the purpose/behavior, each parameter (`@param` for the
address and uint256), the return value (`@return`) and any important notes about
its malformed behavior (that it returns a single byte and does not perform an
actual transfer) so consumers understand the intentionally abnormal behavior.
- Around line 29-40: The public function transferFrom is missing NatSpec; add a
Solidity NatSpec comment block above the transferFrom function in
MalformedReturningERC20.sol including at minimum `@notice` describing the mock
behavior, `@dev` explaining it returns a single byte without transferring
(malformed token simulation), `@param` annotations for the address parameters and
uint256, and `@return` describing the malformed boolean-like return value so
tooling and consumers can understand the purpose and behavior of transferFrom.

In `@pkg/rpc/node.go`:
- Around line 33-38: Add a proper Go doc comment for the exported struct
ByteTokenBucket describing its purpose and usage (what it represents, how
tokens/bytes are consumed/refilled, and any concurrency guarantees) immediately
above the type declaration for ByteTokenBucket so it satisfies Go exported-name
documentation rules; ensure the comment starts with "ByteTokenBucket" and is
formatted as a full sentence.

---

Nitpick comments:
In `@contracts/src/Utils.sol`:
- Around line 19-31: Add a verification that the hardcoded 0xC0 size used in
getChannelId matches the actual ChannelDefinition layout: implement either a
compile-time/assertion or a unit test that computes the struct size for
ChannelDefinition and fails if it differs from 6*32 (192) so future edits can’t
silently break getChannelId; reference the getChannelId function and the
ChannelDefinition struct when adding the check (e.g., a solidity constant or
assert in an initialization/test function that compares abi.encode(def).length
or a computed SIZE constant against 192).

In `@contracts/test/mocks/OversizedReturnERC20.sol`:
- Around line 11-12: Update the comment in OversizedReturnERC20 (the test mock
contract) to explicitly state that the “a zero return value signals failure
without a state change” pattern is a deliberate deviation from the ERC20
standard used only for testing; reference the contract name OversizedReturnERC20
and its transfer/transferFrom behavior so readers know this mock intentionally
returns 0 instead of reverting to simulate legacy/non-standard token behavior
for tests.

In `@docs/api.yaml`:
- Around line 847-863: The docs currently say the pagination `sort` field "is
not supported and must be omitted" but the type allows it; update the endpoint
description for the pagination field to explicitly state the validation
behavior: clarify whether the server will reject requests containing
`pagination.sort` (returning a 400 with a validation error like "pagination.sort
is not supported") or will silently ignore it; then make the text precise (e.g.,
"The server validates and will reject requests that include `pagination.sort`
with HTTP 400: 'pagination.sort is not supported'") and apply the same wording
to the `pagination` description under this endpoint so consumers know the exact
validation outcome.
- Around line 648-664: Clarify whether the unsupported `sort` field in the
`pagination_params` is rejected or ignored: update the endpoint description for
the `user_address`/`session_key` retrieval to state explicitly whether requests
containing `pagination.sort` will produce a validation error or will be silently
ignored, and ensure the documentation text references `pagination_params` and
the `sort` field and matches the actual API validation behavior (or update
validation to match the doc) so clients know to omit `sort` when calling this
endpoint.

In `@nitronode/api/app_session_v1/submit_session_key_state.go`:
- Around line 89-122: The TODO about the race condition when registering
different new session keys needs to be expanded to document the trade-off and a
tracked mitigation: update the comment near
LockSessionKeyState/CountSessionKeysForUser to state that concurrent new-key
writers can exceed h.maxSessionKeysPerUser by up to (concurrent writers - 1),
mark this as an intentional soft DOS bound, and add a tracked follow-up (e.g.,
TODO/ISSUE) recommending using a per-user advisory lock via
pg_advisory_xact_lock(hashtext(user_address)) if a hard quota is later required;
reference the symbols LockSessionKeyState, CountSessionKeysForUser and
h.maxSessionKeysPerUser in the comment so maintainers can find the relevant code
and the suggested fix.

In `@nitronode/api/channel_v1/submit_state.go`:
- Around line 178-186: The MutualLock case duplicates the earlier check for
channelStatus != core.ChannelStatusOpen (which already returns "home channel is
not materialized onchain"); remove the redundant if-block inside the
TransitionTypeMutualLock branch so the code relies on the prior check, leaving
only the MutualLock-specific logic (or, if you are implementing MutualLock
support now, replace the duplicate guard with the actual MutualLock handling
instead of returning the duplicated error). Ensure you update/remove the
duplicated return rpc.Errorf("home channel is not materialized onchain") and
keep the surrounding comment about needing the home channel materialized for
MutualLock.

In `@nitronode/blockchain_worker.go`:
- Around line 49-57: Add a GoDoc comment for the exported constructor
NewBlockchainWorker: insert a comment immediately above the NewBlockchainWorker
function that starts with "NewBlockchainWorker" and briefly describes what the
constructor returns and its purpose (e.g., creates and initializes a
BlockchainWorker for the given blockchainID using the provided client, store,
channelSigner, assetStore, logger, and metrics exporter). Keep the comment
concise, follow GoDoc style (first word is the function name) and ensure it
reads as documentation for callers.

In
`@nitronode/config/migrations/postgres/20260513000000_add_channel_status_closing.sql`:
- Line 14: This migration remaps the numeric enum for Closed from 3 to 4 (UPDATE
channels SET status = 4 WHERE status = 3) and must be applied only when all
readers/writers understand the new encoding; change the rollout to a safe,
coordinated two-step migration: 1) deploy an app update that treats both 3 and 4
as Closed (make Channel status parsing in the app accept old and new values), 2)
run the SQL backfill (the existing UPDATE in
20260513000000_add_channel_status_closing.sql) to rewrite rows to 4, and 3)
deploy a final app that treats 4 as canonical and removes 3 support;
alternatively add a short drain/maintenance window or a runtime migration guard
that verifies all nodes are the new version before executing the UPDATE.

In `@nitronode/runtime.go`:
- Around line 87-88: Doc/code mismatch: update the WsBytesPerSec field comment
to match the runtime gate that enables byte-limiting only when bytesPerSec > 0.
Change the comment on WsBytesPerSec (the struct field named WsBytesPerSec) to
state that values <= 0 disable byte limiting (or explicitly "only values > 0
enable byte limiting"), so the docs and the check in the runtime (the
bytesPerSec > 0 gate) are consistent.

In `@pkg/rpc/node.go`:
- Around line 135-139: The comment for WsConnMaxMessageSize is ambiguous about
where the default is applied; update the docstring to say that non-positive
values are normalized to the 128 KiB default in NewWebsocketConnection so
readers know where default/validation logic lives, e.g., mention "Non-positive
values are normalized to 128 KiB in NewWebsocketConnection"; reference
WsConnMaxMessageSize and NewWebsocketConnection to help locate the handling.

In `@sdk/go/app_session.go`:
- Around line 417-429: SignAppSessionKeyOwnership currently signs without
verifying the signer actually owns the session key; add a runtime check inside
SignAppSessionKeyOwnership that compares sessionKeySigner.PublicKey().Address()
(or the appropriate PublicKey().Address()/AddressHex() accessor) to
state.SessionKey and return a descriptive error if they differ before
packing/signing; keep the existing packing and Sign call (use the existing
sessionKeySigner and state types) so callers get an immediate error when the
signer does not match the declared SessionKey.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e87d920-8753-46d1-ae21-86af4121c2ef

📥 Commits

Reviewing files that changed from the base of the PR and between 8449372 and ba58e89.

⛔ Files ignored due to path filters (2)
  • sdk/ts-compat/test/unit/__snapshots__/public-api-drift.test.ts.snap is excluded by !**/*.snap
  • sdk/ts/test/unit/__snapshots__/public-api-drift.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (141)
  • cerebro/commands.go
  • contracts/SECURITY.md
  • contracts/src/ChannelHub.sol
  • contracts/src/Utils.sol
  • contracts/src/interfaces/Types.sol
  • contracts/test/ChannelHub_Node.t.sol
  • contracts/test/ChannelHub_challenge/ChannelHub_challengeSessionKeyValidator.t.sol
  • contracts/test/ChannelHub_lifecycle/ChannelHub_singlechain.lifecycle.t.sol
  • contracts/test/ChannelHub_nonRevertingPushFunds.t.sol
  • contracts/test/ChannelHub_sigValidator.t.sol
  • contracts/test/ChannelHub_units/ChannelHub_challenge.t.sol
  • contracts/test/ChannelHub_units/ChannelHub_createChannel.t.sol
  • contracts/test/ChannelHub_units/ChannelHub_finalizeEscrowDeposit.t.sol
  • contracts/test/ChannelHub_units/ChannelHub_finalizeEscrowWithdrawal.t.sol
  • contracts/test/ChannelHub_units/ChannelHub_initiateEscrowDeposit.t.sol
  • contracts/test/Utils.t.sol
  • contracts/test/mocks/MalformedReturningERC20.sol
  • contracts/test/mocks/OversizedReturnERC20.sol
  • docs/api.yaml
  • docs/data_models.mmd
  • docs/protocol/enforcement.md
  • docs/protocol/overview.md
  • docs/protocol/security-and-limitations.md
  • nitronode/README.md
  • nitronode/api/app_session_v1/README.md
  • nitronode/api/app_session_v1/create_app_session.go
  • nitronode/api/app_session_v1/create_app_session_test.go
  • nitronode/api/app_session_v1/get_last_key_states.go
  • nitronode/api/app_session_v1/get_last_key_states_test.go
  • nitronode/api/app_session_v1/handler.go
  • nitronode/api/app_session_v1/interface.go
  • nitronode/api/app_session_v1/rebalance_app_sessions_test.go
  • nitronode/api/app_session_v1/submit_app_state_test.go
  • nitronode/api/app_session_v1/submit_deposit_state.go
  • nitronode/api/app_session_v1/submit_deposit_state_test.go
  • nitronode/api/app_session_v1/submit_session_key_state.go
  • nitronode/api/app_session_v1/submit_session_key_state_test.go
  • nitronode/api/app_session_v1/testing.go
  • nitronode/api/app_session_v1/utils.go
  • nitronode/api/channel_v1/get_channels.go
  • nitronode/api/channel_v1/get_home_channel.go
  • nitronode/api/channel_v1/get_home_channel_test.go
  • nitronode/api/channel_v1/get_last_key_states.go
  • nitronode/api/channel_v1/get_last_key_states_test.go
  • nitronode/api/channel_v1/handler.go
  • nitronode/api/channel_v1/interface.go
  • nitronode/api/channel_v1/request_creation.go
  • nitronode/api/channel_v1/request_creation_test.go
  • nitronode/api/channel_v1/submit_session_key_state.go
  • nitronode/api/channel_v1/submit_session_key_state_test.go
  • nitronode/api/channel_v1/submit_state.go
  • nitronode/api/channel_v1/submit_state_test.go
  • nitronode/api/channel_v1/testing.go
  • nitronode/api/channel_v1/utils.go
  • nitronode/api/rate_limits.go
  • nitronode/api/rpc_router.go
  • nitronode/blockchain_worker.go
  • nitronode/chart/README.md
  • nitronode/chart/config/stress-v1/nitronode.yaml.gotmpl
  • nitronode/chart/templates/helpers/_common.tpl
  • nitronode/chart/values.yaml
  • nitronode/config/migrations/postgres/20251222000000_initial_schema.sql
  • nitronode/config/migrations/postgres/20260507000000_add_current_session_key_states.sql
  • nitronode/config/migrations/postgres/20260508000000_session_key_ownership_constraints.sql
  • nitronode/config/migrations/postgres/20260513000000_add_channel_status_closing.sql
  • nitronode/event_handlers/service.go
  • nitronode/event_handlers/service_test.go
  • nitronode/event_handlers/testing.go
  • nitronode/main.go
  • nitronode/metrics/exporter.go
  • nitronode/metrics/interface.go
  • nitronode/runtime.go
  • nitronode/runtime_config_test.go
  • nitronode/store/database/app_session_key_state.go
  • nitronode/store/database/app_session_key_state_test.go
  • nitronode/store/database/blockchain_action.go
  • nitronode/store/database/channel.go
  • nitronode/store/database/channel_session_key_state.go
  • nitronode/store/database/channel_session_key_state_test.go
  • nitronode/store/database/channel_test.go
  • nitronode/store/database/current_session_key_state.go
  • nitronode/store/database/current_session_key_state_test.go
  • nitronode/store/database/database.go
  • nitronode/store/database/database_test.go
  • nitronode/store/database/db_store.go
  • nitronode/store/database/db_store_test.go
  • nitronode/store/database/interface.go
  • nitronode/store/database/state.go
  • nitronode/store/database/testing.go
  • pkg/app/session_key_v1.go
  • pkg/app/session_key_v1_test.go
  • pkg/blockchain/evm/channel_hub_reactor.go
  • pkg/blockchain/evm/channel_hub_reactor_test.go
  • pkg/blockchain/evm/validator_watcher.go
  • pkg/core/event.go
  • pkg/core/interface.go
  • pkg/core/session_key.go
  • pkg/core/session_key_test.go
  • pkg/core/types.go
  • pkg/core/utils.go
  • pkg/rpc/api.go
  • pkg/rpc/connection.go
  • pkg/rpc/connection_hub.go
  • pkg/rpc/connection_test.go
  • pkg/rpc/node.go
  • pkg/rpc/rate_limiter.go
  • pkg/rpc/rate_limiter_test.go
  • pkg/rpc/types.go
  • pkg/sign/eth_msg_signer.go
  • protocol-description.md
  • sdk/go/README.md
  • sdk/go/app_session.go
  • sdk/go/channel.go
  • sdk/go/examples/app_sessions/lifecycle.go
  • sdk/go/examples/validator_watcher/main.go
  • sdk/go/utils.go
  • sdk/go/validator_watcher.go
  • sdk/ts-compat/README.md
  • sdk/ts-compat/src/client.ts
  • sdk/ts-compat/src/types.ts
  • sdk/ts-compat/test/unit/client-mapping.test.ts
  • sdk/ts-compat/test/unit/public-api-drift.test.ts
  • sdk/ts/README.md
  • sdk/ts/examples/app_sessions/lifecycle.ts
  • sdk/ts/examples/example-app/README.md
  • sdk/ts/examples/example-app/src/components/WalletDashboard.tsx
  • sdk/ts/src/app/types.ts
  • sdk/ts/src/blockchain/evm/channel_hub_abi.ts
  • sdk/ts/src/blockchain/evm/index.ts
  • sdk/ts/src/blockchain/evm/validator_watcher.ts
  • sdk/ts/src/client.ts
  • sdk/ts/src/core/event.ts
  • sdk/ts/src/core/types.ts
  • sdk/ts/src/core/utils.ts
  • sdk/ts/src/rpc/api.ts
  • sdk/ts/src/rpc/types.ts
  • sdk/ts/src/session_key_state_transforms.ts
  • sdk/ts/src/utils.ts
  • sdk/ts/test/unit/blockchain/evm/validator_watcher.test.ts
  • sdk/ts/test/unit/rpc-drift.test.ts
  • sdk/ts/test/unit/transform-drift.test.ts

Comment thread contracts/test/mocks/OversizedReturnERC20.sol
Comment thread contracts/test/mocks/OversizedReturnERC20.sol
Comment thread nitronode/api/app_session_v1/get_last_key_states_test.go
Comment thread nitronode/blockchain_worker.go
Comment thread nitronode/event_handlers/service.go
Comment thread pkg/blockchain/evm/validator_watcher.go
Comment thread sdk/go/channel.go
Comment thread sdk/go/validator_watcher.go
Comment thread sdk/ts/src/client.ts
Comment thread sdk/ts/src/client.ts
philanton and others added 2 commits May 13, 2026 20:45
…astAppKeyStates

The SDK method was renamed in MF-L08; the runtime smoke script still called
the old name and broke the Protocol Drift Runtime CI check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (1)
pkg/blockchain/evm/channel_hub_reactor_test.go (1)

416-418: ⚡ Quick win

Tighten checkpointed-event matcher to include channel/version fields.

The matcher currently validates only UserSig, so a miswired ChannelID/StateVersion could still pass this test.

Suggested diff
-	handler.On("HandleHomeChannelCheckpointed", mock.Anything, mock.Anything, mock.MatchedBy(func(ev *core.HomeChannelCheckpointedEvent) bool {
-		return ev.UserSig == hexutil.Encode([]byte{0xde, 0xad, 0xbe, 0xef})
-	})).Return(nil)
+	handler.On("HandleHomeChannelCheckpointed", mock.Anything, mock.Anything, mock.MatchedBy(func(ev *core.HomeChannelCheckpointedEvent) bool {
+		return ev.ChannelID == hexutil.Encode(channelID[:]) &&
+			ev.StateVersion == 7 &&
+			ev.UserSig == hexutil.Encode([]byte{0xde, 0xad, 0xbe, 0xef})
+	})).Return(nil)
🤖 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 `@pkg/blockchain/evm/channel_hub_reactor_test.go` around lines 416 - 418, The
mock matcher for handler.On("HandleHomeChannelCheckpointed", ...) only asserts
UserSig and should also assert the checkpointed event's ChannelID and
StateVersion to prevent false positives; update the MatchedBy anonymous function
that takes *core.HomeChannelCheckpointedEvent to additionally compare
ev.ChannelID and ev.StateVersion against the test's expected values (the same
variables used to create the checkpoint in this test) along with the existing
UserSig check so all three fields must match before returning true.
🤖 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.

Nitpick comments:
In `@pkg/blockchain/evm/channel_hub_reactor_test.go`:
- Around line 416-418: The mock matcher for
handler.On("HandleHomeChannelCheckpointed", ...) only asserts UserSig and should
also assert the checkpointed event's ChannelID and StateVersion to prevent false
positives; update the MatchedBy anonymous function that takes
*core.HomeChannelCheckpointedEvent to additionally compare ev.ChannelID and
ev.StateVersion against the test's expected values (the same variables used to
create the checkpoint in this test) along with the existing UserSig check so all
three fields must match before returning true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6177bce3-6913-4c01-bb67-abc1f9336b87

📥 Commits

Reviewing files that changed from the base of the PR and between 6152e74 and af32b71.

📒 Files selected for processing (12)
  • contracts/src/ChannelHub.sol
  • contracts/test/ChannelHub_escrowDepositPurge/ChannelHub_purgeEscrowDeposits.t.sol
  • contracts/test/ChannelHub_units/ChannelHub_finalizeEscrowDeposit.t.sol
  • contracts/test/ChannelHub_units/ChannelHub_finalizeEscrowWithdrawal.t.sol
  • nitronode/event_handlers/service.go
  • nitronode/event_handlers/service_test.go
  • pkg/blockchain/evm/channel_hub_abi.go
  • pkg/blockchain/evm/channel_hub_reactor.go
  • pkg/blockchain/evm/channel_hub_reactor_test.go
  • pkg/core/event.go
  • pkg/core/interface.go
  • sdk/ts/src/blockchain/evm/channel_hub_abi.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • contracts/test/ChannelHub_units/ChannelHub_finalizeEscrowDeposit.t.sol
  • pkg/blockchain/evm/channel_hub_reactor.go

@nksazonov nksazonov merged commit 80af194 into main May 13, 2026
16 checks passed
@nksazonov nksazonov deleted the fix/audit-findings-final branch May 13, 2026 18:33
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.

3 participants