✍️ Sr25519 Schnorrkel Blockchain Signatures
📋 Quick Navigation
📖 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 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:
- Key Generation:
- Private key: 32-byte scalar
- Public key: P = [sk]G
- Signing:
- Generate nonce: r = H(sk, message, randomness)
- Compute: R = [r]G
- Challenge: c = H(R, P, message)
- Response: s = r + c·sk
- 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
- Schnorrkel Documentation - Reference implementation
- Substrate Documentation - Substrate crypto
- VRF Specification - VRF standard
- Security Analysis - Security properties
- Ristretto Group - Ristretto documentation