YNU-941: apply final round audit findings#758
Conversation
## 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>
…ific execution paths (#737)
…nd requirements (#738)
## 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 [](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. [](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>
…escrow finalization (#754)
…ng abandoned migration (#755)
…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 --> [](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 --> [](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>
📝 WalkthroughDo 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
🧪 Generate unit tests (beta)
|
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>
There was a problem hiding this comment.
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 winAdd NatSpec documentation to public functions.
The
mintfunction 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 winAdd NatSpec documentation to public functions.
The
transferfunction 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 winAdd NatSpec documentation to public functions.
The
transferFromfunction 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 winAdd doc comment to exported
ByteTokenBucket.The exported struct
ByteTokenBucketis 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
gofmtformatting 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 winVerify struct layout invariant is maintained.
The
getChannelIdfunction uses hardcoded0xC0(192 bytes) based onChannelDefinitionhaving 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.solthat 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 winClarify 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 winClarify validation behavior for unsupported
sortfield.Same issue as in the channels endpoint - the description states
sortis 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 winClarify validation behavior for unsupported
sortfield.The description states "The
sortfield is not supported by this endpoint and must be omitted," butpagination_paramsincludessortas an optional field. Consider clarifying whether:
- Requests with
sortwill be rejected with a validation error- Or if
sortis 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 valueDocument 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 valueConsider consolidating the home channel status checks.
The
MutualLockbranch duplicates thechannelStatus != Opencheck 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
MutualLocksupport, 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 winAdd an exported doc comment for
NewBlockchainWorker.
NewBlockchainWorkeris exported and was modified, but it still has no GoDoc comment.As per coding guidelines,
**/*.go: Followgofmtformatting 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 valueDoc/code mismatch on the byte-rate "disable" sentinel.
Line 87 comment says
Set <0 to disable, but the gate at line 251 usesif bytesPerSec > 0, so0also disables byte limiting. Either tighten the gate to>= 0or relax the doc to match actual behavior — operators following the doc and setting-1will get the same result as0, which is fine, but anyone parsing the doc literally as "must be>= 0to 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 winCoordinate this enum remap with the application rollout.
This migration silently changes the on-disk encoding of
Closedfrom 3 to 4. If the migration runs before the new application binary that knowsClosed = 4is fully rolled out (or vice versa), live rows will be read with the wrong status: any in-flight reads on the old binary that seestatus = 4will misinterpret it, and new-binary writes that land before the UPDATE flips existing rows will leave a mix of encodings forClosed.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 valueConsider 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 inNewWebsocketConnection."🤖 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 valueConsider validating session key address matches signer.
The function accepts a
sessionKeySignerbut doesn't verify thatsessionKeySigner.PublicKey().Address()matchesstate.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
⛔ Files ignored due to path filters (2)
sdk/ts-compat/test/unit/__snapshots__/public-api-drift.test.ts.snapis excluded by!**/*.snapsdk/ts/test/unit/__snapshots__/public-api-drift.test.ts.snapis excluded by!**/*.snap
📒 Files selected for processing (141)
cerebro/commands.gocontracts/SECURITY.mdcontracts/src/ChannelHub.solcontracts/src/Utils.solcontracts/src/interfaces/Types.solcontracts/test/ChannelHub_Node.t.solcontracts/test/ChannelHub_challenge/ChannelHub_challengeSessionKeyValidator.t.solcontracts/test/ChannelHub_lifecycle/ChannelHub_singlechain.lifecycle.t.solcontracts/test/ChannelHub_nonRevertingPushFunds.t.solcontracts/test/ChannelHub_sigValidator.t.solcontracts/test/ChannelHub_units/ChannelHub_challenge.t.solcontracts/test/ChannelHub_units/ChannelHub_createChannel.t.solcontracts/test/ChannelHub_units/ChannelHub_finalizeEscrowDeposit.t.solcontracts/test/ChannelHub_units/ChannelHub_finalizeEscrowWithdrawal.t.solcontracts/test/ChannelHub_units/ChannelHub_initiateEscrowDeposit.t.solcontracts/test/Utils.t.solcontracts/test/mocks/MalformedReturningERC20.solcontracts/test/mocks/OversizedReturnERC20.soldocs/api.yamldocs/data_models.mmddocs/protocol/enforcement.mddocs/protocol/overview.mddocs/protocol/security-and-limitations.mdnitronode/README.mdnitronode/api/app_session_v1/README.mdnitronode/api/app_session_v1/create_app_session.gonitronode/api/app_session_v1/create_app_session_test.gonitronode/api/app_session_v1/get_last_key_states.gonitronode/api/app_session_v1/get_last_key_states_test.gonitronode/api/app_session_v1/handler.gonitronode/api/app_session_v1/interface.gonitronode/api/app_session_v1/rebalance_app_sessions_test.gonitronode/api/app_session_v1/submit_app_state_test.gonitronode/api/app_session_v1/submit_deposit_state.gonitronode/api/app_session_v1/submit_deposit_state_test.gonitronode/api/app_session_v1/submit_session_key_state.gonitronode/api/app_session_v1/submit_session_key_state_test.gonitronode/api/app_session_v1/testing.gonitronode/api/app_session_v1/utils.gonitronode/api/channel_v1/get_channels.gonitronode/api/channel_v1/get_home_channel.gonitronode/api/channel_v1/get_home_channel_test.gonitronode/api/channel_v1/get_last_key_states.gonitronode/api/channel_v1/get_last_key_states_test.gonitronode/api/channel_v1/handler.gonitronode/api/channel_v1/interface.gonitronode/api/channel_v1/request_creation.gonitronode/api/channel_v1/request_creation_test.gonitronode/api/channel_v1/submit_session_key_state.gonitronode/api/channel_v1/submit_session_key_state_test.gonitronode/api/channel_v1/submit_state.gonitronode/api/channel_v1/submit_state_test.gonitronode/api/channel_v1/testing.gonitronode/api/channel_v1/utils.gonitronode/api/rate_limits.gonitronode/api/rpc_router.gonitronode/blockchain_worker.gonitronode/chart/README.mdnitronode/chart/config/stress-v1/nitronode.yaml.gotmplnitronode/chart/templates/helpers/_common.tplnitronode/chart/values.yamlnitronode/config/migrations/postgres/20251222000000_initial_schema.sqlnitronode/config/migrations/postgres/20260507000000_add_current_session_key_states.sqlnitronode/config/migrations/postgres/20260508000000_session_key_ownership_constraints.sqlnitronode/config/migrations/postgres/20260513000000_add_channel_status_closing.sqlnitronode/event_handlers/service.gonitronode/event_handlers/service_test.gonitronode/event_handlers/testing.gonitronode/main.gonitronode/metrics/exporter.gonitronode/metrics/interface.gonitronode/runtime.gonitronode/runtime_config_test.gonitronode/store/database/app_session_key_state.gonitronode/store/database/app_session_key_state_test.gonitronode/store/database/blockchain_action.gonitronode/store/database/channel.gonitronode/store/database/channel_session_key_state.gonitronode/store/database/channel_session_key_state_test.gonitronode/store/database/channel_test.gonitronode/store/database/current_session_key_state.gonitronode/store/database/current_session_key_state_test.gonitronode/store/database/database.gonitronode/store/database/database_test.gonitronode/store/database/db_store.gonitronode/store/database/db_store_test.gonitronode/store/database/interface.gonitronode/store/database/state.gonitronode/store/database/testing.gopkg/app/session_key_v1.gopkg/app/session_key_v1_test.gopkg/blockchain/evm/channel_hub_reactor.gopkg/blockchain/evm/channel_hub_reactor_test.gopkg/blockchain/evm/validator_watcher.gopkg/core/event.gopkg/core/interface.gopkg/core/session_key.gopkg/core/session_key_test.gopkg/core/types.gopkg/core/utils.gopkg/rpc/api.gopkg/rpc/connection.gopkg/rpc/connection_hub.gopkg/rpc/connection_test.gopkg/rpc/node.gopkg/rpc/rate_limiter.gopkg/rpc/rate_limiter_test.gopkg/rpc/types.gopkg/sign/eth_msg_signer.goprotocol-description.mdsdk/go/README.mdsdk/go/app_session.gosdk/go/channel.gosdk/go/examples/app_sessions/lifecycle.gosdk/go/examples/validator_watcher/main.gosdk/go/utils.gosdk/go/validator_watcher.gosdk/ts-compat/README.mdsdk/ts-compat/src/client.tssdk/ts-compat/src/types.tssdk/ts-compat/test/unit/client-mapping.test.tssdk/ts-compat/test/unit/public-api-drift.test.tssdk/ts/README.mdsdk/ts/examples/app_sessions/lifecycle.tssdk/ts/examples/example-app/README.mdsdk/ts/examples/example-app/src/components/WalletDashboard.tsxsdk/ts/src/app/types.tssdk/ts/src/blockchain/evm/channel_hub_abi.tssdk/ts/src/blockchain/evm/index.tssdk/ts/src/blockchain/evm/validator_watcher.tssdk/ts/src/client.tssdk/ts/src/core/event.tssdk/ts/src/core/types.tssdk/ts/src/core/utils.tssdk/ts/src/rpc/api.tssdk/ts/src/rpc/types.tssdk/ts/src/session_key_state_transforms.tssdk/ts/src/utils.tssdk/ts/test/unit/blockchain/evm/validator_watcher.test.tssdk/ts/test/unit/rpc-drift.test.tssdk/ts/test/unit/transform-drift.test.ts
…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>
…andle it in Nitronode (#757)
There was a problem hiding this comment.
🧹 Nitpick comments (1)
pkg/blockchain/evm/channel_hub_reactor_test.go (1)
416-418: ⚡ Quick winTighten checkpointed-event matcher to include channel/version fields.
The matcher currently validates only
UserSig, so a miswiredChannelID/StateVersioncould 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
📒 Files selected for processing (12)
contracts/src/ChannelHub.solcontracts/test/ChannelHub_escrowDepositPurge/ChannelHub_purgeEscrowDeposits.t.solcontracts/test/ChannelHub_units/ChannelHub_finalizeEscrowDeposit.t.solcontracts/test/ChannelHub_units/ChannelHub_finalizeEscrowWithdrawal.t.solnitronode/event_handlers/service.gonitronode/event_handlers/service_test.gopkg/blockchain/evm/channel_hub_abi.gopkg/blockchain/evm/channel_hub_reactor.gopkg/blockchain/evm/channel_hub_reactor_test.gopkg/core/event.gopkg/core/interface.gosdk/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
Summary by CodeRabbit
New Features
Improvements
Security / Enforcement