mirror of
https://github.com/luxfi/safe-magnetar.git
synced 2026-10-30 15:18:11 +00:00
133 lines
7.1 KiB
Solidity
133 lines
7.1 KiB
Solidity
// SPDX-License-Identifier: GPL-3.0-only
|
|
pragma solidity ^0.8.29;
|
|
|
|
/// @title Magnetar Library
|
|
/// @notice Library for verifying Magnetar public-DKG MPC threshold SLH-DSA
|
|
/// (FIPS 205) signatures through the Lux on-chain `magnetarVerify` precompile
|
|
/// at `0x012207`.
|
|
///
|
|
/// @dev DECOMPLECTION. This library does ONE thing: turn a Magnetar
|
|
/// `(mode, pubkey, signature, hash)` into a boolean by framing the calldata
|
|
/// the on-chain precompile expects and staticcalling it under a strict
|
|
/// success-word check. It holds no state, enforces no policy, and never
|
|
/// reverts on a cryptographic "false". Ownership / threshold policy lives in
|
|
/// the Safe; pubkey↔owner binding lives in {SafeMagnetarSigner} / the account.
|
|
///
|
|
/// CRYPTOGRAPHIC IDENTITY. Magnetar (`github.com/luxfi/magnetar`) is a
|
|
/// public-DKG MPC threshold signature built from Pedersen-VSS DKG plus MPC
|
|
/// evaluation over the WOTS+/FORS/Merkle signing tree of FIPS 205. The ceremony
|
|
/// produces a signature byte-equal to a single-party FIPS 205 SLH-DSA signature
|
|
/// on the same message and group public key — verification IS the standard
|
|
/// FIPS 205 verifier. The dedicated `0x012207` slot binds the verification to
|
|
/// "this came from a threshold ceremony" and carries its own domain-separation
|
|
/// context.
|
|
///
|
|
/// DOMAIN SEPARATION. The precompile binds the FIPS context string
|
|
/// `"lux-evm-precompile-magnetar-v1"` into verification. A signature is valid
|
|
/// only under that exact context: a Magnetar signature cannot be replayed
|
|
/// against the single-party SLH-DSA precompile (`0x012203`, context
|
|
/// `"lux-evm-precompile-slhdsa-v1"`) or any other PQ slot. The signing client
|
|
/// MUST sign with the Magnetar context — see {Magnetar.CONTEXT}.
|
|
///
|
|
/// WIRE FORMAT (must match `github.com/luxfi/precompile/magnetar`.Run
|
|
/// byte-for-byte):
|
|
///
|
|
/// mode(1) ‖ pkLen:uint16(2) ‖ pubkey ‖ msgLen:uint16(2) ‖ message ‖ signature
|
|
///
|
|
/// For all Safe uses the "message" is the 32-byte Safe-tx hash, so `msgLen`
|
|
/// is fixed to 32.
|
|
///
|
|
/// FAIL-CLOSED. {verify} treats anything other than the precompile's exact
|
|
/// success word `bytes32(1)` — revert, wrong-size return, zero word, missing
|
|
/// precompile — as `false`. Mirrors the hardened pattern in
|
|
/// `luxfi/safe/contracts/pq/PQVerifier.sol`.
|
|
library Magnetar {
|
|
/// @notice The canonical LP-4200 on-chain address of the Magnetar
|
|
/// MPC-threshold SLH-DSA verify precompile.
|
|
address internal constant PRECOMPILE = 0x0000000000000000000000000000000000012207;
|
|
|
|
/// @notice The FIPS domain-separation context the precompile binds. The
|
|
/// signing client MUST use this exact context or the signature will not
|
|
/// verify on-chain. Exposed for off-chain reference; the precompile
|
|
/// re-derives it and it is not part of the calldata.
|
|
bytes internal constant CONTEXT = "lux-evm-precompile-magnetar-v1";
|
|
|
|
// --- FIPS 205 parameter-set mode bytes (high nibble = hash family,
|
|
// 0=SHA2 / 1=SHAKE; low nibble = size/variant) -----------------------
|
|
|
|
uint8 internal constant MODE_SHA2_128S = 0x00;
|
|
uint8 internal constant MODE_SHA2_128F = 0x01;
|
|
uint8 internal constant MODE_SHA2_192S = 0x02;
|
|
uint8 internal constant MODE_SHA2_192F = 0x03;
|
|
uint8 internal constant MODE_SHA2_256S = 0x04;
|
|
uint8 internal constant MODE_SHA2_256F = 0x05;
|
|
uint8 internal constant MODE_SHAKE_128S = 0x10;
|
|
uint8 internal constant MODE_SHAKE_128F = 0x11;
|
|
uint8 internal constant MODE_SHAKE_192S = 0x12;
|
|
uint8 internal constant MODE_SHAKE_192F = 0x13;
|
|
uint8 internal constant MODE_SHAKE_256S = 0x14;
|
|
uint8 internal constant MODE_SHAKE_256F = 0x15;
|
|
|
|
/// @notice Returns the FIPS 205 public-key and signature byte sizes for a
|
|
/// Magnetar parameter-set mode.
|
|
/// @dev Reverts on an unknown mode — an unsupported mode can never produce a
|
|
/// valid signature, so a hard revert at the size lookup prevents binding an
|
|
/// owner to an unverifiable configuration. Public key = 2n; signature sizes
|
|
/// per FIPS 205 Table 8 (SHA2 and SHAKE share sizes per security/variant).
|
|
/// @param mode The Magnetar parameter-set byte.
|
|
/// @return pkLen The public key length in bytes.
|
|
/// @return sigLen The signature length in bytes.
|
|
function sizes(uint8 mode) internal pure returns (uint256 pkLen, uint256 sigLen) {
|
|
// 128-bit (pk = 32)
|
|
if (mode == MODE_SHA2_128S || mode == MODE_SHAKE_128S) return (32, 7856);
|
|
if (mode == MODE_SHA2_128F || mode == MODE_SHAKE_128F) return (32, 17088);
|
|
// 192-bit (pk = 48)
|
|
if (mode == MODE_SHA2_192S || mode == MODE_SHAKE_192S) return (48, 16224);
|
|
if (mode == MODE_SHA2_192F || mode == MODE_SHAKE_192F) return (48, 35664);
|
|
// 256-bit (pk = 64)
|
|
if (mode == MODE_SHA2_256S || mode == MODE_SHAKE_256S) return (64, 29792);
|
|
if (mode == MODE_SHA2_256F || mode == MODE_SHAKE_256F) return (64, 49856);
|
|
revert("Magnetar: bad mode");
|
|
}
|
|
|
|
/// @notice Whether `mode` is a recognised Magnetar parameter set.
|
|
/// @param mode The Magnetar parameter-set byte.
|
|
/// @return ok True iff the mode is a supported FIPS 205 parameter set.
|
|
function isValidMode(uint8 mode) internal pure returns (bool ok) {
|
|
return (mode <= 0x05) || (mode >= 0x10 && mode <= 0x15);
|
|
}
|
|
|
|
/// @notice Verify a Magnetar threshold-SLH-DSA signature over the 32-byte
|
|
/// `hash` against `pubkey`, under the `mode` parameter set.
|
|
/// @dev Rejects up-front if `pubkey`/`sig` are not the exact size for the
|
|
/// mode, then frames the exact wire bytes and staticcalls {PRECOMPILE}
|
|
/// fail-closed.
|
|
/// @param hash The 32-byte message digest that was signed (the Safe tx hash).
|
|
/// @param mode The Magnetar parameter-set byte (e.g. 0x00 for SHA2-128s).
|
|
/// @param pubkey The raw FIPS 205 group public key bytes.
|
|
/// @param sig The raw FIPS 205-byte-equal Magnetar signature bytes.
|
|
/// @return ok True iff the precompile returned the exact success word.
|
|
function verify(bytes32 hash, uint8 mode, bytes memory pubkey, bytes memory sig) internal view returns (bool ok) {
|
|
if (!isValidMode(mode)) return false;
|
|
(uint256 pkLen, uint256 sigLen) = sizes(mode);
|
|
if (pubkey.length != pkLen || sig.length != sigLen) return false;
|
|
|
|
// mode(1) ‖ pkLen:uint16 ‖ pubkey ‖ msgLen:uint16=32 ‖ hash(32) ‖ sig
|
|
bytes memory input = abi.encodePacked(mode, uint16(pubkey.length), pubkey, uint16(32), hash, sig);
|
|
return _callStrict(input);
|
|
}
|
|
|
|
/// @notice Staticcall {PRECOMPILE} with `input`, returning true iff the
|
|
/// call succeeded AND returned exactly the 32-byte word `bytes32(1)`.
|
|
/// @dev Fail-closed on staticcall failure, any `returndatasize != 32`, any
|
|
/// returned word != 1, and a missing precompile (`returndatasize == 0`).
|
|
/// @param input The framed precompile calldata.
|
|
/// @return ok Whether the strict success word was returned.
|
|
function _callStrict(bytes memory input) private view returns (bool ok) {
|
|
assembly ("memory-safe") {
|
|
let success := staticcall(gas(), PRECOMPILE, add(input, 0x20), mload(input), 0x00, 0x20)
|
|
ok := and(success, and(eq(returndatasize(), 0x20), eq(mload(0x00), 1)))
|
|
}
|
|
}
|
|
}
|