Skip to content

feat: add support for default and fast finality rate limits#2053

Open
chris-de-leon-cll wants to merge 2 commits into
mainfrom
feat/dual-rate-limits
Open

feat: add support for default and fast finality rate limits#2053
chris-de-leon-cll wants to merge 2 commits into
mainfrom
feat/dual-rate-limits

Conversation

@chris-de-leon-cll
Copy link
Copy Markdown
Collaborator

@chris-de-leon-cll chris-de-leon-cll commented May 11, 2026

Add Support for Explicit Default + Fast-Finality Rate Limits

Summary

This change replaces the previous idea that v2 token pool tooling could infer which TPRL bucket to update by reading the pool’s AllowedFinalityConfig. That model was incorrect: allowed finality can combine modes such that both default-finality and fast-finality paths remain relevant over time, so both buckets may matter operationally.

Instead, this PR now allows you to specify both the default and fast finality (a.k.a. custom) rate limits. The current InboundRateLimiterConfig / OutboundRateLimiterConfig is now an alias for the default (FastFinality=false) bucket for backwards compatibility purposes, and an optional fast-finality bucket is only applied if the pool supports it (it is a warning + no-op for other pre-v2 token adapters).

Mixed pre-V2 pool and V2+ pool behavior:

What you set for the lane Pre‑V2 pool V2 pool
Default only Applies default RL bucket. Applies default RL bucket.
FF only Ignores FF RL bucket. Applies FF RL bucket.
Default + FF Applies default RL only. Applies default and FF buckets in one batch.
Empty (no default, no FF) Rate limits remain as-is. Rate-limits remain as-is.

YAML Examples

Setting Just the Default Rate Limit (works for all versions)

# Option 1: use the `rateLimit` field (remains for backwards compatibility - easy shortcut)
input:
  '124615329519749600':
    remoteOutbounds:
      '5009297550715158000':
        rateLimit:
          isEnabled: true
          capacity: 10000
          rate: 0.11574074
  '5009297550715158000':
    remoteOutbounds:
      '124615329519749600':
        rateLimit:
          isEnabled: false
          capacity: 0
          rate: 0

# Option 2: use `outbounds` field (equivalent to above, slightly more verbose)
input:
  '124615329519749600':
    remoteOutbounds:
      '5009297550715158000':
        outbounds:
          - fastFinality: false
            rateLimit:
              isEnabled: true
              capacity: 10000
              rate: 0.11574074
  '5009297550715158000':
    remoteOutbounds:
      '124615329519749600':
        outbounds:
          - fastFinality: false
            rateLimit:
              isEnabled: false
              capacity: 0
              rate: 0

Setting Just the Custom Rate Limit (applies on-chain on v2 only)

input:
  "124615329519749600":
    remoteOutbounds:
      "5009297550715158000":
        outbounds:
          - fastFinality: true
            rateLimit:
              isEnabled: true
              capacity: 10000
              rate: 0.11574074
  "5009297550715158000":
    remoteOutbounds:
      "124615329519749600":
        outbounds:
          - fastFinality: true
            rateLimit:
              isEnabled: false
              capacity: 0
              rate: 0

Setting Both the Default and Custom Rate Limits (v2 applies both; legacy applies default only)

input:
  "124615329519749600":
    remoteOutbounds:
      "5009297550715158000":
        outbounds:
          - fastFinality: true
            rateLimit:
              isEnabled: true
              capacity: 10000
              rate: 0.11574074
          - fastFinality: false
            rateLimit:
              isEnabled: true
              capacity: 10000
              rate: 0.11574074
  "5009297550715158000":
    remoteOutbounds:
      "124615329519749600":
        outbounds:
          - fastFinality: true
            rateLimit:
              isEnabled: false
              capacity: 0
              rate: 0
          - fastFinality: false
            rateLimit:
              isEnabled: false
              capacity: 0
              rate: 0

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the token deployment/configuration tooling to support two explicit TPRL rate-limit buckets (default-finality and fast-finality) without inferring bucket selection from on-chain AllowedFinalityConfig. It introduces canonical per-bucket configuration structures + normalization, propagates bucket data through the transfer-configuration flow, and updates the EVM v2.0.0 configure logic + adapter to diff and batch-set per-bucket rate limits.

Changes:

  • Introduces explicit rate-limit bucket modeling (RateLimitConfig, Outbounds, Normalize(), per-bucket validation) and extends RemoteChainConfig to carry per-direction bucket slices.
  • Updates ConfigureTokensForTransfers to pass/normalize full counterpart config and reconcile inbound/outbound buckets independently.
  • Updates EVM v2.0.0 configure sequence + adapter to build/apply a list of desired buckets (default always, FF only when explicitly paired) and batch SetRateLimitConfig per bucket.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
deployment/tokens/rate_limits.go Adds bucket model + normalization, TPRL verification changes, and lane bucket building logic.
deployment/tokens/product.go Adds float-input validation and extends RemoteChainConfig with inbound/outbound bucket slices + helpers.
deployment/tokens/configure_tokens_for_transfers.go Propagates normalized bucket slices using full counterpart RemoteChainConfig.
deployment/tokens/configure_tokens_for_transfers_test.go Adds unit tests for normalization + bucket access helpers.
chains/evm/deployment/v2_0_0/sequences/tokens/configure_token_pool_for_remote_chain.go Switches from finality-based inference to explicit per-bucket desired updates with batched writes.
chains/evm/deployment/v2_0_0/sequences/tokens/configure_token_pool_for_remote_chain_test.go Updates tests to assert scalar fields only affect the default bucket unless FF slices are explicitly provided.
chains/evm/deployment/v2_0_0/sequences/tokens/configure_token_for_transfers_test.go Updates expectations/documentation around which bucket scalar floats populate.
chains/evm/deployment/v2_0_0/adapters/tokens.go Updates adapter to map RateLimitBuckets directly to SetRateLimitConfig args (no finality inference).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread deployment/tokens/product.go Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread integration-tests/deployment/token_pool_rate_limits_test.go Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

deployment/tokens/rate_limits.go:134

  • When Outbounds is non-empty but has no default bucket, Normalize still promotes the legacy RateLimit field into a default bucket, but verification skips validating RateLimit in that case. An invalid enabled alias (for example zero capacity/rate) can therefore pass verify and fail later or generate an invalid on-chain update.
				if len(input.Outbounds) == 0 {
					if err := input.RateLimit.Validate(); err != nil {
						return fmt.Errorf("outbound rate limiter config for remote chain %d is invalid: %w", remoteSelector, err)
					}
				}
				if len(input.Outbounds) > 2 {
					return fmt.Errorf("at most two outbound rate limit buckets allowed for remote chain %d", remoteSelector)
				}

				defaultCount, fastFinCount := 0, 0
				for _, rl := range input.Outbounds {
					if err := rl.RateLimit.Validate(); err != nil {
						return fmt.Errorf("outbound rate limiter config for remote chain %d is invalid: %w", remoteSelector, err)

Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread integration-tests/deployment/token_pool_rate_limits_test.go Outdated
Comment thread deployment/tokens/rate_limits.go Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

deployment/tokens/rate_limits.go:305

  • For a lane that contains only fastFinality=true buckets, this function leaves the legacy default OutboundRateLimiterConfig/InboundRateLimiterConfig as zero values while returning the fast-finality entry only in RateLimitBuckets. Pre-v2 EVM and Solana adapters ignore RateLimitBuckets and still write those legacy fields, so a fast-finality-only config on a non-v2 pool will disable the default rate limits instead of being ignored as documented. Either reject fast-finality-only buckets for adapters that only consume the legacy fields, or preserve/no-op the legacy default config in that case.
	defaultOutboundRL := RateLimiterConfig{}
	defaultInboundRL := RateLimiterConfig{}
	buckets := []TPRLRateLimitBucket{}

Comment thread integration-tests/deployment/token_pool_rate_limits_test.go Outdated
Comment thread deployment/tokens/rate_limits.go Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

chains/evm/deployment/v2_0_0/adapters/tokens.go:387

  • When RateLimitBuckets is empty (for example an empty RemoteOutbounds entry or a finality-only TPRL run), this still schedules a setRateLimitConfig transaction with an empty args array. The contract treats that as a no-op after permission checks, so the changeset can generate unnecessary proposal transactions. Skip this operation when there are no bucket args to apply.
			report, err := cldf_ops.ExecuteOperation(b, token_pool.SetRateLimitConfig, evmChain, contract.FunctionInput[[]token_pool.RateLimitConfigArgs]{
				ChainSelector: input.ChainSelector,
				Address:       tokenPoolAddr,
				Args:          args,
			})

Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread integration-tests/deployment/token_pool_rate_limits_test.go
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread deployment/tokens/rate_limits.go Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (1)

deployment/tokens/configure_tokens_for_transfers_test.go:812

  • This assertion has the same pointer/value mismatch: out.RateLimit is a *RateLimiterConfigFloatInput, so comparing it directly to cfgs[0].RateLimit will fail despite correct normalization. Dereference the normalized alias after a non-nil check.
	require.Equal(t, cfgs[0].RateLimit, out.RateLimit)

Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread deployment/tokens/configure_tokens_for_transfers_test.go Outdated
Comment thread deployment/tokens/rate_limits.go Outdated
Comment thread integration-tests/deployment/token_pool_rate_limits_test.go Outdated
Comment thread integration-tests/deployment/token_pool_rate_limits_test.go Outdated
Comment thread deployment/tokens/product.go Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Comment thread deployment/tokens/rate_limits.go Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Comment thread chains/evm/deployment/v1_0_0/adapters/pool_adapter.go Outdated
Comment thread chains/solana/deployment/v1_6_0/sequences/tokens.go Outdated
Comment thread deployment/tokens/product.go Outdated
Comment thread deployment/tokens/configure_tokens_for_transfers.go
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 29 out of 29 changed files in this pull request and generated 5 comments.

Comment thread chains/evm/deployment/v2_0_0/sequences/tokens/README.md Outdated
Comment thread integration-tests/deployment/token_pool_rate_limits_test.go
Comment thread integration-tests/deployment/token_pool_rate_limits_test.go Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 29 out of 29 changed files in this pull request and generated 3 comments.

Comment thread chains/evm/deployment/v2_0_0/adapters/tokens.go
Comment thread deployment/tokens/configure_tokens_for_transfers.go
Comment thread integration-tests/deployment/token_pool_rate_limits_test.go
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 6 comments.

Comment thread chains/evm/deployment/v1_5_1/adapters/tokens.go
Comment thread deployment/tokens/configure_tokens_for_transfers.go Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.

Comment on lines +162 to +166
bucket, ok := input.GetBucketForFinality(false)
if !ok {
b.Logger.Warnf("skipping rate limiter config for token pool (%s) on chain %d since no default bucket was provided", datastore_utils.SprintRef(input.TokenPoolRef), input.ChainSelector)
return nil, nil
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

NOTE: this this check has been added to the pre-V2 EVM and Solana token adapters as a defense-in-depth measure to prevent accidental overwrites for the following scenario:

  1. Suppose the user only wants to edit the fast finality rate limits for a pair of pools, so they omit the default rate limits from the input and only include bidirectional FF rate limits
  2. During runtime, the changeset discovers that one of the pools that the user wants to update is v1.5.1 (which doesn't support FF rate limits) and the other is v2.0.0 (which does support FF rate limits)
  3. In this case, the original code checks the input for the default rate limits for the v1.5.1 pool and discovers that it only contains FF rate limits
  4. Consequently, the adapter can't tell whether the user wants to reset the default rate limits for the v1.5.1 pool or keep the existing onchain ones (the original code does NOT use a pointer type so this case is ambiguous)
  5. Without this check, the original code would reset the existing rate limits for the v1.5.1 pool and ignore the FF buckets
  6. This will cause the user to be confused about why things were unexpectedly reset for the v1.5.1 pool despite them only specifying FF rate limits

Test cases have been added to cover this situation.

Comment thread integration-tests/go.mod
github.com/smartcontractkit/chainlink-ccip/chains/solana/deployment v0.0.0-00010101000000-000000000000
github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260312233953-f588f8dc6d7c
github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260506222857-f76eec39d0c0
github.com/smartcontractkit/chainlink-ccip/deployment v0.0.0-20260516222345-f2f143454dbd
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@github-actions
Copy link
Copy Markdown

Metric feat/dual-rate-limits main
Coverage 70.1% 69.8%

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