Skip to content

Commit

Permalink
Verifier2: fully assemblify _preparePcsInfo() and _verifyOpeningProof
Browse files Browse the repository at this point in the history
  • Loading branch information
alxiong committed Aug 11, 2024
1 parent c21f484 commit c05259f
Show file tree
Hide file tree
Showing 11 changed files with 1,101 additions and 88 deletions.
4 changes: 2 additions & 2 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
LightClient_newFinalizedState_Test:testCorrectUpdateBench() (gas: 589288)
PlonkVerifier_verify_Test:test_verify_succeeds() (gas: 424059)
LightClientBench:testCorrectUpdateBench() (gas: 608445)
PlonkVerifier2_verify_Test:test_verify_succeeds() (gas: 387251)
2 changes: 1 addition & 1 deletion contract-bindings/artifacts/LightClientMock_bytecode.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion contract-bindings/artifacts/LightClient_bytecode.json

Large diffs are not rendered by default.

77 changes: 2 additions & 75 deletions contract-bindings/src/light_client.rs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions contract-bindings/src/light_client_mock.rs

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions contracts/src/LightClient.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { UUPSUpgradeable } from

import { BN254 } from "bn254/BN254.sol";
import { IPlonkVerifier } from "./interfaces/IPlonkVerifier.sol";
import { PlonkVerifier } from "./libraries/PlonkVerifier.sol";
import { PlonkVerifier2 } from "./libraries/PlonkVerifier2.sol";
import { LightClientStateUpdateVK as VkLib } from "./libraries/LightClientStateUpdateVK.sol";

/// @title Light Client Contract
Expand Down Expand Up @@ -59,6 +59,9 @@ contract LightClient is Initializable, OwnableUpgradeable, UUPSUpgradeable {

// === Storage ===

/// @notice SNARK verifier
PlonkVerifier2 verifier;

/// @notice current (finalized) epoch number
uint64 public currentEpoch;
/// @notice The commitment of the stake table used in current voting (i.e. snapshot at the start
Expand Down Expand Up @@ -167,6 +170,7 @@ contract LightClient is Initializable, OwnableUpgradeable, UUPSUpgradeable {
__UUPSUpgradeable_init();
genesisState = 0;
finalizedState = 1;
verifier = new PlonkVerifier2();
_initializeState(genesis, numBlocksPerEpoch);
}

Expand Down Expand Up @@ -315,7 +319,7 @@ contract LightClient is Initializable, OwnableUpgradeable, UUPSUpgradeable {
publicInput[6] = BN254.ScalarField.unwrap(states[finalizedState].stakeTableSchnorrKeyComm);
publicInput[7] = BN254.ScalarField.unwrap(states[finalizedState].stakeTableAmountComm);

if (!PlonkVerifier.verify(vk, publicInput, proof)) {
if (!verifier.verify(vk, publicInput, proof)) {
revert InvalidProof();
}
}
Expand Down
927 changes: 927 additions & 0 deletions contracts/src/libraries/PlonkVerifier2.sol

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion contracts/test/LightClientBenchmark.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { IPlonkVerifier as V } from "../src/interfaces/IPlonkVerifier.sol";
import { LightClient as LC } from "../src/LightClient.sol";
import { LightClientCommonTest } from "./LightClient.t.sol";

contract LightClient_newFinalizedState_Test is LightClientCommonTest {
contract LightClientBench is LightClientCommonTest {
function setUp() public {
init();
}
Expand Down
11 changes: 8 additions & 3 deletions contracts/test/LightClientV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { UUPSUpgradeable } from

import { BN254 } from "bn254/BN254.sol";
import { IPlonkVerifier } from "../src/interfaces/IPlonkVerifier.sol";
import { PlonkVerifier } from "../src/libraries/PlonkVerifier.sol";
import { PlonkVerifier2 } from "../src/libraries/PlonkVerifier2.sol";
import { LightClientStateUpdateVK as VkLib } from "../src/libraries/LightClientStateUpdateVK.sol";

/// @notice A light client for HotShot consensus. Keeping track of its finalized states in safe,
Expand Down Expand Up @@ -39,7 +39,10 @@ contract LightClientV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
uint32 internal finalizedState;

// === Storage ===
//

/// @notice SNARK verifier
PlonkVerifier2 verifier;

/// @notice current (finalized) epoch number
uint64 public currentEpoch;
/// @notice The commitment of the stake table used in current voting (i.e. snapshot at the start
Expand Down Expand Up @@ -124,6 +127,8 @@ contract LightClientV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
internal
onlyOwner
{
verifier = new PlonkVerifier2();

// stake table commitments and threshold cannot be zero, otherwise it's impossible to
// generate valid proof to move finalized state forward.
// Whereas blockCommRoot can be zero, if we use special value zero to denote empty tree.
Expand Down Expand Up @@ -236,7 +241,7 @@ contract LightClientV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
IPlonkVerifier.VerifyingKey memory vk = VkLib.getVk();
uint256[8] memory publicInput = preparePublicInput(state);

if (!PlonkVerifier.verify(vk, publicInput, proof)) {
if (!verifier.verify(vk, publicInput, proof)) {
revert InvalidProof();
}
}
Expand Down
150 changes: 150 additions & 0 deletions contracts/test/PlonkVerifier2.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-License-Identifier: Unlicensed

/* solhint-disable contract-name-camelcase, func-name-mixedcase, one-contract-per-file */
/* solhint-disable no-inline-assembly */

// NOTE: For developers and auditors: we mainly test the consistency between the outputs between
// Solidity and Jellyfish library, with the help of fuzzer-generated inputs from Forge Testing.
// Inside the logic of `verify()`, variables values only need to be consistent and valid
// (i.e. valid group or field elements) and don't need to be from a correct proof/public input.
// Only the last step `_verifyOpeningProof` will test *correctness* of these parameters.
// Therefore, we employ more randomly generated dummy inputs for most tests for robustness,
// and only rely on Rust-code to generate correct inputs for the `_verifyOpeningProof`.

pragma solidity ^0.8.0;

// Libraries
import "forge-std/Test.sol";
import { BN254 } from "bn254/BN254.sol";
import { IPlonkVerifier } from "../src/interfaces/IPlonkVerifier.sol";
import { LightClientStateUpdateVKMock as VkTest } from "./mocks/LightClientStateUpdateVKMock.sol";

// Target contract
import { PlonkVerifier2 as V } from "../src/libraries/PlonkVerifier2.sol";

/// @dev Common helpers/utils for PlonkVerifier tests
contract PlonkVerifierCommonTest is Test {
V verifier;

function setUp() public {
verifier = new V();
}

/// @dev Sanitize a single value to be valid scalar field Bn254::Fr.
function sanitizeScalarField(uint256 a) public pure returns (uint256) {
a = bound(a, 0, BN254.R_MOD - 1);
BN254.validateScalarField(BN254.ScalarField.wrap(a));
return a;
}

/// @dev Sanitize all values in `a` to be valid scalar fields Bn254::Fr.
/// This is helpful to sanitize fuzzer-generated random `uint[]` values.
function sanitizeScalarFields(uint256[8] memory a) public pure returns (uint256[8] memory) {
for (uint256 i = 0; i < a.length; i++) {
a[i] = sanitizeScalarField(a[i]);
}
return a;
}

/// @dev Generate a random valid (format-wise) proof from a random seed
function dummyProof(uint64 seed) public returns (IPlonkVerifier.PlonkProof memory) {
string[] memory cmds = new string[](3);
cmds[0] = "diff-test";
cmds[1] = "dummy-proof";
cmds[2] = vm.toString(seed);

bytes memory result = vm.ffi(cmds);
(IPlonkVerifier.PlonkProof memory proof) = abi.decode(result, (IPlonkVerifier.PlonkProof));
return proof;
}
}

contract PlonkVerifier2_verify_Test is PlonkVerifierCommonTest {
/// @dev Test happy path of `verify`.
function test_verify_succeeds() external {
vm.pauseGasMetering();
string[] memory cmds = new string[](2);
cmds[0] = "diff-test";
cmds[1] = "plonk-verify";

bytes memory result = vm.ffi(cmds);
(
IPlonkVerifier.VerifyingKey memory verifyingKey,
uint256[8] memory publicInput,
IPlonkVerifier.PlonkProof memory proof
) = abi.decode(result, (IPlonkVerifier.VerifyingKey, uint256[8], IPlonkVerifier.PlonkProof));

vm.resumeGasMetering();
assert(verifier.verify(verifyingKey, publicInput, proof));
}

/// @dev Test when bad verifying key is supplied, the verification should fail
function testFuzz_badVerifyingKey_fails(uint256 nthPoint) external {
string[] memory cmds = new string[](2);
cmds[0] = "diff-test";
cmds[1] = "plonk-verify";

bytes memory result = vm.ffi(cmds);
(
IPlonkVerifier.VerifyingKey memory verifyingKey,
uint256[8] memory publicInput,
IPlonkVerifier.PlonkProof memory proof
) = abi.decode(result, (IPlonkVerifier.VerifyingKey, uint256[8], IPlonkVerifier.PlonkProof));

// there are 18 points in verifying key
// randomly choose one to mutate
nthPoint = bound(nthPoint, 0, 17);

BN254.G1Point memory badPoint;
assembly {
// the first point offset is 0x40
let badPointRef := add(verifyingKey, add(mul(nthPoint, 0x20), 0x40))
badPoint := mload(badPointRef)
}

// modify the point to be invalid
badPoint = BN254.add(badPoint, BN254.P1());

assembly {
let badPointRef := add(verifyingKey, add(mul(nthPoint, 0x20), 0x40))
mstore(badPointRef, badPoint)
}

assert(!verifier.verify(verifyingKey, publicInput, proof));
}

// @dev Test when bad public input is supplied, the verification should fail
// We know our `gen_circuit_for_test` in `diff_test.rs` has only 8 public inputs
function testFuzz_badPublicInput_fails(uint256[8] calldata randPublicInput) external {
uint256[8] memory badPublicInput;
for (uint256 i = 0; i < 8; i++) {
badPublicInput[i] = randPublicInput[i];
}
badPublicInput = sanitizeScalarFields(badPublicInput);

string[] memory cmds = new string[](2);
cmds[0] = "diff-test";
cmds[1] = "plonk-verify";

bytes memory result = vm.ffi(cmds);
(IPlonkVerifier.VerifyingKey memory verifyingKey,, IPlonkVerifier.PlonkProof memory proof) =
abi.decode(result, (IPlonkVerifier.VerifyingKey, uint256[8], IPlonkVerifier.PlonkProof));

assert(!verifier.verify(verifyingKey, badPublicInput, proof));
}

/// @dev Test when bad proof is supplied, the verification should fail
function testFuzz_badProof_fails(uint64 seed) external {
IPlonkVerifier.PlonkProof memory badProof = dummyProof(seed);

string[] memory cmds = new string[](2);
cmds[0] = "diff-test";
cmds[1] = "plonk-verify";

bytes memory result = vm.ffi(cmds);
(IPlonkVerifier.VerifyingKey memory verifyingKey, uint256[8] memory publicInput,) =
abi.decode(result, (IPlonkVerifier.VerifyingKey, uint256[8], IPlonkVerifier.PlonkProof));

assert(!verifier.verify(verifyingKey, publicInput, badProof));
}
}
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ lc-contract-profiling-sepolia:
gas-benchmarks:
cargo build --bin diff-test --release
forge snapshot --mt "test_verify_succeeds|testCorrectUpdateBench"
forge snapshot --mt "test_verify_succeeds|testCorrectUpdateBench" --mc "PlonkVerifier2|LightClientBench"
@[ -n "$(git diff --name-only .gas-snapshot)" ] && echo "⚠️ Uncommitted gas benchmarks, please stage them before committing." && exit 1 || exit 0

# This is meant for local development and produces HTML output. In CI
Expand Down

0 comments on commit c05259f

Please sign in to comment.