Warning

Code in this repository is not audited and may contain serious security holes. Use at your own risk.

Safe + Pulsar

This repository brings Pulsar post-quantum threshold signatures to the Safe smart account on the Lux C-Chain EVM. Pulsar is a Module-LWE (MLWE) threshold signature, FIPS 204 byte-equal (FIPS 204 (ML-DSA) — a Pulsar ceremony signature is byte-equal to a single-party ML-DSA signature).

Unlike safe-frost — which ships a self-contained secp256k1 Schnorr verifier in Solidity — Pulsar verification is performed by the on-chain pulsarVerify precompile at 0x012204 (part of the Lux LP-4200 unified PQCrypto block). The Solidity in this repository is a thin, decomplected adapter: it frames the precompile's exact calldata, staticcalls it under a strict success-word check, and binds the verified key to a Safe owner.

safe-pulsar supports the same three use cases as safe-frost:

  • as a Safe signer (an EIP-1271 owner — PulsarSigner)
  • as a Safe co-signer (a transaction guard — SafePulsarCoSigner)
  • as an ERC-4337 account (PulsarAccount)

Precompile

Field Value
Address 0x012204
Verify ABI (raw wire format) mode:uint8(1) ‖ pubkey ‖ msgLen:uint256(32)=32 ‖ message(32) ‖ signature
Domain-separation context lux-evm-precompile-pulsar-v1
Return value 32-byte word: bytes32(1) iff valid, bytes32(0) otherwise (never reverts on a cryptographic false)
Reference implementation github.com/luxfi/precompile/pulsar

The "message" for every Safe use is the 32-byte Safe-transaction hash (or the ERC-4337 userOpHash), which is EIP-712-bound to the Safe address, chain id and nonce — so a signature authorises exactly one Safe / chain / nonce. The precompile additionally binds the domain-separation context above, so a Pulsar signature cannot be replayed against any other precompile in the PQCrypto block.

Important

The precompile reads its calldata as a raw packed layout, not an ABI-encoded function call — there is no 4-byte selector. Pulsar.sol frames the bytes exactly as the Go Run() parses them. Sending an ABI-encoded call (with a selector) would misalign every field.

Contracts

  • contracts/Pulsar.sol — the verifier library. One function, verify(...), that frames a single mode byte (0x44 / 0x65 / 0x87 for the three NIST security levels), the public key / signature and the 32-byte hash into the precompile's wire format and strict-staticcalls 0x012204. Holds no state and enforces no policy (decomplected: dispatch only).
  • contracts/SafePulsarSigner.sol — an EIP-1271 validator that makes a single Pulsar key a first-class Safe owner. Commit the key once via addOwnerWithThreshold; the Safe's checkNSignatures validates this owner's slice through the v = 0 contract-signature path. M-of-N thresholds mixing ECDSA, secp256r1 and Pulsar owners work natively.
  • contracts/SafePulsarCoSigner.sol — a Safe transaction guard requiring every execTransaction to additionally carry a valid Pulsar co-signature.
  • contracts/PulsarAccount.sol — an ERC-4337 account authorised by a committed Pulsar key.

Key binding

Pulsar signatures are a standard FIPS 204 ML-DSA signature (3309 bytes for ML-DSA-65). An owner / account therefore commits a keccak256 of the FIPS 204 group public key at construction (immutable, so the validators are stateless view functions and side-step the Safe's state-changing-validator guard). At verify time the supplied key is re-hashed against the commitment, closing the "attacker supplies an arbitrary key" attack via second-preimage resistance.

Signature payload

The contract-signature / userOp signature payload is abi.encode(bytes pubkey, bytes pulsarSignature) — a standard ABI (bytes, bytes) tuple of the full Pulsar public key and signature.

Testing

forge test

The tests prove verify() against a real known-answer-test (KAT) vector — 3309-byte ML-DSA-65 signature produced by a genuine Pulsar ceremony. The KAT was generated by lux/precompile/cmd/safekatdump and independently proven to verify against the real Go precompile by lux/precompile/cmd/safekatverify, so the bytes the Solidity frames are known to be accepted on-chain.

Because the precompile is a luxd-native Go precompile (absent from a stock Foundry EVM), the tests install a mock at 0x012204 via vm.etch whose sole job is to assert calldata-framing conformance: it returns the success word iff the incoming calldata is byte-for-byte identical to the proven KAT input. Any drift in the wire format (field order, length-prefix widths, an accidental ABI selector) changes the calldata and the test fails. The headline test is Pulsar.t.sol::test_CalldataFramingMatchesPrecompileABI.

The fixture lives at tests/kat.json.

Integration with the Lux ecosystem

This package is part of the Lux blockchain ecosystem.

S
Description
Safe smart-wallet variant: Pulsar Module-LWE (ML-DSA) threshold signatures (verifier + EIP-1271 signer + ERC-4337 account) via the Lux pulsarVerify precompile 0x012204
Readme BSD-3-Clause
1.1 MiB
Languages
Solidity 98.2%
Python 1.8%