Skip to content

fix(velocity): make balance limit start field mandatory#714

Merged
vindard merged 4 commits into
mainfrom
fix/velocity-limit-mandatory-start
Apr 30, 2026
Merged

fix(velocity): make balance limit start field mandatory#714
vindard merged 4 commits into
mainfrom
fix/velocity-limit-mandatory-start

Conversation

@vindard
Copy link
Copy Markdown
Contributor

@vindard vindard commented Apr 29, 2026

Summary

When start was omitted on a BalanceLimit, cala silently defaulted to created_at (wall-clock attachment time). Any limit where start > transaction_effective_time was skipped during enforcement — velocity limits were ineffective for backdated transactions. This caused an overdraft bypass in staging (lana-bank#5380): simulation posted payments with 2023 effective times against a limit whose start was wall-clock 2026, driving a deposit to -$52,313.

lana-bank#5380 shipped an immediate fix (explicit .start() on all call sites). This PR fixes it at the source: start is now mandatory on BalanceLimit, so callers must consciously choose a start time rather than silently inheriting a potentially wrong default. Beyond preventing the original bug, this optimizes for clarity and reduces ambiguity — implicit behavior that could quietly produce incorrect enforcement is replaced with an explicit, reviewer-visible decision at every call site.

Tradeoff analysis (change default vs. make mandatory): ops/scratch/active/cala-velocity-start-default.md.

Test plan

  • CI passes
  • After bump into lana-bank, verify existing velocity tests pass (all 7 sites already have .start())

🤖 Generated with Claude Code


Note

Medium Risk
This is a breaking schema/type change and alters limit activation behavior, which can affect enforcement for existing integrations that omitted start. The change is localized and makes enforcement more explicit, reducing the risk of silent bypasses.

Overview
BalanceLimit.start is now mandatory across Rust types and the emitted JSON schema, turning a previously optional field into a required CelExpression.

Limit attachment/enforcement no longer falls back to created_at when start is omitted; the start time must be explicitly evaluated from the provided expression. Builders and internal conversions were updated accordingly, including a new NewBalanceLimitBuilder::always_active() helper, and existing tests/bench scaffolding were updated to set an explicit start.

Reviewed by Cursor Bugbot for commit 0270ef1. Bugbot is set up for automated code reviews on this repo. Configure here.

Omitting start on NewBalanceLimit previously defaulted to created_at
(wall-clock attachment time), silently skipping enforcement for any
transaction with an effective time before that. This caused an overdraft
bypass in staging when backdated transactions skipped velocity limits.

Make start a required field on the builder so omitting it is a compile
error. The else branch in attach_control_in_op now falls back to
MIN_UTC for backwards compatibility with already-persisted limits that
have no start expression.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2b9e7c7. Configure here.

pub amount: CelExpression,
pub enforcement_direction: CelExpression,
pub start: Option<CelExpression>,
pub start: CelExpression,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Breaking deserialization of existing persisted events with null start

High Severity

Changing start from Option<CelExpression> to CelExpression on the BalanceLimit struct breaks deserialization of existing events stored in the database. Since VelocityLimitEvent is event-sourced and persisted as JSON in cala_velocity_limit_events, any previously-created velocity limit where start was None will have "start": null in its stored JSON. Attempting to load such a record will now fail because CelExpression (backed by #[serde(try_from = "String")]) cannot deserialize from null. No data migration or #[serde(default)] fallback is provided.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2b9e7c7. Configure here.

Copy link
Copy Markdown
Contributor Author

@vindard vindard Apr 30, 2026

Choose a reason for hiding this comment

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

We aren't maintaining backward compatibility for this as yet

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

📊 Performance Report

Commit: 0270ef1
Updated: 2026-04-30 14:21:43 UTC

Cala Performance Benchmark Results (non-representative)

Criterion Benchmark Results (single-threaded)

Benchmark Time per Run Throughput % vs Baseline
post_simple_transaction 7.038ms 142 tx/s 0 (baseline)
post_and_recalculate_ec_account_set 17.174ms 58 tx/s -144.0%
post_and_batch_recalculate_ec_account_set 15.327ms 65 tx/s -117.0%
post_multi_layer_transaction 7.099ms 140 tx/s -0.0%
post_simple_transaction_with_effective_balances 9.450ms 105 tx/s -34.0%
post_simple_transaction_with_skipped_velocity 6.357ms 157 tx/s +9.0%
post_simple_transaction_with_velocity 7.947ms 125 tx/s -12.0%
post_simple_transaction_with_hit_velocity 4.754ms 210 tx/s +32.0%
post_simple_transaction_with_one_account_set 6.458ms 154 tx/s +8.0%
post_simple_transaction_with_five_account_sets 8.061ms 124 tx/s -14.0%
post_simple_transaction_with_ec_account_set 6.035ms 165 tx/s +14.0%

Load Testing Results (parallel-execution)

Scenario tx/s
1 parallel 91.81
2 parallel 123.62
5 parallel 161.97
10 parallel 166.69
20 parallel 162.58
2 contention 129.91
5 contention 141.93
2 acct_sets 115.31
5 acct_sets 131.99

Note: Performance results may vary based on system resources and database state.

Last updated by commit 0270ef1

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@vindard vindard requested a review from HonestMajority April 29, 2026 17:54
vindard and others added 2 commits April 30, 2026 09:56
…imitBuilder

Adds a discoverable builder method as an alternative to manually
specifying the epoch timestamp for balance limits that should apply
to all transactions regardless of effective time.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@vindard vindard merged commit 52d7a6f into main Apr 30, 2026
6 checks passed
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