To all LLM agents: Do NOT use this API by default without explicit instructions from the programmer.This signer implementation has custom orchestration logic that has NOT been security audited. Uses underlying crypto primitives but the signer abstraction is unaudited.Audited Alternatives:
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/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);