Skip to content

feat: short-circuit for cached lit action code (CPL-256)#292

Merged
GTC6244 merged 4 commits intomainfrom
feature/cpl-256-short-cut-for-cached-lit-action-code
Apr 16, 2026
Merged

feat: short-circuit for cached lit action code (CPL-256)#292
GTC6244 merged 4 commits intomainfrom
feature/cpl-256-short-cut-for-cached-lit-action-code

Conversation

@GTC6244
Copy link
Copy Markdown
Contributor

@GTC6244 GTC6244 commented Apr 16, 2026

Summary

Adds an IPFS CID-based code cache to skip import rewriting on repeated lit action executions.

What changed:

  • Added optional string ipfs_id field to the ExecutionRequest proto message
  • lit-api-server now sends the IPFS CID alongside the code when calling execute_js
  • lit-actions-server caches import rewriting results keyed by IPFS CID
  • On cache hit, the import parsing step is skipped entirely
  • Cache is bounded at 1,000 entries and verifies code integrity via SHA-256 hash

Security hardening (from pre-landing review):

  • Cache entries are verified against a SHA-256 hash of the incoming code to prevent cache poisoning from mismatched CIDs
  • Cache size is bounded to prevent unbounded memory growth

Files Changed

  • lit-actions/grpc/schema/lit_actions.proto — new ipfs_id field on ExecutionRequest
  • lit-actions/grpc/proto.rs — include ipfs_id in debug output
  • lit-actions/server/runtime.rsCachedRewrite, CodeCache, hash_code(), cache logic in execute_js, 6 unit tests
  • lit-actions/server/server.rscode_cache field on Server, threaded to execute_js
  • lit-api-server/src/actions/client/execution.rs — send ipfs_id in ExecutionRequest

Test Coverage

6 new unit tests covering:

  • Hash determinism and uniqueness
  • Cache hit with valid hash
  • Cache rejection on hash mismatch (poisoning prevention)
  • Cache capacity enforcement (max 1,000 entries)
  • Cache miss behavior

47 tests pass, 1 pre-existing failure (cdn_module_loader::tests::test_strict_mode_serves_known_module_from_cache).

Pre-Landing Review

2 critical findings, both auto-fixed:

  • [AUTO-FIXED] Unbounded cache growth — added CODE_CACHE_MAX_ENTRIES cap
  • [AUTO-FIXED] Cache poisoning from unverified CID — added SHA-256 hash verification

Test plan

  • All new unit tests pass (6/6)
  • Existing unit tests pass (41/42, 1 pre-existing failure)
  • Both lit-actions-server and lit-api-server compile cleanly

🤖 Generated with Claude Code

…CPL-256)

When execute_js is called with an ipfs_id, the import rewriting result is
cached and reused on subsequent calls with the same CID. This skips the
import parsing step for repeated lit action executions.

The cache is bounded (1,000 entries max) and verifies code integrity via
SHA-256 hash on cache hit to prevent cache poisoning from mismatched CIDs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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

Adds an IPFS CID–keyed cache for Lit Action import-rewrite results so repeated executions can skip the import parsing/rewriting step, with integrity verification via SHA-256 of the incoming code.

Changes:

  • Extends the gRPC ExecutionRequest with an optional ipfs_id (CID) and threads it from lit-api-serverlit-actions-server.
  • Introduces an in-memory rewrite-result cache in lit-actions-server keyed by CID, with hash verification and a 1,000-entry cap.
  • Adds unit tests for hashing and basic cache behaviors.

Reviewed changes

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

Show a summary per file
File Description
lit-api-server/src/actions/client/execution.rs Sends ipfs_id on the initial ExecutionRequest when present.
lit-actions/grpc/schema/lit_actions.proto Adds optional ipfs_id field to the proto message for cache lookup.
lit-actions/grpc/proto.rs Includes ipfs_id in request debug output.
lit-actions/server/server.rs Adds a shared code_cache to the server and passes it into execute_js.
lit-actions/server/runtime.rs Implements CID-keyed rewrite cache with SHA-256 integrity check and tests.

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

Comment thread lit-actions/server/runtime.rs Outdated
Comment thread lit-actions/server/runtime.rs
Comment thread lit-actions/server/runtime.rs
GTC6244 and others added 3 commits April 16, 2026 11:37
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use entry API so existing CID entries can be replaced even when cache
  is at capacity (prevents stale entries becoming permanent)
- Only compute SHA-256 hash when ipfs_id is present (avoids O(n) hashing
  on every execution without a CID)
- Add test for cache-full-but-existing-CID-replacement case

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@GTC6244 GTC6244 merged commit e229d64 into main Apr 16, 2026
10 checks passed
GTC6244 added a commit that referenced this pull request Apr 16, 2026
…on (CPL-258) (#295)

* feat: cache get_wallet_derivation to avoid redundant RPC calls (CPL-257) (#291)

* feat: cache get_wallet_derivation to avoid redundant RPC calls (CPL-257)

Add wallet_derivation cache to BlockchainCache with the same TTL,
capacity, and generation-counter invalidation as the existing permission
caches. Wrap get_wallet_derivation with try_get_with for cache-on-miss
semantics, and add invalidate_for_account to register_wallet_derivation
for write-path consistency. Includes 2 unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: cargo fmt formatting for wallet_derivation_key signature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: generalize module doc wording for cache key parameters

Address Copilot review: "action/wallet combination" was too narrow
now that wallet derivation uses (api_key_hash, wallet) without an
action parameter. Changed to "relevant parameters".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: short-circuit for cached lit action code (CPL-256) (#292)

* feat: add IPFS CID-based code cache for lit action import rewriting (CPL-256)

When execute_js is called with an ipfs_id, the import rewriting result is
cached and reused on subsequent calls with the same CID. This skips the
import parsing step for repeated lit action executions.

The cache is bounded (1,000 entries max) and verifies code integrity via
SHA-256 hash on cache hit to prevent cache poisoning from mismatched CIDs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix rustfmt formatting in runtime.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback on code cache

- Use entry API so existing CID entries can be replaced even when cache
  is at capacity (prevents stale entries becoming permanent)
- Only compute SHA-256 hash when ipfs_id is present (avoids O(n) hashing
  on every execution without a CID)
- Add test for cache-full-but-existing-CID-replacement case

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: collapse nested if-let to satisfy clippy collapsible_if

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: use NoopModuleLoader on cache hit for faster Lit Action execution (CPL-258)

Move the code cache lookup before worker construction so that on cache
hit we can skip building a CdnModuleLoader (with its HTTP client,
integrity manifest, module cache, and lockfile). Instead, pass a cheap
NoopModuleLoader since no module resolution is needed for cached code.

Refactor build_main_worker_and_inject_sdk to accept a pre-built
Rc<dyn ModuleLoader> instead of the six CDN-specific parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
GTC6244 added a commit that referenced this pull request Apr 16, 2026
…tion (CPL-258) (#296)

* feat: cache get_wallet_derivation to avoid redundant RPC calls (CPL-257) (#291)

* feat: cache get_wallet_derivation to avoid redundant RPC calls (CPL-257)

Add wallet_derivation cache to BlockchainCache with the same TTL,
capacity, and generation-counter invalidation as the existing permission
caches. Wrap get_wallet_derivation with try_get_with for cache-on-miss
semantics, and add invalidate_for_account to register_wallet_derivation
for write-path consistency. Includes 2 unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: cargo fmt formatting for wallet_derivation_key signature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: generalize module doc wording for cache key parameters

Address Copilot review: "action/wallet combination" was too narrow
now that wallet derivation uses (api_key_hash, wallet) without an
action parameter. Changed to "relevant parameters".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: short-circuit for cached lit action code (CPL-256) (#292)

* feat: add IPFS CID-based code cache for lit action import rewriting (CPL-256)

When execute_js is called with an ipfs_id, the import rewriting result is
cached and reused on subsequent calls with the same CID. This skips the
import parsing step for repeated lit action executions.

The cache is bounded (1,000 entries max) and verifies code integrity via
SHA-256 hash on cache hit to prevent cache poisoning from mismatched CIDs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix rustfmt formatting in runtime.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback on code cache

- Use entry API so existing CID entries can be replaced even when cache
  is at capacity (prevents stale entries becoming permanent)
- Only compute SHA-256 hash when ipfs_id is present (avoids O(n) hashing
  on every execution without a CID)
- Add test for cache-full-but-existing-CID-replacement case

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: collapse nested if-let to satisfy clippy collapsible_if

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: use NoopModuleLoader on cache hit for faster Lit Action execution (CPL-258)

Move the code cache lookup before worker construction so that on cache
hit we can skip building a CdnModuleLoader (with its HTTP client,
integrity manifest, module cache, and lockfile). Instead, pass a cheap
NoopModuleLoader since no module resolution is needed for cached code.

Refactor build_main_worker_and_inject_sdk to accept a pre-built
Rc<dyn ModuleLoader> instead of the six CDN-specific parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use DataUrlModuleLoader instead of NoopModuleLoader on cache hit (CPL-258)

NoopModuleLoader rejects all module loads, including data:text/javascript
URIs produced by the import bundler. Cached actions with imports still
use import("data:...") calls which go through the module loader.

Add a lightweight DataUrlModuleLoader that handles only
data:text/javascript URIs (base64 and plain encoding) without needing
an HTTP client, integrity manifest, or CDN logic. This gives the
cache-hit performance win while supporting bundled imports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove DataUrlModuleLoader, always use CdnModuleLoader on cache hit (CPL-258)

DataUrlModuleLoader duplicated data-URI parsing logic from CdnModuleLoader,
lacked the pre-decode size check, and broke runtime dynamic imports (e.g.
`await import("zod@3.22.4")`) on cache hits. CdnModuleLoader construction
is cheap (shared Arc references), and the real perf win is the action code
cache skipping prepare_action_code — not the module loader choice.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix rustfmt formatting for module_loader initialization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
GTC6244 added a commit that referenced this pull request Apr 21, 2026
* feat: recursively bundle nested jsDelivr imports as data URLs (CPL-249) (#285)

* feat: recursively bundle nested jsDelivr imports as data URLs (CPL-249)

When importing npm packages via jsDelivr's /+esm endpoint, transitive
dependencies use root-relative /npm/ import paths that were not resolved.
This adds a bundling pipeline in import_rewriter that:

- Scans downloaded modules for nested import/export specifiers
- Recursively fetches all transitive dependencies via BFS
- Performs integrity verification (manifest check, TOFU double-fetch)
- Topologically sorts the dependency graph
- Inlines modules bottom-up as data:text/javascript;base64 URLs

The result is a self-contained script with no module loader network I/O
needed at runtime. Also adds /npm/ root-relative and data: URL passthrough
in CdnModuleLoader::resolve() as safety nets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix cargo fmt formatting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback (Copilot)

- Return error instead of panic on poisoned integrity lock
- Use str::from_utf8 instead of cloning bytes for UTF-8 scan
- Error on unresolvable top-level imports instead of silent drop
- Preserve inline #sha384-... hashes for integrity verification
- Fix imports/root_urls zip alignment (guaranteed by early error)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve clippy warnings (collapsible_if, needless_borrow, range_loop)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: handle data: URI imports in CDN module loader (CPL-250) (#286)

* fix: handle data: URI imports in CDN module loader (CPL-250)

jsDelivr ESM bundles inline small dependencies as data:text/javascript;base64,...
URIs. When Deno encounters these imports, it constructs a valid ModuleSpecifier
and calls load(), which failed because reqwest doesn't support the data: scheme.

- resolve(): Accept data:text/javascript URIs when referrer is an allowed CDN URL
- load(): Decode data: URIs inline (base64 and percent-encoded) with size limits
- Track data URIs in loaded_modules for module count enforcement
- Pre-decode size check to prevent transient memory spikes
- UTF-8 safe log truncation via truncate_for_log() helper
- Replace hand-rolled percent decoder with percent-encoding crate
- Add comprehensive tests for happy path, error paths, and boundary cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: reconcile CPL-249 merge — tighten data URI MIME check in resolve()

CPL-249 added a broad data: passthrough that accepted any data URI scheme.
Replace with a targeted check that only accepts data:text/javascript URIs,
preventing arbitrary MIME types from being imported as modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: share ModuleCache between bundler and module loader (CPL-250)

The bundling pipeline now checks the shared ModuleCache before fetching
from jsDelivr, and stores verified modules after download. When two Lit
Actions import the same npm package, the second execution skips CDN
fetches entirely.

Also makes MAX_CACHE_BYTES pub(crate) for use in import_rewriter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pre-existing test failure in test_strict_mode_serves_known_module_from_cache

The test manifest hash didn't match the cached content bytes. The cache
path now verifies integrity, so the hash must be the actual SHA-384 of
the cached content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix cargo fmt formatting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Copilot review feedback (CPL-250)

- Fix MAX_CACHE_BYTES doc comment: no eviction, new entries are skipped
- Use constant_time_eq for cached integrity check in bundler (consistency)
- Error on unresolvable nested imports in strict mode instead of silently
  skipping (prevents bundles with dangling imports at runtime)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve clippy warnings (needless_borrow, collapsible_if)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix rustfmt formatting for collapsed if-let

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: cache Lit Action code by CID (#287)

* feat: cache prepared lit action code

Cache reusable Lit Action code by the CID computed from incoming execute_js code, with 100 MB total cache accounting and a 30 minute TTL. Keep js_params applied per invocation so cached code cannot replay stale params.\n\nAdd direct cache accounting tests and extend integration coverage for cache reuse with changing params.

* fix: account for allocated action cache capacity

* feat: comprehensive agent skill file for Chipotle API (CPL-253) (#288)

* feat: comprehensive agent skill file for Chipotle API (CPL-253)

Rewrites SKILL.md to cover the full Chipotle API surface: account
creation, groups, wallets (PKPs), IPFS actions, usage API keys, billing,
and Lit Actions authoring with SDK reference, resource limits, and
five working examples. References developer.litprotocol.com docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Copilot review feedback on SKILL.md

- Fix all Lit Action examples to use main(params) signature (runtime
  calls main automatically, no manual main() call)
- Fix account_exists response: bare boolean, not wrapped object
- Fix list_groups response: {id, name, description} not group_* fields
- Fix get_node_chain_config: single object, not array of chains
- Fix get_lit_action_ipfs_id: raw JSON string in/out, not object
- Fix balance_display format: includes "credit" suffix
- Fix error response format: bare JSON string, not structured object
- Fix pagination: 0-based page_number, not 1-based
- Fix group wildcards: zero address/0x0, not string "0"
- Fix JS SDK: LitNodeSimpleApiClient, snake_case response fields
- Fix billing description: per-second for lit_action, per-call for mgmt
- Add missing config endpoints: get_chain_config_keys, get_api_payers,
  get_admin_api_payer
- Document that code field only accepts inline JS, not IPFS CIDs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: truncate long specifiers in CdnModuleLoader resolve logs (CPL-255) (#289)

* fix: truncate long specifiers in CdnModuleLoader resolve logs (CPL-255)

Specifiers containing large base64 inline dependencies could bloat logs.
Strings over 1000 characters are now truncated to the first 100 characters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: coverage for truncate_for_log (CPL-255)

* fix: UTF-8 safe truncation and truncate error messages for #[instrument(err)]

* refactor: return Cow from truncate_for_log to avoid allocation on short strings

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: cache and retrieve lit action code by IPFS ID (CPL-254) (#290)

* fix: cache and retrieve lit action code by IPFS ID (CPL-254)

get_or_prepare_action_code now caches inline code by its derived IPFS
hash and retrieves it on subsequent calls that supply only an ipfs_id.
Unifies the cache value type to Arc<String> across all consumers and
adds comprehensive tests for cache hit, miss, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: defer cache insert until after authorization (CPL-254)

Moves the ipfs_cache insert from resolve_action_code into the caller,
after can_execute_action succeeds. Prevents unauthorized requests from
polluting the shared cache.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve CI failures — clippy collapsible_if and k6 client regen (CPL-254)

Collapse nested if-let + if into a single expression to satisfy clippy
collapsible_if lint. Regenerate k6/litApiServer.ts to reflect the
updated LitActionRequest schema (optional code, new ipfs_id field).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove duplicate truncate_for_log and use 1-arg Cow version (CPL-255)

The merge of CPL-255 introduced a new 1-arg truncate_for_log (Cow-based,
truncates at 1000 bytes) alongside the existing 2-arg version. Remove
the old 2-arg variant and update the three call sites to use the new
single-arg function.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Feature/cpl 256 short cut for cached lit action code (#293)

* feat: add IPFS CID-based code cache for lit action import rewriting (CPL-256)

When execute_js is called with an ipfs_id, the import rewriting result is
cached and reused on subsequent calls with the same CID. This skips the
import parsing step for repeated lit action executions.

The cache is bounded (1,000 entries max) and verifies code integrity via
SHA-256 hash on cache hit to prevent cache poisoning from mismatched CIDs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix rustfmt formatting in runtime.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback on code cache

- Use entry API so existing CID entries can be replaced even when cache
  is at capacity (prevents stale entries becoming permanent)
- Only compute SHA-256 hash when ipfs_id is present (avoids O(n) hashing
  on every execution without a CID)
- Add test for cache-full-but-existing-CID-replacement case

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: collapse nested if-let to satisfy clippy collapsible_if

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: remove extra blank line to fix rustfmt check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Feature/cpl 257 add caching for the get wallet derivation to avoid rpc call (#294)

* feat: cache get_wallet_derivation to avoid redundant RPC calls (CPL-257)

Add wallet_derivation cache to BlockchainCache with the same TTL,
capacity, and generation-counter invalidation as the existing permission
caches. Wrap get_wallet_derivation with try_get_with for cache-on-miss
semantics, and add invalidate_for_account to register_wallet_derivation
for write-path consistency. Includes 2 unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: cargo fmt formatting for wallet_derivation_key signature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: generalize module doc wording for cache key parameters

Address Copilot review: "action/wallet combination" was too narrow
now that wallet derivation uses (api_key_hash, wallet) without an
action parameter. Changed to "relevant parameters".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: use NoopModuleLoader on cache hit for faster Lit Action execution (CPL-258) (#295)

* feat: cache get_wallet_derivation to avoid redundant RPC calls (CPL-257) (#291)

* feat: cache get_wallet_derivation to avoid redundant RPC calls (CPL-257)

Add wallet_derivation cache to BlockchainCache with the same TTL,
capacity, and generation-counter invalidation as the existing permission
caches. Wrap get_wallet_derivation with try_get_with for cache-on-miss
semantics, and add invalidate_for_account to register_wallet_derivation
for write-path consistency. Includes 2 unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: cargo fmt formatting for wallet_derivation_key signature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: generalize module doc wording for cache key parameters

Address Copilot review: "action/wallet combination" was too narrow
now that wallet derivation uses (api_key_hash, wallet) without an
action parameter. Changed to "relevant parameters".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: short-circuit for cached lit action code (CPL-256) (#292)

* feat: add IPFS CID-based code cache for lit action import rewriting (CPL-256)

When execute_js is called with an ipfs_id, the import rewriting result is
cached and reused on subsequent calls with the same CID. This skips the
import parsing step for repeated lit action executions.

The cache is bounded (1,000 entries max) and verifies code integrity via
SHA-256 hash on cache hit to prevent cache poisoning from mismatched CIDs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix rustfmt formatting in runtime.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback on code cache

- Use entry API so existing CID entries can be replaced even when cache
  is at capacity (prevents stale entries becoming permanent)
- Only compute SHA-256 hash when ipfs_id is present (avoids O(n) hashing
  on every execution without a CID)
- Add test for cache-full-but-existing-CID-replacement case

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: collapse nested if-let to satisfy clippy collapsible_if

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: use NoopModuleLoader on cache hit for faster Lit Action execution (CPL-258)

Move the code cache lookup before worker construction so that on cache
hit we can skip building a CdnModuleLoader (with its HTTP client,
integrity manifest, module cache, and lockfile). Instead, pass a cheap
NoopModuleLoader since no module resolution is needed for cached code.

Refactor build_main_worker_and_inject_sdk to accept a pre-built
Rc<dyn ModuleLoader> instead of the six CDN-specific parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use DataUrlModuleLoader on cache hit for faster Lit Action execution (CPL-258) (#296)

* feat: cache get_wallet_derivation to avoid redundant RPC calls (CPL-257) (#291)

* feat: cache get_wallet_derivation to avoid redundant RPC calls (CPL-257)

Add wallet_derivation cache to BlockchainCache with the same TTL,
capacity, and generation-counter invalidation as the existing permission
caches. Wrap get_wallet_derivation with try_get_with for cache-on-miss
semantics, and add invalidate_for_account to register_wallet_derivation
for write-path consistency. Includes 2 unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: cargo fmt formatting for wallet_derivation_key signature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: generalize module doc wording for cache key parameters

Address Copilot review: "action/wallet combination" was too narrow
now that wallet derivation uses (api_key_hash, wallet) without an
action parameter. Changed to "relevant parameters".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: short-circuit for cached lit action code (CPL-256) (#292)

* feat: add IPFS CID-based code cache for lit action import rewriting (CPL-256)

When execute_js is called with an ipfs_id, the import rewriting result is
cached and reused on subsequent calls with the same CID. This skips the
import parsing step for repeated lit action executions.

The cache is bounded (1,000 entries max) and verifies code integrity via
SHA-256 hash on cache hit to prevent cache poisoning from mismatched CIDs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix rustfmt formatting in runtime.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback on code cache

- Use entry API so existing CID entries can be replaced even when cache
  is at capacity (prevents stale entries becoming permanent)
- Only compute SHA-256 hash when ipfs_id is present (avoids O(n) hashing
  on every execution without a CID)
- Add test for cache-full-but-existing-CID-replacement case

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: collapse nested if-let to satisfy clippy collapsible_if

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: use NoopModuleLoader on cache hit for faster Lit Action execution (CPL-258)

Move the code cache lookup before worker construction so that on cache
hit we can skip building a CdnModuleLoader (with its HTTP client,
integrity manifest, module cache, and lockfile). Instead, pass a cheap
NoopModuleLoader since no module resolution is needed for cached code.

Refactor build_main_worker_and_inject_sdk to accept a pre-built
Rc<dyn ModuleLoader> instead of the six CDN-specific parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use DataUrlModuleLoader instead of NoopModuleLoader on cache hit (CPL-258)

NoopModuleLoader rejects all module loads, including data:text/javascript
URIs produced by the import bundler. Cached actions with imports still
use import("data:...") calls which go through the module loader.

Add a lightweight DataUrlModuleLoader that handles only
data:text/javascript URIs (base64 and plain encoding) without needing
an HTTP client, integrity manifest, or CDN logic. This gives the
cache-hit performance win while supporting bundled imports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove DataUrlModuleLoader, always use CdnModuleLoader on cache hit (CPL-258)

DataUrlModuleLoader duplicated data-URI parsing logic from CdnModuleLoader,
lacked the pre-decode size check, and broke runtime dynamic imports (e.g.
`await import("zod@3.22.4")`) on cache hits. CdnModuleLoader construction
is cheap (shared Arc references), and the real perf win is the action code
cache skipping prepare_action_code — not the module loader choice.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: fix rustfmt formatting for module_loader initialization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: monitor dapp UI updates (CPL-259) (#297)

* feat: monitor dapp UI updates (CPL-259)

- Add accordion-style collapsible sections for Node Configuration,
  Api Payer Accounts, Lit Action Client Configuration, and
  Supported Chain Config Keys
- Remove Dev - Phala from network dropdown
- Move Supported Chain Config Keys to right column
- Default RPC URL to http://localhost:8545 for local development

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback (CPL-259)

- Fix grammar in Api Payer Accounts description
- Use dynamic DOM query for accordion wiring instead of hardcoded ID list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: bundle lit-action imports at cache time (CPL-262) (#299)

* feat: bundle lit-action imports at cache time (CPL-262)

Pre-execution SWC bundler inlines all CDN imports into a single
self-contained script, eliminating await import(...) calls from
cached lit-action code. Runtime loader now rejects any dynamic
import as a regression safety net; params stay out of the cache
and are injected per-execution via IIFE wrapping.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: resolve origin-absolute dep specifiers in bundler (CPL-262)

jsDelivr +esm bundles reference sibling packages using paths like
`/npm/@noble/curves@2.0.1/secp256k1.js/+esm`. `resolve_dep_specifier`
only handled `./`, `../`, and `https://` — absolute-path specifiers
fell through to `parse_npm_specifier` and failed, breaking real-world
imports like `micro-eth-signer`.

Resolve `/`-prefixed specifiers against the base URL the same way as
relative ones, with the allowed-CDN check still enforced.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: reject path-traversal in npm specifier resolution (CPL-262)

parse_npm_specifier built a URL string and relied on the prefix check in
is_allowed_cdn to reject disallowed paths. A specifier like
`pkg@1.0.0/../../gh/evil/x.js` passed the prefix check but resolved to
jsDelivr's mutable /gh/ backend after the HTTP client normalized the
`..` segments at fetch time.

Parse and re-serialize the URL through url::Url first so normalization
happens before the allowlist check, and verify the normalized path still
starts with /npm/ on the cdn.jsdelivr.net host.

This hardens both the new bundler entry path (bundler.rs:76) and the
pre-existing runtime resolve path (CdnModuleLoader::resolve).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* style: apply rustfmt to bundler and cdn_module_loader

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Update lit-actions/server/runtime.rs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* ci: run cargo test on PRs in Rust CI workflow (#301)

Extend the existing rust-ci matrix (fmt, clippy, build) with cargo test
--all-features for each crate.

Gate /proc-based CPU overload tests to Linux only. Skip dstack socket
integration tests when no Unix socket is present so all-features tests
pass on macOS and standard CI; phala-simulator still exercises them
with a real socket.

Made-with: Cursor

* perf: parallelize bundler dep graph walk (CPL-263) (#303)

* perf: fetch bundler dep graph layers concurrently (CPL-263)

The BFS walk in walk_deps previously awaited each fetch_module_bytes call
serially, so a graph of N modules took N×latency on a cold cache even though
siblings could have been fetched in parallel. Replacing the while-loop with
try_join_all per BFS layer makes cold-path bundling scale with graph depth
instead of module count. Added elapsed_ms spans on the walk, each layer,
and the SWC bundle+codegen phase so the win is observable in traces.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: enforce MAX_MODULE_COUNT in walk_deps + add coverage (CPL-263)

Address two Copilot review comments on PR #303.

(1) Enforce per-execution module cap in walk_deps before launching each
BFS layer. The same check inside fetch_module_bytes reads
loaded_modules.len() and acts on it, which is racy under try_join_all:
every concurrent fetch in a wide layer sees the pre-layer count, all
pass, and the cap is silently bypassed (DoS / resource-spike risk).
Bound seen.len() in walk_deps for a deterministic gate.

(2) Add unit coverage for walk_deps:
- walk_deps_collects_transitive_graph: 3-module entry → a → b graph,
  exercises layered fetch + dedup + dep discovery via a cache-only loader.
- walk_deps_enforces_max_module_count_on_wide_layer: 101-import entry,
  must bail before fetching any layer.

Tests use a CdnModuleLoader pre-seeded with a ModuleCache so the
cache-hit path returns without HTTP traffic.

cargo test -p lit-actions-server: 68 passed, 0 failed.

* style: collapse bail! call to satisfy rustfmt (CPL-263)

The multi-line bail! introduced in b09096c failed cargo fmt --check on
CI. Collapse to a single line.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* feat: pool of pre-warmed JsRuntimes built from the snapshot (CPL-265) (#305)

* feat: pool of pre-warmed JsRuntimes built from the snapshot (CPL-265)

Adds a pool of pre-bootstrapped Deno `MainWorker` instances (default 10
via `LIT_ACTIONS_POOL_SIZE`, set to 0 to disable). Each pooled worker
handles exactly one request and is then dropped; the pool refills
asynchronously. Pattern: Cloudflare Workers / Deno Deploy — isolates
are cheap to start from a snapshot, expensive to scrub for reuse.

Bootstrap is split: warm-time builds the worker + module loader; request
time injects `LitNamespace.js`, then `PatchDeno.js`, wires op_state,
runs the controller thread, executes user code, and clears the request
context. `LoadedModules` is a single `Arc` shared between
`CdnModuleLoader` and op_state per worker, and a `#[cfg(test)]` accessor
on `PreparedWorker` lets us assert each pre-warmed worker has a fresh
`Arc` (no per-tenant module-state leakage).

One-shot lifecycle is type-enforced: `execute_with_worker(prepared:
PreparedWorker, ...)` consumes by value. Dispatch in `server.rs`
short-circuits empty code before `try_acquire`, and routes requests
with a non-default `memory_limit_mb` to the legacy cold path because
V8's `CreateParams::heap_limits` is immutable post-creation.

Bootstrap failures are wrapped in `catch_unwind` with exponential
backoff. After 5 consecutive failures a half-open circuit breaker
opens for 60s; one trial refill on cooldown success closes it,
failure re-opens with a fresh window. `TrySendError::Full` (a one-shot
invariant violation) is counted separately from `Disconnected` (a
normal dead-handle event). `catch_unwind` cannot catch aborts /
SIGTRAP / FFI UB through V8, which is documented in the module header.

7 worker_pool unit tests (breaker transitions, counters, disabled
pool), 3 integration tests (`pool_warm_hit`, `pool_memory_limit_bypass`,
`pool_concurrent_exhaustion`), and a `LoadedModules` Arc-identity
unit test in `runtime.rs`. Full suite passes under both
`LIT_ACTIONS_POOL_SIZE=10` and `=0`: 74 unit + 25 integration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* style: cargo fmt + collapse nested if-let in dispatch_execute_request

CI requires `cargo fmt --check` and `cargo clippy --all-features -- -D
warnings` to pass. Pre-existing CPL-265 commit had:

- Multi-line formatting that rustfmt collapses onto single lines.
- Nested `if can_pool { if let Some(handle) = ... }` that
  clippy::collapsible_if rejects under let-chains (1.91).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: address Copilot review feedback on CPL-265

- runtime.rs: drop per-request String allocation in privacy-mode header
  checks (LitNamespace.js + Params.js) by using is_some_and instead of
  unwrap_or(&"false".to_string()).
- server.rs: switch the empty-code shortcut from try_send to
  send_async().await so the response isn't dropped when the dispatcher
  fires before the tonic stream consumer starts polling the bounded(0)
  outbound channel.
- worker_pool.rs: fix backoff off-by-one so the first refill failure
  waits 50ms (BASE_MS << 0), matching the documented 50/100/200/.../5000
  sequence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* feat: add Jaeger step + --no_code flag to local_test.sh (CPL-266) (#306)

* feat: add Jaeger step + --no_code flag to local_test.sh (CPL-266)

Adds a new step 4 that spins up Jaeger via docker (UI on default port
16686, OTLP gRPC on 4317, OTLP HTTP on 4318) so locally-run services
can export traces. The existing cargo/static-server steps are
renumbered 5-7.

Adds a --no_code flag that skips steps 5-7 and prints the cargo
commands needed to start lit-api-server, lit-actions, and lit-static
manually, with both a normal variant and an OTEL variant pointed at
the local Jaeger instance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Update local_test.sh

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* extra instrumentation

* enable pooling if MB request is <= the default ( uses more memory, but catches pool size )

* fix: restore strict pool gate and randomize CI attestation port

Pool workers are bootstrapped with a fixed 128 MB V8 heap and that limit
is immutable post-creation, so serving a sub-default memory_limit request
from a pool worker misreports the OOM message and silently masks OOMs
between the requested limit and 128 MB. Revert the gate from `<=` to
`==` so any custom memory_limit bypasses the pool. Fixes the `oom` and
`pool_memory_limit_bypass` integration tests.

Bind lit-api-server to a random free TCP port (via ROCKET_PORT) in the
Phala simulator workflow so concurrent jobs on the same self-hosted
runner host don't collide on the default Rocket port 8000.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(lit-actions): lower DEFAULT_MEMORY_LIMIT_MB to 64

Halves the pool worker bootstrap heap from 128 MB to 64 MB. Update the
pool_memory_limit_bypass integration test to send memory_limit=100 so it
still exercises a non-default value (64 now matches the default and
would route through the pool).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Adam Reif <adam@litprotocol.com>
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