Skip to content

YNU-812: Introduce Gated Actions (WIP)#601

Open
dimast-x wants to merge 1 commit intomainfrom
feat/staking-integration
Open

YNU-812: Introduce Gated Actions (WIP)#601
dimast-x wants to merge 1 commit intomainfrom
feat/staking-integration

Conversation

@dimast-x
Copy link
Contributor

@dimast-x dimast-x commented Mar 3, 2026

Summary by CodeRabbit

  • New Features

    • Added user staking functionality to track token amounts per blockchain.
    • Introduced action logging system to record user interactions.
    • Implemented gated action authorization framework with configurable limits per action.
  • Refactor

    • Replaced session-key based access control with new gated action system.

Co-authored-by: Anton Filonenko <philanton@users.noreply.github.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

The PR replaces session-key based access control with a gated-action system, introducing user staking tracking and action logging for rate limiting. New database tables store user stakes and action logs, while an in-memory action limits store configured via YAML computes access allowances based on staked token amounts.

Changes

Cohort / File(s) Summary
Core Type System
pkg/core/types.go
Replaced SessionKey and AssetAllowance types with new GatedAction string type with predefined constants (Transfer, AppSessionCreation, AppSessionOperation, AppSessionDeposit, AppSessionWithdrawal). Added ID() method mapping each action to a numeric identifier.
Database Schema
clearnode/config/migrations/postgres/20251222000000_initial_schema.sql
Modified app_ledger_v1 id column from CHAR(36) to UUID with auto-generation. Added user_staked_v1 table with composite key for per-blockchain user stakes. Added action_log_v1 table for action tracking with indexed wallet/method/path queries.
Database Models
clearnode/store/database/user_staked.go, clearnode/store/database/action_log.go
Introduced UserStakedV1 and ActionLogEntryV1 GORM models with repository methods: UpdateUserStaked (upsert by wallet/blockchain), GetTotalUserStaked, RecordAction, and GetUserActionCount for rate-limit windowed queries.
Database Interface & Setup
clearnode/store/database/interface.go, clearnode/store/database/database.go, clearnode/store/database/testing.go
Extended DatabaseStore interface with four new public methods. Updated migration calls in both SQLite and PostgreSQL test/production paths to include the two new models.
Action Limits Store
clearnode/store/memory/action_limits_store.go
New in-memory action limits store with YAML configuration. Added ActionLimitConfig and ActionGateConfig types. Implemented StakedToAppCount, StakedTo24hActionsAllowance, and StakedTo24hActions methods to compute access allowances based on staked token tiers.

Sequence Diagram

sequenceDiagram
    participant User as User/App
    participant DB as Database
    participant Limits as ActionLimitsStore
    participant Config as YAML Config

    Config->>Limits: Load action limits config<br/>(LevelStepTokens, AppCost,<br/>ActionGates)
    
    User->>DB: UpdateUserStaked(wallet,<br/>blockchainID, amount)
    DB->>DB: Upsert user_staked_v1
    
    User->>DB: GetTotalUserStaked(wallet)
    DB->>DB: SELECT SUM(amount)<br/>WHERE user_wallet
    DB-->>User: totalStaked
    
    User->>Limits: StakedToAppCount(totalStaked)
    Limits->>Limits: Compute: totalStaked / AppCost
    Limits-->>User: maxAppCount
    
    User->>DB: RecordAction(wallet, gatedAction)
    DB->>DB: INSERT INTO action_log_v1
    
    User->>DB: GetUserActionCount(wallet,<br/>gatedAction, 24h window)
    DB->>DB: SELECT COUNT(*)<br/>WHERE created_at >= now-24h
    DB-->>User: actionCount
    
    User->>Limits: StakedTo24hActionsAllowance<br/>(gatedAction, totalStaked)
    Limits->>Limits: Compute level from staked<br/>allowance = free + (level × increase)
    Limits-->>User: maxAllowed
    
    User->>User: Verify actionCount ≤ maxAllowed
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A gated garden of actions bright,
Where staking wallets find their might!
With limits locked and logs that gleam,
We rate-protect the user's dream. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: introducing gated actions as a new access control system throughout the codebase.

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

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

Tip

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


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

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

Inline comments:
In `@clearnode/config/migrations/postgres/20251222000000_initial_schema.sql`:
- Around line 287-288: The DOWN migration currently drops
idx_action_log_v1_wallet_method_created and
idx_action_log_v1_wallet_method_path_created but does not match the index name
introduced by the UP migration; update the DROP INDEX statements so they exactly
match the renamed index(s) created in the UP migration (replace
idx_action_log_v1_wallet_method_created with the corrected index name used in
the UP migration and ensure idx_action_log_v1_wallet_method_path_created matches
exactly), so rollback will remove the new index created by the schema fix.
- Around line 275-289: The DB schema for action_log_v1 uses columns method and
path but the Go model and queries expect gated_action (see
clearnode/store/database/action_log.go), causing undefined-column errors; update
the CREATE TABLE to replace method SMALLINT and path SMALLINT with a single
gated_action SMALLINT NOT NULL (or the exact type used in the Go model), then
update the indexes (e.g., idx_action_log_v1_wallet_method_path_created and
idx_action_log_v1_wallet_method_created) to reference gated_action (rename
indexes if desired) and adjust the Down section to DROP the updated index names
and table accordingly so the SQL matches the Go model.

In `@clearnode/store/database/action_log.go`:
- Around line 24-31: Reject invalid gated action IDs and non-positive windows in
DBStore.RecordAction: check gatedAction.ID() and the action window (e.g.,
gatedAction.Window()) before constructing ActionLogEntryV1 or performing any DB
queries/writes; if gatedAction.ID() == 0 or the window <= 0 return a descriptive
error immediately (do not proceed to create ActionLogEntryV1 or call DB methods)
so unknown actions and non-positive windows cannot silently distort counts.

In `@clearnode/store/database/interface.go`:
- Around line 211-213: Update the stale doc comment for GetUserActionCount to
match its signature: replace references to "method" and "path" with
"gatedAction" and clearly state that the function counts actions for the given
wallet and gatedAction within the provided time window (time.Duration), and
returns the number of matching actions and an error; ensure the comment sits
immediately above the GetUserActionCount declaration so it accurately documents
the wallet, gatedAction, and window parameters.

In `@clearnode/store/database/user_staked.go`:
- Around line 25-33: Validate inputs at the start of DBStore.UpdateUserStaked:
ensure the wallet (after strings.ToLower) is not empty, blockchainID is > 0, and
amount is not negative (use decimal's IsNegative or equivalent); if any check
fails return a descriptive error before constructing UserStakedV1 or performing
the upsert so invalid wallets, zero chain IDs, or negative amounts cannot be
written.
- Around line 13-15: The struct fields UserWallet and BlockchainID in
user_staked.go are missing gorm `primaryKey` tags, so AutoMigrate won't create
the composite primary key required by the PostgreSQL migration and the
OnConflict upsert (lines ~37-40) may target the wrong constraint; update the
struct tag for UserWallet (`gorm:"column:user_wallet;not null;primaryKey"`) and
for BlockchainID (`gorm:"column:blockchain_id;not null;primaryKey"`) so GORM
recognizes the composite primary key during AutoMigrate and keeps behavior
consistent with the UserBalance pattern.

In `@clearnode/store/memory/action_limits_store.go`:
- Around line 31-33: When config loading/decoding fails in the ActionLimitsStore
creation path, the code currently returns a non-nil empty &ActionLimitsStore{}
with an error; change those error returns to return nil, err so callers can't
mistakenly use a non-nil-but-invalid store. Update the return sites referenced
in action_limits_store.go (the error branch after the decode/load failures
around the current returns at lines returning &ActionLimitsStore{}) to return
nil, err, ensuring functions that construct or load ActionLimitsStore (e.g., the
constructor/loader that decodes config into ActionLimitsStore) propagate nil on
error.
- Around line 44-52: NewActionLimitsStoreFromConfig validates config but returns
an empty ActionLimitsStore, leaving the validated config unused; update the
constructor (NewActionLimitsStoreFromConfig) to populate the store's config
field (e.g., set the ActionLimitsStore.config or equivalent struct fields) with
the validated ActionLimitConfig so subsequent methods (that read
LevelStepTokens, AppCost, etc.) use the correct values and avoid zero-value
panics; ensure any other internal derived fields are initialized as needed
before returning the store.
- Around line 55-57: The StakedToAppCount method can return a huge uint if
stakedYellowTokens.Div(a.config.AppCost).IntPart() is negative; update
ActionLimitsStore.StakedToAppCount to mirror the defensive checks in
StakedTo24hActionsAllowance and StakedTo24hActions: compute the int64 value from
stakedYellowTokens.Div(a.config.AppCost).IntPart(), if it is negative return 0,
otherwise cast to uint and return it, referencing the same a.config.AppCost and
the StakedToAppCount method name so the change is localized and consistent.

In `@pkg/core/types.go`:
- Around line 912-915: The constants AppSessionCreation and AppSessionDeposit
currently share the same string value causing ID() to misclassify actions;
change AppSessionCreation to a distinct value (e.g., "app_session_creation") and
audit the neighboring GatedAction constants (the block around
AppSessionCreation/AppSessionOperation/AppSessionDeposit/AppSessionWithdrawal
and lines 919-930) to ensure each constant has a unique string literal; after
updating, confirm the ID() switch/lookup (the function that maps GatedAction
values to IDs) covers the corrected symbols so creation and deposit are
unambiguous.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc528c9 and 9edb22d.

📒 Files selected for processing (8)
  • clearnode/config/migrations/postgres/20251222000000_initial_schema.sql
  • clearnode/store/database/action_log.go
  • clearnode/store/database/database.go
  • clearnode/store/database/interface.go
  • clearnode/store/database/testing.go
  • clearnode/store/database/user_staked.go
  • clearnode/store/memory/action_limits_store.go
  • pkg/core/types.go

Comment on lines +275 to +289
CREATE TABLE action_log_v1 (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_wallet CHAR(42) NOT NULL,
method SMALLINT NOT NULL,
path SMALLINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_action_log_v1_wallet_method_path_created ON action_log_v1(user_wallet, method, path, created_at DESC);
CREATE INDEX idx_action_log_v1_wallet_method_created ON action_log_v1(user_wallet, method, created_at DESC);

-- +goose Down
DROP INDEX IF EXISTS idx_action_log_v1_wallet_method_created;
DROP INDEX IF EXISTS idx_action_log_v1_wallet_method_path_created;
DROP TABLE IF EXISTS action_log_v1;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Schema mismatch: action_log_v1 columns do not match the Go model

Lines 278-279 define method/path, but the DB model and queries use gated_action (clearnode/store/database/action_log.go, Line 15 and Line 48). This will fail at runtime on Postgres with undefined-column errors.

💡 Proposed schema fix
 CREATE TABLE action_log_v1 (
     id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
     user_wallet CHAR(42) NOT NULL,
-    method SMALLINT NOT NULL,
-    path SMALLINT NOT NULL,
+    gated_action SMALLINT NOT NULL,
     created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
 );

-CREATE INDEX idx_action_log_v1_wallet_method_path_created ON action_log_v1(user_wallet, method, path, created_at DESC);
-CREATE INDEX idx_action_log_v1_wallet_method_created ON action_log_v1(user_wallet, method, created_at DESC);
+CREATE INDEX idx_action_log_v1_wallet_action_created ON action_log_v1(user_wallet, gated_action, created_at DESC);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/config/migrations/postgres/20251222000000_initial_schema.sql`
around lines 275 - 289, The DB schema for action_log_v1 uses columns method and
path but the Go model and queries expect gated_action (see
clearnode/store/database/action_log.go), causing undefined-column errors; update
the CREATE TABLE to replace method SMALLINT and path SMALLINT with a single
gated_action SMALLINT NOT NULL (or the exact type used in the Go model), then
update the indexes (e.g., idx_action_log_v1_wallet_method_path_created and
idx_action_log_v1_wallet_method_created) to reference gated_action (rename
indexes if desired) and adjust the Down section to DROP the updated index names
and table accordingly so the SQL matches the Go model.

Comment on lines +287 to +288
DROP INDEX IF EXISTS idx_action_log_v1_wallet_method_created;
DROP INDEX IF EXISTS idx_action_log_v1_wallet_method_path_created;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Down migration index names must match the corrected action-log index

If you apply the schema fix above, Lines 287-288 should drop the renamed index; otherwise rollback will fail to clean up the new index.

💡 Proposed down-migration alignment
-DROP INDEX IF EXISTS idx_action_log_v1_wallet_method_created;
-DROP INDEX IF EXISTS idx_action_log_v1_wallet_method_path_created;
+DROP INDEX IF EXISTS idx_action_log_v1_wallet_action_created;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DROP INDEX IF EXISTS idx_action_log_v1_wallet_method_created;
DROP INDEX IF EXISTS idx_action_log_v1_wallet_method_path_created;
DROP INDEX IF EXISTS idx_action_log_v1_wallet_action_created;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/config/migrations/postgres/20251222000000_initial_schema.sql`
around lines 287 - 288, The DOWN migration currently drops
idx_action_log_v1_wallet_method_created and
idx_action_log_v1_wallet_method_path_created but does not match the index name
introduced by the UP migration; update the DROP INDEX statements so they exactly
match the renamed index(s) created in the UP migration (replace
idx_action_log_v1_wallet_method_created with the corrected index name used in
the UP migration and ensure idx_action_log_v1_wallet_method_path_created matches
exactly), so rollback will remove the new index created by the schema fix.

Comment on lines +24 to +31
func (s *DBStore) RecordAction(wallet string, gatedAction core.GatedAction) error {
wallet = strings.ToLower(wallet)

entry := ActionLogEntryV1{
ID: uuid.New(),
UserWallet: wallet,
GatedAction: gatedAction.ID(),
CreatedAt: time.Now(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject unknown action IDs and non-positive windows before querying/writing

Line 30/48 accepts gatedAction.ID()==0 (unknown action), and Line 45 accepts zero/negative windows. Both can silently distort action counts.

💡 Proposed fix
 func (s *DBStore) RecordAction(wallet string, gatedAction core.GatedAction) error {
 	wallet = strings.ToLower(wallet)
+	actionID := gatedAction.ID()
+	if actionID == 0 {
+		return fmt.Errorf("unknown gated action: %s", gatedAction)
+	}

 	entry := ActionLogEntryV1{
 		ID:          uuid.New(),
 		UserWallet:  wallet,
-		GatedAction: gatedAction.ID(),
+		GatedAction: actionID,
 		CreatedAt:   time.Now(),
 	}
@@
 func (s *DBStore) GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error) {
+	if window <= 0 {
+		return 0, fmt.Errorf("window must be positive")
+	}
 	wallet = strings.ToLower(wallet)
+	actionID := gatedAction.ID()
+	if actionID == 0 {
+		return 0, fmt.Errorf("unknown gated action: %s", gatedAction)
+	}
 	since := time.Now().Add(-window)

 	query := s.db.Model(&ActionLogEntryV1{}).
-		Where("user_wallet = ? AND gated_action = ? AND created_at >= ?", wallet, gatedAction.ID(), since)
+		Where("user_wallet = ? AND gated_action = ? AND created_at >= ?", wallet, actionID, since)

Also applies to: 43-49

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

In `@clearnode/store/database/action_log.go` around lines 24 - 31, Reject invalid
gated action IDs and non-positive windows in DBStore.RecordAction: check
gatedAction.ID() and the action window (e.g., gatedAction.Window()) before
constructing ActionLogEntryV1 or performing any DB queries/writes; if
gatedAction.ID() == 0 or the window <= 0 return a descriptive error immediately
(do not proceed to create ActionLogEntryV1 or call DB methods) so unknown
actions and non-positive windows cannot silently distort counts.

Comment on lines +211 to +213
// GetUserActionCount returns the number of actions matching the given wallet, method, and path
// within the specified time window.
GetUserActionCount(wallet string, gatedAction core.GatedAction, window time.Duration) (uint64, error)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Stale comment: refers to method/path but signature uses gatedAction

Line 211-212 no longer matches the method contract at Line 213. Please update wording to avoid confusion.

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

In `@clearnode/store/database/interface.go` around lines 211 - 213, Update the
stale doc comment for GetUserActionCount to match its signature: replace
references to "method" and "path" with "gatedAction" and clearly state that the
function counts actions for the given wallet and gatedAction within the provided
time window (time.Duration), and returns the number of matching actions and an
error; ensure the comment sits immediately above the GetUserActionCount
declaration so it accurately documents the wallet, gatedAction, and window
parameters.

Comment on lines +13 to +15
UserWallet string `gorm:"column:user_wallet;not null"`
BlockchainID uint64 `gorm:"column:blockchain_id;not null"`
Amount decimal.Decimal `gorm:"column:amount;type:varchar(78);not null"`
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "user_staked.go" -type f

Repository: layer-3/nitrolite

Length of output: 102


🏁 Script executed:

cat -n ./clearnode/store/database/user_staked.go

Repository: layer-3/nitrolite

Length of output: 2230


🏁 Script executed:

rg -t go "UserStakedV1" --context 5

Repository: layer-3/nitrolite

Length of output: 5538


🌐 Web query:

GORM OnConflict Columns primaryKey tag required

💡 Result:

In GORM, clause.OnConflict.Columns is not tied to the primaryKey tag—it’s simply the list of database columns that form the conflict target (must correspond to a PRIMARY KEY or UNIQUE constraint in your DB). (gorm.io)

When is primaryKey tag required?

  • Not required if you use GORM’s default convention: a field named ID is treated as the primary key automatically (or you embed gorm.Model, which already defines ID as primaryKey). (gorm.io)
  • Required if your primary key field is not the default ID (or you have a composite primary key). You must mark it with gorm:"primaryKey" so GORM knows which fields are primary keys. (gorm.io)

Do you have to set Columns?

  • If you explicitly set Columns, you don’t need primaryKey for that purpose—you’re telling GORM the conflict target directly (e.g., Columns: []clause.Column{{Name: "email"}}). (gorm.io)
  • If you don’t set Columns and rely on UpdateAll: true, GORM uses the model’s primary key fields as the conflict target—so in that case, having the correct primaryKey definition matters for non-ID keys. (blog.51cto.com)

Minimal examples

Default ID PK (no primaryKey tag needed):

type User struct {
  ID   uint
  Name string
}

db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&u)

(gorm.io)

Custom PK (tag required if you rely on PK inference):

type User struct {
  UUID string `gorm:"primaryKey"`
  Name string
}

db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&u)

(gorm.io)

Citations:


🏁 Script executed:

rg -t go "user_staked_v1" -i --context 3

Repository: layer-3/nitrolite

Length of output: 495


🏁 Script executed:

rg -t go "primaryKey" --context 2

Repository: layer-3/nitrolite

Length of output: 7933


🏁 Script executed:

rg -t go "UserStakedV1" -A 20 | grep -E "(uniqueIndex|unique|index)"

Repository: layer-3/nitrolite

Length of output: 43


🏁 Script executed:

find . -name "*.go" -type f -exec grep -l "user_staked" {} \; | head -20

Repository: layer-3/nitrolite

Length of output: 102


🏁 Script executed:

rg -t go "user_staked_v1" -i -B 5 -A 10 | grep -E "(UNIQUE|PRIMARY|Constraint|Index)"

Repository: layer-3/nitrolite

Length of output: 43


🏁 Script executed:

rg -t go "constraint|UNIQUE|PRIMARY" clearnode/store/database/ --context 2

Repository: layer-3/nitrolite

Length of output: 43


🏁 Script executed:

find . -name "*migrat*" -type f | head -10

Repository: layer-3/nitrolite

Length of output: 234


🏁 Script executed:

cat -n ./clearnode/store/database/database.go | head -100

Repository: layer-3/nitrolite

Length of output: 4242


🏁 Script executed:

cat -n ./clearnode/store/database/database.go | tail -100

Repository: layer-3/nitrolite

Length of output: 3397


🏁 Script executed:

find . -path "*/migrations/postgres*" -name "*.sql" | head -20

Repository: layer-3/nitrolite

Length of output: 133


🏁 Script executed:

cat -n ./clearnode/config/migrations/postgres/20251222000000_initial_schema.sql | grep -A 20 "user_staked"

Repository: layer-3/nitrolite

Length of output: 2661


Add primaryKey tags to composite key fields for correct AutoMigrate behavior

Lines 13-14 don't declare primaryKey, but the PostgreSQL migration explicitly creates a composite primary key on (user_wallet, blockchain_id). This causes a schema mismatch in SQLite, which relies on AutoMigrate to infer the primary key from struct tags. Without the tags, AutoMigrate won't create the composite key constraint, causing the OnConflict upsert at line 37-40 to fail silently or target an incorrect constraint.

The codebase pattern (e.g., UserBalance in state.go) uses primaryKey tags for composite keys. This fix ensures consistency and makes the struct definition self-contained.

Proposed fix
 type UserStakedV1 struct {
-	UserWallet   string          `gorm:"column:user_wallet;not null"`
-	BlockchainID uint64          `gorm:"column:blockchain_id;not null"`
+	UserWallet   string          `gorm:"column:user_wallet;not null;primaryKey"`
+	BlockchainID uint64          `gorm:"column:blockchain_id;not null;primaryKey"`
 	Amount       decimal.Decimal `gorm:"column:amount;type:varchar(78);not null"`
 	CreatedAt    time.Time
 	UpdatedAt    time.Time
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
UserWallet string `gorm:"column:user_wallet;not null"`
BlockchainID uint64 `gorm:"column:blockchain_id;not null"`
Amount decimal.Decimal `gorm:"column:amount;type:varchar(78);not null"`
type UserStakedV1 struct {
UserWallet string `gorm:"column:user_wallet;not null;primaryKey"`
BlockchainID uint64 `gorm:"column:blockchain_id;not null;primaryKey"`
Amount decimal.Decimal `gorm:"column:amount;type:varchar(78);not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/store/database/user_staked.go` around lines 13 - 15, The struct
fields UserWallet and BlockchainID in user_staked.go are missing gorm
`primaryKey` tags, so AutoMigrate won't create the composite primary key
required by the PostgreSQL migration and the OnConflict upsert (lines ~37-40)
may target the wrong constraint; update the struct tag for UserWallet
(`gorm:"column:user_wallet;not null;primaryKey"`) and for BlockchainID
(`gorm:"column:blockchain_id;not null;primaryKey"`) so GORM recognizes the
composite primary key during AutoMigrate and keeps behavior consistent with the
UserBalance pattern.

Comment on lines +25 to +33
func (s *DBStore) UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error {
wallet = strings.ToLower(wallet)
now := time.Now()

record := UserStakedV1{
UserWallet: wallet,
BlockchainID: blockchainID,
Amount: amount,
CreatedAt: now,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate wallet, chain ID, and amount before upsert

Line 25 currently accepts empty wallet, zero blockchainID, and negative stake values. This can corrupt stake totals and downstream gating calculations.

💡 Proposed fix
 func (s *DBStore) UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error {
+	if strings.TrimSpace(wallet) == "" {
+		return fmt.Errorf("wallet cannot be empty")
+	}
+	if blockchainID == 0 {
+		return fmt.Errorf("blockchain ID cannot be 0")
+	}
+	if amount.IsNegative() {
+		return fmt.Errorf("staked amount cannot be negative")
+	}
 	wallet = strings.ToLower(wallet)
 	now := time.Now()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *DBStore) UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error {
wallet = strings.ToLower(wallet)
now := time.Now()
record := UserStakedV1{
UserWallet: wallet,
BlockchainID: blockchainID,
Amount: amount,
CreatedAt: now,
func (s *DBStore) UpdateUserStaked(wallet string, blockchainID uint64, amount decimal.Decimal) error {
if strings.TrimSpace(wallet) == "" {
return fmt.Errorf("wallet cannot be empty")
}
if blockchainID == 0 {
return fmt.Errorf("blockchain ID cannot be 0")
}
if amount.IsNegative() {
return fmt.Errorf("staked amount cannot be negative")
}
wallet = strings.ToLower(wallet)
now := time.Now()
record := UserStakedV1{
UserWallet: wallet,
BlockchainID: blockchainID,
Amount: amount,
CreatedAt: now,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/store/database/user_staked.go` around lines 25 - 33, Validate
inputs at the start of DBStore.UpdateUserStaked: ensure the wallet (after
strings.ToLower) is not empty, blockchainID is > 0, and amount is not negative
(use decimal's IsNegative or equivalent); if any check fails return a
descriptive error before constructing UserStakedV1 or performing the upsert so
invalid wallets, zero chain IDs, or negative amounts cannot be written.

Comment on lines +31 to +33
if err != nil {
return &ActionLimitsStore{}, err
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Return nil store when loading/decoding config fails

Lines 32 and 38 currently return a non-nil empty store with an error. That makes misuse easier in callers that check only for nil.

💡 Proposed fix
 	if err != nil {
-		return &ActionLimitsStore{}, err
+		return nil, err
 	}
@@
 	if err := yaml.NewDecoder(f).Decode(&cfg); err != nil {
-		return &ActionLimitsStore{}, err
+		return nil, err
 	}

Also applies to: 37-39

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

In `@clearnode/store/memory/action_limits_store.go` around lines 31 - 33, When
config loading/decoding fails in the ActionLimitsStore creation path, the code
currently returns a non-nil empty &ActionLimitsStore{} with an error; change
those error returns to return nil, err so callers can't mistakenly use a
non-nil-but-invalid store. Update the return sites referenced in
action_limits_store.go (the error branch after the decode/load failures around
the current returns at lines returning &ActionLimitsStore{}) to return nil, err,
ensuring functions that construct or load ActionLimitsStore (e.g., the
constructor/loader that decodes config into ActionLimitsStore) propagate nil on
error.

Comment on lines +44 to +52
func NewActionLimitsStoreFromConfig(config ActionLimitConfig) (*ActionLimitsStore, error) {
if config.LevelStepTokens.IsZero() {
return nil, errors.New("LevelStepTokens cannot be 0")
}
if config.AppCost.IsZero() {
return nil, errors.New("AppCost cannot be 0")
}
return &ActionLimitsStore{}, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Constructor returns an unusable store (config is never assigned)

Line 51 returns &ActionLimitsStore{} instead of storing validated config, so later methods read zero values and can panic or produce wrong limits.

💡 Proposed fix
 func NewActionLimitsStoreFromConfig(config ActionLimitConfig) (*ActionLimitsStore, error) {
-	if config.LevelStepTokens.IsZero() {
+	if !config.LevelStepTokens.GreaterThan(decimal.Zero) {
 		return nil, errors.New("LevelStepTokens cannot be 0")
 	}
-	if config.AppCost.IsZero() {
+	if !config.AppCost.GreaterThan(decimal.Zero) {
 		return nil, errors.New("AppCost cannot be 0")
 	}
-	return &ActionLimitsStore{}, nil
+	return &ActionLimitsStore{config: config}, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/store/memory/action_limits_store.go` around lines 44 - 52,
NewActionLimitsStoreFromConfig validates config but returns an empty
ActionLimitsStore, leaving the validated config unused; update the constructor
(NewActionLimitsStoreFromConfig) to populate the store's config field (e.g., set
the ActionLimitsStore.config or equivalent struct fields) with the validated
ActionLimitConfig so subsequent methods (that read LevelStepTokens, AppCost,
etc.) use the correct values and avoid zero-value panics; ensure any other
internal derived fields are initialized as needed before returning the store.

Comment on lines +55 to +57
func (a *ActionLimitsStore) StakedToAppCount(stakedYellowTokens decimal.Decimal) uint {
return uint(stakedYellowTokens.Div(a.config.AppCost).IntPart())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file in question
find . -name "action_limits_store.go" -type f

Repository: layer-3/nitrolite

Length of output: 108


🏁 Script executed:

# Check the structure of the ActionLimitsStore and surrounding context
cat -n clearnode/store/memory/action_limits_store.go

Repository: layer-3/nitrolite

Length of output: 3602


🏁 Script executed:

# Search for callers of StakedToAppCount to understand how stakedYellowTokens is provided
rg "StakedToAppCount" --type go

Repository: layer-3/nitrolite

Length of output: 345


🏁 Script executed:

# Check the decimal library being used and its IntPart() behavior
rg "decimal\." clearnode/store/memory/action_limits_store.go -A 2 -B 2

Repository: layer-3/nitrolite

Length of output: 1807


🏁 Script executed:

# Look for validation of stakedYellowTokens before this method is called
rg "stakedYellowTokens|StakedToAppCount" --type go -B 5 -A 5 | head -100

Repository: layer-3/nitrolite

Length of output: 3389


🏁 Script executed:

# Search for all calls to StakedToAppCount
rg "StakedToAppCount" --type go -B 3 -A 3

Repository: layer-3/nitrolite

Length of output: 726


🏁 Script executed:

# Look for where stakes/balances come from - check imports and broader context
find . -name "*.go" -type f | xargs grep -l "StakedToAppCount" | head -5

Repository: layer-3/nitrolite

Length of output: 108


🏁 Script executed:

# Search across all files for StakedToAppCount
rg "StakedToAppCount" --type go -r

Repository: layer-3/nitrolite

Length of output: 474


🏁 Script executed:

# Check the entire codebase structure to understand what uses ActionLimitsStore
rg "ActionLimitsStore" --type go -B 2 -A 2

Repository: layer-3/nitrolite

Length of output: 3692


🏁 Script executed:

# Search for callers of StakedToAppCount
rg "StakedToAppCount" --type go

Repository: layer-3/nitrolite

Length of output: 345


🏁 Script executed:

# Search for files that might use ActionLimitsStore
find . -name "*.go" -type f | xargs grep -l "StakedToAppCount\|ActionLimitsStore" | head -20

Repository: layer-3/nitrolite

Length of output: 108


🏁 Script executed:

# Search more broadly without restricting to go files
rg "StakedToAppCount" --no-ignore

Repository: layer-3/nitrolite

Length of output: 345


🏁 Script executed:

# Check if this is a new method or if there are tests for it
find . -name "*.go" -type f -exec grep -l "test\|Test" {} \; | xargs grep -l "ActionLimitsStore\|StakedToAppCount" 2>/dev/null

Repository: layer-3/nitrolite

Length of output: 45


🏁 Script executed:

# Let's understand the structure and find related tests
find . -type f -name "*test*.go" | head -10

Repository: layer-3/nitrolite

Length of output: 546


🏁 Script executed:

# Search for action_limits tests
find . -name "*action_limits*test*.go" -o -name "*test*.go" | xargs grep -l "ActionLimitsStore\|StakedToAppCount" 2>/dev/null

Repository: layer-3/nitrolite

Length of output: 45


🏁 Script executed:

# Let's search the entire codebase for any reference to the method
grep -r "StakedToAppCount" . 2>/dev/null

Repository: layer-3/nitrolite

Length of output: 349


🏁 Script executed:

# Let's also check what the shopspring/decimal library does with negative values and IntPart()
# First understand the issue better - in Go, what happens when casting negative int64 to uint?
cat > /tmp/test_cast.go << 'EOF'
package main
import "fmt"

func main() {
	var negInt int64 = -1
	result := uint(negInt)
	fmt.Printf("int64(-1) cast to uint: %d\n", result)
	fmt.Printf("That's MaxUint64: %d\n", ^uint(0))
}
EOF

Repository: layer-3/nitrolite

Length of output: 43


🏁 Script executed:

# Check if decimal.Decimal can be negative by looking at how it's used in stake context
rg "stakedYellowTokens\|StakedToAppCount\|YellowToken" --type go -B 2 -A 2 | head -80

Repository: layer-3/nitrolite

Length of output: 43


🏁 Script executed:

# Look for where this method might be called in the API layer
find . -name "*.go" -type f | xargs grep -l "AppCount\|app.*count" 2>/dev/null | head -5

Repository: layer-3/nitrolite

Length of output: 255


🏁 Script executed:

# Let's examine the metrics/exporter.go and other files that reference app count
cat -n ./clearnode/metrics/exporter.go | head -50

Repository: layer-3/nitrolite

Length of output: 1671


🏁 Script executed:

# Search for database interface to see if StakedToAppCount is used
rg "StakedToAppCount" ./clearnode/store/database/

Repository: layer-3/nitrolite

Length of output: 43


🏁 Script executed:

# Check the database interface
head -100 ./clearnode/store/database/interface.go

Repository: layer-3/nitrolite

Length of output: 4977


Add guard against negative stake to prevent uint wraparound

The StakedToAppCount method lacks the negative value check present in the two similar methods (StakedTo24hActionsAllowance at line 66 and StakedTo24hActions at line 78). In Go, casting a negative int64 to uint wraps to a huge value. This method should follow the same defensive pattern:

Proposed fix
 func (a *ActionLimitsStore) StakedToAppCount(stakedYellowTokens decimal.Decimal) uint {
+	if !stakedYellowTokens.GreaterThan(decimal.Zero) {
+		return 0
+	}
 	return uint(stakedYellowTokens.Div(a.config.AppCost).IntPart())
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (a *ActionLimitsStore) StakedToAppCount(stakedYellowTokens decimal.Decimal) uint {
return uint(stakedYellowTokens.Div(a.config.AppCost).IntPart())
}
func (a *ActionLimitsStore) StakedToAppCount(stakedYellowTokens decimal.Decimal) uint {
if !stakedYellowTokens.GreaterThan(decimal.Zero) {
return 0
}
return uint(stakedYellowTokens.Div(a.config.AppCost).IntPart())
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/store/memory/action_limits_store.go` around lines 55 - 57, The
StakedToAppCount method can return a huge uint if
stakedYellowTokens.Div(a.config.AppCost).IntPart() is negative; update
ActionLimitsStore.StakedToAppCount to mirror the defensive checks in
StakedTo24hActionsAllowance and StakedTo24hActions: compute the int64 value from
stakedYellowTokens.Div(a.config.AppCost).IntPart(), if it is negative return 0,
otherwise cast to uint and return it, referencing the same a.config.AppCost and
the StakedToAppCount method name so the change is localized and consistent.

Comment on lines +912 to +915
AppSessionCreation GatedAction = "app_session_deposit"
AppSessionOperation GatedAction = "app_session_operation"
AppSessionDeposit GatedAction = "app_session_deposit"
AppSessionWithdrawal GatedAction = "app_session_withdrawal"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

AppSessionCreation collides with AppSessionDeposit, breaking action ID mapping

Line 912 sets AppSessionCreation to "app_session_deposit". In ID() this makes creation/deposit ambiguous, so one action is misclassified.

💡 Proposed fix
-var (
+const (
 	Transfer GatedAction = "transfer"

-	AppSessionCreation   GatedAction = "app_session_deposit"
+	AppSessionCreation   GatedAction = "app_session_creation"
 	AppSessionOperation  GatedAction = "app_session_operation"
 	AppSessionDeposit    GatedAction = "app_session_deposit"
 	AppSessionWithdrawal GatedAction = "app_session_withdrawal"
 )

Also applies to: 919-930

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

In `@pkg/core/types.go` around lines 912 - 915, The constants AppSessionCreation
and AppSessionDeposit currently share the same string value causing ID() to
misclassify actions; change AppSessionCreation to a distinct value (e.g.,
"app_session_creation") and audit the neighboring GatedAction constants (the
block around
AppSessionCreation/AppSessionOperation/AppSessionDeposit/AppSessionWithdrawal
and lines 919-930) to ensure each constant has a unique string literal; after
updating, confirm the ID() switch/lookup (the function that maps GatedAction
values to IDs) covers the corrected symbols so creation and deposit are
unambiguous.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant