Overview
Signers provide a unified interface for cryptographic signing operations in Ethereum. The Signer interface abstracts private key management and supports:
- EIP-191 Personal Sign - Sign human-readable messages with Ethereum prefix
- Transaction Signing - Sign all transaction types (Legacy, EIP-2930, EIP-1559, EIP-4844, EIP-7702)
- EIP-712 Typed Data - Sign structured data for dApps and protocols
Private keys are encapsulated securely - never exposed after signer creation.
Quick Start
import { PrivateKeySignerImpl } from '@tevm/voltaire/crypto/signers';
// Create signer from private key
const signer = PrivateKeySignerImpl.fromPrivateKey({
privateKey: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
});
// Get derived address
console.log(signer.address); // '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
// Sign a message (EIP-191)
const signature = await signer.signMessage('Hello, Ethereum!');
// Returns: '0x...' (65-byte signature as hex)
// Sign typed data (EIP-712)
const typedSig = await signer.signTypedData({
domain: { name: 'MyApp', version: '1', chainId: 1n },
types: { Message: [{ name: 'content', type: 'string' }] },
primaryType: 'Message',
message: { content: 'Hello' }
});
Signer Interface
All signers implement the Signer interface:
interface Signer {
/** Checksummed Ethereum address */
address: string;
/** 64-byte uncompressed public key (without 0x04 prefix) */
publicKey: Uint8Array;
/** Sign message with EIP-191 prefix */
signMessage(message: string | Uint8Array): Promise<string>;
/** Sign transaction (any type) */
signTransaction(transaction: any): Promise<any>;
/** Sign EIP-712 typed data */
signTypedData(typedData: any): Promise<string>;
}
PrivateKeySignerImpl
WASM-based implementation using Zig cryptographic primitives.
Construction
import { PrivateKeySignerImpl } from '@tevm/voltaire/crypto/signers';
// From hex string (with or without 0x prefix)
const signer1 = PrivateKeySignerImpl.fromPrivateKey({
privateKey: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
});
// From Uint8Array (32 bytes)
const privateKeyBytes = new Uint8Array(32).fill(1);
const signer2 = PrivateKeySignerImpl.fromPrivateKey({
privateKey: privateKeyBytes
});
Throws Error if private key is not exactly 32 bytes.
Properties
| Property | Type | Description |
|---|
address | string | EIP-55 checksummed Ethereum address |
publicKey | Uint8Array | 64-byte uncompressed public key |
signMessage
Signs a message using EIP-191 personal sign format.
const signer = PrivateKeySignerImpl.fromPrivateKey({ privateKey: '0x...' });
// Sign string message
const sig1 = await signer.signMessage('Hello, Ethereum!');
// Sign bytes
const msgBytes = new TextEncoder().encode('Hello');
const sig2 = await signer.signMessage(msgBytes);
// Returns hex string: 0x + r(32 bytes) + s(32 bytes) + v(1 byte)
console.log(sig1.length); // 132 (0x + 130 hex chars)
Message format: \x19Ethereum Signed Message:\n${length}${message}
The message is prefixed, then hashed with Keccak256 before signing.
signTransaction
Signs Ethereum transactions of any type.
Legacy (Type 0)
EIP-1559 (Type 2)
EIP-4844 (Type 3)
const signer = PrivateKeySignerImpl.fromPrivateKey({ privateKey: '0x...' });
const signedTx = await signer.signTransaction({
type: 0,
nonce: 0n,
gasPrice: 20000000000n,
gasLimit: 21000n,
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
value: 1000000000000000000n,
data: new Uint8Array()
});
// Legacy uses v (includes chainId per EIP-155)
console.log(signedTx.v); // 37n or 38n (for chainId=1)
console.log(signedTx.r); // Uint8Array (32 bytes)
console.log(signedTx.s); // Uint8Array (32 bytes)
const signer = PrivateKeySignerImpl.fromPrivateKey({ privateKey: '0x...' });
const signedTx = await signer.signTransaction({
type: 2,
chainId: 1n,
nonce: 0n,
maxPriorityFeePerGas: 1000000000n,
maxFeePerGas: 20000000000n,
gasLimit: 21000n,
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
value: 1000000000000000000n,
data: new Uint8Array(),
accessList: []
});
// EIP-1559+ uses yParity (0 or 1)
console.log(signedTx.yParity); // 0 or 1
console.log(signedTx.r); // Uint8Array (32 bytes)
console.log(signedTx.s); // Uint8Array (32 bytes)
const signer = PrivateKeySignerImpl.fromPrivateKey({ privateKey: '0x...' });
const signedTx = await signer.signTransaction({
type: 3,
chainId: 1n,
nonce: 0n,
maxPriorityFeePerGas: 1000000000n,
maxFeePerGas: 20000000000n,
gasLimit: 21000n,
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
value: 0n,
data: new Uint8Array(),
accessList: [],
maxFeePerBlobGas: 1000000000n,
blobVersionedHashes: ['0x01...']
});
console.log(signedTx.yParity); // 0 or 1
Signature format by transaction type:
- Type 0 (Legacy):
v includes chainId per EIP-155 (v = recovery_id + 35 + chainId * 2)
- Type 1+ (Modern): Uses
yParity (0 or 1) instead of v
signTypedData
Signs EIP-712 structured typed data.
const signer = PrivateKeySignerImpl.fromPrivateKey({ privateKey: '0x...' });
const signature = await signer.signTypedData({
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' }
],
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' }
]
},
primaryType: 'Person',
domain: {
name: 'MyApp',
version: '1',
chainId: 1n
},
message: {
name: 'Alice',
wallet: '0x0000000000000000000000000000000000000000'
}
});
// Returns: '0x...' (65-byte signature as hex)
EIP-712 provides protection against signature replay across different dApps through domain separation.
Utility Functions
getAddress
Extract address from any signer instance.
import { PrivateKeySignerImpl, getAddress } from '@tevm/voltaire/crypto/signers';
const signer = PrivateKeySignerImpl.fromPrivateKey({ privateKey: '0x...' });
const address = getAddress(signer);
// Same as signer.address
recoverTransactionAddress
Not yet implemented. Requires RLP deserialization and signature recovery bindings.
import { recoverTransactionAddress } from '@tevm/voltaire/crypto/signers';
// Future API:
// const signerAddress = await recoverTransactionAddress(signedTransaction);
Security Considerations
Private Key Protection: The private key is stored in a closure and never exposed after signer creation. However, JavaScript memory is not secure - avoid using in untrusted environments.
Best practices:
- Never log or serialize the private key
- Clear sensitive data from memory when possible
- Use hardware wallets or secure enclaves for production
- Validate all inputs before signing
Signature malleability: All signatures use low-s normalization per EIP-2 to prevent malleability attacks.
Implementation Details
PrivateKeySignerImpl uses:
- @noble/curves/secp256k1 for public key derivation
- WASM Zig primitives for signing operations (
primitives.secp256k1Sign)
- Keccak256Wasm for message and address hashing
- Eip712Wasm for typed data hashing
The hybrid approach provides:
- Audited public key derivation (noble)
- High-performance signing (native Zig via WASM)
- Consistent cross-platform behavior
Usage Patterns
Wallet Integration
import { PrivateKeySignerImpl } from '@tevm/voltaire/crypto/signers';
class Wallet {
private signer: PrivateKeySignerImpl;
constructor(privateKey: string) {
this.signer = PrivateKeySignerImpl.fromPrivateKey({ privateKey });
}
get address(): string {
return this.signer.address;
}
async personalSign(message: string): Promise<string> {
return this.signer.signMessage(message);
}
async sendTransaction(tx: any): Promise<any> {
const signedTx = await this.signer.signTransaction(tx);
// ... broadcast to network
return signedTx;
}
}
Multi-Signature Workflow
import { PrivateKeySignerImpl } from '@tevm/voltaire/crypto/signers';
async function collectSignatures(
message: string,
signers: PrivateKeySignerImpl[]
): Promise<string[]> {
return Promise.all(
signers.map(signer => signer.signMessage(message))
);
}
const signers = [
PrivateKeySignerImpl.fromPrivateKey({ privateKey: '0x...' }),
PrivateKeySignerImpl.fromPrivateKey({ privateKey: '0x...' }),
PrivateKeySignerImpl.fromPrivateKey({ privateKey: '0x...' })
];
const signatures = await collectSignatures('Approve proposal #1', signers);
SIWE (Sign-In with Ethereum)
import { PrivateKeySignerImpl } from '@tevm/voltaire/crypto/signers';
import * as Siwe from '@tevm/voltaire/primitives/Siwe';
const signer = PrivateKeySignerImpl.fromPrivateKey({ privateKey: '0x...' });
// Create SIWE message
const message = Siwe.create({
domain: 'example.com',
address: signer.address,
statement: 'Sign in to Example',
uri: 'https://example.com',
version: '1',
chainId: 1n,
nonce: 'random-nonce'
});
// Sign the message
const messageString = Siwe.toString(message);
const signature = await signer.signMessage(messageString);