Fatskills
Practice. Master. Repeat.
Study Guide: Blockchain and Web3 Development: Blockchain Fundamentals - Public/Private Key Cryptography and Digital Signatures, ECDSA
Source: https://www.fatskills.com/cryptocurrency-bitcoin-blockchain-and-more/chapter/blockchain-and-web3-development-blockchain-and-web3-development-blockchain-fundamentals-publicprivate-key-cryptography-and-digital-signatures-ecdsa

Blockchain and Web3 Development: Blockchain Fundamentals - Public/Private Key Cryptography and Digital Signatures, ECDSA

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~6 min read

What This Is

Public?key cryptography lets anyone generate a pair of keys: a public key that can be shared openly and a private key that must stay secret. In Ethereum the private key signs transactions; the network verifies the signature with the public key (actually the derived address). This mechanism underpins every trust?less action—whether a user swaps tokens on Uniswap, mints an NFT, or casts a vote in a DAO—because it proves who sent the transaction without a central authority.


Key Terms & Code Snippets

  • ECDSA (Elliptic Curve Digital Signature Algorithm): The signature scheme Ethereum uses (secp256k1 curve). It turns a 32?byte hash into a (v,r,s) tuple that can be recovered on?chain.
    solidity // Solidity: recover signer address from a signed message function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public pure returns (address) { return ecrecover(hash, v, r, s); }

  • Private Key: A 256?bit random number that must never be exposed. It is the seed for all signatures and the only thing that can move funds from an address.

  • Public Key-Address: Ethereum derives the address by keccak256(publicKey)[12:]. The address is what you share with the world.

  • Message Hash (keccak256): Before signing, data is hashed with Keccak?256 (Ethereum’s SHA?3 variant). The hash is what actually gets signed, not the raw data.
    js const ethers = require('ethers'); const msg = "I approve Uniswap swap #42"; const hash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(msg));

  • Signature (v, r, s): The three components returned by eth_sign or wallet.signMessage. v is the recovery id (27/28 or 0/1), r and s are 32?byte values.

  • eth_sign vs personal_sign vs eth_signTypedData: Different JSON?RPC methods that sign raw data, a prefixed message, or a typed EIP?712 struct. Use personal_sign for simple messages and eth_signTypedData for structured data (e.g., DAO proposals).

  • ecrecover (Solidity built?in): Takes a hash and a signature and returns the address that created it. It’s the on?chain verification primitive.

  • Nonce: A per?account counter that prevents replay attacks. Each signed transaction must include the current nonce; the network increments it after a successful inclusion.

  • Replay Attack: Re?using a valid signature on another chain or after a nonce reset. Mitigate by signing chain?specific data (include chainId) and by never re?using the same signed payload.

  • chainId (EIP?155): Embedded in the transaction signature to bind a signature to a specific network (e.g., 1 for Mainnet, 5 for Goerli). Prevents cross?chain replay.

  • Hardware Wallet (Ledger/Trezor): Stores the private key in a secure element and performs signing internally, exposing only the signature to the host computer.


Step?by?Step / Process Flow

  1. Generate a wallet (offline or via a hardware device).
    js const wallet = ethers.Wallet.createRandom(); // returns mnemonic, privateKey, address
  2. Compose the data you want to sign (e.g., a DAO proposal struct).
    js const proposal = { id: 7, title: "Fund Dev", amount: ethers.utils.parseEther("10") }; const typedData = { types: { Proposal: [{ name: "id", type: "uint256" }, { name: "title", type: "string" }, { name: "amount", type: "uint256" }] }, primaryType: "Proposal", domain: { name: "MyDAO", version: "1", chainId: 5, verifyingContract: "0xABC…" }, message: proposal, };
  3. Sign the data with the private key (via MetaMask, Ledger, or wallet._signTypedData).
    js const signature = await wallet._signTypedData(typedData.domain, typedData.types, typedData.message);
  4. Send the signed payload to the contract (e.g., vote(uint256 proposalId, bytes signature)).
    js const tx = await daoContract.vote(7, signature); await tx.wait();
  5. Contract verifies the signature using ecrecover (or OpenZeppelin’s ECDSA library).
    solidity function vote(uint256 proposalId, bytes memory sig) external { bytes32 hash = keccak256(abi.encodePacked(address(this), proposalId)); address signer = ECDSA.recover(hash, sig); require(signer == member, "Not a member"); // record vote… }

Common Mistakes

  • Mistake: Signing raw transaction data with eth_sign and then sending it to a contract that expects an EIP?712 typed hash.
    Correction: Always match the signing method to the contract’s verification logic; use eth_signTypedData for typed structs and personal_sign for simple messages.

  • Mistake: Storing the private key in plain text (e.g., in a .env file).
    Correction: Use a hardware wallet or an encrypted secret manager (AWS KMS, HashiCorp Vault). Plain?text keys are a single point of failure.

  • Mistake: Forgetting to include chainId in the signed payload, leading to cross?chain replay attacks.
    Correction: Always sign the EIP?155 replay?protected format ({nonce, gasPrice, gasLimit, to, value, data, chainId}) or include chainId in your custom hash.

  • Mistake: Assuming ecrecover returns the public key; it actually returns the address derived from the public key.
    Correction: If you need the full public key, use secp256k1 libraries off?chain; on?chain you only get the address, which is sufficient for most auth checks.

  • Mistake: Using tx.origin for access control.
    Correction: Always use msg.sender because tx.origin can be spoofed through a malicious contract that forwards calls.


Blockchain Developer Interview / Practical Insights

  1. Explain the difference between call and delegatecall. Interviewers look for understanding that call runs code in the callee’s context (its storage, msg.sender), while delegatecall runs in the caller’s context, allowing proxy patterns but also opening up storage?collision risks.

  2. Why is EIP?155 important for signatures? It binds a signature to a specific chainId, preventing replay attacks across networks. Auditors will check that every signed transaction includes the correct chainId.

  3. Distinguish ERC?20 vs ERC?777. ERC?777 adds hooks (tokensReceived) and a richer operator model, enabling “send?and?receive” callbacks that ERC?20 lacks. Knowing the extra security surface (re?entrancy via hooks) is a plus.

  4. Optimistic Rollup vs ZK Rollup trade?offs. Optimistic rollups assume transactions are valid and provide a fraud?proof window; ZK rollups generate succinct validity proofs on?chain. Interviewers may ask which is better for latency?sensitive DeFi vs privacy?preserving applications.


Quick Check Questions

  1. Scenario: A contract uses tx.origin to restrict a function to the contract owner.
    Answer: Dangerous – a malicious contract can call the vulnerable contract on behalf of the owner, making tx.origin equal the owner’s address and bypassing the check.

  2. Scenario: You receive a signature (v,r,s) from a user, but the v value is 0 instead of 27/28.
    Answer: Modern clients return 0/1; you must add 27 before calling ecrecover, otherwise the recovered address will be wrong.

  3. Scenario: A developer signs a message with eth_sign and later re?uses the same signature on a different chain.
    Answer: This is a replay attack; because the signature lacks a chainId, the attacker can replay it on any network that accepts the same message format.


Last?Minute Cram Sheet (10 one?liners)

  1. Never expose a private key; treat it like a password to a bank vault.
  2. keccak256 is Ethereum’s hash function – always hash before signing.
  3. ecrecover(hash, v, r, s) returns an address, not the full public key.
  4. EIP?155 adds chainId to signatures-prevents cross?chain replay.
  5. v = 27/28 for legacy signatures; add 27 if you get 0/1 from modern wallets.
  6. Nonce = per?account counter; a mismatched nonce causes “nonce too low/high” errors.
  7. Hardware wallets sign inside the device; the host only sees the signature.
  8. personal_sign prefixes the message with "\x19Ethereum Signed Message:\n" – protects against signing raw transaction data unintentionally.
  9. OpenZeppelin’s ECDSA library handles malleability (s must be-secp256k1n/2).
  10. Using tx.origin for auth is a classic phishing vector; always prefer msg.sender.