Conversation
Co-authored-by: Anton Filonenko <philanton@users.noreply.github.com>
📝 WalkthroughWalkthroughThe 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
Sequence DiagramsequenceDiagram
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
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
clearnode/config/migrations/postgres/20251222000000_initial_schema.sqlclearnode/store/database/action_log.goclearnode/store/database/database.goclearnode/store/database/interface.goclearnode/store/database/testing.goclearnode/store/database/user_staked.goclearnode/store/memory/action_limits_store.gopkg/core/types.go
| 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; |
There was a problem hiding this comment.
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.
| DROP INDEX IF EXISTS idx_action_log_v1_wallet_method_created; | ||
| DROP INDEX IF EXISTS idx_action_log_v1_wallet_method_path_created; |
There was a problem hiding this comment.
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.
| 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.
| 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(), |
There was a problem hiding this comment.
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.
| // 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) |
There was a problem hiding this comment.
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.
| 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"` |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "user_staked.go" -type fRepository: layer-3/nitrolite
Length of output: 102
🏁 Script executed:
cat -n ./clearnode/store/database/user_staked.goRepository: layer-3/nitrolite
Length of output: 2230
🏁 Script executed:
rg -t go "UserStakedV1" --context 5Repository: 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
IDis treated as the primary key automatically (or you embedgorm.Model, which already definesIDasprimaryKey). (gorm.io) - Required if your primary key field is not the default
ID(or you have a composite primary key). You must mark it withgorm:"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 needprimaryKeyfor that purpose—you’re telling GORM the conflict target directly (e.g.,Columns: []clause.Column{{Name: "email"}}). (gorm.io) - If you don’t set
Columnsand rely onUpdateAll: true, GORM uses the model’s primary key fields as the conflict target—so in that case, having the correctprimaryKeydefinition matters for non-IDkeys. (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:
- 1: https://gorm.io/gen/create.html?utm_source=openai
- 2: https://gorm.io/hi_IN/docs/conventions.html?utm_source=openai
- 3: https://gorm.io/hi_IN/docs/conventions.html?utm_source=openai
- 4: https://gorm.io/gen/create.html?utm_source=openai
- 5: https://blog.51cto.com/u_15087084/2598238?utm_source=openai
- 6: https://gorm.io/hi_IN/docs/conventions.html?utm_source=openai
- 7: https://gorm.io/hi_IN/docs/conventions.html?utm_source=openai
🏁 Script executed:
rg -t go "user_staked_v1" -i --context 3Repository: layer-3/nitrolite
Length of output: 495
🏁 Script executed:
rg -t go "primaryKey" --context 2Repository: 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 -20Repository: 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 2Repository: layer-3/nitrolite
Length of output: 43
🏁 Script executed:
find . -name "*migrat*" -type f | head -10Repository: layer-3/nitrolite
Length of output: 234
🏁 Script executed:
cat -n ./clearnode/store/database/database.go | head -100Repository: layer-3/nitrolite
Length of output: 4242
🏁 Script executed:
cat -n ./clearnode/store/database/database.go | tail -100Repository: layer-3/nitrolite
Length of output: 3397
🏁 Script executed:
find . -path "*/migrations/postgres*" -name "*.sql" | head -20Repository: 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.
| 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.
| 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, |
There was a problem hiding this comment.
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.
| 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.
| if err != nil { | ||
| return &ActionLimitsStore{}, err | ||
| } |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| func (a *ActionLimitsStore) StakedToAppCount(stakedYellowTokens decimal.Decimal) uint { | ||
| return uint(stakedYellowTokens.Div(a.config.AppCost).IntPart()) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate and examine the file in question
find . -name "action_limits_store.go" -type fRepository: 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.goRepository: layer-3/nitrolite
Length of output: 3602
🏁 Script executed:
# Search for callers of StakedToAppCount to understand how stakedYellowTokens is provided
rg "StakedToAppCount" --type goRepository: 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 2Repository: 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 -100Repository: layer-3/nitrolite
Length of output: 3389
🏁 Script executed:
# Search for all calls to StakedToAppCount
rg "StakedToAppCount" --type go -B 3 -A 3Repository: 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 -5Repository: layer-3/nitrolite
Length of output: 108
🏁 Script executed:
# Search across all files for StakedToAppCount
rg "StakedToAppCount" --type go -rRepository: 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 2Repository: layer-3/nitrolite
Length of output: 3692
🏁 Script executed:
# Search for callers of StakedToAppCount
rg "StakedToAppCount" --type goRepository: 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 -20Repository: layer-3/nitrolite
Length of output: 108
🏁 Script executed:
# Search more broadly without restricting to go files
rg "StakedToAppCount" --no-ignoreRepository: 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/nullRepository: 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 -10Repository: 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/nullRepository: 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/nullRepository: 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))
}
EOFRepository: 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 -80Repository: 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 -5Repository: 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 -50Repository: 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.goRepository: 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.
| 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.
| AppSessionCreation GatedAction = "app_session_deposit" | ||
| AppSessionOperation GatedAction = "app_session_operation" | ||
| AppSessionDeposit GatedAction = "app_session_deposit" | ||
| AppSessionWithdrawal GatedAction = "app_session_withdrawal" |
There was a problem hiding this comment.
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.
Summary by CodeRabbit
New Features
Refactor