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).
| Property | Value |
|---|---|
| Address | 0x0a00000000000000000000000000000000000001 |
| Input | message || public_key (32 bytes) || signature (64 bytes) |
| Output | 0x01 (valid) or 0x00 (invalid) |
| Gas Cost | 3,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.
| Property | Value |
|---|---|
| Address | 0x0200000000000000000000000000000000000005 |
| Input | message || public_key (32 bytes) || signature (64 bytes) |
| Output | 0x01 (valid) or 0x00 (invalid) |
| Gas Cost | 3,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.
| Property | Value |
|---|---|
| Address | 0x0200000000000000000000000000000000000004 |
| Input | message_hash (32 bytes) || r (32 bytes) || s (32 bytes) || x (32 bytes) || y (32 bytes) |
| Output | 0x01 (valid) or 0x00 (invalid) |
| Gas Cost | 3,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.
| Property | Value |
|---|---|
| Address | 0x0200000000000000000000000000000000000006 |
| Input | message || public_key || signature |
| Output | 0x01 (valid) or 0x00 (invalid) |
| Supported Levels | ML-DSA-44, ML-DSA-65, ML-DSA-87 |
| Gas Cost | 10,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 Set | Security Level | Public Key | Signature |
|---|---|---|---|
| ML-DSA-44 | NIST Level 2 (128-bit) | 1,312 bytes | 2,420 bytes |
| ML-DSA-65 | NIST Level 3 (192-bit) | 1,952 bytes | 3,293 bytes |
| ML-DSA-87 | NIST Level 5 (256-bit) | 2,592 bytes | 4,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.
| Property | Value |
|---|---|
| Address | 0x0600000000000000000000000000000000000001 |
| Input | message || public_key || signature |
| Output | 0x01 (valid) or 0x00 (invalid) |
| Supported Variants | SLH-DSA-128s, SLH-DSA-128f, SLH-DSA-192s, SLH-DSA-192f, SLH-DSA-256s, SLH-DSA-256f |
| Gas Cost | 50,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 Set | Security | Public Key | Signature (small) | Signature (fast) |
|---|---|---|---|---|
| SLH-DSA-128 | NIST Level 1 | 32 bytes | 7,856 bytes | 17,088 bytes |
| SLH-DSA-192 | NIST Level 3 | 48 bytes | 16,224 bytes | 35,664 bytes |
| SLH-DSA-256 | NIST Level 5 | 64 bytes | 29,792 bytes | 49,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
| Precompile | Gas Cost | Key Size | Signature Size |
|---|---|---|---|
| ecrecover (secp256k1) | 3,000 | 64 bytes | 65 bytes |
| SR25519 | 3,000 | 32 bytes | 64 bytes |
| Ed25519 | 3,000 | 32 bytes | 64 bytes |
| secp256r1 | 3,450 | 64 bytes | 64 bytes |
| ML-DSA-44 | 10,000 | 1,312 bytes | 2,420 bytes |
| ML-DSA-65 | 15,000 | 1,952 bytes | 3,293 bytes |
| ML-DSA-87 | 20,000 | 2,592 bytes | 4,595 bytes |
| SLH-DSA-128s | 50,000 | 32 bytes | 7,856 bytes |
| SLH-DSA-256f | 200,000 | 64 bytes | 49,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?