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

from metamui_crypto import Sr25519

# Create master keypair from seed
master_seed = b"master seed for HD derivation..." # 32 bytes
master_keypair = Sr25519.from_seed(master_seed)

# Derive child keys using chain codes
chain_code = b"//polkadot//kusama//0"
child_keypair = Sr25519.derive_keypair(
    master_keypair,
    chain_code
)

# Hard derivation (more secure)
hard_chain_code = b"//polkadot//kusama//0"
hard_child = Sr25519.hard_derive_keypair(
    master_keypair,
    hard_chain_code
)

# Soft derivation (public key derivable)
soft_chain_code = b"/polkadot/kusama/0"
soft_child = Sr25519.soft_derive_keypair(
    master_keypair,
    soft_chain_code
)

# Can derive public key without private key (soft only)
soft_public = Sr25519.derive_public_key(
    master_keypair.public_key,
    soft_chain_code
)
assert soft_public == soft_child.public_key

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: Operations avoid timing leaks
  • Memory Safety: Clear sensitive data after use
  • Side Channels: Protected against power analysis

Common Pitfalls

1. Incorrect HD Derivation

# Bad: Mixing hard and soft derivation syntax
# path = "//hard/soft"  # Confusing!

# Good: Clear derivation paths
hard_path = "//polkadot//kusama//0"  # All hard
soft_path = "/polkadot/kusama/0"     # All soft
mixed_path = "//polkadot/kusama/0"   # Hard then soft

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