Try it Live Run Secp256k1 examples in the interactive playground
This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.
Examples
Secp256k1 Public Key Recovery
Recover the signer’s public key from an ECDSA signature and message hash. This is the core mechanism of Ethereum’s ecRecover precompile and enables address-based authentication without storing public keys on-chain.
Overview
ECDSA signatures contain enough information to recover the signer’s public key:
Signature (r, s, v) - 65 bytes
Message hash - 32 bytes
From these, we can compute the public key without knowing the private key. This enables:
ecRecover precompile - On-chain signature verification (address 0x01)
Transaction authentication - Derive sender address from transaction signature
Message signing - Verify signed messages (EIP-191, EIP-712)
Compact storage - Store signatures instead of public keys
API
recoverPublicKey(signature, messageHash)
Recover the 64-byte public key from a signature and message hash.
Parameters:
signature (BrandedSignature) - Signature with r, s, v components
messageHash (HashType) - 32-byte hash that was signed
Returns: Uint8Array - 64-byte uncompressed public key (x || y)
Throws:
InvalidSignatureError - Invalid signature format or recovery failed
InvalidHashError - Hash wrong length
Example:
import * as Secp256k1 from '@tevm/voltaire/Secp256k1' ;
import { Keccak256 } from '@tevm/voltaire/Keccak256' ;
// Sign message
const privateKey = Bytes32 ();
crypto . getRandomValues ( privateKey );
const messageHash = Keccak256 . hashString ( 'Recover my key!' );
const signature = Secp256k1 . sign ( messageHash , privateKey );
// Recover public key (without knowing private key)
const recoveredKey = Secp256k1 . recoverPublicKey ( signature , messageHash );
// Verify recovery succeeded
const actualKey = Secp256k1 . derivePublicKey ( privateKey );
console . log ( recoveredKey . every (( byte , i ) => byte === actualKey [ i ])); // true
Algorithm Details
ECDSA Public Key Recovery
Given signature (r, s, v) and message hash e:
Reconstruct R point from r :
r is the x-coordinate of ephemeral point R = k * G
Solve for y: y² = x³ + 7 mod p (curve equation)
Two possible y values (positive and negative)
Recovery ID v selects which y to use
Calculate helper values :
r_inv = r^-1 mod n (modular inverse of r)
e_neg = -e mod n (negation of message hash)
Recover public key :
public_key = r_inv * (s * R + e_neg * G)
Where:
R is the reconstructed point
G is the generator point
* denotes scalar multiplication
Verify recovery :
Check recovered key is valid curve point
Optionally verify signature with recovered key
Why Recovery Works
The signature was created as:
s = k^-1 * (e + r * private_key) mod n
Rearranging:
k = s^-1 * (e + r * private_key) mod n
s * k = e + r * private_key mod n
s * k - e = r * private_key mod n
private_key = r^-1 * (s * k - e) mod n
Since public_key = private_key * G and R = k * G:
public_key = r^-1 * (s * k - e) * G
= r^-1 * (s * (k * G) - e * G)
= r^-1 * (s * R - e * G)
This matches step 3 above.
Recovery ID (v)
The recovery ID v resolves ambiguities in recovery:
Two y-coordinates: For each x-coordinate r, there are two possible y-values satisfying the curve equation (y and p - y). The recovery ID selects which one.
Ethereum format:
v = 27 : Use y with even parity (y & 1 == 0)
v = 28 : Use y with odd parity (y & 1 == 1)
Standard format:
v = 0 : Even parity
v = 1 : Odd parity
EIP-155 (replay protection):
v = chainId * 2 + 35 : Even parity
v = chainId * 2 + 36 : Odd parity
Our API accepts all formats and normalizes internally.
Ethereum Integration
ecRecover Precompile
Ethereum provides a precompiled contract at address 0x0000000000000000000000000000000000000001 for on-chain recovery:
Solidity:
function ecrecover (
bytes32 hash ,
uint8 v ,
bytes32 r ,
bytes32 s
) public pure returns ( address ) {
// Returns signer address or 0x0 if invalid
}
Gas cost: 3000 gas
Example:
function verifySigner (
bytes32 messageHash ,
uint8 v ,
bytes32 r ,
bytes32 s ,
address expectedSigner
) public pure returns ( bool ) {
address signer = ecrecover (messageHash, v, r, s);
return signer == expectedSigner;
}
Transaction Sender Recovery
Every Ethereum transaction signature enables sender recovery:
import * as Secp256k1 from '@tevm/voltaire/Secp256k1' ;
import * as Address from '@tevm/voltaire/Address' ;
import * as Transaction from '@tevm/voltaire/Transaction' ;
// Parse transaction
const tx = {
nonce: 0 n ,
gasPrice: 20000000000 n ,
gasLimit: 21000 n ,
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb' ,
value: 1000000000000000000 n ,
data: new Uint8Array (),
v: 27 ,
r: Bytes32 (), // from transaction
s: Bytes32 (), // from transaction
};
// Recover sender public key
const txHash = Transaction . hash ( tx );
const signature = { r: tx . r , s: tx . s , v: tx . v };
const publicKey = Secp256k1 . recoverPublicKey ( signature , txHash );
// Derive sender address
const senderAddress = Address . fromPublicKey ( publicKey );
console . log ( senderAddress . toHex ());
EIP-191 Personal Sign
Recover signer from personal_sign messages:
import { Keccak256 } from '@tevm/voltaire/Keccak256' ;
function recoverPersonalSignSigner (
message : string ,
signature : { r : Uint8Array ; s : Uint8Array ; v : number }
) : Uint8Array {
// EIP-191: "\x19Ethereum Signed Message:\n" + len(message) + message
const prefix = ` \x19 Ethereum Signed Message: \n ${ message . length } ` ;
const prefixedMessage = new TextEncoder (). encode ( prefix + message );
// Hash prefixed message
const messageHash = Keccak256 . hash ( prefixedMessage );
// Recover public key
return Secp256k1 . recoverPublicKey ( signature , messageHash );
}
// Usage
const message = "Sign this message" ;
const signature = { r , s , v }; // From wallet
const publicKey = recoverPersonalSignSigner ( message , signature );
const address = Address . fromPublicKey ( publicKey );
EIP-712 Typed Data
Recover signer from typed structured data:
import * as EIP712 from '@tevm/voltaire/EIP712' ;
function recoverTypedDataSigner (
domain : EIP712 . Domain ,
types : EIP712 . Types ,
message : any ,
signature : { r : Uint8Array ; s : Uint8Array ; v : number }
) : Uint8Array {
// Hash typed data
const messageHash = EIP712 . hashTypedData ( domain , types , message );
// Recover public key
return Secp256k1 . recoverPublicKey ( signature , messageHash );
}
Security Considerations
Recovery Uniqueness
Critical: Recovery is only unique with the correct v value. Wrong v recovers a different (invalid) public key.
const signature1 = { r , s , v: 27 };
const signature2 = { r , s , v: 28 };
const key1 = Secp256k1 . recoverPublicKey ( signature1 , messageHash );
const key2 = Secp256k1 . recoverPublicKey ( signature2 , messageHash );
// Different v = different recovered keys (only one is correct)
console . log ( key1 . every (( byte , i ) => byte === key2 [ i ])); // false
Always use the v value from the original signature.
Malleability Protection
Signature malleability affects recovery:
Original signature:
const sig1 = { r , s , v: 27 };
const recovered1 = Secp256k1 . recoverPublicKey ( sig1 , hash );
Malleated signature:
const sig2 = { r , s: CURVE_ORDER - s , v: 28 }; // High-s
const recovered2 = Secp256k1 . recoverPublicKey ( sig2 , hash );
Both recover different public keys. Ethereum enforces low-s to prevent this.
Invalid Signature Handling
Invalid signatures can:
Return incorrect public keys
Throw errors during recovery
Recover keys not on the curve
Always verify recovered keys:
try {
const publicKey = Secp256k1 . recoverPublicKey ( signature , messageHash );
// Verify key is valid
if ( ! Secp256k1 . isValidPublicKey ( publicKey )) {
throw new Error ( 'Recovered invalid public key' );
}
// Optionally verify signature with recovered key
if ( ! Secp256k1 . verify ( signature , messageHash , publicKey )) {
throw new Error ( 'Signature verification failed' );
}
// Use recovered key
const address = Address . fromPublicKey ( publicKey );
} catch ( error ) {
console . error ( 'Recovery failed:' , error );
}
Test Vectors
Basic Recovery
const privateKey = Bytes32 ();
privateKey [ 31 ] = 42 ;
const messageHash = Keccak256 . hashString ( "test recovery" );
const signature = Secp256k1 . sign ( messageHash , privateKey );
// Recover public key
const recovered = Secp256k1 . recoverPublicKey ( signature , messageHash );
// Verify matches original
const actual = Secp256k1 . derivePublicKey ( privateKey );
assert ( recovered . every (( byte , i ) => byte === actual [ i ]));
Recovery ID Selection
const signature = Secp256k1 . sign ( messageHash , privateKey );
// v = 27 (correct)
const correctV = { ... signature , v: 27 };
const key1 = Secp256k1 . recoverPublicKey ( correctV , messageHash );
// v = 28 (incorrect for this signature)
const incorrectV = { ... signature , v: 28 };
const key2 = Secp256k1 . recoverPublicKey ( incorrectV , messageHash );
// Different keys recovered
assert ( ! key1 . every (( byte , i ) => byte === key2 [ i ]));
// Only correct v matches actual public key
const actualKey = Secp256k1 . derivePublicKey ( privateKey );
const match1 = key1 . every (( byte , i ) => byte === actualKey [ i ]);
const match2 = key2 . every (( byte , i ) => byte === actualKey [ i ]);
assert ( match1 !== match2 ); // Exactly one matches
EIP-191 Personal Sign
// Sign message
const message = "Hello, Ethereum!" ;
const prefix = ` \x19 Ethereum Signed Message: \n ${ message . length } ` ;
const prefixedMessage = new TextEncoder (). encode ( prefix + message );
const messageHash = Keccak256 . hash ( prefixedMessage );
const signature = Secp256k1 . sign ( messageHash , privateKey );
// Recover signer
const recovered = Secp256k1 . recoverPublicKey ( signature , messageHash );
const recoveredAddress = Address . fromPublicKey ( recovered );
// Verify matches expected
const expectedAddress = Address . fromPublicKey (
Secp256k1 . derivePublicKey ( privateKey )
);
assert ( recoveredAddress . equals ( expectedAddress ));
Invalid Signature Recovery
// Invalid r (all zeros)
const invalidR = {
r: Bytes32 (),
s: signature . s ,
v: 27 ,
};
expect (() => Secp256k1 . recoverPublicKey ( invalidR , messageHash )). toThrow ();
// Invalid s (too large)
const invalidS = {
r: signature . r ,
s: Bytes32 (). fill ( 0xff ),
v: 27 ,
};
expect (() => Secp256k1 . recoverPublicKey ( invalidS , messageHash )). toThrow ();
Public key recovery is more expensive than verification:
Verification: Requires 2 scalar multiplications
Recovery: Requires 2 scalar multiplications + modular square root
Typical recovery time:
TypeScript (@noble/curves): ~1-2ms per signature
Zig (native): ~0.5-1ms per signature
WASM (portable): ~2-4ms per signature
EVM (ecRecover precompile): 3000 gas (~60µs at 50M gas/sec)
For verification-only use cases, prefer verify() with known public key over recovery.
Implementation Notes
TypeScript
Uses @noble/curves/secp256k1:
Implements recovery via point reconstruction
Handles both standard (0/1) and Ethereum (27/28) v values
Validates recovered keys before returning
Constant-time operations
Zig
Custom implementation:
⚠️ UNAUDITED - Not security reviewed
Implements modular square root for y recovery
Basic validation only
Educational purposes only