Ed25519 Algorithm
Ed25519 is the digital signature algorithm used by Solana, offering high security, fast performance, and small key/signature sizes.
Ed25519 Overview
Text
┌─────────────────────────────────────────────────────────────────┐
│ Ed25519 Specification │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Curve: Edwards25519 (twisted Edwards curve) │
│ Equation: -x² + y² = 1 + dx²y² where d = -121665/121666 │
│ │
│ Parameters: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Prime p = 2²⁵⁵ - 19 │ │
│ │ Order n = 2²⁵² + 27742317777372353535851937790883648493 │ │
│ │ Cofactor h = 8 │ │
│ │ Base point G = (x, 4/5) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Key Sizes: │
│ • Private key: 32 bytes (256 bits) │
│ • Public key: 32 bytes (256 bits) │
│ • Signature: 64 bytes (512 bits) │
│ │
│ Security Level: ~128 bits (equivalent to 3072-bit RSA) │
│ │
└─────────────────────────────────────────────────────────────────┘
Key Generation
TypeScript
import nacl from "tweetnacl";
import { sha512 } from "@noble/hashes/sha512";
// Ed25519 key generation process
function generateEd25519Keypair(seed?: Uint8Array): {
publicKey: Uint8Array;
secretKey: Uint8Array;
} {
// 1. Generate or use provided 32-byte seed
const privateSeed = seed || nacl.randomBytes(32);
// 2. Hash seed with SHA-512 to get 64 bytes
const hash = sha512(privateSeed);
// 3. Clamp the first 32 bytes (scalar)
// - Clear lowest 3 bits (ensure divisible by 8)
// - Set bit 254 (ensure high bit)
// - Clear bit 255 (ensure less than 2^255)
hash[0] &= 248; // Clear bits 0, 1, 2
hash[31] &= 127; // Clear bit 255
hash[31] |= 64; // Set bit 254
// 4. Scalar multiplication: A = s * G
// (This is the public key derivation)
const keypair = nacl.sign.keyPair.fromSeed(privateSeed);
return {
publicKey: keypair.publicKey, // 32 bytes
secretKey: keypair.secretKey, // 64 bytes (seed + public key)
};
}
// Solana keypair is just an Ed25519 keypair
import { Keypair } from "@solana/web3.js";
const keypair = Keypair.generate();
console.log("Private (seed):", keypair.secretKey.slice(0, 32)); // First 32 bytes
console.log("Public key:", keypair.publicKey.toBytes()); // Last 32 bytes
Signing Algorithm
TypeScript
// Ed25519 signature algorithm (EdDSA)
// This is a simplified explanation of the actual algorithm
/*
To sign message M with private key (seed s, public key A):
1. Hash the private seed to get (a, prefix)
h = SHA512(s)
a = h[0:32] (clamped scalar)
prefix = h[32:64]
2. Compute r = SHA512(prefix || M) mod L
(L is the curve order)
3. Compute R = r * G
(Point on the curve)
4. Compute k = SHA512(R || A || M) mod L
5. Compute S = (r + k * a) mod L
6. Signature = (R, S) - each 32 bytes = 64 bytes total
*/
import nacl from "tweetnacl";
function signEd25519(message: Uint8Array, secretKey: Uint8Array): Uint8Array {
// nacl.sign.detached returns just the signature (64 bytes)
// nacl.sign returns signature + message concatenated
return nacl.sign.detached(message, secretKey);
}
// Example
const keypair = nacl.sign.keyPair();
const message = new TextEncoder().encode("Hello, Solana!");
const signature = signEd25519(message, keypair.secretKey);
console.log("Signature length:", signature.length); // 64
console.log("Signature (hex):", Buffer.from(signature).toString("hex"));
Verification Algorithm
TypeScript
/*
To verify signature (R, S) on message M with public key A:
1. Check S < L (curve order) - reject if not
2. Compute k = SHA512(R || A || M) mod L
3. Check: S * G == R + k * A
- Left side: scalar multiplication
- Right side: point addition
If the equation holds, signature is valid.
This works because:
S * G = (r + k * a) * G
= r * G + k * a * G
= R + k * A ✓
*/
import nacl from "tweetnacl";
function verifyEd25519(
message: Uint8Array,
signature: Uint8Array,
publicKey: Uint8Array,
): boolean {
return nacl.sign.detached.verify(message, signature, publicKey);
}
// Example verification
const isValid = verifyEd25519(message, signature, keypair.publicKey);
console.log("Signature valid:", isValid);
// Tampered message
const tamperedMessage = new TextEncoder().encode("Hello, Solana!!");
const isValidTampered = verifyEd25519(
tamperedMessage,
signature,
keypair.publicKey,
);
console.log("Tampered valid:", isValidTampered); // false
Deterministic Signatures
TypeScript
// Ed25519 produces DETERMINISTIC signatures
// Same message + same key = same signature every time
const keypair = nacl.sign.keyPair();
const message = new TextEncoder().encode("Test message");
const sig1 = nacl.sign.detached(message, keypair.secretKey);
const sig2 = nacl.sign.detached(message, keypair.secretKey);
const sig3 = nacl.sign.detached(message, keypair.secretKey);
// All signatures are identical
console.log("sig1 === sig2:", Buffer.from(sig1).equals(Buffer.from(sig2))); // true
console.log("sig2 === sig3:", Buffer.from(sig2).equals(Buffer.from(sig3))); // true
// This is DIFFERENT from ECDSA which requires a random nonce
// Benefits:
// - No random number generator vulnerabilities
// - Reproducible for testing
// - No risk of nonce reuse attacks
Batch Verification
TypeScript
// Ed25519 supports efficient batch verification
// Verify multiple signatures faster than individually
import * as ed from "@noble/ed25519";
interface SignatureBundle {
message: Uint8Array;
signature: Uint8Array;
publicKey: Uint8Array;
}
async function batchVerify(bundles: SignatureBundle[]): Promise<boolean> {
// Batch verification equation:
// Σ(z_i * S_i) * G = Σ(z_i * R_i) + Σ(z_i * k_i * A_i)
// Where z_i are random scalars to prevent attacks
// Using @noble/ed25519 for batch verification
const results = await Promise.all(
bundles.map((b) => ed.verifyAsync(b.signature, b.message, b.publicKey)),
);
return results.every((r) => r === true);
}
// Performance comparison
async function benchmark() {
const keypairs = Array.from({ length: 100 }, () => nacl.sign.keyPair());
const messages = keypairs.map((_, i) =>
new TextEncoder().encode(`Message ${i}`),
);
const signatures = keypairs.map((kp, i) =>
nacl.sign.detached(messages[i], kp.secretKey),
);
// Individual verification
const start1 = performance.now();
for (let i = 0; i < 100; i++) {
nacl.sign.detached.verify(
messages[i],
signatures[i],
keypairs[i].publicKey,
);
}
console.log("Individual:", performance.now() - start1, "ms");
// Batch verification (conceptual - actual implementation varies)
const bundles = keypairs.map((kp, i) => ({
message: messages[i],
signature: signatures[i],
publicKey: kp.publicKey,
}));
const start2 = performance.now();
await batchVerify(bundles);
console.log("Batch:", performance.now() - start2, "ms");
}
Ed25519 vs Other Curves
| Feature | Ed25519 | secp256k1 (Bitcoin) | P-256 |
|---|---|---|---|
| Key size | 32 bytes | 32 bytes | 32 bytes |
| Signature | 64 bytes | 64-72 bytes | 64 bytes |
| Signing speed | ~15,000/s | ~5,000/s | ~5,000/s |
| Verify speed | ~8,000/s | ~4,000/s | ~4,000/s |
| Deterministic | Yes | No (without RFC6979) | No |
| Side-channel resistant | Yes | Needs care | Needs care |
Security Properties
TypeScript
// Ed25519 security features
// 1. Small subgroup attack resistance
// The cofactor (8) is handled in verification
// No need for separate point validation
// 2. Timing attack resistance
// Operations are constant-time by design
function constantTimeCompare(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) {
diff |= a[i] ^ b[i];
}
return diff === 0;
}
// 3. No weak keys
// All 32-byte values are valid private keys
// (after clamping)
// 4. Canonicality
// Only one valid signature exists for each message+key pair
// Prevents signature malleability attacks
// Verify signature is canonical (standard Ed25519)
function isCanonicalSignature(sig: Uint8Array): boolean {
// S must be less than L (curve order)
// R must be a valid curve point
return sig.length === 64;
// (Actual validation is done in verify function)
}
Using Ed25519 with Solana
TypeScript
import { Keypair, PublicKey, Transaction } from "@solana/web3.js";
import nacl from "tweetnacl";
// Solana uses Ed25519 for all signatures
class SolanaEd25519 {
// Create keypair
static generateKeypair(): Keypair {
return Keypair.generate();
}
// Sign arbitrary message
static signMessage(message: Uint8Array, keypair: Keypair): Uint8Array {
return nacl.sign.detached(message, keypair.secretKey);
}
// Verify message signature
static verifyMessage(
message: Uint8Array,
signature: Uint8Array,
publicKey: PublicKey,
): boolean {
return nacl.sign.detached.verify(message, signature, publicKey.toBytes());
}
// Sign transaction
static signTransaction(tx: Transaction, signers: Keypair[]): Transaction {
for (const signer of signers) {
tx.partialSign(signer);
}
return tx;
}
// Verify transaction signatures
static verifyTransaction(tx: Transaction): boolean {
return tx.verifySignatures();
}
// Check if address is on Ed25519 curve
static isOnCurve(publicKey: PublicKey): boolean {
return PublicKey.isOnCurve(publicKey.toBytes());
}
}
// Usage
const keypair = SolanaEd25519.generateKeypair();
const message = Buffer.from("Sign this message");
const signature = SolanaEd25519.signMessage(message, keypair);
const isValid = SolanaEd25519.verifyMessage(
message,
signature,
keypair.publicKey,
);
console.log("Public key:", keypair.publicKey.toBase58());
console.log("Signature valid:", isValid);
console.log("On curve:", SolanaEd25519.isOnCurve(keypair.publicKey));
Ed25519ph (Pre-hashed)
TypeScript
// Ed25519ph variant for large messages
// Hash the message first, then sign the hash
import { sha512 } from "@noble/hashes/sha512";
function ed25519phSign(message: Uint8Array, secretKey: Uint8Array): Uint8Array {
// Pre-hash the message with SHA-512
const hash = sha512(message);
// Sign the hash with domain separation
const domainSeparator = new TextEncoder().encode(
"SigEd25519 no Ed25519 collisions\x01",
);
const toSign = new Uint8Array([...domainSeparator, ...hash]);
return nacl.sign.detached(toSign, secretKey);
}
// Standard Ed25519 is preferred for most uses
// Ed25519ph is useful for:
// - Very large messages
// - Streaming scenarios
// - Hardware constraints
Key Takeaways
| Aspect | Details |
|---|---|
| Algorithm | EdDSA on Edwards25519 curve |
| Security | 128-bit (equivalent to RSA-3072) |
| Performance | ~70,000 signatures/second |
| Deterministic | Yes, same input = same signature |
| Solana usage | All transaction and message signing |
Next: Zero-Knowledge Proofs - Proving without revealing.