From c059ba1f7a613ac51a86fc1c388ffdaf94a1c8dd Mon Sep 17 00:00:00 2001 From: makemake Date: Wed, 19 Aug 2026 12:58:07 +0200 Subject: [PATCH] feat(lighter): add experimental gnark PLONK proof assertion --- examples/lighter/README.md | 5 ++ .../src/LighterBatchProofAssertion.sol | 57 +++++++++++++++++++ src/PhEvm.sol | 18 ++++++ 3 files changed, 80 insertions(+) create mode 100644 examples/lighter/src/LighterBatchProofAssertion.sol diff --git a/examples/lighter/README.md b/examples/lighter/README.md index c9390e8..b5822a7 100644 --- a/examples/lighter/README.md +++ b/examples/lighter/README.md @@ -37,6 +37,10 @@ The funds-custody outflow rate limit is a separate, independently deployable ass drain past a rolling-window TVL fraction during normal operation; the breaker stands down in desert mode so it never blocks the mass exits the escape hatch exists to enable. Deploy one instance per watched ERC-20 token. +- **Experimental batch-proof verification** (`LighterBatchProofAssertion`) — re-verifies the + one-public-input gnark BN254 PLONK proof attached to each `verifyBatch` call using the executor's + compiled-in VK registry. It is bound to the configured Lighter proxy and fails closed on malformed + calldata, an unknown VK, or a rejected proof. ## Files @@ -46,6 +50,7 @@ The funds-custody outflow rate limit is a separate, independently deployable ass - `src/LighterBridgeHelpers.sol` — fork-aware reads. - `src/LighterBridgeAssertion.sol` — state-machine + desert-mode bundle (trigger wiring + invariants). - `src/LighterOutflowCircuitBreaker.sol` — standalone rolling-window collateral outflow breaker. +- `src/LighterBatchProofAssertion.sol` — Experimental registry-backed gnark PLONK batch-proof guard. - `test/LighterBridgeAssertion.t.sol` — honest + malicious behavior per state-machine invariant. - `test/LighterOutflowCircuitBreaker.t.sol` — breaker decision logic + constructor guards. diff --git a/examples/lighter/src/LighterBatchProofAssertion.sol b/examples/lighter/src/LighterBatchProofAssertion.sol new file mode 100644 index 0000000..6f6c36e --- /dev/null +++ b/examples/lighter/src/LighterBatchProofAssertion.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {Assertion} from "credible-std/Assertion.sol"; +import {PhEvm} from "credible-std/PhEvm.sol"; +import {AssertionSpec} from "credible-std/SpecRecorder.sol"; + +/// @title LighterBatchProofAssertion +/// @notice Independently verifies Lighter's submitted gnark PLONK batch proof. +/// @dev This experimental assertion is deliberately bound to one L1 proxy and +/// one compiled-in verifier key. It validates the matching call context, +/// then checks the batch commitment and proof extracted from verifyBatch. +contract LighterBatchProofAssertion is Assertion { + bytes4 internal constant VERIFY_BATCH_SELECTOR = 0x23ff50e1; + bytes32 internal constant LIGHTER_GNARK_PLONK_VK_ID = + 0x7856ff107f35077ed23aa1cbd1a4ec95204585dc675e0d3632231c49e48109d0; + + address public immutable lighterProxy; + + /// @notice `StoredBatchInfo` has eleven static ABI words in the pinned Lighter ABI. + /// @dev Words are retained verbatim because this assertion only needs the final commitment. + struct StoredBatchInfo { + bytes32[11] words; + } + + constructor(address lighterProxy_) { + require(lighterProxy_ != address(0), "LighterBatchProof: zero proxy"); + lighterProxy = lighterProxy_; + registerAssertionSpec(AssertionSpec.Experimental); + } + + function triggers() external view override { + registerFnCallTrigger(this.assertBatchProof.selector, VERIFY_BATCH_SELECTOR); + } + + /// @notice Verifies the proof submitted with the exact triggering batch. + /// @dev Rejects calls from any adopter other than the configured Lighter proxy, + /// malformed verifyBatch calldata, and algebraically invalid proofs. + function assertBatchProof() external view { + require(ph.getAssertionAdopter() == lighterProxy, "LighterBatchProof: wrong adopter"); + PhEvm.TriggerContext memory ctx = ph.context(); + bytes memory input = ph.callinputAt(ctx.callStart); + require(input.length >= 4, "LighterBatchProof: malformed calldata"); + require(bytes4(input) == VERIFY_BATCH_SELECTOR, "LighterBatchProof: wrong selector"); + + (StoredBatchInfo memory batch, bytes memory proof) = abi.decode(_withoutSelector(input), (StoredBatchInfo, bytes)); + require( + ph.verifyGnarkPlonkProof(proof, batch.words[10], LIGHTER_GNARK_PLONK_VK_ID), + "LighterBatchProof: invalid proof" + ); + } + + function _withoutSelector(bytes memory input) private pure returns (bytes memory args) { + args = new bytes(input.length - 4); + for (uint256 i; i < args.length; ++i) args[i] = input[i + 4]; + } +} diff --git a/src/PhEvm.sol b/src/PhEvm.sol index e4edccc..fcf3157 100644 --- a/src/PhEvm.sol +++ b/src/PhEvm.sol @@ -540,6 +540,24 @@ interface PhEvm { /// @return ctx The inflow rate-of-change context for the current invocation. function inflowRate() external view returns (FlowRateContext memory ctx); + // --------------------------------------------------------------- + // Experimental: gnark BN254 PLONK verification + // --------------------------------------------------------------- + + /// @notice Verifies a registry-backed gnark BN254 PLONK proof. + /// @dev EXPERIMENTAL: requires registerAssertionSpec(AssertionSpec.Experimental). + /// The executor accepts only compiled-in verifier-key identifiers; callers + /// cannot provide verifier-key bytes. This currently supports the demonstrated + /// one-public-input gnark BN254 PLONK layout. + /// @param proof The Solidity-encoded gnark proof. + /// @param commitment The sole public input before BN254 scalar reduction. + /// @param verifierKeyId The immutable registry identifier for the verifier key. + /// @return valid False when the proof is well-formed but algebraically invalid. + function verifyGnarkPlonkProof(bytes calldata proof, bytes32 commitment, bytes32 verifierKeyId) + external + view + returns (bool valid); + // --------------------------------------------------------------- // V2: Anomaly detection // ---------------------------------------------------------------