Skip to main content
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

PropertyTypeDescription
addressstringEIP-55 checksummed Ethereum address
publicKeyUint8Array64-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.
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)
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);