Skip to content

vms/txs/mempool: add AdmissionVerifier hook for encrypted-payload txs (#115)#128

Open
abhicris wants to merge 1 commit into
luxfi:mainfrom
abhicris:feat/2026-06-02-mempool-encrypted-payload-admit
Open

vms/txs/mempool: add AdmissionVerifier hook for encrypted-payload txs (#115)#128
abhicris wants to merge 1 commit into
luxfi:mainfrom
abhicris:feat/2026-06-02-mempool-encrypted-payload-admit

Conversation

@abhicris
Copy link
Copy Markdown

@abhicris abhicris commented Jun 1, 2026

Problem

Issue #115 asks for an admission policy that lets validators accept FHE-ciphertext transactions on (signature + fee + NIZK) without decryption. The mempool's Add() today exits before any extension point — there's no hook a confidential-tx variant can plug into.

The issue lists four items:

  1. Extended transaction type carrying (ciphertext, NIZK proof)
  2. Admission hook running signature + fee + NIZK without decryption
  3. Per-account FIFO inside the encrypted-payload pool partition
  4. Budget enforcement via the FHE precompile's bootstrap meter

This PR lands item 2, the only piece that is strictly mempool-side. Items 1, 3, 4 are downstream consumers and can land independently against this hook.

What this changes

Adds a generic admission-hook interface and a new constructor that wires it through Add():

type AdmissionVerifier[T Tx] interface {
    VerifyAdmit(tx T) error
}

var ErrAdmissionRejected = errors.New("tx admission rejected")

func NewWithAdmissionVerifier[T Tx](
    metrics Metrics,
    verifier AdmissionVerifier[T],
) *mempool[T]

New[T Tx](metrics Metrics) becomes a thin wrapper around NewWithAdmissionVerifier[T](metrics, nil) — behavior is byte-identical for every existing caller (a nil verifier is the no-op gate).

The verifier runs last in Add(), after the existing duplicate / size / space / conflict checks. Ordering rationale: cheap rejects fire first, so NIZK verification cost is only paid on a tx that passes structural admission. A non-nil verifier return is wrapped in ErrAdmissionRejected, records the drop reason via the existing droppedTxIDs LRU, and the tx is never inserted.

What this does NOT do

  • It does not define the encrypted-payload tx type or its NIZK proof format. Those live in vms/platformvm/txs (or wherever the consumer decides) and ship in a follow-up against this hook.
  • It does not wire the FHE precompile's bootstrap-meter consultation — callers implementing AdmissionVerifier do that themselves.
  • It does not add the per-account FIFO partition for encrypted txs. The existing unissuedTxs Hashmap is per-mempool; the partition is a separate construction over multiple mempool instances and is a follow-up.

The point of landing the hook first is that all three follow-ups can be developed against a stable mempool surface instead of racing against churn in Add().

Acceptance

CGO_ENABLED=0 go test -run TestAdmissionVerifier -count=1 ./vms/txs/mempool/... passes locally (verified). The four new tests are:

Test Asserts
TestAdmissionVerifier_nilVerifierMatchesNew NewWithAdmissionVerifier(metrics, nil) is byte-identical to New(metrics) — Add succeeds, Len reflects the tx.
TestAdmissionVerifier_acceptsWhenVerifierReturnsNil A nil-returning verifier admits the tx; the gate fires exactly once on Add and not on Get / Peek / Iterate / Remove.
TestAdmissionVerifier_rejectsWhenVerifierReturnsError An error-returning verifier rejects with an error that wraps both ErrAdmissionRejected and the verifier's specific reason; the drop reason is recorded for inspection.
TestAdmissionVerifier_cheapChecksShortCircuit Duplicate / oversize / conflict short-circuit before the verifier runs — verification cost is never paid on a tx that would have been dropped anyway.

Existing tests in mempool_test.go are untouched and continue to exercise New (the nil-verifier path).

Cost

  • API: New[T Tx](metrics Metrics) signature unchanged; one new exported type (AdmissionVerifier[T Tx]), one new exported var (ErrAdmissionRejected), one new exported constructor (NewWithAdmissionVerifier).
  • Hot path: one nil-check on every Add() for callers that don't configure a verifier. Negligible.
  • Maintenance: the hook is generic over T Tx, so any future tx variant can plug in without touching this package again. The encrypted-payload tx type, the FIFO partition, and the budget meter consultation are all out of scope here and can land independently in their owning packages.

Refs: closes #115 (this repo), LP-066 (F-Chain confidential compute), LP-183 (ZAP envelope decode precompile — downstream consumer for cross-subnet propagation of encrypted txs), luxfi/threshold#20 (real distributed FHE decryption, the block-proposer-side counterpart to this admission-side hook).

…luxfi#115)

Closes luxfi#115 by landing the admission-hook half of the issue.
The hook is the substrate the encrypted-mempool partition will use to
admit FHE-ciphertext transactions on (signature + fee + NIZK) without
decryption per LP-066 / luxfi/precompile/fhe.

Surface added:

  type AdmissionVerifier[T Tx] interface {
      VerifyAdmit(tx T) error
  }

  var ErrAdmissionRejected = errors.New("tx admission rejected")

  func NewWithAdmissionVerifier[T Tx](
      metrics Metrics,
      verifier AdmissionVerifier[T],
  ) *mempool[T]

  // New is now a thin wrapper around NewWithAdmissionVerifier(metrics, nil).
  // No behavioral change for existing callers.

Add() invokes verifier.VerifyAdmit last, after duplicate / size / space /
conflict have all passed. Ordering rationale: cheap rejects fire first,
so NIZK verification cost is only paid on a tx that passes structural
admission. A non-nil verifier return is wrapped in ErrAdmissionRejected,
records the drop reason via the existing droppedTxIDs LRU, and never
inserts the tx.

What this does NOT do:
  - It does not define the encrypted-payload tx type or its NIZK proof
    format. Those live in vms/platformvm/txs (or wherever the consumer
    decides) and ship in a follow-up.
  - It does not wire the FHE precompile's bootstrap-meter consultation —
    callers implementing AdmissionVerifier do that themselves.
  - It does not add the per-account FIFO partition for encrypted txs.
    The existing unissuedTxs Hashmap is per-mempool; the partition is
    a separate construction over multiple mempool instances and is a
    follow-up.

What it DOES do: lands the only mempool-side hook the issue requires
without locking us in on the encrypted-tx representation. The hook is
generic (`AdmissionVerifier[T Tx]`) so any future Tx variant can plug
in the same way.

Tests (CGO_ENABLED=0 go test -run TestAdmissionVerifier ./vms/txs/mempool/...
all pass):
  - nil verifier matches New (byte-identical behavior)
  - verifier returning nil admits the tx; gate fires exactly once on
    Add and not on Get / Peek / Iterate / Remove
  - verifier returning an error rejects with ErrAdmissionRejected
    wrapping the verifier's reason; drop reason recorded
  - cheap checks short-circuit before the gate runs (duplicate /
    oversize / conflict all skip the gate)

Refs:
  - issue luxfi#115 (this repo)
  - LP-066 (F-Chain confidential compute)
  - LP-183 (ZAP envelope decode precompile; downstream consumer once
    encrypted-payload tx propagation through gossip lands)
  - luxfi/threshold issue luxfi#20 (real distributed FHE decryption; the
    block-proposer-side counterpart to this admission-side hook)
@abhicris abhicris requested a review from hanzo-dev as a code owner June 1, 2026 17:03
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.

Mempool: admit encrypted-payload transactions on signature + NIZK without decryption

1 participant