Documentation Index Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt
Use this file to discover all available pages before exploring further.
Try it Live Run Signature examples in the interactive playground
Validation
Signature validation and canonicalization functions.
isCanonical
Check if ECDSA signature has canonical s-value.
Signature
function isCanonical ( signature : BrandedSignature ) : boolean
Parameters
signature - BrandedSignature to check
Returns
true - If signature is canonical (s ≤ n/2) or Ed25519
false - If signature is non-canonical (s > n/2)
Example
const sig = Signature . fromSecp256k1 ( r , s , 27 );
if ( Signature . isCanonical ( sig )) {
console . log ( 'Signature is canonical' );
} else {
console . log ( 'Signature needs normalization' );
const canonical = Signature . normalize ( sig );
}
// Ed25519 always canonical
const ed25519Sig = Signature . fromEd25519 ( bytes );
console . log ( Signature . isCanonical ( ed25519Sig )); // true
Canonicality Rules
ECDSA (secp256k1, P-256)
A signature is canonical if s ≤ n/2, where n is the curve order.
secp256k1:
n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
n/2 = 7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
P-256:
n = FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
n/2 = 7FFFFFFF800000007FFFFFFFFFFFFFFFDE737D56D38BCF4279DCE5617E3192A8
Why Canonicality Matters
ECDSA signatures have inherent malleability: both (r, s) and (r, -s mod n) are valid for the same message and public key.
Security Issues:
Transaction malleability attacks
Signature duplication
Replay attacks
Standards:
Bitcoin BIP-62: Require canonical form
Ethereum: Enforce low s-value
Most modern protocols: Mandate canonicality
Example: Non-Canonical Signature
// High s-value (non-canonical)
const sHigh = new Uint8Array ([
0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff ,
0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff , 0xff ,
0xba , 0xae , 0xdc , 0xe6 , 0xaf , 0x48 , 0xa0 , 0x3b ,
0xbf , 0xd2 , 0x5e , 0x8c , 0xd0 , 0x36 , 0x41 , 0x40 ,
]); // s > n/2
const sig = Signature . fromSecp256k1 ( r , sHigh , 27 );
console . log ( Signature . isCanonical ( sig )); // false
// Normalized (canonical)
const canonical = Signature . normalize ( sig );
console . log ( Signature . isCanonical ( canonical )); // true
normalize
Normalize ECDSA signature to canonical form.
Signature
function normalize ( signature : BrandedSignature ) : BrandedSignature
Parameters
signature - BrandedSignature to normalize
Returns
BrandedSignature with canonical s-value (s ≤ n/2)
If already canonical: Returns input unchanged
If Ed25519: Returns input unchanged (always canonical)
If non-canonical: Returns new signature with s = n - s and flipped v
Example
const sig = Signature . fromSecp256k1 ( r , sHigh , 27 );
// Normalize to canonical form
const canonical = Signature . normalize ( sig );
console . log ( Signature . isCanonical ( sig )); // false
console . log ( Signature . isCanonical ( canonical )); // true
// v flipped when normalizing
console . log ( Signature . getV ( sig )); // 27
console . log ( Signature . getV ( canonical )); // 28
// r unchanged
const rOrig = Signature . getR ( sig );
const rNorm = Signature . getR ( canonical );
console . log ( rOrig . every (( b , i ) => b === rNorm [ i ])); // true
// s normalized
const sOrig = Signature . getS ( sig );
const sNorm = Signature . getS ( canonical );
console . log ( sOrig . every (( b , i ) => b === sNorm [ i ])); // false
Normalization Process
For non-canonical signature (s > n/2):
Calculate s_normalized = n - s
Flip recovery ID: v_new = (v === 27) ? 28 : 27
Return new signature with (r, s_normalized, v_new)
// Before normalization
const sig = Signature . fromSecp256k1 ( r , sHigh , 27 );
// r: [original r]
// s: [high s-value]
// v: 27
// After normalization
const canonical = Signature . normalize ( sig );
// r: [original r] (unchanged)
// s: [n - s] (normalized)
// v: 28 (flipped)
Algorithm Details
// Pseudocode for normalization
function normalize ( sig : BrandedSignature ) : BrandedSignature {
// Ed25519 always canonical
if ( sig . algorithm === 'ed25519' ) {
return sig ;
}
// Already canonical
if ( isCanonical ( sig )) {
return sig ;
}
// Extract components
const r = getR ( sig );
const s = getS ( sig );
// Get curve order
const n = getCurveOrder ( sig . algorithm );
// Calculate s_normalized = n - s (modular subtraction)
const sNormalized = modularSubtract ( n , s );
// Flip v if present
const v = sig . v !== undefined
? ( sig . v === 27 ? 28 : 27 )
: undefined ;
// Return normalized signature
return fromSecp256k1 ( r , sNormalized , v );
}
Recovery ID Flip
When normalizing, the recovery ID must flip:
// Original signature with high s
const sig1 = Signature . fromSecp256k1 ( r , sHigh , 27 );
// Normalization flips v
const sig2 = Signature . normalize ( sig1 );
console . log ( Signature . getV ( sig2 )); // 28
// Double normalization returns to original v
const sig3 = Signature . normalize ( sig2 );
console . log ( Signature . getV ( sig3 )); // 27
// But s is different (both canonical)
const s2 = Signature . getS ( sig2 );
const s3 = Signature . getS ( sig3 );
console . log ( s2 . every (( b , i ) => b === s3 [ i ])); // false
Reason: Negating s changes which of the two possible public keys is correct, so recovery ID must flip.
Use Cases
Ethereum Transaction Signing
// Sign transaction
const txHash = keccak256 ( encodedTx );
const sig = secp256k1 . sign ( txHash , privateKey );
// Ensure canonical before including in transaction
const canonical = Signature . normalize ( sig );
// Only canonical signatures accepted by network
const tx = {
... txData ,
r: Signature . getR ( canonical ),
s: Signature . getS ( canonical ),
v: Signature . getV ( canonical ),
};
Bitcoin Transaction Validation
// Parse DER signature from transaction
const sig = Signature . fromDER ( derBytes , 'secp256k1' );
// Bitcoin requires canonical signatures (BIP-62)
if ( ! Signature . isCanonical ( sig )) {
throw new Error ( 'Non-canonical signature rejected' );
}
// Verify signature
const valid = verifySig ( sig , txHash , publicKey );
Signature Verification
// Accept both canonical and non-canonical
function verifyFlexible (
sig : BrandedSignature ,
message : Uint8Array ,
publicKey : Uint8Array
) : boolean {
// Normalize before verification
const canonical = Signature . normalize ( sig );
return verify ( canonical , message , publicKey );
}
// Strict verification (reject non-canonical)
function verifyStrict (
sig : BrandedSignature ,
message : Uint8Array ,
publicKey : Uint8Array
) : boolean {
if ( ! Signature . isCanonical ( sig )) {
throw new Error ( 'Non-canonical signature rejected' );
}
return verify ( sig , message , publicKey );
}
API Validation
// Validate signature before storing
function storeSignature ( sig : BrandedSignature ) : void {
// Normalize to canonical form
const canonical = Signature . normalize ( sig );
// Store canonical version
db . signatures . insert ({
r: Signature . getR ( canonical ),
s: Signature . getS ( canonical ),
v: Signature . getV ( canonical ),
});
}
// Retrieve and verify is canonical
function getSignature ( id : string ) : BrandedSignature {
const data = db . signatures . findById ( id );
const sig = Signature . fromSecp256k1 ( data . r , data . s , data . v );
// Assert stored signatures are canonical
if ( ! Signature . isCanonical ( sig )) {
throw new Error ( 'Database corruption: non-canonical signature' );
}
return sig ;
}
Validation Patterns
Pre-Normalization Check
// Check before normalizing
if ( ! Signature . isCanonical ( sig )) {
console . log ( 'Signature needs normalization' );
sig = Signature . normalize ( sig );
}
// Now guaranteed canonical
console . assert ( Signature . isCanonical ( sig ));
Enforce Canonical
function ensureCanonical ( sig : BrandedSignature ) : BrandedSignature {
if ( ! Signature . isCanonical ( sig )) {
return Signature . normalize ( sig );
}
return sig ;
}
// Always returns canonical signature
const canonical = ensureCanonical ( anySig );
Reject Non-Canonical
function requireCanonical ( sig : BrandedSignature ) : void {
if ( ! Signature . isCanonical ( sig )) {
throw new NonCanonicalSignatureError (
'Signature must have low s-value'
);
}
}
// Throws if not canonical
requireCanonical ( sig );
Security Considerations
Transaction Malleability
// Problem: Both signatures valid for same transaction
const sig1 = Signature . fromSecp256k1 ( r , s , 27 );
const sig2 = Signature . fromSecp256k1 ( r , sNeg , 28 ); // s_neg = n - s
// Both verify correctly
verify ( sig1 , txHash , pubKey ); // true
verify ( sig2 , txHash , pubKey ); // true
// But produce different transaction hashes
const tx1Hash = hash ( encodeTx ( sig1 ));
const tx2Hash = hash ( encodeTx ( sig2 ));
console . log ( tx1Hash !== tx2Hash ); // true
// Solution: Enforce canonical signatures
const canonical = Signature . normalize ( sig1 );
// Only canonical signatures allowed
Signature Uniqueness
// Without canonicalization: multiple valid signatures
function getAllValidSignatures ( r , s , v ) : BrandedSignature [] {
const n = CURVE_ORDER ;
const sNeg = modSubtract ( n , s );
const vFlipped = v === 27 ? 28 : 27 ;
return [
Signature . fromSecp256k1 ( r , s , v ),
Signature . fromSecp256k1 ( r , sNeg , vFlipped ),
];
}
// With canonicalization: unique signature
function getCanonicalSignature ( r , s , v ) : BrandedSignature {
const sig = Signature . fromSecp256k1 ( r , s , v );
return Signature . normalize ( sig );
}
Replay Attack Prevention
// Store signature hash to prevent replay
const canonical = Signature . normalize ( sig );
const sigHash = keccak256 ( Signature . toBytes ( canonical ));
// Check if signature already used
if ( await db . usedSignatures . exists ( sigHash )) {
throw new Error ( 'Signature already used' );
}
// Mark as used
await db . usedSignatures . insert ( sigHash );
Canonicality Check
// Fast check: O(n) byte comparison
const isCanonical = Signature . isCanonical ( sig );
// Typical performance: < 0.001ms for 32-byte comparison
Normalization
// O(n) operation: modular subtraction
const canonical = Signature . normalize ( sig );
// If already canonical: O(1) (returns input)
// If needs normalization: O(n) (creates new signature)
// Typical performance: < 0.01ms
Optimization
// Avoid unnecessary normalization
if ( Signature . isCanonical ( sig )) {
// Use sig directly
return sig ;
} else {
// Normalize only if needed
return Signature . normalize ( sig );
}
// Or use normalize (checks internally)
return Signature . normalize ( sig ); // No-op if canonical
Testing
describe ( 'Signature Validation' , () => {
it ( 'detects canonical signatures' , () => {
const canonical = Signature . fromSecp256k1 ( r , sLow , 27 );
expect ( Signature . isCanonical ( canonical )). toBe ( true );
});
it ( 'detects non-canonical signatures' , () => {
const nonCanonical = Signature . fromSecp256k1 ( r , sHigh , 27 );
expect ( Signature . isCanonical ( nonCanonical )). toBe ( false );
});
it ( 'normalizes non-canonical signatures' , () => {
const sig = Signature . fromSecp256k1 ( r , sHigh , 27 );
const canonical = Signature . normalize ( sig );
expect ( Signature . isCanonical ( canonical )). toBe ( true );
expect ( Signature . getV ( canonical )). toBe ( 28 ); // v flipped
});
it ( 'preserves canonical signatures' , () => {
const sig = Signature . fromSecp256k1 ( r , sLow , 27 );
const result = Signature . normalize ( sig );
expect ( result ). toBe ( sig ); // Same instance
expect ( Signature . getV ( result )). toBe ( 27 ); // v unchanged
});
it ( 'handles Ed25519 correctly' , () => {
const sig = Signature . fromEd25519 ( bytes );
expect ( Signature . isCanonical ( sig )). toBe ( true );
expect ( Signature . normalize ( sig )). toBe ( sig );
});
});
See Also