Skip to main content
Secp256k1 is the elliptic curve used for all Ethereum signatures. Sign transactions, recover public keys from signatures, and verify signatures.
import { Secp256k1Service, Secp256k1Live, KeccakService, KeccakLive } from 'voltaire-effect/crypto'
import { Effect, Layer } from 'effect'

// Compose layers once
const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)

const program = Effect.gen(function* () {
  const secp = yield* Secp256k1Service
  const keccak = yield* KeccakService

  const messageHash = yield* keccak.hash(new TextEncoder().encode('hello'))
  const signature = yield* secp.sign(messageHash, privateKey)
  const publicKey = yield* secp.recover(signature, messageHash)
  const isValid = yield* secp.verify(signature, messageHash, publicKey)

  return { signature, publicKey, isValid }
}).pipe(Effect.provide(CryptoLayer))

Sign Message Hash

import { Secp256k1Service, Secp256k1Live } from 'voltaire-effect/crypto'
import { Effect } from 'effect'

const signHash = (messageHash: Uint8Array, privateKey: Uint8Array) =>
  Effect.gen(function* () {
    const secp = yield* Secp256k1Service
    return yield* secp.sign(messageHash, privateKey)
  }).pipe(Effect.provide(Secp256k1Live))
The signature includes recovery parameter v for public key recovery.

Sign With Extra Entropy

For additional security against side-channel attacks:
const signature = yield* secp.sign(messageHash, privateKey, {
  extraEntropy: true  // Uses random data
})

// Or with specific entropy
const signature = yield* secp.sign(messageHash, privateKey, {
  extraEntropy: customEntropyBytes
})

Recover Public Key

Recover the signer’s public key from a signature:
const recoverSigner = (signature: Secp256k1SignatureType, messageHash: Uint8Array) =>
  Effect.gen(function* () {
    const secp = yield* Secp256k1Service
    return yield* secp.recover(signature, messageHash)
  }).pipe(Effect.provide(Secp256k1Live))
Returns 65-byte uncompressed public key (0x04 prefix + 64 bytes).

Verify Signature

const verify = (
  signature: Secp256k1SignatureType,
  messageHash: Uint8Array,
  publicKey: Secp256k1PublicKeyType
) =>
  Effect.gen(function* () {
    const secp = yield* Secp256k1Service
    return yield* secp.verify(signature, messageHash, publicKey)
  }).pipe(Effect.provide(Secp256k1Live))

Get Public Key From Private Key

const getPublicKey = (privateKey: Uint8Array) =>
  Effect.gen(function* () {
    const secp = yield* Secp256k1Service
    return yield* secp.getPublicKey(privateKey)
  }).pipe(Effect.provide(Secp256k1Live))

Sign Ethereum Message (EIP-191)

import { Secp256k1Service, Secp256k1Live, KeccakService, KeccakLive } from 'voltaire-effect/crypto'
import { Effect, Layer } from 'effect'

const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)

const signEthereumMessage = (message: string, privateKey: Uint8Array) =>
  Effect.gen(function* () {
    const secp = yield* Secp256k1Service
    const keccak = yield* KeccakService

    // Ethereum message prefix
    const prefix = `\x19Ethereum Signed Message:\n${message.length}`
    const prefixed = new TextEncoder().encode(prefix + message)
    const hash = yield* keccak.hash(prefixed)

    return yield* secp.sign(hash, privateKey)
  }).pipe(Effect.provide(CryptoLayer))

Derive Address From Private Key

import { Address } from 'voltaire-effect'
import { Effect, Layer } from 'effect'
import { Secp256k1Service, Secp256k1Live, KeccakService, KeccakLive } from 'voltaire-effect/crypto'

const CryptoLayer = Layer.mergeAll(Secp256k1Live, KeccakLive)

const privateKeyToAddress = (privateKey: Uint8Array) =>
  Effect.gen(function* () {
    const secp = yield* Secp256k1Service
    const keccak = yield* KeccakService

    const publicKey = yield* secp.getPublicKey(privateKey)
    const hash = yield* keccak.hash(publicKey.slice(1)) // Remove 0x04 prefix
    return Address.fromBytes(hash.slice(-20))
  }).pipe(Effect.provide(CryptoLayer))

Error Handling

import { InvalidPrivateKeyError, CryptoError, InvalidSignatureError } from 'voltaire-effect/crypto'

program.pipe(
  Effect.catchTag('InvalidPrivateKeyError', (e) =>
    Effect.fail(new Error('Bad private key'))
  ),
  Effect.catchTag('InvalidSignatureError', (e) =>
    Effect.fail(new Error('Invalid signature'))
  ),
  Effect.catchTag('CryptoError', (e) =>
    Effect.fail(new Error(`Crypto operation failed: ${e.message}`))
  )
)

Testing

Use Secp256k1Test for deterministic signatures in unit tests:
import { Secp256k1Test } from 'voltaire-effect/crypto'

await Effect.runPromise(program.pipe(Effect.provide(Secp256k1Test)))

Interface

interface Secp256k1ServiceShape {
  readonly sign: (
    messageHash: HashType,
    privateKey: Uint8Array,
    options?: { extraEntropy?: boolean | Uint8Array }
  ) => Effect.Effect<Secp256k1SignatureType, InvalidPrivateKeyError | CryptoError>

  readonly recover: (
    signature: Secp256k1SignatureType,
    messageHash: HashType
  ) => Effect.Effect<Secp256k1PublicKeyType, InvalidSignatureError>

  readonly verify: (
    signature: Secp256k1SignatureType,
    messageHash: HashType,
    publicKey: Secp256k1PublicKeyType
  ) => Effect.Effect<boolean, InvalidSignatureError>

  readonly getPublicKey: (
    privateKey: Uint8Array
  ) => Effect.Effect<Secp256k1PublicKeyType, InvalidPrivateKeyError>
}

Types

// 65-byte signature: r (32) + s (32) + v (1)
type Secp256k1SignatureType = Uint8Array & { readonly __brand: 'Secp256k1Signature' }

// 65-byte uncompressed public key: 0x04 + x (32) + y (32)
type Secp256k1PublicKeyType = Uint8Array & { readonly __brand: 'Secp256k1PublicKey' }

Security Notes

  • Never reuse nonces. The library handles nonce generation via RFC 6979.
  • Protect private keys. Never log or expose them.
  • Use extraEntropy for additional protection in adversarial environments.
  • Constant-time operations. The underlying implementation uses constant-time algorithms.