PQ Precompiles

Signature Verification

Precompiles for verifying classical and post-quantum digital signatures on the Lux EVM.

Overview

The Lux EVM provides precompiles for verifying signatures from five different schemes, ranging from classical elliptic curve cryptography to NIST post-quantum standards. All verification precompiles accept a message, public key, and signature as input, and return 0x01 (valid) or 0x00 (invalid).

SR25519 Verify

Verifies Schnorrkel/Ristretto signatures used by the Substrate ecosystem (Polkadot, Kusama).

PropertyValue
Address0x0a00000000000000000000000000000000000001
Inputmessage || public_key (32 bytes) || signature (64 bytes)
Output0x01 (valid) or 0x00 (invalid)
Gas Cost3,000

SR25519 uses Ristretto255 (a prime-order group constructed over Curve25519) with a Schnorr-like signing scheme. This precompile enables Lux contracts to verify signatures originating from Substrate-based chains, useful for cross-chain bridges and interoperability.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract SR25519Verifier {
    address constant SR25519 = 0x0a00000000000000000000000000000000000001;

    function verify(
        bytes memory message,
        bytes32 publicKey,
        bytes memory signature
    ) external view returns (bool) {
        (bool success, bytes memory result) = SR25519.staticcall(
            abi.encodePacked(message, publicKey, signature)
        );
        return success && result.length > 0 && uint8(result[0]) == 1;
    }
}

Ed25519 Verify

Verifies EdDSA signatures on the Edwards25519 curve, used by Solana, Cardano, TON, and many other protocols.

PropertyValue
Address0x0200000000000000000000000000000000000005
Inputmessage || public_key (32 bytes) || signature (64 bytes)
Output0x01 (valid) or 0x00 (invalid)
Gas Cost3,000

Ed25519 is widely adopted for its performance and security properties. The precompile enables verification of signatures from Ed25519-based chains without the high gas cost of pure-Solidity implementations.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Ed25519Verifier {
    address constant ED25519 = 0x0200000000000000000000000000000000000005;

    function verify(
        bytes memory message,
        bytes32 publicKey,
        bytes memory signature
    ) external view returns (bool) {
        (bool success, bytes memory result) = ED25519.staticcall(
            abi.encodePacked(message, publicKey, signature)
        );
        return success && result.length > 0 && uint8(result[0]) == 1;
    }
}

secp256r1 Verify (P-256)

Verifies ECDSA signatures on the NIST P-256 curve, used by WebAuthn, Apple Passkeys, Android Keystore, and most TLS implementations.

PropertyValue
Address0x0200000000000000000000000000000000000004
Inputmessage_hash (32 bytes) || r (32 bytes) || s (32 bytes) || x (32 bytes) || y (32 bytes)
Output0x01 (valid) or 0x00 (invalid)
Gas Cost3,450

This precompile is essential for account abstraction wallets that use WebAuthn/Passkeys as signers. Users can sign transactions with their device biometrics (Face ID, fingerprint) and the contract verifies the P-256 signature natively.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract PasskeyVerifier {
    address constant SECP256R1 = 0x0200000000000000000000000000000000000004;

    function verifyPasskey(
        bytes32 messageHash,
        bytes32 r,
        bytes32 s,
        bytes32 pubKeyX,
        bytes32 pubKeyY
    ) external view returns (bool) {
        (bool success, bytes memory result) = SECP256R1.staticcall(
            abi.encodePacked(messageHash, r, s, pubKeyX, pubKeyY)
        );
        return success && result.length > 0 && uint8(result[0]) == 1;
    }
}

The secp256r1 precompile is also available as RIP-7212 on other EVM chains. The Lux implementation is ABI-compatible with RIP-7212.

ML-DSA Verify (FIPS 204)

Verifies post-quantum digital signatures based on the Module Learning With Errors (Module-LWE) problem. ML-DSA (formerly known as CRYSTALS-Dilithium) was standardized by NIST as FIPS 204 in August 2024.

PropertyValue
Address0x0200000000000000000000000000000000000006
Inputmessage || public_key || signature
Output0x01 (valid) or 0x00 (invalid)
Supported LevelsML-DSA-44, ML-DSA-65, ML-DSA-87
Gas Cost10,000 (ML-DSA-44), 15,000 (ML-DSA-65), 20,000 (ML-DSA-87)

The security level is inferred from the public key size:

Parameter SetSecurity LevelPublic KeySignature
ML-DSA-44NIST Level 2 (128-bit)1,312 bytes2,420 bytes
ML-DSA-65NIST Level 3 (192-bit)1,952 bytes3,293 bytes
ML-DSA-87NIST Level 5 (256-bit)2,592 bytes4,595 bytes
import { createPublicClient, http, encodePacked, toHex } from 'viem'
import { lux } from 'viem/chains'

const client = createPublicClient({
  chain: lux,
  transport: http(),
})

const ML_DSA_ADDRESS = '0x0200000000000000000000000000000000000006'

// Verify an ML-DSA-65 signature (security level inferred from key size)
const result = await client.call({
  to: ML_DSA_ADDRESS,
  data: encodePacked(
    ['bytes', 'bytes', 'bytes'],
    [message, publicKey, signature]  // publicKey is 1952 bytes -> ML-DSA-65
  ),
})

console.log('Valid:', result.data === '0x01')

ML-DSA signatures and public keys are significantly larger than classical ECDSA. An ML-DSA-65 signature is 3,293 bytes vs 65 bytes for secp256k1. Plan calldata costs accordingly.

SLH-DSA Verify (FIPS 205)

Verifies stateless hash-based post-quantum signatures. SLH-DSA (formerly known as SPHINCS+) was standardized by NIST as FIPS 205. It relies only on hash function security, making it the most conservative post-quantum choice.

PropertyValue
Address0x0600000000000000000000000000000000000001
Inputmessage || public_key || signature
Output0x01 (valid) or 0x00 (invalid)
Supported VariantsSLH-DSA-128s, SLH-DSA-128f, SLH-DSA-192s, SLH-DSA-192f, SLH-DSA-256s, SLH-DSA-256f
Gas Cost50,000 -- 200,000 (varies by parameter set)

The "s" (small) variants produce smaller signatures but are slower to verify. The "f" (fast) variants are faster to verify but produce larger signatures.

Parameter SetSecurityPublic KeySignature (small)Signature (fast)
SLH-DSA-128NIST Level 132 bytes7,856 bytes17,088 bytes
SLH-DSA-192NIST Level 348 bytes16,224 bytes35,664 bytes
SLH-DSA-256NIST Level 564 bytes29,792 bytes49,856 bytes
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract SLHDSAVerifier {
    address constant SLH_DSA = 0x0600000000000000000000000000000000000001;

    function verify(
        bytes memory message,
        bytes memory publicKey,
        bytes memory signature
    ) external view returns (bool) {
        (bool success, bytes memory result) = SLH_DSA.staticcall(
            abi.encodePacked(message, publicKey, signature)
        );
        return success && result.length > 0 && uint8(result[0]) == 1;
    }
}

SLH-DSA is the most conservative post-quantum scheme because it relies solely on hash functions, which are well-understood and resistant to both classical and quantum attacks. Use SLH-DSA when maximum long-term security is more important than performance or signature size.

Gas Cost Comparison

PrecompileGas CostKey SizeSignature Size
ecrecover (secp256k1)3,00064 bytes65 bytes
SR255193,00032 bytes64 bytes
Ed255193,00032 bytes64 bytes
secp256r13,45064 bytes64 bytes
ML-DSA-4410,0001,312 bytes2,420 bytes
ML-DSA-6515,0001,952 bytes3,293 bytes
ML-DSA-8720,0002,592 bytes4,595 bytes
SLH-DSA-128s50,00032 bytes7,856 bytes
SLH-DSA-256f200,00064 bytes49,856 bytes

For comparison, implementing Ed25519 verification in pure Solidity costs approximately 500,000 gas. The precompiles reduce this by 100x or more.

Is this guide helpful?