Skip to main content

Try it Live

Run Transaction examples in the interactive playground

Transaction Signing

Signature verification and sender address recovery using secp256k1.

getSender

Recover sender address from transaction signature.
function getSender(tx: Any): AddressType

Parameters

  • tx: Any - Signed transaction (any type)

Returns

AddressType - 20-byte sender address recovered from signature

Throws

  • Error("Transaction is not signed") - If transaction has zero r or s
  • Error("Unknown transaction type") - If transaction type is invalid
  • Error("Not implemented") - If type-specific recovery not implemented yet
  • Signature recovery errors for invalid signatures

Usage

import { getSender } from 'tevm/Transaction'

const signedTx: Transaction.EIP1559 = {
  type: Transaction.Type.EIP1559,
  chainId: 1n,
  nonce: 0n,
  maxPriorityFeePerGas: 1000000000n,
  maxFeePerGas: 20000000000n,
  gasLimit: 21000n,
  to: recipientAddress,
  value: 1000000000000000000n,
  data: new Uint8Array(),
  accessList: [],
  yParity: 0,
  r: signatureR,
  s: signatureS,
}

// Recover sender
const sender = getSender(signedTx)
console.log('From:', Address.toChecksummed(sender))
// "0x742d35Cc6634C0532925a3b844Bc9e7595f51e3e"
Source: getSender.ts:12-27

verifySignature

Verify transaction signature is valid.
function verifySignature(tx: Any): boolean

Parameters

  • tx: Any - Signed transaction

Returns

boolean - true if signature is valid, false otherwise

Usage

import { verifySignature } from 'tevm/Transaction'

const tx: Transaction.EIP1559 = { /* signed transaction */ }

if (verifySignature(tx)) {
  console.log('Valid signature')
  // Safe to process transaction
} else {
  console.log('Invalid signature')
  // Reject transaction
}
Source: verifySignature.ts

isSigned

Check if transaction has a signature.
function isSigned(tx: Any): boolean

Parameters

  • tx: Any - Transaction to check

Returns

boolean - true if transaction has non-zero r and s, false otherwise

Usage

import { isSigned, getSender } from 'tevm/Transaction'

const tx: Transaction.EIP1559 = { /* ... */ }

if (isSigned(tx)) {
  const sender = getSender(tx)
  console.log('Signed by:', sender)
} else {
  console.log('Unsigned transaction')
}
Source: isSigned.ts:7-14

assertSigned

Assert transaction is signed (throws if not).
function assertSigned(tx: Any): void

Parameters

  • tx: Any - Transaction to check

Throws

  • Error("Transaction is not signed") - If r or s is zero

Usage

import { assertSigned, getSender } from 'tevm/Transaction'

try {
  assertSigned(tx)
  // Safe to proceed - transaction is signed
  const sender = getSender(tx)
  console.log('From:', sender)
} catch (e) {
  console.error('Transaction not signed:', e.message)
}
Source: assertSigned.ts:6-13

Signature Components

Legacy Transactions

Legacy transactions use v/r/s signature format:
type Legacy = {
  v: bigint    // Recovery ID + chain ID (EIP-155)
  r: Uint8Array  // 32 bytes
  s: Uint8Array  // 32 bytes
}
v value calculation:
  • Pre-EIP-155: v = 27 + yParity (yParity is 0 or 1)
  • Post-EIP-155: v = chainId * 2 + 35 + yParity
Example:
// Chain ID 1, yParity 0
v = 1 * 2 + 35 + 0 = 37

// Chain ID 1, yParity 1
v = 1 * 2 + 35 + 1 = 38

// Chain ID 137 (Polygon), yParity 0
v = 137 * 2 + 35 + 0 = 309

Typed Transactions (EIP-2930+)

All typed transactions use yParity/r/s format:
type EIP1559 = {
  yParity: number  // 0 or 1
  r: Uint8Array    // 32 bytes
  s: Uint8Array    // 32 bytes
}
yParity directly encodes the recovery ID (0 or 1), no chain ID encoding needed.

Sender Recovery Process

  1. Get signing hash (transaction data without signature)
  2. Recover public key from signature using secp256k1
  3. Hash public key with keccak256
  4. Take last 20 bytes as address
// Pseudocode
function getSender(tx: Transaction) {
  // 1. Get hash that was signed
  const signingHash = getSigningHash(tx)

  // 2. Recover public key (65 bytes uncompressed)
  const publicKey = secp256k1.recover(
    signingHash,
    tx.r,
    tx.s,
    tx.yParity || getYParity(tx.v)
  )

  // 3. Hash public key
  const publicKeyHash = keccak256(publicKey.slice(1))  // Skip 0x04 prefix

  // 4. Take last 20 bytes
  return publicKeyHash.slice(12) as AddressType
}

Type-Specific Methods

Each transaction type has specialized methods:
import { Legacy, EIP1559 } from 'tevm/Transaction'

// Legacy
const legacySender = Legacy.getSender.call(legacyTx)
const legacyValid = Legacy.verifySignature.call(legacyTx)

// EIP-1559
const eip1559Sender = EIP1559.getSender(eip1559Tx)
const eip1559Valid = EIP1559.verifySignature(eip1559Tx)

Usage Patterns

Transaction Pool Validation

import { verifySignature, getSender } from 'tevm/Transaction'
import { InvalidSignatureError, TransactionError } from 'tevm/errors'

class TransactionPool {
  async add(tx: Transaction.Any): Promise<void> {
    // Verify signature
    if (!verifySignature(tx)) {
      throw new InvalidSignatureError('Invalid signature', {
        context: { tx }
      })
    }

    // Get sender
    const sender = getSender(tx)

    // Check sender balance
    const balance = await getBalance(sender)
    const cost = tx.gasLimit * getGasPrice(tx) + tx.value
    if (balance < cost) {
      throw new TransactionError('Insufficient balance', {
        code: 'INSUFFICIENT_BALANCE',
        context: { balance, cost, sender }
      })
    }

    // Add to pool
    this.transactions.set(hash(tx), { tx, sender })
  }
}

Authorization Check

import { getSender } from 'tevm/Transaction'
import { InvalidSignerError } from 'tevm/errors'

function requireSender(tx: Transaction.Any, expectedSender: AddressType) {
  const actualSender = getSender(tx)

  if (!Address.equals(actualSender, expectedSender)) {
    throw new InvalidSignerError(
      `Unauthorized: expected ${Address.toHex(expectedSender)}, ` +
      `got ${Address.toHex(actualSender)}`,
      {
        code: 'UNAUTHORIZED_SIGNER',
        context: { expected: expectedSender, actual: actualSender }
      }
    )
  }
}

Replay Protection

import { getSender, getChainId, verifySignature } from 'tevm/Transaction'
import { InvalidSignatureError, TransactionError } from 'tevm/errors'

function validateTransaction(tx: Transaction.Any, currentChainId: bigint) {
  // Verify signature
  if (!verifySignature(tx)) {
    throw new InvalidSignatureError('Invalid signature', {
      context: { tx }
    })
  }

  // Check chain ID (replay protection)
  const txChainId = getChainId(tx)
  if (txChainId && txChainId !== currentChainId) {
    throw new TransactionError(`Wrong chain: expected ${currentChainId}, got ${txChainId}`, {
      code: 'WRONG_CHAIN_ID',
      context: { expected: currentChainId, actual: txChainId }
    })
  }

  // Get sender
  const sender = getSender(tx)
  return sender
}

Batch Verification

import { verifySignature } from 'tevm/Transaction'

async function verifyBatch(transactions: Transaction.Any[]): Promise<boolean[]> {
  // Parallelize signature verification
  return Promise.all(
    transactions.map(tx => Promise.resolve(verifySignature(tx)))
  )
}

// Usage
const txs = [tx1, tx2, tx3]
const results = await verifyBatch(txs)

txs.forEach((tx, i) => {
  if (!results[i]) {
    console.error('Invalid signature:', hash(tx))
  }
})

Safe Sender Recovery

import { isSigned, getSender } from 'tevm/Transaction'

function safGetSender(tx: Transaction.Any): AddressType | null {
  try {
    if (!isSigned(tx)) {
      return null
    }
    return getSender(tx)
  } catch (error) {
    console.error('Sender recovery failed:', error)
    return null
  }
}

Signature Malleability

ECDSA signatures have malleability issue - for every valid signature (r, s), there’s another valid signature (r, -s mod n). Ethereum requires s value to be in lower half of curve order:
import { InvalidSignatureError } from 'tevm/errors'

const SECP256K1_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n
const SECP256K1_N_DIV_2 = SECP256K1_N / 2n

// Valid signatures must have s <= N/2
if (s > SECP256K1_N_DIV_2) {
  throw new InvalidSignatureError('Invalid signature: s value too high', {
    code: 'S_VALUE_TOO_HIGH',
    context: { s, maxS: SECP256K1_N_DIV_2 }
  })
}
This is checked automatically in signature verification.

Performance Considerations

Signature recovery is expensive (elliptic curve operations):
// Cache sender addresses
const senderCache = new WeakMap<Transaction.Any, AddressType>()

function getCachedSender(tx: Transaction.Any): AddressType {
  let sender = senderCache.get(tx)
  if (!sender) {
    sender = getSender(tx)
    senderCache.set(tx, sender)
  }
  return sender
}
For batch processing:
// Parallelize if using WebAssembly or worker threads
const senders = await Promise.all(
  transactions.map(tx => Promise.resolve(getSender(tx)))
)

Implementation Status

TypegetSenderverifySignatureStatus
LegacyPartialPartialIn progress
EIP-2930PartialPartialIn progress
EIP-1559PartialPartialIn progress
EIP-4844PartialPartialIn progress
EIP-7702PartialPartialIn progress
Many methods currently throw “Not implemented” - check test files for implementation status.

See Also

EIP References