Skip to main content

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/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);