<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohsen</title>
    <description>The latest articles on DEV Community by Mohsen (@mohsenm4).</description>
    <link>https://dev.to/mohsenm4</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4035029%2F4633c7d3-0f5e-4535-abad-1456779430d9.jpg</url>
      <title>DEV Community: Mohsen</title>
      <link>https://dev.to/mohsenm4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohsenm4"/>
    <language>en</language>
    <item>
      <title>Ethereum cryptography from a Go dev's view</title>
      <dc:creator>Mohsen</dc:creator>
      <pubDate>Sun, 13 Sep 2026 06:20:02 +0000</pubDate>
      <link>https://dev.to/mohsenm4/ethereum-cryptography-from-a-go-devs-view-47h8</link>
      <guid>https://dev.to/mohsenm4/ethereum-cryptography-from-a-go-devs-view-47h8</guid>
      <description>&lt;p&gt;I built an Ethereum wallet CLI in Go because I wanted to understand what a wallet actually does. Not just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generate a key
create an address
sign a transaction
done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was my mental model at the beginning. After implementing the pieces myself, I realized a wallet is less like a key container and more like a collection of cryptographic decisions, where small implementation details can break compatibility, correctness, or security.&lt;/p&gt;

&lt;p&gt;This post is about what I learned while building &lt;a href="https://github.com/mohsenm4/mini-wallet" rel="noopener noreferrer"&gt;mini-wallet&lt;/a&gt;, and the cryptography details that surprised me.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The project is built for learning, not production key management.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why I built a wallet CLI instead of just reading about crypto
&lt;/h2&gt;

&lt;p&gt;Reading about cryptography is useful. Implementing it is different.&lt;/p&gt;

&lt;p&gt;Before this project, my view of wallets was much simpler: I thought they were mostly responsible for transferring assets. Once I started implementing the pieces myself, I started seeing the hidden complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How random data becomes a human-readable recovery phrase.&lt;/li&gt;
&lt;li&gt;How one seed generates thousands of keys.&lt;/li&gt;
&lt;li&gt;How signatures can recover public keys.&lt;/li&gt;
&lt;li&gt;Why different tools sometimes represent the same value differently.&lt;/li&gt;
&lt;li&gt;Why compatibility tests matter as much as the algorithms themselves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose to build a CLI wallet because you understand a system differently when you are responsible for making every part work. A library can hide complexity; building the library exposes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entropy → mnemonic (BIP39)
&lt;/h2&gt;

&lt;p&gt;Most wallets start with entropy. For a 12-word mnemonic, BIP39 starts with 128 bits of random entropy, then calculates SHA-256 of that entropy and takes the first 4 bits as the checksum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;128 bits entropy + 4 bits checksum = 132 bits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those 132 bits are split into groups of 11 bits. Each 11-bit number is an index between 0 and 2047, and BIP39 has a word list with exactly 2048 words, so each index maps to one word. The result is the 12-word mnemonic.&lt;/p&gt;

&lt;p&gt;The interesting part for me was the checksum. The wallet does not store an extra "checksum field" — the checksum is embedded in the words themselves. In my implementation it is calculated directly from the entropy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;checksumBits&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;

&lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sum256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entropy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;firstByteBits&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%08b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;checksum&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;firstByteBits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;checksumBits&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;bitstring&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;entropy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;bitstring&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%08b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;bitstring&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;checksum&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting bit string is then split into 11-bit chunks and mapped to the BIP39 word list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entropy
   |
SHA-256
   |
checksum bits
   |
entropy + checksum
   |
11-bit groups
   |
BIP39 words
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When recovering the wallet, the process runs in reverse: the words are converted back into bits, the entropy is separated from the claimed checksum, SHA-256 is calculated again, and the two checksums are compared. A handful of bits is enough to catch most human input mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seed → keys (BIP32/BIP44)
&lt;/h2&gt;

&lt;p&gt;The mnemonic is not the private key. It is used to generate a seed, and that seed becomes the root of a hierarchical deterministic (HD) wallet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mnemonic
   |
   v
seed
   |
   v
master key
   |
   +---- account
   |       |
   |       +---- address
   |       +---- address
   |
   +---- account
           |
           +---- address
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BIP32 lets us derive a whole tree of keys from one master key. The most important detail here is the difference between hardened and non-hardened derivation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardened derivation
&lt;/h3&gt;

&lt;p&gt;With hardened derivation, creating a child key requires the parent private key — an xpub alone is not enough. In the path parser, I mark hardened indices by adding &lt;code&gt;HardenedOffset&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;hardened&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasSuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hardened&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseUint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invalid path segment %q: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;hardened&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;HardenedOffset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"non-hardened index %d exceeds 2^31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hardened&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;HardenedOffset&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;44'  -&amp;gt; 44 + 0x80000000
60'  -&amp;gt; 60 + 0x80000000
0    -&amp;gt; 0
0    -&amp;gt; 0
5    -&amp;gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Non-hardened derivation
&lt;/h3&gt;

&lt;p&gt;With non-hardened derivation, someone with only an xpub can derive child public keys. That is useful for watch-only wallets, but it has a dangerous edge case. If an attacker has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the parent xpub&lt;/li&gt;
&lt;li&gt;one child private key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;they can compute the parent private key — and once that is compromised, the whole subtree below it is compromised too. This is the well-known xpub + child-private-key attack, and it is why the first three levels of the path are hardened.&lt;/p&gt;

&lt;p&gt;Ethereum uses the BIP44 path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;m/44'/60'/0'/0/n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;m&lt;/code&gt; → master key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;44'&lt;/code&gt; → BIP44 purpose&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;60'&lt;/code&gt; → Ethereum&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0'&lt;/code&gt; → account index&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt; → change&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;n&lt;/code&gt; → address index&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default path is also what my CLI uses when deriving Ethereum addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  secp256k1 signing and recovery
&lt;/h2&gt;

&lt;p&gt;Ethereum uses the secp256k1 elliptic curve. A signature is usually written as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(r, s, v)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is &lt;code&gt;v&lt;/code&gt;. Many cryptographic systems send the public key together with the signature. Ethereum does something different: the signature carries enough information to &lt;em&gt;recover&lt;/em&gt; the public key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signature
    |
    v
recover public key
    |
    v
derive Ethereum address
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because an Ethereum transaction has no &lt;code&gt;from&lt;/code&gt; field. The sender is recovered from the signature, which saves 64 bytes of public key per transaction.&lt;/p&gt;

&lt;p&gt;It is also where compatibility problems start showing up, because different tools represent &lt;code&gt;v&lt;/code&gt; differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;go-ethereum's &lt;code&gt;SigToPub&lt;/code&gt; expects a recovery ID of &lt;code&gt;0/1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;wallet tooling can provide &lt;code&gt;27/28&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They carry the same recovery information, but they are not interchangeable byte-for-byte. I ran into exactly this problem. Instead of teaching the CLI about every wallet's signature format, I moved the compatibility logic into the signer layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;27&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;28&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;normalised&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;65&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalised&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;normalised&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;27&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hashPersonalMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pubKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SigToPub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;normalised&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PubkeyToAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pubKey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;RecoverPersonal&lt;/code&gt; accepts either representation. A small detail — but exactly the kind that makes two otherwise-correct implementations incompatible.&lt;/p&gt;

&lt;p&gt;There is a third use of &lt;code&gt;v&lt;/code&gt; that is easy to confuse with these. For transactions, EIP-155 uses &lt;code&gt;v&lt;/code&gt; for replay protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v = chainId * 2 + 35/36
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is different from the &lt;code&gt;0/1&lt;/code&gt; recovery ID of the signing primitive and from the &lt;code&gt;27/28&lt;/code&gt; convention of personal-message signing. Same field, three conventions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keystore V3: encryption is more than encrypt/decrypt
&lt;/h2&gt;

&lt;p&gt;Ethereum keystore files protect private keys with three pieces:&lt;/p&gt;

&lt;h3&gt;
  
  
  scrypt
&lt;/h3&gt;

&lt;p&gt;scrypt is the key derivation function: it turns the user's password into a 32-byte derived key. That key is split in two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;derivedKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;deriveKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;KeystoreV3&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;encKey&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;derivedKey&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;macKey&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;derivedKey&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first 16 bytes become the AES key; the remaining 16 bytes are used for the MAC.&lt;/p&gt;

&lt;h3&gt;
  
  
  AES-128-CTR
&lt;/h3&gt;

&lt;p&gt;The private key is encrypted using AES-128 in CTR mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;aes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCipher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;KeystoreV3&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cipher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCTR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;XORKeyStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CTR turns AES into a stream cipher, so the ciphertext is exactly as long as the private key and no padding is involved.&lt;/p&gt;

&lt;h3&gt;
  
  
  MAC
&lt;/h3&gt;

&lt;p&gt;The MAC protects the ciphertext against modification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keccak256(derivedKey[16:32] ‖ ciphertext)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sha3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewLegacyKeccak256&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;macKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mac&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the overall flow looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;password
   |
   v
  scrypt
   |
   v
derived key
   |
   +---- first 16 bytes ----&amp;gt; AES-128-CTR key
   |
   +---- last 16 bytes -----&amp;gt; MAC key
                                |
ciphertext &amp;lt;--------------------+
   |
   v
keccak256(macKey || ciphertext)
   |
   v
MAC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, a simple round-trip test looks like enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;encrypt
   |
decrypt
   |
same private key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this can hide bugs: if encryption and decryption share the same mistake, the test still passes. That is why reference vectors matter — they test compatibility with the actual format, not with your own implementation.&lt;/p&gt;

&lt;p&gt;One of the bugs I hit was hard-coded scrypt parameters. The reference vector intentionally used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p = 8
r = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while my decryption path had the parameters hard-coded instead of reading them from the keystore JSON. My own round-trip tests passed, because both sides shared the same assumption. The reference vector did not. That was a much more useful failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Messages, not hashes
&lt;/h2&gt;

&lt;p&gt;A signature alone does not explain what was signed — a hash is just 32 bytes. The same signing primitive could be used for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a transaction&lt;/li&gt;
&lt;li&gt;a login message&lt;/li&gt;
&lt;li&gt;a smart contract interaction&lt;/li&gt;
&lt;li&gt;structured application data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why Ethereum has standards such as EIP-191 and EIP-712.&lt;/p&gt;

&lt;h3&gt;
  
  
  EIP-191
&lt;/h3&gt;

&lt;p&gt;For personal messages, the message is not hashed directly. Ethereum adds a prefix that includes the message length:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"\x19Ethereum Signed Message:\n" + len(message) + message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My implementation does exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;hashPersonalMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hash&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x19&lt;/span&gt;&lt;span class="s"&gt;Ethereum Signed Message:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;%d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Keccak256Hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;\x19&lt;/code&gt; byte is not a valid start of an RLP-encoded transaction, so a signed message can never be replayed as a transaction. The signature is tied to the personal-message format instead of being a raw signature over arbitrary bytes.&lt;/p&gt;

&lt;h3&gt;
  
  
  EIP-712
&lt;/h3&gt;

&lt;p&gt;For structured data, EIP-712 goes further. Instead of an opaque blob, it defines typed data with a schema, a domain separator, and a structured hashing process. The final digest is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keccak256(
    "\x19\x01" ||
    domainSeparator ||
    hashStruct(message)
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last step of my implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x19\x01&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;domainHash&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messageHash&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Keccak256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The domain separator binds the signature to context such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application&lt;/li&gt;
&lt;li&gt;version&lt;/li&gt;
&lt;li&gt;chain ID&lt;/li&gt;
&lt;li&gt;verifying contract&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my tests, a signature made with &lt;code&gt;chainId = 1&lt;/code&gt; recovers to a different address when checked against &lt;code&gt;chainId = 999&lt;/code&gt; — that is the domain separator doing its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three bugs my tests caught
&lt;/h2&gt;

&lt;p&gt;This was the most valuable part of the project. The biggest lessons did not come from implementing algorithms; they came from failing tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 1: &lt;code&gt;v&lt;/code&gt; format mismatch
&lt;/h3&gt;

&lt;p&gt;I took signatures with &lt;code&gt;v = 27/28&lt;/code&gt; from wallet tooling and passed them straight into &lt;code&gt;SigToPub&lt;/code&gt;, which expects &lt;code&gt;0/1&lt;/code&gt;. Recovery returned the wrong address.&lt;/p&gt;

&lt;p&gt;My first fix was inside the CLI command. Then I realized the conversion belonged in the signer layer: &lt;code&gt;RecoverPersonal&lt;/code&gt; should accept both formats and normalize before recovery.&lt;/p&gt;

&lt;p&gt;The lesson:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compatibility logic should live where the concept belongs.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2: hard-coded scrypt parameters
&lt;/h3&gt;

&lt;p&gt;My keystore tests passed — because encryption and decryption made the same assumption. The reference vector intentionally used &lt;code&gt;p=8, r=1&lt;/code&gt;, while my decryption path ignored the parameters in the keystore JSON. The implementation was consistently wrong, and only the external vector exposed it.&lt;/p&gt;

&lt;p&gt;The lesson:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Passing your own tests does not always mean you implemented the standard correctly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reference vectors are not optional polish for cryptographic code. They are part of the implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 3: unsafe type assertion in &lt;code&gt;HashStruct&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The third bug was in EIP-712 &lt;code&gt;HashStruct&lt;/code&gt;. When processing a nested typed-data field, I assumed the value was always a &lt;code&gt;map[string]any&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;nested&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bare type assertion panics when the input has the wrong shape. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;nested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"field %s must be a nested struct (map[string]any), got %T"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now malformed typed data produces an error instead of crashing the process.&lt;/p&gt;

&lt;p&gt;The lesson:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crypto security is not only about cryptographic algorithms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parsing, validation, and handling unexpected input are part of security too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The next milestone is smaller than I originally thought. &lt;code&gt;v0.1.0&lt;/code&gt; is the MVP tag:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EIP-712 in the CLI&lt;/li&gt;
&lt;li&gt;tag &lt;code&gt;v0.1.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;call the MVP done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After that, transaction lifecycle work moves to a separate project, &lt;code&gt;blockchain-insight&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transaction creation&lt;/li&gt;
&lt;li&gt;signing&lt;/li&gt;
&lt;li&gt;broadcasting&lt;/li&gt;
&lt;li&gt;tracking transaction status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Separating those concerns is another thing this project taught me: a wallet does not need to become an entire blockchain application just because it can sign messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The biggest thing I learned is that cryptography is not just mathematics — it is also engineering. The algorithms are one part of the problem. The other part is everything around them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;standards&lt;/li&gt;
&lt;li&gt;byte formats&lt;/li&gt;
&lt;li&gt;compatibility&lt;/li&gt;
&lt;li&gt;serialization&lt;/li&gt;
&lt;li&gt;validation&lt;/li&gt;
&lt;li&gt;reference vectors&lt;/li&gt;
&lt;li&gt;error handling&lt;/li&gt;
&lt;li&gt;API boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;27&lt;/code&gt; instead of a &lt;code&gt;0&lt;/code&gt;. A hard-coded KDF parameter. A type assertion without &lt;code&gt;ok&lt;/code&gt;. Each one looks like a tiny detail, and each one is the difference between "works on my machine" and an implementation that actually interoperates with the Ethereum ecosystem.&lt;/p&gt;

&lt;p&gt;Building this wallet changed how I look at Ethereum. I no longer see a wallet as a key container; I see it as a system where every small decision matters. That is probably the most useful thing I got from building it myself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code: &lt;a href="https://github.com/mohsenm4/mini-wallet" rel="noopener noreferrer"&gt;github.com/mohsenm4/mini-wallet&lt;/a&gt;. Built for learning — not for production key management.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>ethereum</category>
      <category>cryptography</category>
      <category>blockchain</category>
    </item>
  </channel>
</rss>
