igneum/contracts/bridge/test/Verifier.t.sol

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");
}
}