Skip to content

feat(relayer): context aware user decrypt threshold#2317

Open
manoranjith wants to merge 6 commits intofeat/RFC013from
mano/relayer/dynamic-kms-context
Open

feat(relayer): context aware user decrypt threshold#2317
manoranjith wants to merge 6 commits intofeat/RFC013from
mano/relayer/dynamic-kms-context

Conversation

@manoranjith
Copy link
Copy Markdown
Member

Summary

Draft implementation of context aware user decrypt threshold feature.

Refs zama-ai/fhevm-internal#1260

Done

  • When a share is received from the gateway, the context_id is extracted from extra_data and the corresponding user decrypt shares threshold is
    fetched from the ProtocolConfig contract on the Ethereum chain.
  • The fetched value is cached in-memory (moka). On restarts, values are re-fetched from the ProtocolConfig contract on the next share arrival.
  • For requests with extra_data = 0x00 (or empty), the threshold value from config is used, ensuring backward compatibility for older requests during
    the migration period.
  • The resolved threshold is stored in the DB when marking a request as completed, so that the HTTP handler knows how many shares to fetch for the final
    result.
  • To maintain backward compatibility on the read side: COALESCE(resolved_threshold, static_config) is used for the share LIMIT, so pre-migration rows
    (where resolved_threshold is NULL) fall back to the static config value.
  • DB migration adds nullable resolved_threshold BIGINT column (backward-compatible with old software).
  • New config: protocol_config.

Tests

Unit tests — extra_data parsing (extra_data.rs, threshold_resolver.rs):

  1. Empty or 0x00 extra_data returns default context (zero)
  2. Valid v1 and v2 formatted extra_data correctly extracts context_id
  3. Truncated or unknown version extra_data returns an error
  4. Large context_id values (e.g., KMS context counter base) parse correctly

Mock-based tests — threshold resolution (threshold_resolver_test.rs, using ethereum_rpc_mock):

  1. Default context (ID 0) returns the static config threshold without making any RPC call
  2. Non-zero context ID fetches the threshold from the ProtocolConfig contract mock
  3. A previously fetched threshold is served from cache without a second RPC call
  4. When the first RPC attempt fails, the retry logic recovers on the next attempt
  5. When all retry attempts are exhausted, an error is returned to the caller
  6. A failed fetch is not cached, so the next request for the same context retries from scratch

Pending (should not block testing)

  • Eliminate silent type conversions: use U256-compatible type for threshold throughout, no range assumptions
  • Validate context_id on incoming HTTP request to reject invalid ones early
  • Pin fhevm_host_bindings to git tag before merge
  • ProtocolConfig addresses in mainnet/testnet config examples

- Issue: host_chains config, new in this release, failed
  with "invalid type: map, expected sequence" when set
  via indexed K8s env vars

- Fix: add map-or-seq deserializer and try_parsing(true)
  for string-to-numeric coercion

- Prevention: add env override test for all fields,
  which uncovered the same issue in backoff intervals
  and histogram buckets — fixed with a single generic
  deserializer applied to all Vec fields

Refs zama-ai/fhevm-internal#1191
- Previously: doc comment and design doc claimed RFC 7231
  absolute timestamps for 429 responses

- Now: correctly documents relative seconds, matching
  the actual implementation
@manoranjith manoranjith requested review from a team as code owners April 16, 2026 17:40
@cla-bot cla-bot Bot added the cla-signed label Apr 16, 2026
- Resolve user decrypt threshold per context_id from
  ProtocolConfig contract instead of static config

- Parse context_id from extra_data in gateway response;
  0 falls back to static default

- Cache thresholds in moka with dedup and retry on failure

- Persist resolved_threshold in DB; read side uses
  COALESCE for backward compatibility

- New config parameter: protocol_config
@manoranjith manoranjith force-pushed the mano/relayer/dynamic-kms-context branch from 79721c3 to 5c36885 Compare April 16, 2026 17:47
- Previously: threshold was u16 in config, usize in resolver,
  i64 at DB boundary — mixed widths with implicit casts

- Now: u32 end-to-end from config → resolver → handler → repo,
  with explicit i64 conversion only at the DB BIGINT boundary
- Previously: path dep pulled host-contracts into cargo fmt scope

- Now: git dep pinned to 2a6cf82 (ProtocolConfig bindings)
-- Stores the dynamic threshold used when the write side marked the request as completed.
-- Nullable for backward compatibility: old software ignores this column, new rows get the value.
-- NULL means "use static config default" (for rows created before this migration).
ALTER TABLE user_decrypt_req ADD COLUMN resolved_threshold BIGINT;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: maybe to enforce the NOT NULL constraint rule, we can set the current "static config default" instead of leaving it as NULL

pub input_verification_address: String,
/// Number of shares required for user decryption threshold consensus
pub user_decrypt_shares_threshold: u16,
pub user_decrypt_shares_threshold: u32,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: just wondering why not leave it as u16, or even lower like u8?


impl ThresholdResolver {
pub async fn new(
config: &ProtocolConfigSettings,
Copy link
Copy Markdown
Contributor

@isaacdecoded isaacdecoded Apr 21, 2026

Choose a reason for hiding this comment

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

nit: naming this argument as protocol_config_settings could help to reduce a bit of the developer's mental load

/// Thresholds are cached permanently (no TTL) since they don't change after
/// context creation. Context ID 0 is pre-seeded with the static config default.
pub struct ThresholdResolver {
contract: HostProtocolConfig,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
contract: HostProtocolConfig,
protocol_config_contract: HostProtocolConfig,

/// Cached thresholds keyed by context ID. The on-chain uint256 is narrowed
/// to u32 at the contract fetch boundary with an explicit range check.
/// The repository layer handles u32 ↔ i64 conversion for DB storage (BIGINT).
context_thresholds: Cache<U256, u32>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
context_thresholds: Cache<U256, u32>,
context_thresholds_cache: Cache<U256, u32>,


let context_thresholds = Cache::builder().max_capacity(max_capacity).build();

// Pre-seed default: context_id 0 is invalid and maps to the static config value
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If context_id 0 is an invariant violation, why intentionally introduce it in the cache? Also, this could be error-prone somehow since we shouldn't have such a default threshold but only what our source of truth dictates, no?


protocol_config:
ethereum_http_rpc_url: "http://host-node:8545"
address: "0x1234567890123456789012345678901234567890"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
address: "0x1234567890123456789012345678901234567890"
address: "0x52054F36036811ca418be59e41Fc6DD1b9e4F4c8"


protocol_config:
ethereum_http_rpc_url: "http://host-node:8545"
address: "0x1234567890123456789012345678901234567890"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
address: "0x1234567890123456789012345678901234567890"
address: "0x52054F36036811ca418be59e41Fc6DD1b9e4F4c8"

Copy link
Copy Markdown
Contributor

@isaacdecoded isaacdecoded left a comment

Choose a reason for hiding this comment

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

A few nit changes/suggestions here and there.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants