mirror of
https://github.com/luxfi/safe-pulsar.git
synced 2026-10-30 15:17:59 +00:00
117 lines
5.5 KiB
Solidity
117 lines
5.5 KiB
Solidity
// SPDX-License-Identifier: GPL-3.0-only
|
|
pragma solidity ^0.8.29;
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
import {Pulsar} from "contracts/Pulsar.sol";
|
|
|
|
/// @notice A stand-in for the on-chain `pulsarVerify` precompile (`0x012204`),
|
|
/// used in `forge test` where the Go precompile is not present.
|
|
///
|
|
/// @dev GROUND-TRUTH ABI CONFORMANCE. This mock does NOT re-implement ML-DSA.
|
|
/// Instead it asserts the *calldata framing*: it returns the precompile's exact
|
|
/// success word `bytes32(1)` iff the incoming calldata is byte-for-byte equal
|
|
/// to a reference `input` blob whose keccak256 is stored in slot 0. That
|
|
/// reference blob is a real Known-Answer-Test vector produced by
|
|
/// `precompile/cmd/safekatdump` and independently *proven to verify against the
|
|
/// real Go precompile* by `precompile/cmd/safekatverify`. Therefore: if
|
|
/// {Pulsar.verify}'s framed calldata matches this blob, it would also be
|
|
/// accepted by the real precompile; any drift in the wire format (field order,
|
|
/// length prefixes, padding) changes the calldata and this mock returns zero —
|
|
/// failing the test. The expected hash is injected via `vm.store` after `etch`,
|
|
/// so the same deployed code can serve any KAT.
|
|
contract MockPulsarPrecompile {
|
|
fallback(bytes calldata input) external returns (bytes memory) {
|
|
bytes32 expected;
|
|
assembly {
|
|
expected := sload(0)
|
|
}
|
|
bool ok = keccak256(input) == expected;
|
|
return abi.encode(ok ? bytes32(uint256(1)) : bytes32(0));
|
|
}
|
|
}
|
|
|
|
contract PulsarTest is Test {
|
|
// KAT components loaded from tests/kat.json (real vector, proven against
|
|
// the Go precompile by safekatverify).
|
|
uint8 internal mode;
|
|
bytes internal pubkey;
|
|
bytes32 internal msgHash;
|
|
bytes internal sig;
|
|
bytes internal expectedInput;
|
|
|
|
function setUp() public {
|
|
string memory json = vm.readFile(string.concat(vm.projectRoot(), "/tests/kat.json"));
|
|
mode = uint8(vm.parseJsonUint(json, ".mode"));
|
|
pubkey = vm.parseJsonBytes(json, ".pubkey");
|
|
msgHash = bytes32(vm.parseJsonBytes(json, ".msgHash"));
|
|
sig = vm.parseJsonBytes(json, ".sig");
|
|
expectedInput = vm.parseJsonBytes(json, ".input");
|
|
|
|
// Install the mock at the real precompile address and pin the expected
|
|
// calldata hash.
|
|
vm.etch(Pulsar.PRECOMPILE, type(MockPulsarPrecompile).runtimeCode);
|
|
vm.store(Pulsar.PRECOMPILE, bytes32(uint256(0)), keccak256(expectedInput));
|
|
}
|
|
|
|
/// @notice The library frames calldata that byte-matches the KAT input the
|
|
/// real precompile accepts, so {Pulsar.verify} returns true.
|
|
function test_Verify_KAT() public view {
|
|
assertTrue(Pulsar.verify(msgHash, mode, pubkey, sig), "Pulsar KAT must verify");
|
|
}
|
|
|
|
/// @notice Independent, self-contained proof that the library frames the
|
|
/// precompile calldata EXACTLY as the Go `Run()` parses it:
|
|
/// mode(1) || pubkey || msgLen:uint256(32)=32 || msg(32) || sig
|
|
function test_CalldataFramingMatchesPrecompileABI() public view {
|
|
bytes memory framed = abi.encodePacked(mode, pubkey, uint256(32), msgHash, sig);
|
|
assertEq(keccak256(framed), keccak256(expectedInput), "framing must equal precompile wire bytes");
|
|
assertEq(framed.length, expectedInput.length, "framed length must equal precompile input length");
|
|
// 1 (mode) + 1952 (pk-65) + 32 (msgLen) + 32 (msg) + 3309 (sig).
|
|
assertEq(framed.length, 1 + 1952 + 32 + 32 + 3309, "Pulsar-65 input size");
|
|
}
|
|
|
|
/// @notice A tampered signature changes the calldata, so the precompile
|
|
/// (and thus {Pulsar.verify}) rejects it — fail-closed.
|
|
function test_Verify_RejectsTamperedSignature() public view {
|
|
bytes memory bad = bytes.concat(sig);
|
|
bad[100] ^= 0xFF;
|
|
assertFalse(Pulsar.verify(msgHash, mode, pubkey, bad), "tampered sig must not verify");
|
|
}
|
|
|
|
/// @notice A wrong message changes the calldata, so verification fails.
|
|
function test_Verify_RejectsWrongMessage() public view {
|
|
bytes32 wrong = keccak256("not the signed message");
|
|
assertFalse(Pulsar.verify(wrong, mode, pubkey, sig), "wrong message must not verify");
|
|
}
|
|
|
|
/// @notice A public key of the wrong length for the mode is rejected before
|
|
/// any precompile call.
|
|
function test_Verify_RejectsWrongPubkeyLength() public view {
|
|
bytes memory shortPk = new bytes(pubkey.length - 1);
|
|
assertFalse(Pulsar.verify(msgHash, mode, shortPk, sig), "short pubkey must not verify");
|
|
}
|
|
|
|
/// @notice An unsupported mode byte is rejected (no precompile call).
|
|
function test_Verify_RejectsBadMode() public view {
|
|
assertFalse(Pulsar.verify(msgHash, 0x99, pubkey, sig), "bad mode must not verify");
|
|
}
|
|
|
|
/// @notice Strict success-word check: a precompile that returns a non-1
|
|
/// word (e.g. a different success encoding) is treated as failure.
|
|
function test_Verify_FailClosedOnNonOneWord() public {
|
|
// Re-pin to a hash that the framed calldata will NOT match.
|
|
vm.store(Pulsar.PRECOMPILE, bytes32(uint256(0)), keccak256("never matches"));
|
|
assertFalse(Pulsar.verify(msgHash, mode, pubkey, sig), "non-success word must be false");
|
|
}
|
|
|
|
/// @notice The published precompile address is the canonical LP-4200 slot.
|
|
function test_PrecompileAddress() public pure {
|
|
assertEq(Pulsar.PRECOMPILE, address(0x0000000000000000000000000000000000012204));
|
|
}
|
|
|
|
/// @notice The published context matches the precompile's domain separator.
|
|
function test_Context() public pure {
|
|
assertEq(string(Pulsar.CONTEXT), "lux-evm-precompile-pulsar-v1");
|
|
}
|
|
}
|