Skip to content

feat: per-key spending-rule enforcement for Lambda parity (PRs 1-3) - #477

Open
clawdbot-glitch003 wants to merge 12 commits into
mainfrom
glitch003/lambda-parity-payments-storage
Open

clawdbot-glitch003 wants to merge 12 commits into
mainfrom
glitch003/lambda-parity-payments-storage

Conversation

@clawdbot-glitch003

@clawdbot-glitch003 clawdbot-glitch003 commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Implements the Chipotle gateway controls that make a usage API key safe to embed in a frontend, with blast-radius parity to a public AWS Lambda URL (see plans/chipotle-lambda-parity.md). One branch, logical layers as separate commits. Now merged with main (storage layout: the deployed pkpIdToOwnerMaster mapping stays first, usageKeyHasSpendingRules is appended after it).

PR 1 — lit-payments storage. New spending_rules + spending_usage tables (migration) and a spending module: Operator-authed CRUD under /api/spending-rules for the admin UI, and ServiceAuth-authed /internal endpoints for the gateway to fetch rules, record rolling spend, and (new) write/clear rules on behalf of an account owner. Disabled (503) unless a token is set — accepts INTERNAL_SERVICE_TOKEN, falling back to the existing LIT_INTERNAL_SHARED_SECRET, so no second secret is required.

PR 3 — AccountConfig contract. Adds the on-chain hasSpendingRules gate: a new mapping appended to the end of root storage (storage-safe), a setSpendingRulesFlag writer, and canExecuteActionWithSpendingRules / getSpendingRulesFlag views. Rust bindings are regenerated (make generate, forge 1.5.1) and the server calls the new functions through the normal AccountConfig instance. The diamond-cut deploy of the new Views/Writes facets is the remaining ops step.

PR 2 — gateway enforcement. Reads the flag in the same multicall as the execute check (cached in BlockchainCache), so keys without rules pay zero extra work. For a flagged key it enforces, in order:

  • origin allowlist (P2.1) — browser Origin must match an entry (scheme://host[:port], leading *. host wildcard); missing/unlisted → 403,
  • per-key rate limit — token bucket (429),
  • per-client-IP rate limit (P0.2) — second bucket keyed by (key, IP) using Rocket's client_ip (X-Real-IP behind the ingress; see guards::rate_limit for the trust model), idle buckets swept (429),
  • rolling spend cap (402),
  • per-node concurrency cap (429),

recording spend off the response path. Both /lit_action and /lit_binary_action go through the same check so a frontend key can't sidestep its rules via the binary runner. Inert unless LIT_PAYMENTS_INTERNAL_URL + a token are set and the key's flag is on.

PR 4 — owner API wires DB → on-chain flag. Master-key-authed account-management endpoints:

  • POST /set_spending_rules — stores the row in lit-payments, then setSpendingRulesFlag(account, usage, true) (ownership proven by the contract's NoAccountAccess revert),
  • POST /remove_spending_rules — flag off, then delete row + usage counter,
  • GET /get_spending_rules?usage_api_key= — rules, current-window spend, and on_chain_flag (surfaces a half-failed write for reconciliation).

k6 client regenerated for the new endpoints. cargo clippy --tests is clean for both crates; 360 lit-api-server and 151 lit-payments unit tests pass (incl. new origin-matching, per-IP bucket, and admit-path tests).

Still not in this PR (ops / follow-ups): Safe diamond-cut of the new facets (until then set_spending_rules reverts with an unknown-selector error and the gateway's view call fails → cached as "no rules", i.e. fail-open); SWR background refresh for the rules cache (TTL + inline cold-miss fetch today); dashboard UI for the new endpoints (P2.2); spend-cap auto-disable; verifying the dstack ingress sets a trustworthy X-Real-IP so per-IP limits are per-client rather than global.

🤖 Generated with Claude Code

glitch003 and others added 3 commits June 4, 2026 18:02
… PR 1)

First slice of the Lambda-parity work (plans/chipotle-lambda-parity.md): the
durable home for per-key blast-radius controls the gateway will enforce.

- Migration: spending_rules (rolling spend cap+window, rate rps+burst,
  concurrency, origin allowlist, enabled) and spending_usage (rolling spend
  counter with in-SQL window reset).
- New `spending` module: typed rows + validated upsert request, sqlx db layer
  (upsert/get/list/delete rules, atomic record_charge with window reset),
  ServiceAuth bearer guard for gateway calls, and routes.
- Operator-authed CRUD under /api/spending-rules (admin UI) and
  ServiceAuth-authed /internal endpoints (gateway: fetch rules for its cache,
  record spend off the response path).
- INTERNAL_SERVICE_TOKEN config; /internal endpoints disabled (503) when unset.

No production behavior change — nothing reads these tables yet. 9 unit tests
cover request validation, hash canonicalization, and the token comparison.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…parity PR 3)

The on-chain zero-latency gate for per-key spending rules. All additive and
storage-safe: a new mapping appended to the END of root AccountConfigStorage
(not a field on the inline-embedded UsageApiKey struct, which would shift the
Account layout).

- AppStorage: append `mapping(uint256 => bool) usageKeyHasSpendingRules`.
- ViewsFacet: `getSpendingRulesFlag(apiKeyHash)` and
  `canExecuteActionWithSpendingRules(apiKeyHash, cidHash) -> (canExecute,
  hasSpendingRules)` so the gateway reads both in one RPC.
- WritesFacet: `setSpendingRulesFlag(accountApiKeyHash, usageApiKeyHash, bool)`
  + event, same account-access control as setUsageApiKey.

Source only; `forge build` passes. Regenerate the Rust bindings + diamond ABI
with `make generate` on the canonical toolchain, then deploy via Safe diamond-cut
on Base (both ops steps). The gateway reads the new view through a scoped sol!
interface, so it does not depend on the regenerated giant binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(lambda-parity PR 2)

Gateway side of the Lambda-parity work. Reads the on-chain hasSpendingRules gate
in the same multicall as the execute-permission check; keys without rules pay
zero extra work.

- accounts: can_execute_action_with_spending_rules -> (can_execute,
  has_spending_rules), cached in BlockchainCache (new execute_and_spending
  entry, same generation invalidation). Reads the new view via a scoped sol!
  interface (read_only_client_and_address helper) so it tracks the workspace
  alloy version instead of the regenerated giant binding.
- core::spending_rules: flag-gated enforcer. On a flagged key it fetches rules
  (cached, TTL) from lit-payments /internal, then enforces a rolling spend cap
  (402), a per-node token-bucket rate limit (429), and a per-node concurrency
  cap (429). Spend is recorded off the response path (in-memory + fire-and-
  forget POST). Inert unless LIT_PAYMENTS_INTERNAL_URL + INTERNAL_SERVICE_TOKEN
  are set AND the key's on-chain flag is on.
- Wired into core_features::lit_action + the /lit_action route + main state.
- 429 (too_many_requests) ApiStatus helper.

Origin allowlist (P2.1) deferred. SWR background refresh for the rules cache is
a marked follow-up (currently TTL with inline cold-miss fetch). cargo check
clean; 4 unit tests for the token bucket, window reset, and hash format.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clawdbot-glitch003
clawdbot-glitch003 requested a review from a team June 10, 2026 01:13
glitch003 and others added 2 commits June 11, 2026 15:32
…ty-payments-storage

# Conflicts:
#	lit-api-server/src/accounts/signable_contract.rs
#	lit-api-server/src/main.rs
#	lit-payments/src/config.rs
#	lit-payments/src/main.rs
Brings the two design plans from PR #446 into this branch so they ship with the
lambda-parity implementation they describe, and #446 can be closed.

- plans/chipotle-lambda-parity.md — prioritized gateway work (this PR implements
  P0.0/P0.1/P0.2/P1) for per-key blast-radius parity with a public Lambda URL.
- plans/private-apps-backend.md — the backend-as-Lit-Actions framework these
  gateway controls unlock (relay becomes optional).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
glitch003 and others added 7 commits September 15, 2026 12:06
…-477-remaining-work

# Conflicts:
#	lit-api-server/blockchain/lit_node_express/contracts/AccountConfigFacets/AppStorage.sol
…ED_SECRET for /internal

The five hand-built test Configs were missing the new internal_service_token
field. Also let the spending /internal endpoints fall back to the
LIT_INTERNAL_SHARED_SECRET that lit-api-server and lit-payments already share,
so enabling spending rules needs no second secret.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mit (P0.2)

Gateway enforcement for flagged keys now also checks:
- allowed_origins: the browser Origin must match an entry
  (scheme://host[:port], leading *. wildcard on host). Missing Origin -> 403.
- ip_rate_limit_rps/burst: a second token bucket keyed by (key, client IP),
  using Rocket's client_ip (X-Real-IP behind the ingress; see guards::rate_limit
  for the trust model). Per-IP buckets are swept when idle past 1h.

Both are read from the same lit-payments row (new columns + validation).
A SpendingContext request guard carries Origin + client IP into admit().
/lit_binary_action now runs the same combined execute+spending check and
admission as /lit_action so a frontend key can't sidestep its rules there.
Fixes the post-merge compile break (can_execute_action import).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the on-chain flag

Master-key-authed account-management endpoints close the loop the PR body
listed as unwired:
- POST /set_spending_rules  -> PUT rules to lit-payments (/internal, service
  auth), then setSpendingRulesFlag(account, usage, true). Ownership is proven
  by the contract (NoAccountAccess revert) before any row is written for an
  already-flagged key, and by the flag tx otherwise.
- POST /remove_spending_rules -> flag false, then delete row + usage counter.
- GET  /get_spending_rules   -> rules + window spend + on_chain_flag (exposes
  a half-failed write for reconciliation).

setSpendingRulesFlag/getSpendingRulesFlag are called via the scoped sol!
interface (no binding regen needed). Both key caches are invalidated on write.
lit-payments gains ServiceAuth PUT/DELETE /internal/spending-rules/<hash>.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… spending-rules calls

CI's contract-bindings check requires the generated binding to match the
facets, so regenerate it (forge 1.5.1, make generate). With the new functions
in the binding, drop the scoped sol! interface and the address helper and call
canExecuteActionWithSpendingRules / getSpendingRulesFlag / setSpendingRulesFlag
through the regular AccountConfig instance like every other call.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ndpoints

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…_rules migration

- docker-compose.phala.yml reads LIT_PAYMENTS_INTERNAL_URL; deploy-staging and
  manual_phala-envs-update pass vars.LIT_PAYMENTS_STAGING_URL (the same
  lit-payments the next/dev dashboards already talk to). Prod workflows are
  untouched: unset -> the feature stays disabled there.
- Migration renumbered 20260604 -> 20260915 so it sorts after everything already
  applied on the staging database (it has never been applied anywhere).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants