Skip to content

fix(lua): fee-sponsored compute-cap bypass + replay fail-open#225

Merged
lgalabru merged 12 commits into
split/pr216-ci-harness-megafrom
fix/lua-security-hardening
Jul 10, 2026
Merged

fix(lua): fee-sponsored compute-cap bypass + replay fail-open#225
lgalabru merged 12 commits into
split/pr216-ci-harness-megafrom
fix/lua-security-hardening

Conversation

@EfeDurmaz16

@EfeDurmaz16 EfeDurmaz16 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

fix(lua) — Lua security hardening on the #219 base, scoped as one language PR per the maintainer split policy. Same re-delivery model as #222/#224.

Criticals (each with a planted regression that fails pre-fix)

  1. Fee-sponsored compute-cap bypass (false-paid)solana/verifier.luainstructions.parse_compute_budget applied the general 5,000,000 microLamport cap even on server-funded charges. A co-signed charge could set a ~200x priority fee (merchant drain, audit fix: better compatibility / decimals handling #25). The hooks path already had the tight cap; the real verifier path (the one the adapter uses) missed it. Now derives fee_sponsored = fee_payer ~= nil and enforces the tight 10,000 cap — matching Rust validate_compute_budget_instruction(ix, fee_payer.is_some()) and Ruby verifier.rb. Client-paid charges keep the general cap.
  2. Replay fail-openbuild_mpp_server fell back to a volatile in-memory replay store on every network with only a warning. On multi-worker devnet/mainnet a settled signature could be replayed for a second on-chain settlement (double-spend). Now fails CLOSED outside localnet: a durable, process-shared replay_store is required, mirroring the Ruby Mpp.create requirement. BREAKING — a non-localnet MPP deployment on the silent volatile fallback now errors until config.mpp.replay_store is wired; affected in-tree tests updated to inject a store, and a Kong operator doc note added.

Review follow-ups

  • x402 confirmation before replay consume — settlement now polls with bounded retries until the signature reaches confirmed/finalized, tolerates propagation lag and transient RPC errors, and only then atomically consumes replay state.
  • Shared replay protection end-to-end — direct MPP server construction and Kong/APISIX runtime wiring reject process-local replay stores outside localnet; regression tests cover the supported local fallback and production fail-closed paths.
  • Conformance identity preserved — the Lua runner emits language: "lua" and preserves a recoverable vector ID even when malformed JSON is rejected, so harness accounting cannot silently lose the case.

Verification

…ardening)

Two security criticals that survived #216 rated only "required":
- Fee-sponsored compute-unit-price cap bypass (false-paid): the real MPP
  verifier (solana/verifier.lua -> instructions.parse_compute_budget) applied
  the general 5,000,000 microLamport cap even on server-funded charges, letting
  a co-signed charge set a ~200x priority fee (merchant drain, audit #25). Now
  passes fee_sponsored=(fee_payer~=nil) and enforces the tight 10,000 cap,
  matching the Rust spine validate_compute_budget_instruction and the Ruby
  verifier. Client-paid charges keep the general cap.
- Replay fail-open: build_mpp_server fell back to a volatile in-memory replay
  store on every network with only a warning; on multi-worker devnet/mainnet a
  settled signature could be replayed for a second on-chain settlement. Now
  fails CLOSED outside localnet (requires a durable, process-shared
  replay_store), mirroring the Ruby Mpp.create requirement. BREAKING.

Plus the #216 Lua hardening on the #219 base. Regression tests added for both.
busted 617 passed/1 skipped/0 failed; luacheck 0/0; coverage 90.26% >= 90.
@EfeDurmaz16
EfeDurmaz16 requested a review from lgalabru July 9, 2026 19:03
@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown

Greptile Summary

This PR hardens the Lua pay-kit implementation with two critical security fixes — fee-sponsored compute-cap enforcement (audit #25) and replay fail-closed outside localnet — plus the confirm-before-consume ordering fix for x402 broadcast flow. The changes are well-scoped and the core security logic is correct.

  • Fee-sponsored compute cap (audit fix: better compatibility / decimals handling #25): instructions.parse_compute_budget now accepts a fee_sponsored flag; verifier.lua passes fee_payer ~= nil so server-funded charges enforce the tight 10,000 microLamport cap instead of the general 5,000,000 cap that allowed ~200x priority-fee drain.
  • Replay fail-closed: MPP server construction errors on non-localnet without a durable, process-shared replay store; the is_shared() protocol on MemoryStore and SharedDictStore drives the check; _finalize_verification now verifies inner reservations against the actual store rather than trusting a bare consumed = true flag.
  • Confirm-before-consume (x402): await_confirmed_or_finalized polls with bounded retries before the atomic consume_signature call, preventing the pay-but-uncredited race described in the prior review thread.

Confidence Score: 5/5

The two critical security fixes are logically correct and well-covered by regression tests; no correctness issues were found in the changed code paths.

The fee-sponsored compute cap, replay fail-closed enforcement, and confirm-before-consume reordering all implement the stated invariants correctly. Store interactions are exercised by targeted tests. The only findings are code-quality issues: replay_store_is_shared is defined identically in three modules, and default_sleep has a CPU busy-wait fallback that would only fire in environments missing both ngx and LuaSocket.

The duplicated replay_store_is_shared in mpp/server/init.lua, mpp/init.lua, and mpp/server/charge_handler.lua is the most maintenance-sensitive area; divergence between the three copies would silently weaken the shared-store gate on one path.

Important Files Changed

Filename Overview
lua/pay_kit/solana/instructions.lua Adds MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS_FEE_SPONSORED = 10000 and a fee_sponsored parameter to parse_compute_budget; correctly gates the tight cap on the caller-supplied flag
lua/pay_kit/solana/verifier.lua Wires fee_payer ~= nil into parse_compute_budget inside validate_allowlist; fix is minimal and precisely mirrors the Rust spine
lua/pay_kit/protocols/mpp/server/init.lua Adds fail-closed replay-store enforcement at construction time and hardens _finalize_verification to verify inner reservations rather than trusting a bare consumed flag; replay_store_is_shared is duplicated here and in two sibling modules
lua/pay_kit/protocols/mpp/init.lua Fails closed for non-localnet MPP without a durable replay store; passes the same store to both charge_handler and mpp_server; replay_store_is_shared duplicated from sibling modules
lua/pay_kit/protocols/x402/init.lua Reorders broadcast to confirm to consume; adds await_confirmed_or_finalized with bounded retries; requires an explicit replay store at construction; default_sleep has a CPU busy-wait as a last-resort fallback
lua/pay_kit/protocols/mpp/server/charge_handler.lua Adds shared-store validation; as_callback now always uses CONSUMED_PREFIX and silently ignores the previously-documented replay_key_prefix option
lua/pay_kit/protocols/x402/exact/verify.lua Rule 5 authority guard refined to cover multisig signer-tail accounts and derived source ATAs without sweeping non-fund-mover accounts
lua/pay_kit/util/uint.lua Adds M.exact to coerce RPC-decoded Lua doubles to exact decimal strings, rejecting lossy/scientific-notation values before amount comparison
lua/pay_kit/util/json.lua Adds proper surrogate-pair handling for JSON unicode escapes; tags parsed arrays/objects with distinct metatables so is_array no longer relies on key iteration
lua/pay_kit/protocols/mpp/server/solana_verify.lua Uses uint.exact to coerce jsonParsed-decoded lamports/token amounts through pcall, treating lossy doubles as no-match rather than raising
lua/pay_kit/protocols/mpp/store.lua Adds MemoryStore:is_shared() returning false, enabling the shared-store check to reject process-local stores on non-localnet
lua/pay_kit/protocols/mpp/server/store_shared_dict.lua Adds SharedDictStore:is_shared() returning true; the existing get method is relied upon by _finalize_verification to verify inner reservations
lua/plugins/kong/plugins/pay-kit/init.lua Adds mpp_replay_store() helper that wires ngx.shared.mpp_replay as the shared replay store; documents the lua_shared_dict requirement for non-localnet MPP

Reviews (9): Last reviewed commit: "Merge remote-tracking branch 'origin/spl..." | Re-trigger Greptile

Comment thread lua/pay_kit/protocols/x402/init.lua Outdated
@EfeDurmaz16

Copy link
Copy Markdown
Collaborator Author

@greptile-apps please review

@lgalabru
lgalabru merged commit 3a6d749 into split/pr216-ci-harness-mega Jul 10, 2026
15 checks passed
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