Skip to content

MF-M08: fix(rpc): replace Origin label with application_id on connection gauge#745

Merged
philanton merged 2 commits into
fix/audit-findings-finalfrom
fix/nitronode-mf-m08
May 13, 2026
Merged

MF-M08: fix(rpc): replace Origin label with application_id on connection gauge#745
philanton merged 2 commits into
fix/audit-findings-finalfrom
fix/nitronode-mf-m08

Conversation

@philanton
Copy link
Copy Markdown
Contributor

@philanton philanton commented May 12, 2026

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

  • go build ./...
  • go vet ./...
  • 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

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

Replace the client-controlled Origin header label on the rpc_connections_active
gauge with application_id, which is parsed and validated against
IsValidApplicationID at WebSocket upgrade. The hub now tracks per-application
counts and deletes the gauge series when an application's count drops to zero,
so disconnected buckets do not accumulate as zero-valued time series.

The connection-level Origin field is retained for logging.

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

coderabbitai Bot commented May 12, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ce1166a-14ca-435e-b53f-a138279123c4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds CI drift guards and scripts; updates Solidity and ABI; introduces paginated key-state APIs with SDK transforms; implements DB pointer table with locks and caps; escrow-operation gating; event user-sig backfill; WS read-limit/rate-limiter and metrics relabeling; DB SSL config; docs updates.

Changes

Unified Change Set

Layer / File(s) Summary
Protocol drift CI and scripts
.github/workflows/*, scripts/*, sdk/PROTOCOL_DRIFT_GUARDS.md
Adds static/runtime drift checks, external smoke workflow, and TS/compat drift guard tests.
Contracts and ABI alignment
contracts/src/*, contracts/test/*, sdk/ts/src/blockchain/evm/channel_hub_abi.ts
Adjusts errors/transfer handling; utils hashing; tests for edge ERC20 behaviors; ABI stateMutability/error updates.
SDK transforms and guards
sdk/ts/src/*, sdk/ts/test/unit/*, sdk/ts-compat/*
Adds key-state/app transforms, public/RPC/ABI/signing drift guards, and client mapping with pagination.
RPC API and handlers pagination
pkg/rpc/*, nitronode/api/*
Adds Pagination params/metadata, handler page math, and tests for channels/app sessions.
DB pointer table + session-key caps
nitronode/store/database/*, migrations/*
Introduces current_session_key_states_v1, locking, per-user key counting, and paginated queries.
Escrow-operation gating + sig backfill
nitronode/api/*, nitronode/event_handlers/*, pkg/blockchain/evm/*, pkg/core/*
Replaces last-state checks with EnsureNoOngoingEscrowOperation and backfills missing user signatures.
WS limits and metrics relabeling
pkg/rpc/*, nitronode/runtime.go, nitronode/metrics/*, chart/*
Adds frame-size cap, byte token-bucket limiter, application_id metrics, and chart/runtime wiring.
Docs and DB SSL config
docs/*, protocol-description.md, nitronode/*README*, database.go
Updates enforcement/trust/token docs, DB SSLMode and URL handling, and Helm values.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

ready

Suggested reviewers

  • dimast-x
  • ihsraham
  • nksazonov

Poem

I thump the ground where workflows bloom,
Guard drift, paged keys, and sockets’ room;
I count the bytes in steady light,
Lock rows before a race takes flight.
With ears up high I scan the log—
All hops aligned; ship-ready fog. 🐇✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/nitronode-mf-m08

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: 6

Caution

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

⚠️ Outside diff range comments (1)
nitronode/store/database/channel_session_key_state.go (1)

64-83: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make history insert + pointer upsert atomic.

Line 64-83 currently allows partial commit (state inserted, pointer not updated). A transient pointer-upsert failure can permanently stale latest-state reads because retries hit duplicate insert first.

💡 Suggested fix
-	if err := s.db.Create(&dbState).Error; err != nil {
-		return fmt.Errorf("failed to store channel session key state: %w", err)
-	}
-
-	if len(state.Assets) > 0 {
-		assets := make([]ChannelSessionKeyAssetV1, len(state.Assets))
-		for i, asset := range state.Assets {
-			assets[i] = ChannelSessionKeyAssetV1{
-				SessionKeyStateID: id,
-				Asset:             strings.ToLower(asset),
-			}
-		}
-		if err := s.db.Create(&assets).Error; err != nil {
-			return fmt.Errorf("failed to store channel session key assets: %w", err)
-		}
-	}
-
-	if err := upsertCurrentSessionKeyState(s.db, userAddress, sessionKey, SessionKeyKindChannel, state.Version); err != nil {
-		return err
-	}
-
-	return nil
+	if err := s.db.Transaction(func(tx *gorm.DB) error {
+		if err := tx.Create(&dbState).Error; err != nil {
+			return fmt.Errorf("failed to store channel session key state: %w", err)
+		}
+
+		if len(state.Assets) > 0 {
+			assets := make([]ChannelSessionKeyAssetV1, len(state.Assets))
+			for i, asset := range state.Assets {
+				assets[i] = ChannelSessionKeyAssetV1{
+					SessionKeyStateID: id,
+					Asset:             strings.ToLower(asset),
+				}
+			}
+			if err := tx.Create(&assets).Error; err != nil {
+				return fmt.Errorf("failed to store channel session key assets: %w", err)
+			}
+		}
+
+		if err := upsertCurrentSessionKeyState(tx, userAddress, sessionKey, SessionKeyKindChannel, state.Version); err != nil {
+			return err
+		}
+		return nil
+	}); err != nil {
+		return err
+	}
+
+	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 `@nitronode/store/database/channel_session_key_state.go` around lines 64 - 83,
The insert of dbState and assets plus the call to upsertCurrentSessionKeyState
must be executed inside a single DB transaction to avoid partial commits: begin
a transaction from s.db, replace s.db.Create calls for dbState and assets with
tx.Create, and call a transaction-aware version of upsertCurrentSessionKeyState
(or modify upsertCurrentSessionKeyState to accept a *gorm.DB and pass tx) so the
pointer upsert uses the same tx; on any error roll back the tx and return the
error, otherwise commit the tx. Use the existing symbols dbState, assets,
ChannelSessionKeyAssetV1, upsertCurrentSessionKeyState (or a new
upsertCurrentSessionKeyStateTx that takes tx), SessionKeyKindChannel,
userAddress, sessionKey and state.Version when implementing this change.
🧹 Nitpick comments (8)
sdk/ts/test/unit/app-signing-drift.test.ts (1)

40-251: ⚡ Quick win

Refactor repeated drift assertions to data-driven .forEach() cases.

This suite is highly repetitive and can be collapsed into case tables for vector and adversarial checks, which will make updates to expected hashes easier and keep style consistent with repo test guidelines.

As per coding guidelines "Use data-driven tests with .forEach() pattern and manual mocks instead of jest.mock()".

🤖 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/ts/test/unit/app-signing-drift.test.ts` around lines 40 - 251, The tests
repeat many expect(...) assertions for packCreateAppSessionRequestV1,
generateAppSessionIDV1, packAppStateUpdateV1, and packAppSessionKeyStateV1;
collapse these into data-driven tables (arrays of case objects) and iterate with
.forEach() (or test.each) to drive assertions, and convert the adversarial
comparisons into similar case pairs in arrays so each vector and adversarial
check is a single table-driven loop; update references to AppStateUpdateIntent,
packAppStateUpdateV1, packCreateAppSessionRequestV1, generateAppSessionIDV1, and
packAppSessionKeyStateV1 in the new case objects to locate inputs, and ensure
any mocking follows the guideline by using manual mocks rather than jest.mock()
if mocking is needed.
sdk/ts/test/unit/abi-drift.test.ts (1)

112-278: ⚡ Quick win

Convert repeated adversarial cases into a single table-driven .forEach() suite.

The current pattern repeats near-identical assertion scaffolding. A shared case matrix would make future ABI drift cases easier to add and maintain.

As per coding guidelines "Use data-driven tests with .forEach() pattern and manual mocks instead of jest.mock()".

🤖 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/ts/test/unit/abi-drift.test.ts` around lines 112 - 278, Consolidate the
repeated adversarial test cases at the end of the "contract ABI drift guards"
suite into a single table-driven array and iterate with .forEach(): collect each
case's inputs and expected output (cases currently implemented as individual
it(...) blocks that call diffConsumedFunctions or diffSdkSubsetAgainstManifest),
then in the loop call the appropriate function (diffConsumedFunctions or
diffSdkSubsetAgainstManifest) and assert toEqual(expected); reference the
existing identifiers to find code: the suite name "contract ABI drift guards",
the helper functions diffConsumedFunctions and diffSdkSubsetAgainstManifest, and
the individual test names like 'reports adversarial ChannelHub function
signature changes...', 'reports adversarial ERC20 missing consumed functions',
and 'reports adversarial AppRegistry manifest signature changes' to build the
case matrix and replace the separate it(...) blocks with a single data-driven
.forEach() test.
sdk/ts/test/unit/public-api-drift.test.ts (1)

181-247: ⚡ Quick win

Refactor adversarial drift checks into data-driven .forEach() cases.

These tests share the same structure and can be condensed into parameterized cases to simplify future maintenance.

As per coding guidelines "Use data-driven tests with .forEach() pattern and manual mocks instead of jest.mock()".

🤖 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/ts/test/unit/public-api-drift.test.ts` around lines 181 - 247, Condense
the repeated "proves adversarial..." test cases into a data-driven array and
iterate with .forEach to generate the individual it() tests: collect test
descriptors (name, precomputed api via serializePublicApi(), target member name
like 'Client'/'Config'/'packAppStateUpdateV1'/'AppStateUpdateIntent' or expected
predicate, and the specific mutation + assertion), then replace each explicit
it(...) block with a single .forEach that calls expect/mutates accordingly;
reference serializePublicApi(), publicApi and the existing property checks
(e.g., checking properties for 'ping:', kind === 'interface' for 'Config',
signatures for 'packAppStateUpdateV1', members for 'AppStateUpdateIntent', and
adding '__FakeExport') to implement each case, and ensure you follow the
guideline to use manual setup/mocks instead of jest.mock() while keeping
assertions identical.
nitronode/store/database/db_store.go (1)

214-215: 💤 Low value

Consider extracting the mutual lock check for readability.

The combined condition correctly validates both home and escrow channel versions, but could be extracted for clarity.

♻️ Optional refactor
-	if result.HomeChannelVersion == nil || result.StateVersion != *result.HomeChannelVersion ||
-		result.EscrowChannelVersion == nil || result.StateVersion != *result.EscrowChannelVersion {
+	homeCaughtUp := result.HomeChannelVersion != nil && result.StateVersion == *result.HomeChannelVersion
+	escrowCaughtUp := result.EscrowChannelVersion != nil && result.StateVersion == *result.EscrowChannelVersion
+	if !homeCaughtUp || !escrowCaughtUp {
		return fmt.Errorf("mutual lock is still ongoing")
	}
🤖 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/store/database/db_store.go` around lines 214 - 215, Extract the
combined mutual-lock condition into a small helper to improve readability:
create a function (e.g., isMutuallyLocked or validateMutualLock) that accepts
the same result type and returns bool using the existing logic that checks
result.HomeChannelVersion, result.EscrowChannelVersion and compares both to
result.StateVersion; then replace the inline condition (the long if that
references result.HomeChannelVersion, result.EscrowChannelVersion and
result.StateVersion) with a call to the new helper and keep behavior identical.
sdk/ts/test/unit/rpc-drift.test.ts (1)

49-52: 💤 Low value

Indentation-dependent regex is brittle but adequately guarded.

The regex on line 50 hardcodes exactly 2 spaces (^\s{2}). If client method indentation changes, this will silently break. The guard at line 130 (expect(clientMethods.size).toBeGreaterThan(20)) mitigates this, but consider using a more flexible pattern like ^\s+ with additional context to match class methods more robustly.

🤖 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/ts/test/unit/rpc-drift.test.ts` around lines 49 - 52, The regex in
extractTsClientMethods is brittle because it requires exactly two leading
spaces; update the pattern used in extractTsClientMethods to allow flexible
indentation (e.g. use ^\s+ instead of ^\s{2}) and keep the existing
method-capturing groups (still matching optional "static"/"async" and the
identifier) so it robustly finds class methods even if indentation changes;
ensure the new pattern still uses the gm flags and preserves the return type
Set<string> behavior.
contracts/test/mocks/OversizedReturnERC20.sol (1)

23-39: ⚡ Quick win

Add NatSpec comments for public functions.

transfer and mint are public, but they currently have no function-level NatSpec blocks.

Proposed update
+    /**
+     * `@notice` Transfers tokens and returns custom-sized returndata for transfer-behavior tests.
+     * `@dev` Performs `_transfer` only when `FIRST_WORD != 0`; then returns `RETURN_DATA_SIZE` bytes.
+     */
     function transfer(address to, uint256 amount) public override returns (bool) {
         if (FIRST_WORD != 0) {
             _transfer(msg.sender, to, amount);
         }
@@
     }
 
+    /**
+     * `@notice` Mints tokens for test setup.
+     */
     function mint(address to, uint256 amount) public {
         _mint(to, amount);
     }

As per coding guidelines **/*.sol: 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/OversizedReturnERC20.sol` around lines 23 - 39, Add
Solidity NatSpec comments for the public functions transfer and mint: add a
`@notice` describing what each function does, `@param` tags for the address and
amount parameters (e.g., to, amount), and for transfer include a `@return` tag
describing the boolean result; place these function-level NatSpec blocks
immediately above the transfer and mint function declarations in
OversizedReturnERC20.sol (identify the functions by name: transfer and mint)
following the repository's NatSpec style guidelines.
nitronode/api/app_session_v1/interface.go (1)

55-59: 💤 Low value

Missing doc comments on new/changed exported interface methods.

LockSessionKeyState, CountSessionKeysForUser, and the updated GetLastAppSessionKeyStates are exported interface methods but lack doc comments, unlike the adjacent CheckActiveChannel/EnsureNoOngoingEscrowOperation which were documented in this PR. Sibling interface channel_v1/interface.go already documents the equivalents (lines 80-86, 95-97), so the asymmetry seems unintentional.

📝 Proposed doc comments
 	// App Session key state operations
+	// LockSessionKeyState locks the (user, session_key, kind) pointer row for the surrounding
+	// transaction, returning the current version (0 if newly created).
 	LockSessionKeyState(userAddress, sessionKey string, kind database.SessionKeyKind) (uint64, error)
+	// CountSessionKeysForUser returns the number of distinct session keys for the wallet
+	// across both kinds, used to enforce the per-user cap at submit time.
 	CountSessionKeysForUser(userAddress string) (uint32, error)
 	StoreAppSessionKeyState(state app.AppSessionKeyStateV1) error
 	GetLastAppSessionKeyVersion(wallet, sessionKey string) (uint64, error)
+	// GetLastAppSessionKeyStates retrieves the latest app-session key states for a user,
+	// optionally filtered by session key. Results are paginated; the second return is the
+	// unpaginated total.
 	GetLastAppSessionKeyStates(wallet string, sessionKey *string, limit, offset uint32) ([]app.AppSessionKeyStateV1, uint32, error)

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 `@nitronode/api/app_session_v1/interface.go` around lines 55 - 59, Add Go doc
comments for the exported interface methods that are missing documentation:
LockSessionKeyState, CountSessionKeysForUser, and GetLastAppSessionKeyStates
(also review GetLastAppSessionKeyVersion and StoreAppSessionKeyState to ensure
they have comments). Place concise Godoc-style comments immediately above each
method in the app_session_v1 interface mirroring the style/phrasing used in the
sibling channel_v1/interface.go (describe what the method does, its important
parameters and return values) so all exported methods have proper documentation.
pkg/rpc/connection.go (1)

408-419: 💤 Low value

Consider logging frame size against the configured limit.

The warning log records frame_bytes but not the limiter's configured rate/threshold, which makes it hard to distinguish "client sent a huge frame" from "limiter ran out of tokens." Optional: pass an explicit reason or the limiter's effective settings into the log to help operators triage frame rate limit exceeded alerts.

🤖 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/connection.go` around lines 408 - 419, The warning only logs
frame_bytes but not the limiter's configured limit or reason; update the block
where conn.frameRateLimiter.Admit(...) is checked (the
conn.frameRateLimiter.Admit call and the conn.logger.Warn invocation) to include
the limiter's configured settings and/or remaining tokens in the log message
(e.g., add fields like "limit_bytes", "burst", or "tokens_remaining" and an
explicit "reason" such as "token_exhausted" vs "oversize_frame"); keep the same
close flow that sends to conn.closeConnCh and calls handleClosure(nil).
🤖 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 `@nitronode/api/app_session_v1/get_last_key_states_test.go`:
- Around line 45-85: The tests
TestGetLastKeyStates_DefaultsToPageOneOnEmptyResult and
TestGetLastKeyStates_PaginationMetadata_AlignedOffset set expectations on
mockStore via mockStore.On(...) but never verify them; add
mockStore.AssertExpectations(t) at the end of each test to ensure the mocked
GetLastAppSessionKeyStates call was invoked as expected (i.e., call
mockStore.AssertExpectations(t) in both
TestGetLastKeyStates_DefaultsToPageOneOnEmptyResult and
TestGetLastKeyStates_PaginationMetadata_AlignedOffset).

In `@nitronode/store/database/current_session_key_state.go`:
- Around line 98-117: The non-Postgres fallback has a race where two callers
both see gorm.ErrRecordNotFound and both run s.db.Create(&seed) which can fail
on PK conflict; update the seed creation logic in the CurrentSessionKeyStateV1
path (the block that runs after checking err == gorm.ErrRecordNotFound from
s.db.Where(...).First(&existing).Error) to be idempotent—either use GORM's
OnConflict{DoNothing: true} when creating the seed or catch/ignore the unique
constraint/primary-key conflict error from s.db.Create(&seed) so the second
concurrent insert doesn't return an error, then return version 0 as before.

In `@pkg/rpc/rate_limiter.go`:
- Around line 64-69: The Admit method currently converts size to cost and
updates b.tokens, but it lacks a guard against negative sizes which would
erroneously increase tokens; update Admit (in pkg/rpc/rate_limiter.go) to check
the incoming size parameter and immediately return false (or an error boolean)
if size is negative before computing cost or mutating b.tokens, ensuring the
existing logic that checks b.tokens < cost and subtracts cost only runs for
non-negative sizes.

In `@scripts/drift/generate-app-signing-vectors.go`:
- Around line 134-137: The must function currently discards the error returned
by fmt.Fprintf; change it to capture that return (e.g., writeErr :=
fmt.Fprintf(...)) and handle it explicitly: if writeErr != nil, attempt a
fallback (for example write the combined message to os.Stdout or use fmt.Println
to emit both the writeErr and the original err), then call os.Exit(1) as before
so the function still terminates; update the function name must accordingly to
check and handle the fprintf error rather than ignoring it.

In `@sdk/ts/src/session_key_state_transforms.ts`:
- Around line 4-56: Replace the custom runtime validators (asRecord,
requireStringField, requireStringArrayField) with zod schemas and use
schema.safeParse(raw) inside transformChannelSessionKeyState and
transformAppSessionKeyState; define a Zod object for the
ChannelSessionKeyStateV1 shape and one for AppSessionKeyStateV1 (matching fields
user_address, session_key, version, assets, expires_at, user_sig and for app:
application_ids, app_session_ids) and on safeParse failure throw an Error with
the parse error message, otherwise return the parsed.data object; remove the
helper functions and ensure the transforms return the typed parsed result from
the Zod schemas.

In `@sdk/ts/src/utils.ts`:
- Around line 367-373: transformAppSessionInfo currently maps raw.app_session_id
directly to appSessionId without checking type; validate that raw.app_session_id
exists and is a string (and optionally non-empty) before returning — if it's
missing or not a string, throw a descriptive error (e.g., "invalid
app_session_id") or handle it according to SDK conventions; update the mapping
(appSessionId) and any tests to rely on this validation and reference
transformAppSessionInfo and raw.app_session_id when making the change.

---

Outside diff comments:
In `@nitronode/store/database/channel_session_key_state.go`:
- Around line 64-83: The insert of dbState and assets plus the call to
upsertCurrentSessionKeyState must be executed inside a single DB transaction to
avoid partial commits: begin a transaction from s.db, replace s.db.Create calls
for dbState and assets with tx.Create, and call a transaction-aware version of
upsertCurrentSessionKeyState (or modify upsertCurrentSessionKeyState to accept a
*gorm.DB and pass tx) so the pointer upsert uses the same tx; on any error roll
back the tx and return the error, otherwise commit the tx. Use the existing
symbols dbState, assets, ChannelSessionKeyAssetV1, upsertCurrentSessionKeyState
(or a new upsertCurrentSessionKeyStateTx that takes tx), SessionKeyKindChannel,
userAddress, sessionKey and state.Version when implementing this change.

---

Nitpick comments:
In `@contracts/test/mocks/OversizedReturnERC20.sol`:
- Around line 23-39: Add Solidity NatSpec comments for the public functions
transfer and mint: add a `@notice` describing what each function does, `@param` tags
for the address and amount parameters (e.g., to, amount), and for transfer
include a `@return` tag describing the boolean result; place these function-level
NatSpec blocks immediately above the transfer and mint function declarations in
OversizedReturnERC20.sol (identify the functions by name: transfer and mint)
following the repository's NatSpec style guidelines.

In `@nitronode/api/app_session_v1/interface.go`:
- Around line 55-59: Add Go doc comments for the exported interface methods that
are missing documentation: LockSessionKeyState, CountSessionKeysForUser, and
GetLastAppSessionKeyStates (also review GetLastAppSessionKeyVersion and
StoreAppSessionKeyState to ensure they have comments). Place concise Godoc-style
comments immediately above each method in the app_session_v1 interface mirroring
the style/phrasing used in the sibling channel_v1/interface.go (describe what
the method does, its important parameters and return values) so all exported
methods have proper documentation.

In `@nitronode/store/database/db_store.go`:
- Around line 214-215: Extract the combined mutual-lock condition into a small
helper to improve readability: create a function (e.g., isMutuallyLocked or
validateMutualLock) that accepts the same result type and returns bool using the
existing logic that checks result.HomeChannelVersion,
result.EscrowChannelVersion and compares both to result.StateVersion; then
replace the inline condition (the long if that references
result.HomeChannelVersion, result.EscrowChannelVersion and result.StateVersion)
with a call to the new helper and keep behavior identical.

In `@pkg/rpc/connection.go`:
- Around line 408-419: The warning only logs frame_bytes but not the limiter's
configured limit or reason; update the block where
conn.frameRateLimiter.Admit(...) is checked (the conn.frameRateLimiter.Admit
call and the conn.logger.Warn invocation) to include the limiter's configured
settings and/or remaining tokens in the log message (e.g., add fields like
"limit_bytes", "burst", or "tokens_remaining" and an explicit "reason" such as
"token_exhausted" vs "oversize_frame"); keep the same close flow that sends to
conn.closeConnCh and calls handleClosure(nil).

In `@sdk/ts/test/unit/abi-drift.test.ts`:
- Around line 112-278: Consolidate the repeated adversarial test cases at the
end of the "contract ABI drift guards" suite into a single table-driven array
and iterate with .forEach(): collect each case's inputs and expected output
(cases currently implemented as individual it(...) blocks that call
diffConsumedFunctions or diffSdkSubsetAgainstManifest), then in the loop call
the appropriate function (diffConsumedFunctions or diffSdkSubsetAgainstManifest)
and assert toEqual(expected); reference the existing identifiers to find code:
the suite name "contract ABI drift guards", the helper functions
diffConsumedFunctions and diffSdkSubsetAgainstManifest, and the individual test
names like 'reports adversarial ChannelHub function signature changes...',
'reports adversarial ERC20 missing consumed functions', and 'reports adversarial
AppRegistry manifest signature changes' to build the case matrix and replace the
separate it(...) blocks with a single data-driven .forEach() test.

In `@sdk/ts/test/unit/app-signing-drift.test.ts`:
- Around line 40-251: The tests repeat many expect(...) assertions for
packCreateAppSessionRequestV1, generateAppSessionIDV1, packAppStateUpdateV1, and
packAppSessionKeyStateV1; collapse these into data-driven tables (arrays of case
objects) and iterate with .forEach() (or test.each) to drive assertions, and
convert the adversarial comparisons into similar case pairs in arrays so each
vector and adversarial check is a single table-driven loop; update references to
AppStateUpdateIntent, packAppStateUpdateV1, packCreateAppSessionRequestV1,
generateAppSessionIDV1, and packAppSessionKeyStateV1 in the new case objects to
locate inputs, and ensure any mocking follows the guideline by using manual
mocks rather than jest.mock() if mocking is needed.

In `@sdk/ts/test/unit/public-api-drift.test.ts`:
- Around line 181-247: Condense the repeated "proves adversarial..." test cases
into a data-driven array and iterate with .forEach to generate the individual
it() tests: collect test descriptors (name, precomputed api via
serializePublicApi(), target member name like
'Client'/'Config'/'packAppStateUpdateV1'/'AppStateUpdateIntent' or expected
predicate, and the specific mutation + assertion), then replace each explicit
it(...) block with a single .forEach that calls expect/mutates accordingly;
reference serializePublicApi(), publicApi and the existing property checks
(e.g., checking properties for 'ping:', kind === 'interface' for 'Config',
signatures for 'packAppStateUpdateV1', members for 'AppStateUpdateIntent', and
adding '__FakeExport') to implement each case, and ensure you follow the
guideline to use manual setup/mocks instead of jest.mock() while keeping
assertions identical.

In `@sdk/ts/test/unit/rpc-drift.test.ts`:
- Around line 49-52: The regex in extractTsClientMethods is brittle because it
requires exactly two leading spaces; update the pattern used in
extractTsClientMethods to allow flexible indentation (e.g. use ^\s+ instead of
^\s{2}) and keep the existing method-capturing groups (still matching optional
"static"/"async" and the identifier) so it robustly finds class methods even if
indentation changes; ensure the new pattern still uses the gm flags and
preserves the return type Set<string> behavior.
🪄 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: 8fd6f7d9-21ae-4218-8e70-8b987ede82f0

📥 Commits

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

⛔ Files ignored due to path filters (3)
  • sdk/ts-compat/test/unit/__snapshots__/public-api-drift.test.ts.snap is excluded by !**/*.snap
  • sdk/ts/package-lock.json is excluded by !**/package-lock.json
  • sdk/ts/test/unit/__snapshots__/public-api-drift.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (105)
  • .github/workflows/main-pr.yml
  • .github/workflows/main-push.yml
  • .github/workflows/protocol-drift-external-smoke.yml
  • .github/workflows/test-sdk.yml
  • 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/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_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/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/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/rate_limits.go
  • nitronode/api/rpc_router.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/20260507000000_add_current_session_key_states.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/store/database/app_session_key_state.go
  • nitronode/store/database/app_session_key_state_test.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/blockchain/evm/channel_hub_reactor.go
  • pkg/blockchain/evm/channel_hub_reactor_test.go
  • pkg/core/event.go
  • pkg/core/interface.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
  • protocol-description.md
  • scripts/check-protocol-drift.sh
  • scripts/drift/generate-app-signing-vectors.go
  • scripts/drift/runtime-smoke.mjs
  • sdk/PROTOCOL_DRIFT_GUARDS.md
  • sdk/ts-compat/package.json
  • sdk/ts-compat/test/unit/client.test.ts
  • sdk/ts-compat/test/unit/public-api-drift.test.ts
  • sdk/ts/package.json
  • sdk/ts/src/blockchain/evm/channel_hub_abi.ts
  • sdk/ts/src/client.ts
  • sdk/ts/src/rpc/api.ts
  • sdk/ts/src/session_key_state_transforms.ts
  • sdk/ts/src/utils.ts
  • sdk/ts/test/unit/abi-drift.test.ts
  • sdk/ts/test/unit/app-signing-drift.test.ts
  • sdk/ts/test/unit/public-api-drift.test.ts
  • sdk/ts/test/unit/rpc-drift.test.ts
  • sdk/ts/test/unit/rpc-dto-drift.test.ts
  • sdk/ts/test/unit/transform-drift.test.ts

Comment thread nitronode/api/app_session_v1/get_last_key_states_test.go
Comment thread nitronode/store/database/current_session_key_state.go
Comment thread pkg/rpc/rate_limiter.go
Comment thread scripts/drift/generate-app-signing-vectors.go
Comment thread sdk/ts/src/session_key_state_transforms.ts
Comment thread sdk/ts/src/utils.ts
@philanton philanton changed the base branch from main to fix/audit-findings-final May 12, 2026 11:51
Copy link
Copy Markdown
Contributor

@nksazonov nksazonov left a comment

Choose a reason for hiding this comment

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

Clean approach to bounding metric cardinality. Two things need attention before this lands.

Field naming: rpcConnectionsTotal *prometheus.GaugeVec (line 180 of exporter.go) names a gauge as if it were a counter — minor but confusing. Consider renaming to rpcConnectionsActive alongside this change.

Comment thread pkg/rpc/connection_hub.go Outdated
Comment thread pkg/rpc/connection_hub.go Outdated
Comment thread nitronode/metrics/exporter.go
ConnectionHub.Add and Remove invoked observeConnections (-> Prometheus
SetRPCConnections, which takes metric-internal mutexes) while still
holding hub.mu, so all readers — including Publish — serialized behind
Prometheus writes during connection churn. Snapshot the post-mutation
count, release hub.mu, then call the observer outside the lock.

Also gate Remove's observer call on whether the bucket actually changed,
so map-miss / already-zero paths no longer emit a spurious
DeleteLabelValues for an app_id the gauge never tracked.

Annotate the rpc_connections_active Help with the remaining cardinality
bound: the format regex on application_id plus disconnect-driven series
shedding — no registry check and no per-app connection cap.

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

@nksazonov nksazonov left a comment

Choose a reason for hiding this comment

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

Well done!

@philanton philanton merged commit aaac790 into fix/audit-findings-final May 13, 2026
3 checks passed
@philanton philanton deleted the fix/nitronode-mf-m08 branch May 13, 2026 13:25
nksazonov pushed a commit that referenced this pull request May 13, 2026
- MF-L01: fix(contracts/ChannelHub): cap ERC20 transfer returndata copy to 32 bytes (#726)                                                
- MF-H01: fix(nitronode): paginate get_last_key_states endpoints (#724)                                                                   
- MF-I01-I02: fix(contracts): address security audit findings I-01 and I-02 (#728)                                                        
- MF-C01: rpc: cap inbound WebSocket frame size and rate-limit per connection (#723)                                                      
- MF-L02: docs(protocol): qualify enforcement guarantee for intent-specific execution paths (#737)                                        
- MF-L02-I03-I04_I05: fix(contracts): add more Node trust assumptions and requirements (#738)                                             
- MF-M01: backfill state user_sig from on-chain events (#731)               
- MF-M02: fix(rpc): release Serve wait group on processSink overflow (#732)                                                               
- Fix SDK acknowledgement before home channel creation (#734)               
- MF-I06: fix(nitronode): gate escrow transitions on home channel onchain materialization (#730)                                          
- MF-M05: fix(nitronode): enforce TLS by default for Postgres (#733)                                                                      
- MF-M07: Unblock receiver states after finalized escrow operations (#735)
- MF-M04: feat: provide tooling for and enhance docs on ValidatorRegistered event (#744)                                                  
- MF-L04: fix(contracts): reject redundant native value (#741)              
- MF-H02: bind session key registration to a single owner per kind (#739)                                                                 
- MF-I07: fix(contracts): enforce max challenge duration (#752)             
- MF-M08: fix(rpc): replace Origin label with application_id on connection gauge (#745)                                                   
- MF-C02: fix(core): add ChannelStatusClosing to gate post-finalize state transitions (#746)                                              
- MF-L06: fix(contracts): clear stale challengeExpireAt on cooperative escrow finalization (#754)
- MF-I08: docs: document ChannelClosed event orientation ambiguity during abandoned migration (#755)                                      
- MF-M09: fix(nitronode): auto-challenge home channel on withheld escrow finalize (#753)
- MF-L09: fix(nitronode): validate parsed app session nonce (#751)                                                                        
- MF-L05: docs(contracts): document informational events not guaranteed to emit (#756)
- MF-L08: fix(nitronode/api): default get_last_key_states to active-only with include_inactive opt-in (#749)                              
- MF-L10: fix: emit escrowIds array in EscrowDepositsPurged event and handle it in Nitronode (#757)
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.

2 participants