144 lines
8.3 KiB
Solidity
144 lines
8.3 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.24;
|
|
|
|
import {Vm, VM_ADDRESS} from "./Vm.sol";
|
|
import {IgneumCertificateVerifier} from "../src/IgneumCertificateVerifier.sol";
|
|
import {BLS12381} from "../src/BLS12381.sol";
|
|
|
|
/// The verifier on Foundry's Prague EVM (the EIP-2537 precompiles): the synthetic vectors made by
|
|
/// test/vectors/gen.mjs (five keys, a certificate by four of them, a small account trie) and, when present, a real
|
|
/// certificate from the chain (test/vectors/chain.json, as /api/checkpoint serves it, decompressed by gen.mjs).
|
|
contract VerifierTest {
|
|
Vm constant vm = Vm(VM_ADDRESS);
|
|
|
|
string json;
|
|
IgneumCertificateVerifier v;
|
|
|
|
function setUp() public {
|
|
json = vm.readFile("test/vectors/synthetic.json");
|
|
v = new IgneumCertificateVerifier(vm.parseJsonString(json, ".chain_id"));
|
|
_install(v, json);
|
|
}
|
|
|
|
function _install(IgneumCertificateVerifier target, string memory j) internal {
|
|
bytes[] memory keys = vm.parseJsonBytesArray(j, ".keys");
|
|
uint256[] memory w = vm.parseJsonUintArray(j, ".weights");
|
|
bytes memory packed;
|
|
uint64[] memory weights = new uint64[](w.length);
|
|
for (uint256 i = 0; i < keys.length; i++) {
|
|
packed = abi.encodePacked(packed, keys[i]);
|
|
weights[i] = uint64(w[i]);
|
|
}
|
|
target.installTable(uint64(vm.parseJsonUint(j, ".index")), packed, weights);
|
|
}
|
|
|
|
function test_vote_message_matches_the_node() public view {
|
|
bytes memory want = vm.parseJsonBytes(json, ".vote_message");
|
|
bytes memory got = v.voteMessage(uint64(vm.parseJsonUint(json, ".index")), vm.parseJsonBytes32(json, ".checkpoint"));
|
|
require(keccak256(want) == keccak256(got), "vote message");
|
|
}
|
|
|
|
function test_expand_message_xmd_known_answer() public view {
|
|
// RFC 9380 appendix K.1 (expand_message_xmd with SHA-256, DST "QUUX-V01-CS02-with-expander-SHA256-128"): the
|
|
// empty message at 32 bytes is the appendix's own first answer; the "abc" at 128 bytes answer comes from noble
|
|
bytes memory dst = bytes(vm.parseJsonString(json, ".xmd_dst"));
|
|
bytes memory out = BLS12381.expandMessageXmd("", dst, 32);
|
|
require(keccak256(out) == keccak256(hex"68a985b87eb6b46952128911f2a4412bbc302a9d759667f87f7a21d803f07235"), "xmd 32 (RFC)");
|
|
require(keccak256(out) == keccak256(vm.parseJsonBytes(json, ".xmd_empty_32")), "xmd 32 (noble)");
|
|
bytes memory out2 = BLS12381.expandMessageXmd("abc", dst, 128);
|
|
require(keccak256(out2) == keccak256(vm.parseJsonBytes(json, ".xmd_abc_128")), "xmd 128 (noble)");
|
|
}
|
|
|
|
function test_certificate_verifies() public view {
|
|
(bool ok, uint256 signed, uint256 total) = v.verifyCertificate(
|
|
uint64(vm.parseJsonUint(json, ".index")), vm.parseJsonBytes32(json, ".checkpoint"), vm.parseJsonBytes(json, ".bitmap"), vm.parseJsonBytes(json, ".signature")
|
|
);
|
|
require(ok, "certificate");
|
|
require(signed == vm.parseJsonUint(json, ".signed_weight") && total == vm.parseJsonUint(json, ".total_weight"), "weights");
|
|
}
|
|
|
|
function test_certificate_under_two_thirds_is_refused() public view {
|
|
(bool ok, uint256 signed,) = v.verifyCertificate(
|
|
uint64(vm.parseJsonUint(json, ".index")), vm.parseJsonBytes32(json, ".checkpoint"), vm.parseJsonBytes(json, ".weak_bitmap"), vm.parseJsonBytes(json, ".weak_signature")
|
|
);
|
|
require(!ok && signed * 3 < vm.parseJsonUint(json, ".total_weight") * 2, "weak certificate accepted");
|
|
}
|
|
|
|
function test_wrong_checkpoint_or_index_fails() public view {
|
|
bytes32 cp = vm.parseJsonBytes32(json, ".checkpoint");
|
|
uint64 index = uint64(vm.parseJsonUint(json, ".index"));
|
|
bytes memory bm = vm.parseJsonBytes(json, ".bitmap");
|
|
bytes memory sig = vm.parseJsonBytes(json, ".signature");
|
|
(bool ok1,,) = v.verifyCertificate(index, cp ^ bytes32(uint256(1)), bm, sig);
|
|
(bool ok2,,) = v.verifyCertificate(index + 1, cp, bm, sig);
|
|
require(!ok1 && !ok2, "forged certificate accepted");
|
|
// the right signers' weight with a bitmap naming a different signer set does not match the signature
|
|
bytes memory other = vm.parseJsonBytes(json, ".weak_bitmap");
|
|
other[0] = bytes1(uint8(other[0]) | 0x1f);
|
|
(bool ok3,,) = v.verifyCertificate(index, cp, other, sig);
|
|
require(!ok3, "wrong signer set accepted");
|
|
}
|
|
|
|
function test_submit_records_the_checkpoint() public {
|
|
bytes32 cp = vm.parseJsonBytes32(json, ".checkpoint");
|
|
uint64 index = uint64(vm.parseJsonUint(json, ".index"));
|
|
v.submitCertificate(index, cp, vm.parseJsonBytes(json, ".bitmap"), vm.parseJsonBytes(json, ".signature"));
|
|
require(v.isFinal(cp) && v.finalCheckpoint(index) == cp, "not recorded");
|
|
vm.expectRevert(abi.encodeWithSelector(IgneumCertificateVerifier.BadCertificate.selector, "certificate does not verify"));
|
|
v.submitCertificate(index, cp, vm.parseJsonBytes(json, ".weak_bitmap"), vm.parseJsonBytes(json, ".weak_signature"));
|
|
}
|
|
|
|
function test_account_proof_present_and_absent() public view {
|
|
bytes32 root = vm.parseJsonBytes32(json, ".state_root");
|
|
(bool exists, uint256 nonce, uint256 balance, bytes32 sroot, bytes32 chash) =
|
|
v.verifyAccount(root, vm.parseJsonAddress(json, ".account"), vm.parseJsonBytesArray(json, ".account_proof"));
|
|
require(exists, "account absent");
|
|
require(nonce == vm.parseJsonUint(json, ".account_nonce") && balance == vm.parseJsonUint(json, ".account_balance"), "account fields");
|
|
require(sroot == vm.parseJsonBytes32(json, ".account_storage_root") && chash == vm.parseJsonBytes32(json, ".account_code_hash"), "account roots");
|
|
(bool exists2,,,,) = v.verifyAccount(root, vm.parseJsonAddress(json, ".absent_account"), vm.parseJsonBytesArray(json, ".absent_proof"));
|
|
require(!exists2, "absent account present");
|
|
}
|
|
|
|
function test_account_proof_against_a_wrong_root_reverts() public {
|
|
bytes32 root = vm.parseJsonBytes32(json, ".state_root") ^ bytes32(uint256(1));
|
|
bytes[] memory proof = vm.parseJsonBytesArray(json, ".account_proof");
|
|
address a = vm.parseJsonAddress(json, ".account");
|
|
vm.expectRevert(abi.encodeWithSelector(bytes4(keccak256("BadProof(string)")), "node hash mismatch"));
|
|
v.verifyAccount(root, a, proof);
|
|
}
|
|
|
|
/// A certificate the chain actually carried (test/vectors/chain.json; skipped when the file is absent).
|
|
function test_chain_certificate_verifies() public {
|
|
string memory j;
|
|
try vm.readFile("test/vectors/chain.json") returns (string memory s) {
|
|
j = s;
|
|
} catch {
|
|
return;
|
|
}
|
|
IgneumCertificateVerifier c = new IgneumCertificateVerifier(vm.parseJsonString(j, ".chain_id"));
|
|
_install(c, j);
|
|
(bool ok, uint256 signed, uint256 total) = c.verifyCertificate(
|
|
uint64(vm.parseJsonUint(j, ".index")), vm.parseJsonBytes32(j, ".checkpoint"), vm.parseJsonBytes(j, ".bitmap"), vm.parseJsonBytes(j, ".signature")
|
|
);
|
|
require(signed == vm.parseJsonUint(j, ".signed_weight") && total == vm.parseJsonUint(j, ".total_weight"), "chain weights");
|
|
require(ok, "the chain's certificate does not verify");
|
|
}
|
|
|
|
/// One Devnet 3 account under a real Devnet 3 state root (test/vectors/dn3-account.json from a node's eth_getProof;
|
|
/// skipped when the file is absent). The root is the chain's own; the link from a certified checkpoint to that root
|
|
/// is the gap the doc names.
|
|
function test_devnet3_account_balance_proven() public {
|
|
string memory j;
|
|
try vm.readFile("test/vectors/dn3-account.json") returns (string memory s) {
|
|
j = s;
|
|
} catch {
|
|
return;
|
|
}
|
|
(bool exists, uint256 nonce, uint256 balance, bytes32 sroot, bytes32 chash) = v.verifyAccount(
|
|
vm.parseJsonBytes32(j, ".state_root"), vm.parseJsonAddress(j, ".account"), vm.parseJsonBytesArray(j, ".account_proof")
|
|
);
|
|
require(exists, "the Devnet 3 account is absent under its root");
|
|
require(nonce == vm.parseJsonUint(j, ".account_nonce") && balance == vm.parseJsonUint(j, ".account_balance"), "Devnet 3 account fields");
|
|
require(sroot == vm.parseJsonBytes32(j, ".account_storage_root") && chash == vm.parseJsonBytes32(j, ".account_code_hash"), "Devnet 3 account roots");
|
|
}
|
|
}
|