// SPDX-License-Identifier: MIT pragma solidity 0.8.36; /// @title Quicknet BLS12-381 verification through EIP-2537 /// @notice Verifies unchained quicknet signatures supplied as canonical, uncompressed G1 points. /// @dev Derived from randa-mu/bls-solidity commit /// 11af179a8287d978659aae07adb66aa60f64b8a6 (MIT). library QuicknetBLS { uint256 private constant MODEXP = 0x05; uint256 private constant BLS12_G1ADD = 0x0b; uint256 private constant BLS12_PAIRING_CHECK = 0x0f; uint256 private constant BLS12_MAP_FP_TO_G1 = 0x10; /// @dev EIP-2537 charges 102,900 gas for a two-pair check. The margin covers future client /// accounting differences without allowing invalid caller-supplied points to burn all gas. uint256 private constant PAIRING_CHECK_GAS = 120_000; bytes internal constant DST = "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"; uint64 internal constant SELF_TEST_ROUND = 20_791_007; bytes internal constant SELF_TEST_SIGNATURE = hex"0d2c8bbc37170dbacc5e280a21d4e195cff5f32a19fd6a58633fa4e4670478b5fb39bc13dd8f8c4372c5a76191198ac50823ff37364b4060af65c7ec4dde05a428e4a444713680d95c34a4b109f112af1792643c742b75d85940c4bdcfdfbfa1"; struct PointG1 { uint128 xHi; uint256 xLo; uint128 yHi; uint256 yLo; } struct PointG2 { uint128 x1Hi; uint256 x1Lo; uint128 x0Hi; uint256 x0Lo; uint128 y1Hi; uint256 y1Lo; uint128 y0Hi; uint256 y0Lo; } // BLS12-381 base-field modulus. uint128 private constant P_HI = 0x1a0111ea397fe69a4b1ba7b6434bacd7; uint256 private constant P_LO = 0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab; // Negated G2 generator used in e(signature, -G2) * e(message, publicKey) == 1. uint128 private constant N_G2_X0_HI = 0x024aa2b2f08f0a91260805272dc51051; uint256 private constant N_G2_X0_LO = 0xc6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8; uint128 private constant N_G2_X1_HI = 0x13e02b6052719f607dacd3a088274f65; uint256 private constant N_G2_X1_LO = 0x596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e; uint128 private constant N_G2_Y0_HI = 0x0d1b3cc2c7027888be51d9ef691d77bc; uint256 private constant N_G2_Y0_LO = 0xb679afda66c73f17f9ee3837a55024f78c71363275a75d75d86bab79f74782aa; uint128 private constant N_G2_Y1_HI = 0x13fa4d4a0ad8b1ce186ed5061789213d; uint256 private constant N_G2_Y1_LO = 0x993923066dddaf1040bc3ff59f825c78df74f2d75467e25e0f55f8a00fa030ed; // Fixed quicknet distributed public key. uint128 private constant QUICKNET_X1_HI = 0x03cf0f2896adee7eb8b5f01fcad39122; uint256 private constant QUICKNET_X1_LO = 0x12c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d106451; uint128 private constant QUICKNET_X0_HI = 0x0d1fec758c921cc22b0e17e63aaf4bcb; uint256 private constant QUICKNET_X0_LO = 0x5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a; uint128 private constant QUICKNET_Y1_HI = 0x01a714f2edb74119a2f2b0d5a7c75ba9; uint256 private constant QUICKNET_Y1_LO = 0x02d163700a61bc224ededd8e63aef7be1aaf8e93d7a9718b047ccddb3eb5d68b; uint128 private constant QUICKNET_Y0_HI = 0x0e5db2b6bfbb01c867749cadffca88b3; uint256 private constant QUICKNET_Y0_LO = 0x6c24f3012ba09fc4d3022c5c37dce0f977d3adb5d183c7477c442b1f04515273; /// @notice Verifies a signature under quicknet's fixed distributed public key. function verify(bytes memory signature, uint64 round) internal view returns (bool) { (bool valid,) = verifyAndHash(signature, round); return valid; } /// @notice Verifies and hashes the unique canonical signature representation used by Luckotto. /// @dev Returning the hash from the verifier keeps seed derivation coupled to the validated /// 96-byte affine encoding even if other signature encodings are supported in the future. function verifyAndHash(bytes memory signature, uint64 round) internal view returns (bool valid, bytes32 canonicalHash) { if (round == 0 || signature.length != 96) return (false, bytes32(0)); PointG1 memory sig = unmarshalG1(signature); if (!_isCanonical(sig) || _isInfinity(sig)) return (false, bytes32(0)); bytes32 roundDigest = sha256(abi.encodePacked(round)); PointG1 memory message = hashToPoint(DST, abi.encodePacked(roundDigest)); (bool pairingSuccess, bool callSuccess) = _verifyPinnedPairing(sig, message); if (!callSuccess || !pairingSuccess) return (false, bytes32(0)); return (true, sha256(signature)); } /// @notice Exercises the complete verifier path with a pinned public quicknet beacon. /// @dev Used during lottery construction so deployment fails on chains without working /// EIP-2537 precompiles. function selfTest() internal view returns (bool) { return verify(SELF_TEST_SIGNATURE, SELF_TEST_ROUND); } /// @dev Exposed internally so the test harness can exercise wrong-key rejection. function verifyWithPublicKey(bytes memory signature, uint64 round, PointG2 memory key) internal view returns (bool) { if (round == 0 || signature.length != 96) return false; PointG1 memory sig = unmarshalG1(signature); if (!_isCanonical(sig) || _isInfinity(sig)) return false; bytes32 roundDigest = sha256(abi.encodePacked(round)); PointG1 memory message = hashToPoint(DST, abi.encodePacked(roundDigest)); (bool pairingSuccess, bool callSuccess) = _verifyPairing(sig, key, message); return callSuccess && pairingSuccess; } /// @notice The fixed quicknet G2 key in the coordinate order required by EIP-2537. function publicKey() internal pure returns (PointG2 memory) { return PointG2({ x1Hi: QUICKNET_X1_HI, x1Lo: QUICKNET_X1_LO, x0Hi: QUICKNET_X0_HI, x0Lo: QUICKNET_X0_LO, y1Hi: QUICKNET_Y1_HI, y1Lo: QUICKNET_Y1_LO, y0Hi: QUICKNET_Y0_HI, y0Lo: QUICKNET_Y0_LO }); } function publicKeyHash() internal pure returns (bytes32) { PointG2 memory key = publicKey(); return keccak256( abi.encode( key.x1Hi, key.x1Lo, key.x0Hi, key.x0Lo, key.y1Hi, key.y1Lo, key.y0Hi, key.y0Lo ) ); } function unmarshalG1(bytes memory encoded) private pure returns (PointG1 memory point) { assembly ("memory-safe") { mstore(point, shr(128, mload(add(encoded, 0x20)))) mstore(add(point, 0x20), mload(add(encoded, 0x30))) mstore(add(point, 0x40), shr(128, mload(add(encoded, 0x50)))) mstore(add(point, 0x60), mload(add(encoded, 0x60))) } } /// @notice RFC 9380 hash_to_field + EIP-2537 mapping for quicknet's G1 ciphersuite. function hashToPoint(bytes memory dst, bytes memory message) internal view returns (PointG1 memory out) { bytes memory uniformBytes = expandMessage(dst, message); bytes memory modexpInput = new bytes(225); bytes memory mappedPoints = new bytes(256); bool ok; for (uint256 i; i < 2; ++i) { assembly ("memory-safe") { let fieldInput := add(add(uniformBytes, 0x20), mul(64, i)) let cursor := add(modexpInput, 0x20) mstore(cursor, 64) cursor := add(cursor, 0x20) mstore(cursor, 1) cursor := add(cursor, 0x20) mstore(cursor, 64) cursor := add(cursor, 0x20) mcopy(cursor, fieldInput, 64) cursor := add(cursor, 64) mstore8(cursor, 1) cursor := add(cursor, 1) mstore(cursor, P_HI) cursor := add(cursor, 0x20) mstore(cursor, P_LO) ok := staticcall(gas(), MODEXP, add(modexpInput, 0x20), 225, fieldInput, 64) ok := and(ok, eq(returndatasize(), 64)) } if (!ok) revert PrecompileFailure(MODEXP); assembly ("memory-safe") { let fieldInput := add(add(uniformBytes, 0x20), mul(64, i)) let mappedOutput := add(add(mappedPoints, 0x20), mul(128, i)) ok := staticcall(gas(), BLS12_MAP_FP_TO_G1, fieldInput, 64, mappedOutput, 128) ok := and(ok, eq(returndatasize(), 128)) } if (!ok) revert PrecompileFailure(BLS12_MAP_FP_TO_G1); } assembly ("memory-safe") { ok := staticcall(gas(), BLS12_G1ADD, add(mappedPoints, 0x20), 256, out, 128) ok := and(ok, eq(returndatasize(), 128)) } if (!ok) revert PrecompileFailure(BLS12_G1ADD); } /// @notice RFC 9380 expand_message_xmd using SHA-256. function expandMessage(bytes memory dst, bytes memory message) internal pure returns (bytes memory out) { if (dst.length > 255) revert InvalidDomainLength(); bytes32 b0 = sha256( abi.encodePacked( bytes32(0), bytes32(0), message, uint8(0), uint8(128), uint8(0), dst, uint8(dst.length) ) ); bytes32 bi = sha256(abi.encodePacked(b0, uint8(1), dst, uint8(dst.length))); out = new bytes(128); for (uint256 i = 1; i < 4; ++i) { assembly ("memory-safe") { mstore(add(add(out, 0x20), mul(0x20, sub(i, 1))), bi) } bi = sha256(abi.encodePacked(b0 ^ bi, uint8(i + 1), dst, uint8(dst.length))); } assembly ("memory-safe") { mstore(add(out, 0x80), bi) } } function _verifyPairing(PointG1 memory signature, PointG2 memory key, PointG1 memory message) private view returns (bool pairingSuccess, bool callSuccess) { uint256[24] memory input = [ signature.xHi, signature.xLo, signature.yHi, signature.yLo, N_G2_X0_HI, N_G2_X0_LO, N_G2_X1_HI, N_G2_X1_LO, N_G2_Y0_HI, N_G2_Y0_LO, N_G2_Y1_HI, N_G2_Y1_LO, message.xHi, message.xLo, message.yHi, message.yLo, key.x0Hi, key.x0Lo, key.x1Hi, key.x1Lo, key.y0Hi, key.y0Lo, key.y1Hi, key.y1Lo ]; return _callPairing(input); } function _verifyPinnedPairing(PointG1 memory signature, PointG1 memory message) private view returns (bool pairingSuccess, bool callSuccess) { uint256[24] memory input = [ signature.xHi, signature.xLo, signature.yHi, signature.yLo, N_G2_X0_HI, N_G2_X0_LO, N_G2_X1_HI, N_G2_X1_LO, N_G2_Y0_HI, N_G2_Y0_LO, N_G2_Y1_HI, N_G2_Y1_LO, message.xHi, message.xLo, message.yHi, message.yLo, QUICKNET_X0_HI, QUICKNET_X0_LO, QUICKNET_X1_HI, QUICKNET_X1_LO, QUICKNET_Y0_HI, QUICKNET_Y0_LO, QUICKNET_Y1_HI, QUICKNET_Y1_LO ]; return _callPairing(input); } function _callPairing(uint256[24] memory input) private view returns (bool pairingSuccess, bool callSuccess) { uint256[1] memory output; assembly ("memory-safe") { callSuccess := staticcall( PAIRING_CHECK_GAS, BLS12_PAIRING_CHECK, input, 768, output, 0x20 ) callSuccess := and(callSuccess, eq(returndatasize(), 0x20)) } pairingSuccess = callSuccess && output[0] == 1; } function _isCanonical(PointG1 memory point) private pure returns (bool) { return _isFieldElement(point.xHi, point.xLo) && _isFieldElement(point.yHi, point.yLo); } function _isFieldElement(uint128 hi, uint256 lo) private pure returns (bool) { return hi < P_HI || (hi == P_HI && lo < P_LO); } function _isInfinity(PointG1 memory point) private pure returns (bool) { return point.xHi == 0 && point.xLo == 0 && point.yHi == 0 && point.yLo == 0; } error InvalidDomainLength(); error PrecompileFailure(uint256 precompile); }