Side-channel claim limit. Nothing on this page claims constant-time behaviour or side-channel resistance for any target: the side-channel harness covers nothing (finding A48) and no measurement exists. The Rust and C cores are designed for constant time and unmeasured; managed runtimes (JIT, GC, bounds checks) make no timing claim. Where this page names constant-time or side-channel techniques, in prose or in code, they are design intent or recommendations, not measured properties.

Digital Signatures

✍️ Sr25519 Schnorrkel Blockchain Signatures

Security Level 128-bit
Performance ⭐⭐⭐⭐⭐ Excellent
Quantum Resistant ❌ No
Group Ristretto over Curve25519
Key Size 32 bytes
Signature Size 64 bytes

📖 Overview

Sr25519 (Schnorrkel) is a Schnorr signature scheme over Ristretto compressed Ed25519 points. It's designed specifically for blockchain applications, providing additional features like VRF capabilities and hierarchical deterministic key derivation for the Substrate/Polkadot ecosystem.

✨ Key Features

🔏

Schnorr Signatures

Simple, efficient, and provably secure signature scheme

🎲

VRF Support

Built-in Verifiable Random Function capability

🔑

HD Derivation

Hierarchical deterministic key derivation

🚀

Batch Verification

Efficient multi-signature verification

🔗

Substrate Native

Primary signature scheme for Polkadot/Substrate

🛡️

Ristretto Group

Prime-order group eliminating cofactor issues

🎯 Blockchain Applications

🔗 Substrate Ecosystem

  • Polkadot Network: Primary signature scheme for consensus
  • Kusama Network: Canary network implementation
  • Substrate Chains: Custom blockchain implementations
  • Parachain Validation: Cross-chain communication signatures

🎲 VRF Applications

  • BABE Consensus: Block production randomness
  • Validator Selection: Deterministic randomness for consensus
  • Slot Assignment: Fair distribution of block production slots
  • Random Beacons: Publicly verifiable randomness

🔧 Algorithm Parameters

📊 Sr25519 Parameters

Security Level
128 bits
Elliptic curve discrete log security
Public Key Size
32 bytes
Ristretto point encoding
Private Key Size
32 bytes
Secret scalar value
Signature Size
64 bytes
R (32 bytes) + S (32 bytes)
VRF Proof Size
96 bytes
VRF output + proof data
Group
Ristretto
Prime-order abstraction over Curve25519

🛡️ Security Properties

🔏

Schnorr Security

Provably secure under discrete log assumption

🎲

VRF Properties

Uniqueness, pseudorandomness, and verifiability

🛡️

No Cofactor Issues

Ristretto eliminates small subgroup attacks

⚡

Batch Verification

Efficient verification of multiple signatures

💻 Usage Examples

Basic Signing and Verification

from metamui_crypto import Sr25519

# Generate keypair
keypair = Sr25519.generate_keypair()

# Sign a message
message = b"Substrate transaction data"
signature = Sr25519.sign(message, keypair.private_key)

# Verify signature
is_valid = Sr25519.verify(signature, message, keypair.public_key)
print(f"Signature valid: {is_valid}")

Signing with Context

from metamui_crypto import Sr25519

# Sr25519 supports signing contexts for domain separation
keypair = Sr25519.generate_keypair()
message = b"Transfer 100 DOT to Alice"

# Sign with context
context = b"substrate"
signature = Sr25519.sign_with_context(
    message,
    keypair.private_key,
    context
)

# Verify with same context
is_valid = Sr25519.verify_with_context(
    signature,
    message,
    keypair.public_key,
    context
)

# Different context will fail
wrong_context = b"polkadot"
is_invalid = Sr25519.verify_with_context(
    signature,
    message,
    keypair.public_key,
    wrong_context
)
assert not is_invalid

🎲 VRF (Verifiable Random Function)

from metamui_crypto import Sr25519

# Generate VRF keypair
vrf_keypair = Sr25519.generate_keypair()

# Create VRF proof
input_data = b"block_number:12345"
vrf_output, vrf_proof = Sr25519.vrf_sign(
    input_data,
    vrf_keypair.private_key
)

# Verify VRF proof
is_valid, verified_output = Sr25519.vrf_verify(
    input_data,
    vrf_output,
    vrf_proof,
    vrf_keypair.public_key
)

print(f"VRF valid: {is_valid}")
print(f"Random output: {vrf_output.hex()}")

# VRF provides deterministic randomness
vrf_output2, _ = Sr25519.vrf_sign(input_data, vrf_keypair.private_key)
assert vrf_output == vrf_output2  # Same input = same output

🔑 Hierarchical Deterministic (HD) Derivation

Derivation paths are Substrate’s, computed exactly as sp_core::sr25519::Pair::derive does, so //Alice from the development phrase’s mini-secret is the account every Substrate node prints for Alice (5GrwvaEF…, public key d43593c7…).

  • A hard junction //name or //0: the chain code is the SCALE encoding of the junction — a u64 (so //+7 and //007 are //7) as 8 little-endian bytes, or a string as compact-length ‖ UTF-8 — zero-padded to 32 bytes, or BLAKE2b-256 of the encoding when longer. The child mini-secret is schnorrkel’s hard_derive_mini_secret_key over the parent’s expanded secret scalar.
  • A soft junction /name: the same transcript over the parent public key; the 64-byte HDKD-scalar challenge, wide-reduced, is added to the parent scalar. A soft child has no mini-secret behind it (it is an expanded scalar ‖ nonce), and a hard junction after a soft one hashes that expanded scalar back to a mini-secret.
  • //Alice/ (trailing slash) and ///password (Substrate’s password separator, not implemented) are refused, never silently reinterpreted.

Which binding implements what — all of them are gated by the same vector file, test-vectors/sr25519/sr25519-hdkd-vectors.json (generated from schnorrkel 0.11.4 and anchored on the dev accounts):

binding hard junctions soft junctions entry point
Rust (metamui-sr25519) ✓ ✓ (secret and public-only) derive_keypair_from_path(seed, path), derive_public_key_from_path(public_key, path)
WASM (metamui-crypto-wasm/sr25519) ✓ ✓ sr25519_derive_keypair_from_path (returns pk ‖ secret, 64 or 96 bytes), sr25519_derive_public_from_public_with_path
Rust (metamui-mnemonic) ✓ (via metamui-sr25519) ✓ derive_sr25519_keypair_with_path(phrase, password, path) — the mini-secret is substrate-bip39’s
Go ✓ refused (ErrSoftDerivationUnsupported) sr25519.DeriveKeyPairFromPath(seed, path)
C ✓ refused (SR25519_ERROR_UNSUPPORTED) sr25519_derive_from_path(&kp, mini, seed, path)
TypeScript (@metamui/sr25519) ✓ refused (SoftDerivationUnsupported) deriveKeypair(seed, path), deriveFromMnemonic(phrase, path)
Kotlin ✓ refused (Sr25519SoftDerivationUnsupportedException) Sr25519.derive(miniSecret, path)
Python ✓ refused (Sr25519SoftDerivationUnsupportedError) metamui_crypto.sr25519.derive_keypair_from_path(seed, path), MetaMUIMnemonic.derive_sr25519_keypair(phrase, derivation_path=…)
Swift, Java, C# — — no derivation API
use metamui_sr25519::{derive_keypair_from_path, derive_public_key_from_path};

// the Substrate dev phrase's mini-secret (substrate-bip39 output)
let dev: [u8; 32] = hex!("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e");

let alice = derive_keypair_from_path(&dev, "//Alice")?;        // private_key: 32-byte mini-secret
assert_eq!(hex::encode(&alice.public_key), "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d");

let soft = derive_keypair_from_path(&dev, "//Alice/0")?;       // private_key: 64-byte expanded secret
let parent_pk = derive_keypair_from_path(&dev, "//Alice")?.public_key;
assert_eq!(derive_public_key_from_path(&parent_pk, "/0")?.to_vec(), soft.public_key); // public-only soft derivation
from metamui_crypto.sr25519 import derive_keypair_from_path
from metamui_crypto.mnemonic.mnemonic import MetaMUIMnemonic

alice = derive_keypair_from_path(dev_mini_secret, "//Alice")       # hard junctions only
alice_from_phrase = MetaMUIMnemonic().derive_sr25519_keypair(
    "bottom drive obey lake curtain smoke basket hold race lonely fit walk",
    derivation_path="//Alice",
)

From a phrase. Substrate does not use the BIP-39 seed for sr25519: the mini-secret is substrate-bip39’s — PBKDF2-HMAC-SHA512 with the BIP-39 entropy as the password, salt "mnemonic" ‖ password, 2048 rounds, first 32 bytes. metamui-mnemonic (Rust), @metamui/sr25519’s mnemonicToMiniSecret, MetaMUIMnemonic (Python) and Swift’s Mnemonic.miniSecret(passphrase:) / deriveSr25519KeyPair(passphrase:) all implement it (Swift has no path derivation on top of it).

Multi-Signature Aggregation

from metamui_crypto import Sr25519

# Multiple parties sign same message
message = b"Multisig transaction"
parties = [Sr25519.generate_keypair() for _ in range(3)]

# Each party creates their signature
signatures = []
for keypair in parties:
    sig = Sr25519.sign(message, keypair.private_key)
    signatures.append(sig)

# Aggregate signatures (simplified - real implementation needs MuSig)
class MultiSig:
    @staticmethod
    def aggregate_public_keys(public_keys):
        """Aggregate multiple public keys"""
        # Simplified - real MuSig needs proper key aggregation
        return public_keys[0]  # Placeholder
    
    @staticmethod
    def verify_multisig(signatures, message, public_keys, threshold):
        """Verify threshold signatures"""
        valid_count = 0
        for sig, pk in zip(signatures, public_keys):
            if Sr25519.verify(sig, message, pk):
                valid_count += 1
        
        return valid_count >= threshold

# Verify 2-of-3 multisig
public_keys = [kp.public_key for kp in parties]
is_valid = MultiSig.verify_multisig(
    signatures[:2],  # Only need 2 signatures
    message,
    public_keys[:2],
    threshold=2
)

Implementation Details

Ristretto Group

Sr25519 uses Ristretto, which provides:

  • Prime-order group: No cofactor issues
  • Complete formulas: No exceptional cases
  • Encoding/Decoding: Canonical representation
  • Security: Eliminates small subgroup attacks

Schnorr Signatures

The signature scheme:

  1. Key Generation:
    • Private key: 32-byte scalar
    • Public key: P = [sk]G
  2. Signing:
    • Generate nonce: r = H(sk, message, randomness)
    • Compute: R = [r]G
    • Challenge: c = H(R, P, message)
    • Response: s = r + c·sk
  3. Verification:
    • Check: [s]G = R + [c]P

VRF Construction

Sr25519’s VRF provides:

  • Uniqueness: One valid output per input
  • Pseudorandomness: Output indistinguishable from random
  • Verifiability: Anyone can verify the output

Performance Characteristics

Operation Benchmarks

Operation Time (μs) Notes
Key Generation 18 From secure random
Sign 30 Including nonce generation
Verify 55 Single signature
VRF Sign 45 Includes proof generation
VRF Verify 70 Proof verification
HD Derivation 25 Per level

Batch Verification Performance

Batch Size Time (ms) Throughput (sigs/sec)
1 0.055 18,000
64 1.8 35,000
256 6.5 39,000
1024 25 41,000

Advanced Usage

Threshold Signatures with FROST

from metamui_crypto import Sr25519
import secrets

class FROSTSr25519:
    """FROST threshold signatures for Sr25519"""
    
    def __init__(self, threshold, total_parties):
        self.t = threshold
        self.n = total_parties
        self.commitments = {}
        self.shares = {}
    
    def keygen(self):
        """Distributed key generation"""
        # Each party generates polynomial coefficients
        for i in range(self.n):
            coeffs = [secrets.randbelow(Sr25519.ORDER) for _ in range(self.t)]
            self.shares[i] = coeffs
            
            # Compute and broadcast commitments
            commitments = []
            for coeff in coeffs:
                commitment = Sr25519.scalar_mult_base(coeff)
                commitments.append(commitment)
            self.commitments[i] = commitments
        
        # Compute shares for each party
        party_shares = {}
        for i in range(self.n):
            share = 0
            for j in range(self.n):
                # Evaluate polynomial at i+1
                x = i + 1
                poly_value = sum(
                    coeff * pow(x, k, Sr25519.ORDER) 
                    for k, coeff in enumerate(self.shares[j])
                ) % Sr25519.ORDER
                share = (share + poly_value) % Sr25519.ORDER
            party_shares[i] = share
        
        return party_shares
    
    def sign_round1(self, message, party_id, share):
        """First round of signing"""
        # Generate nonces
        d = secrets.randbelow(Sr25519.ORDER)
        e = secrets.randbelow(Sr25519.ORDER)
        
        # Compute commitments
        D = Sr25519.scalar_mult_base(d)
        E = Sr25519.scalar_mult_base(e)
        
        return {
            'party_id': party_id,
            'D': D,
            'E': E,
            'nonces': (d, e)
        }
    
    def sign_round2(self, message, round1_data, share):
        """Second round of signing"""
        # Aggregate commitments
        R = self._aggregate_commitments(round1_data)
        
        # Compute challenge
        c = Sr25519.compute_challenge(R, self.group_public_key, message)
        
        # Compute signature share
        party_data = round1_data[self.party_id]
        d, e = party_data['nonces']
        s = (d + e * c + share * c) % Sr25519.ORDER
        
        return s

Account Abstraction

from metamui_crypto import Sr25519, Blake2b

class Sr25519Account:
    """Substrate account abstraction"""
    
    def __init__(self, keypair):
        self.keypair = keypair
        self.nonce = 0
    
    @property
    def address(self):
        """Derive SS58 address"""
        # Address = base58(type_byte + pubkey + checksum)
        addr_type = b'\x00'  # Generic Substrate
        
        # Create address payload
        payload = addr_type + self.keypair.public_key
        
        # Compute checksum
        context = b"SS58PRE"
        hasher = Blake2b(key=context, digest_size=64)
        hasher.update(payload)
        checksum = hasher.finalize()[:2]
        
        # Encode address
        full_payload = payload + checksum
        return base58.b58encode(full_payload).decode()
    
    def sign_transaction(self, tx_data):
        """Sign transaction with replay protection"""
        # Include nonce for replay protection
        signing_payload = {
            'nonce': self.nonce,
            'data': tx_data,
            'genesis_hash': self.genesis_hash,
            'spec_version': self.spec_version
        }
        
        # Encode payload
        encoded = scale_encode(signing_payload)
        
        # Sign with context
        signature = Sr25519.sign_with_context(
            encoded,
            self.keypair.private_key,
            b"substrate-transaction"
        )
        
        self.nonce += 1
        return signature

Ring Signatures

class RingSr25519:
    """Ring signatures using Sr25519"""
    
    @staticmethod
    def ring_sign(message, private_key, public_keys, signer_index):
        """Create ring signature"""
        n = len(public_keys)
        
        # Initialize arrays
        c = [None] * n
        s = [None] * n
        
        # Random starting point
        start = (signer_index + 1) % n
        c[start] = secrets.randbelow(Sr25519.ORDER)
        
        # Generate fake responses
        for i in range(n):
            if i != signer_index:
                s[i] = secrets.randbelow(Sr25519.ORDER)
        
        # Complete the ring
        for i in range(start, start + n):
            idx = i % n
            if idx == signer_index:
                continue
                
            # Compute next challenge
            R = Sr25519.scalar_mult_base(s[idx])
            R = Sr25519.point_add(R, Sr25519.scalar_mult(c[idx], public_keys[idx]))
            
            next_idx = (idx + 1) % n
            c[next_idx] = Sr25519.hash_to_scalar(R + message)
        
        # Compute real response
        r = secrets.randbelow(Sr25519.ORDER)
        R_real = Sr25519.scalar_mult_base(r)
        c[signer_index] = Sr25519.hash_to_scalar(R_real + message)
        s[signer_index] = (r - c[signer_index] * private_key) % Sr25519.ORDER
        
        return {'c0': c[0], 's': s}

🔒 Security Considerations

Key Security

  • Secure Generation: Always use cryptographically secure randomness
  • HD Derivation: Use hard derivation for high-security keys
  • Key Storage: Protect private keys with encryption

Protocol Security

  • Context Binding: Always use contexts to prevent cross-protocol attacks
  • Nonce Generation: Deterministic nonces prevent randomness failures
  • Batch Verification: Randomize verification equations

Implementation Security

  • Constant Time: not claimed (unmeasured, A48)
  • Memory Safety: Clear sensitive data after use
  • Side Channels: not claimed (unmeasured, A48)

Common Pitfalls

1. Incorrect HD Derivation

# Hard (//) and soft (/) junctions may be mixed; this is sp_core's syntax:
hard_path  = "//polkadot//kusama//0"  # all hard — every binding
mixed_path = "//polkadot/kusama/0"    # hard then soft — Rust/WASM only; the other bindings refuse
soft_path  = "/polkadot/kusama/0"     # all soft — derivable from the public key alone (Rust)

# Refused everywhere, never reinterpreted:
# "//Alice/"        trailing slash (empty junction)
# "///password"     Substrate's password separator — pass the password to the mnemonic API instead
# "Alice"           no leading slash

# Numeric junctions are u64: "//+7" and "//007" are "//7".

2. VRF Misuse

# Bad: Using VRF output directly as randomness
# random_value = vrf_output  # Predictable to key holder!

# Good: Additional hashing for randomness extraction
vrf_output, proof = Sr25519.vrf_sign(input, private_key)
random_value = Blake2b.hash(vrf_output + b"randomness-extraction")

3. Context Confusion

# Bad: No context or wrong context
# signature = Sr25519.sign(message, private_key)

# Good: Always use appropriate context
signature = Sr25519.sign_with_context(
    message,
    private_key,
    b"my-protocol-v1"  # Protocol-specific context
)

Integration Examples

Substrate Runtime Integration

class SubstrateExtrinsic:
    """Substrate extrinsic (transaction) creation"""
    
    def __init__(self, signer_keypair):
        self.signer = signer_keypair
        self.era = "immortal"
        self.nonce = 0
        self.tip = 0
    
    def create_signed_extrinsic(self, call_data):
        """Create signed extrinsic"""
        # Create extra data
        extra = {
            'era': self.era,
            'nonce': self.nonce,
            'tip': self.tip
        }
        
        # Create additional signed data
        additional = {
            'spec_version': 1000,
            'genesis_hash': "0x1234...",
            'block_hash': "0x5678..."
        }
        
        # Encode signing payload
        signing_payload = encode_compact(call_data + extra + additional)
        
        # Sign payload
        if len(signing_payload) > 256:
            # Long payload - sign hash
            to_sign = Blake2b.hash(signing_payload, size=32)
        else:
            to_sign = signing_payload
        
        signature = Sr25519.sign(to_sign, self.signer.private_key)
        
        # Create extrinsic
        extrinsic = {
            'signature': {
                'signer': self.signer.public_key,
                'signature': signature,
                'extra': extra
            },
            'call': call_data
        }
        
        return scale_encode(extrinsic)

Polkadot.js Compatible

import json

def to_polkadot_js_format(keypair):
    """Export keypair in Polkadot.js format"""
    return {
        "encoded": base64.b64encode(
            keypair.private_key + keypair.public_key
        ).decode(),
        "encoding": {
            "content": ["pkcs8", "sr25519"],
            "type": ["scrypt", "xsalsa20-poly1305"],
            "version": "3"
        },
        "address": derive_ss58_address(keypair.public_key),
        "meta": {
            "name": "Account Name",
            "whenCreated": int(time.time() * 1000)
        }
    }

Resources