Skip to content

feat: construct extraData and add context signer fetching#414

Open
zmalatrax wants to merge 12 commits intofeat/kms-contextfrom
feat/1065/add-context-signer-fetching
Open

feat: construct extraData and add context signer fetching#414
zmalatrax wants to merge 12 commits intofeat/kms-contextfrom
feat/1065/add-context-signer-fetching

Conversation

@zmalatrax
Copy link
Contributor

Closes https://github.com/zama-ai/fhevm-internal/issues/1065

Implements context-aware KMS signer resolution and dynamic extraData construction for all decryption request types, as specified in issue https://github.com/zama-ai/fhevm-internal/issues/1065 / RFC 003. Part of a coordinated release (https://github.com/zama-ai/planning-blockchain/issues/1097), notably with the updated KMSVerifier contract and relayer.

Summary

  • Add extraData parser, builder, and legacy detection (extraData.ts)
  • Add KmsContextCache for per-context signer caching with TTL-based current context ID
  • Wire KmsContextCache into FhevmHostChain, public decrypt, user decrypt, and delegated user decrypt
  • Expose getExtraData() on the public FhevmInstance API for user/delegated user decrypt flows
  • Add extraData to relayer response types and V2 guards

Motivation

The SDK previously loaded KMS signers once at initialization and hardcoded extraData to '0x00'. After a KMS context switch, the SDK held stale signers and could not verify responses signed by a new signer set. This PR makes both sides context-aware:

  1. Request side: queries getCurrentKmsContextId() and builds extraData = [0x01 | contextId(32B)] before every decryption request.
  2. Response side: extracts the context ID from the response's extraData, fetches the matching signer set via getSignersForKmsContext(contextId), and passes those signers to TKMS verification.

Changes by commit

Commit Scope Description
dd3a724 kms/extraData.ts New module: isLegacyExtraData, parseExtraData, buildRequestExtraData with full test coverage (round-trip, boundary values, version dispatch)
bbe62d8 kms/KmsContextCache.ts New class: per-context signer cache (immutable, no TTL) + current context ID cache (TTL, stale fallback, cold-start retry). Deduplicates concurrent RPC calls. 253-line test suite
aa7c272 fhevmHostChain.ts FhevmHostChain eagerly creates KmsContextCache (no RPC until first use) and exposes it via kmsContextCache getter
f3610ee relayer-provider/ Adds extraData: BytesHex to RelayerUserDecryptResult items and RelayerUserDecryptOptionsType. Updates V2 guards to assert extraData. Test coverage for missing/invalid extraData. V1 routes receive minimal passthrough changes only (deprecated, not the focus)
353ba45 publicDecrypt.ts Public decrypt now builds dynamic extraData from kmsContextCache.getCurrentContextId(), reads response extraData for EIP-712 verification, and resolves context-specific signers (legacy fallback to init-time signers)
711928e userDecrypt.ts User and delegated user decrypt accept caller-provided extraData (default '0x00'), resolve context-aware signers via resolveEffectiveSigners() with mixed-context assertion, and fail closed on RPC errors
243f298 index.ts Exposes getExtraData() on FhevmInstance, adds optional extraData param to createEIP712 and createDelegatedUserDecryptEIP712, passes kmsContextCache to all three decrypt closures

Design decisions

  • Public decrypt fetches context dynamically — the SDK owns the full lifecycle, so it calls getCurrentContextId() internally before each request.
  • User/delegated decrypt accepts extraData from caller — the EIP-712 signature covers extraData, so the caller must obtain it first via getExtraData() and pass the same value to both createEIP712() and the decrypt call. Single source of truth: getExtraData() -> createEIP712(extraData) -> userDecrypt(options.extraData).
  • No backward compatibility with previous relayer versions — this PR ships as part of a coordinated update (KMSVerifier contract + relayer + SDK). The updated relayer always returns extraData in responses. isLegacyExtraData exists only as a defensive code path, not as a compatibility layer for older relayers.
  • V1 routes are not updated — V1 routes are being deprecated. They receive minimal passthrough changes to compile, but context-aware logic targets V2 routes exclusively.
  • Fail closed — context-bearing responses that fail signer resolution propagate the error (no silent fallback to init-time signers).
  • Signer cache has no TTL — signer sets are immutable per context on-chain. Context ID cache uses a 5s TTL with stale fallback on RPC failure.

Test coverage

extraData construction

  • extraData is correctly formatted: version 0x01 + 32-byte context ID (extraData.test.ts)
  • Round-trip: parseExtraData(buildRequestExtraData(id)).contextId === id for boundary values
  • Legacy detection covers '0x', '0x00', and '0x00...' variants
  • Unsupported versions and truncated payloads throw

KmsContextCache

  • getCurrentContextId() returns context ID from contract (KmsContextCache.test.ts)
  • TTL-based caching prevents redundant RPC calls
  • Stale fallback on RPC failure with valid cached value
  • Cold-start retry on first RPC failure, throws on double failure
  • Concurrent call deduplication
  • getSignersForContext() cache miss/hit, concurrent dedup, rejection eviction
  • Rejects non-checksummed addresses, propagates RPC errors with cause

V2 guards

  • V2 guard asserts extraData on response items (RelayerV2ResultUserDecrypt.test.ts)
  • Missing and invalid extraData throw

Integration (decrypt closures)

  • publicDecrypt and userDecrypt tests pass with mock KmsContextCache

Follow-up notes

Since this is a coordinated release (afaiu the updated relayer always returns v1 context-bearing extraData), the isLegacyExtraData branches in publicDecrypt.ts and resolveEffectiveSigners() would then be dead code. Removing them would:

  • Drop isLegacyExtraData and its tests — every response is now context-bearing, so the version-dispatch branches (if (isLegacyExtraData(...)) { use init-time signers }) are never reached.
  • Remove kmsSigners from decrypt closures — init-time signers are only used in those legacy branches. Without them, decrypt closures only need kmsContextCache.
  • Simplify resolveEffectiveSigners — becomes a straight parseExtraData + getSignersForContext call, no branching.
  • Consider making extraData required in createEIP712 / createDelegatedUserDecryptEIP712 / RelayerUserDecryptOptionsType — currently defaults to '0x00', which lets callers silently skip getExtraData() and send legacy requests. Making it required catches integration errors at compile time.

Test plan

  • Run npx jest --colors --passWithNoTests — all existing and new tests pass
  • Integration test against devnet with the updated KMSVerifier and relayer: verify context switch triggers new signer set and old context signers still verify in-flight responses

@cla-bot cla-bot bot added the cla-signed label Mar 2, 2026
@zmalatrax zmalatrax changed the base branch from main to feat/kms-context March 2, 2026 17:17
@zmalatrax zmalatrax requested a review from isaacdecoded March 2, 2026 17:17
@zmalatrax
Copy link
Contributor Author

Tests targeting Testnet are expected to fail, as testnet is not updated yet...

Failing tests
RelayerV2Provider:public-decrypt:sepolia: › v2: succeeded
RelayerV2Provider:public-decrypt:sepolia: › v1: succeeded
FhevmInstance.userDecrypot:sepolia: › v1: FhevmInstance.userDecrypt succeeded
FhevmInstance.userDecrypot:sepolia: › v2: FhevmInstance.userDecrypt succeeded

): Promise<UserDecryptResults> => {
const extraData: BytesHex = '0x00';
// Accept caller-provided extraData, default to legacy '0x00' when omitted
const extraData: BytesHex = options?.extraData ?? '0x00';
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: not sure if it's accurate to include the extraData in the options type instead of having it as another argument in the userDecryptRequest function. I mean, the RelayerUserDecryptOptionsType seems more specific to a Relayer endpoint configurations/options.

Also, it seems a bit weird not having the publicDecryptRequest function receiving such an extraData argument. Do you know why? @zmalatrax

Copy link
Contributor Author

@zmalatrax zmalatrax Mar 3, 2026

Choose a reason for hiding this comment

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

not sure if it's accurate to include the extraData in the options type instead of having it as another argument in the userDecryptRequest function. I mean, the RelayerUserDecryptOptionsType seems more specific to a Relayer endpoint configurations/options.

Indeed, it was added to options to avoid adding a new argument to the public api of userDecrypt but it's semantically wrong..

I'll refactor with a new optional param instead

Also, it seems a bit weird not having the publicDecryptRequest function receiving such an extraData argument. Do you know why?

Initially I wanted to avoid exposing extraData to the public API, and fetch it in the internals of the relayer-sdk.
But for user decryption, as the user creates an EIP712 signature, and that the extraData is required I had to expose it to the public api. But for public decryption it's not mandatory, so I didn't add it.

For consistency we could have both userDecrypt and publicDecrypt handle the extraData at the public api level

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