<?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>What I learned from reading Go's chan.go</title>
      <dc:creator>Mohsen</dc:creator>
      <pubDate>Thu, 24 Sep 2026 07:11:04 +0000</pubDate>
      <link>https://dev.to/mohsenm4/what-i-learned-from-reading-gos-chango-4jbj</link>
      <guid>https://dev.to/mohsenm4/what-i-learned-from-reading-gos-chango-4jbj</guid>
      <description>&lt;p&gt;Before reading &lt;code&gt;chan.go&lt;/code&gt;, my mental model of a channel was one sentence: &lt;em&gt;a safe way for goroutines to send values to each other.&lt;/em&gt; That sentence was true, but it was not enough. I could not answer simple follow-up questions: where does a blocked goroutine actually wait? Is an unbuffered channel secretly a buffer of size one? Why does &lt;code&gt;select&lt;/code&gt; pick a random case?&lt;/p&gt;

&lt;p&gt;So I spent a week reading &lt;code&gt;src/runtime/chan.go&lt;/code&gt; (and later &lt;code&gt;select.go&lt;/code&gt;) from Go 1.26. This post is what I found, written as the questions I had.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What is a channel, really?
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;make(chan T, n)&lt;/code&gt; gives you a pointer to an &lt;code&gt;hchan&lt;/code&gt;. Here it is, lightly trimmed:&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;type&lt;/span&gt; &lt;span class="n"&gt;hchan&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;qcount&lt;/span&gt;   &lt;span class="kt"&gt;uint&lt;/span&gt;           &lt;span class="c"&gt;// values currently in the buffer&lt;/span&gt;
    &lt;span class="n"&gt;dataqsiz&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt;           &lt;span class="c"&gt;// buffer capacity (the n in make)&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt;      &lt;span class="n"&gt;unsafe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pointer&lt;/span&gt; &lt;span class="c"&gt;// the ring buffer itself&lt;/span&gt;
    &lt;span class="n"&gt;elemsize&lt;/span&gt; &lt;span class="kt"&gt;uint16&lt;/span&gt;
    &lt;span class="n"&gt;closed&lt;/span&gt;   &lt;span class="kt"&gt;uint32&lt;/span&gt;
    &lt;span class="n"&gt;elemtype&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_type&lt;/span&gt;
    &lt;span class="n"&gt;sendx&lt;/span&gt;    &lt;span class="kt"&gt;uint&lt;/span&gt;  &lt;span class="c"&gt;// next slot to write&lt;/span&gt;
    &lt;span class="n"&gt;recvx&lt;/span&gt;    &lt;span class="kt"&gt;uint&lt;/span&gt;  &lt;span class="c"&gt;// next slot to read&lt;/span&gt;
    &lt;span class="n"&gt;recvq&lt;/span&gt;    &lt;span class="n"&gt;waitq&lt;/span&gt; &lt;span class="c"&gt;// goroutines blocked on receive&lt;/span&gt;
    &lt;span class="n"&gt;sendq&lt;/span&gt;    &lt;span class="n"&gt;waitq&lt;/span&gt; &lt;span class="c"&gt;// goroutines blocked on send&lt;/span&gt;
    &lt;span class="n"&gt;lock&lt;/span&gt;     &lt;span class="n"&gt;mutex&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three groups of fields: a &lt;strong&gt;ring buffer&lt;/strong&gt; (&lt;code&gt;buf&lt;/code&gt;, &lt;code&gt;sendx&lt;/code&gt;, &lt;code&gt;recvx&lt;/code&gt;, &lt;code&gt;qcount&lt;/code&gt;), &lt;strong&gt;two wait queues&lt;/strong&gt; (&lt;code&gt;recvq&lt;/code&gt;, &lt;code&gt;sendq&lt;/code&gt;), and &lt;strong&gt;one lock&lt;/strong&gt; that protects all of it.&lt;/p&gt;

&lt;p&gt;The lock is &lt;code&gt;runtime.mutex&lt;/code&gt;, not &lt;code&gt;sync.Mutex&lt;/code&gt;. At first I thought that was just "the runtime likes its own tools". The real reason is circularity: &lt;code&gt;sync.Mutex&lt;/code&gt; parks goroutines through the scheduler, and the scheduler is exactly the code that uses &lt;code&gt;hchan&lt;/code&gt;. The runtime cannot build its foundation on top of something that is built on top of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Is an unbuffered channel a buffer of size one?
&lt;/h2&gt;

&lt;p&gt;No. When &lt;code&gt;dataqsiz == 0&lt;/code&gt;, &lt;code&gt;buf&lt;/code&gt; holds nothing. A send and a receive have to &lt;em&gt;meet&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If a receiver is already waiting, the sender copies its value &lt;strong&gt;directly into the receiver's variable&lt;/strong&gt;, which lives on the receiver's stack. The source has a comment about this that surprised me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sends and receives on unbuffered or empty-buffered channels are the only operations where one running goroutine writes to the stack of another running goroutine.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is why an unbuffered channel is a synchronization point and not just a pipe: there is no place for the value to sit, so both sides must be there.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. How many ways can a send end?
&lt;/h2&gt;

&lt;p&gt;Reading &lt;code&gt;chansend&lt;/code&gt; top to bottom, I counted these paths (for a normal, blocking send):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;nil channel&lt;/strong&gt;: the goroutine parks forever. There is no one to wake it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;closed channel&lt;/strong&gt;: &lt;code&gt;panic("send on closed channel")&lt;/code&gt;. It does not return quietly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;a receiver is waiting&lt;/strong&gt; in &lt;code&gt;recvq&lt;/code&gt;: dequeue it and hand the value over directly, even if the channel is buffered. The buffer is skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;buffer has space&lt;/strong&gt;: copy into &lt;code&gt;buf[sendx]&lt;/code&gt;, advance &lt;code&gt;sendx&lt;/code&gt;, done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;otherwise&lt;/strong&gt;: wrap this goroutine in a &lt;code&gt;sudog&lt;/code&gt;, put it on &lt;code&gt;sendq&lt;/code&gt;, and &lt;code&gt;gopark&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Path 3 was the most surprising one for me. If a receiver is already waiting, the value never touches the buffer. The source even writes this as an invariant: in a buffered channel, &lt;code&gt;qcount &amp;gt; 0&lt;/code&gt; implies &lt;code&gt;recvq&lt;/code&gt; is empty. You never have data in the buffer &lt;em&gt;and&lt;/em&gt; a waiting receiver at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Full buffer and a waiting sender: who gets what?
&lt;/h2&gt;

&lt;p&gt;Say the buffer is full and a sender is parked on &lt;code&gt;sendq&lt;/code&gt;. Now a receiver arrives. The naive idea is "give the receiver the sender's value". That would break FIFO order, because older values are still in the buffer.&lt;/p&gt;

&lt;p&gt;What &lt;code&gt;recv&lt;/code&gt; actually does:&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="c"&gt;// Queue is full. Take the item at the head of the queue.&lt;/span&gt;
&lt;span class="c"&gt;// Make the sender enqueue its item at the tail of the queue.&lt;/span&gt;
&lt;span class="c"&gt;// Since the queue is full, those are both the same slot.&lt;/span&gt;
&lt;span class="n"&gt;qp&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;chanbuf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recvx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;typedmemmove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elemtype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="c"&gt;// oldest value -&amp;gt; receiver&lt;/span&gt;
&lt;span class="n"&gt;typedmemmove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elemtype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;     &lt;span class="c"&gt;// sender's value -&amp;gt; freed slot&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recvx&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recvx&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataqsiz&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recvx&lt;/span&gt; &lt;span class="o"&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sendx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recvx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The receiver takes the oldest value, and the waiting sender's value drops into the slot that just became free. Because the buffer is full, the head and the tail are the same slot. One read, one write, order preserved. I think this is the nicest piece of code in the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. What is a &lt;code&gt;sudog&lt;/code&gt;, and why not just queue the goroutine?
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;sudog&lt;/code&gt; is "a goroutine waiting on one thing". It holds a pointer to the goroutine (&lt;code&gt;g&lt;/code&gt;), a pointer to the value being sent or received (&lt;code&gt;elem&lt;/code&gt;), and links for the wait queue.&lt;/p&gt;

&lt;p&gt;Why a separate struct instead of putting the goroutine itself in &lt;code&gt;recvq&lt;/code&gt;? Because &lt;strong&gt;one goroutine can wait on many channels at once&lt;/strong&gt;. That is exactly what &lt;code&gt;select&lt;/code&gt; does. Each case gets its own &lt;code&gt;sudog&lt;/code&gt;, all pointing to the same goroutine. When one of them fires, the runtime has to remove the others. So the relation is one goroutine to many &lt;code&gt;sudog&lt;/code&gt;s, and the queue holds &lt;code&gt;sudog&lt;/code&gt;s.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. How does &lt;code&gt;select&lt;/code&gt; use all of this?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;select.go&lt;/code&gt; builds on the same pieces. &lt;code&gt;selectgo&lt;/code&gt; works in three passes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Poll&lt;/strong&gt;: look at every case in a &lt;em&gt;random&lt;/em&gt; order (&lt;code&gt;pollorder&lt;/code&gt;). If one is ready, do it and return. Random order is for fairness: a busy channel written first in your code should not starve the others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enqueue and park&lt;/strong&gt;: nothing ready and no &lt;code&gt;default&lt;/code&gt;? Put a &lt;code&gt;sudog&lt;/code&gt; on the queue of &lt;strong&gt;every&lt;/strong&gt; channel in the select, then &lt;code&gt;gopark&lt;/code&gt; once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wake up and clean up&lt;/strong&gt;: whoever woke us set &lt;code&gt;gp.param&lt;/code&gt; to the winning &lt;code&gt;sudog&lt;/code&gt;. Dequeue all the other &lt;code&gt;sudog&lt;/code&gt;s from their channels.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before touching the channels, &lt;code&gt;select&lt;/code&gt; locks them all. The lock order is not the order in your code. It is sorted by the address of each &lt;code&gt;hchan&lt;/code&gt; (&lt;code&gt;lockorder&lt;/code&gt;). If two goroutines run selects over the same channels in opposite order, a code-order lock could deadlock. A single global order (addresses) makes that impossible.&lt;/p&gt;

&lt;p&gt;One practical trick falls out of this: a &lt;strong&gt;nil channel case is never ready&lt;/strong&gt;. The runtime just leaves it out. So inside a loop you can set &lt;code&gt;ch = nil&lt;/code&gt; to turn a case off without restructuring the select.&lt;/p&gt;

&lt;p&gt;Honest note: I read &lt;code&gt;selectgo&lt;/code&gt; carefully, wrote notes, and five days later I could only explain half of it (I scored myself 5/10). Things I only &lt;em&gt;read&lt;/em&gt; faded. Things I &lt;em&gt;built&lt;/em&gt; stayed. That is the reason for the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. I built one myself
&lt;/h2&gt;

&lt;p&gt;To check whether I understood &lt;code&gt;hchan&lt;/code&gt;, I wrote a bounded channel with a &lt;code&gt;sync.Mutex&lt;/code&gt; and two &lt;code&gt;sync.Cond&lt;/code&gt;s (&lt;code&gt;notFull&lt;/code&gt;, &lt;code&gt;notEmpty&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;My first version stored items in a slice and received with &lt;code&gt;buf = buf[1:]&lt;/code&gt;. It worked in small tests. The problem: re-slicing moves the start forward, and the space at the beginning is never reused. After enough sends and receives, the buffer looked full even when it had free space. The fix was the same idea as &lt;code&gt;hchan&lt;/code&gt;: a fixed array with a read index and a write index that wrap around.&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;BoundedChannelTest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"send on closed channel"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notFull&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// releases mu while waiting&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;closed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"send on closed channel"&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;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sendx&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="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sendx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sendx&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="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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notEmpty&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Signal&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;I even named the indexes &lt;code&gt;sendx&lt;/code&gt; and &lt;code&gt;recvx&lt;/code&gt;, like &lt;code&gt;hchan&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A week later I closed it and rewrote it from memory, without notes or AI. It passed with &lt;code&gt;go test -race&lt;/code&gt; (5 producers, 5 consumers, 1000 items each). That rebuild is what convinced me I understood it.&lt;/p&gt;

&lt;p&gt;What my version does &lt;em&gt;not&lt;/em&gt; have, compared to the real one: direct handoff to a waiting receiver (path 3), &lt;code&gt;sudog&lt;/code&gt;s, or any way to take part in a &lt;code&gt;select&lt;/code&gt;. &lt;code&gt;sync.Cond&lt;/code&gt; wakes someone up. &lt;code&gt;hchan&lt;/code&gt; knows exactly who is waiting and gives them the value.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. What I take away
&lt;/h2&gt;

&lt;p&gt;A Go channel is not just a queue. It is a ring buffer, two wait queues, and a lock, plus a scheduler that can park and wake goroutines and copy values straight between their stacks.&lt;/p&gt;

&lt;p&gt;After &lt;code&gt;chan.go&lt;/code&gt;, the rest of the &lt;code&gt;sync&lt;/code&gt; package read differently. &lt;code&gt;sync.Once&lt;/code&gt; is a mutex plus an atomic flag, and the detail I missed when I rebuilt it was that the standard library marks it done with a &lt;code&gt;defer&lt;/code&gt;, so even a panicking &lt;code&gt;f&lt;/code&gt; counts as "done". &lt;code&gt;sync.Pool&lt;/code&gt; mostly avoids locks by giving each P (the scheduler's processor) its own local pool. Same questions every time: where does the data live, who waits, and who wakes them?&lt;/p&gt;

&lt;p&gt;If you are learning Go concurrency, my advice is: read &lt;code&gt;chan.go&lt;/code&gt; once, then close it and build a small version yourself. The reading gives you the words. The building makes them stay.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: Go 1.26, &lt;code&gt;src/runtime/chan.go&lt;/code&gt; and &lt;code&gt;src/runtime/select.go&lt;/code&gt;. Exercises: &lt;a href="https://github.com/mohsenm4/go-fundamentals" rel="noopener noreferrer"&gt;github.com/mohsenm4/go-fundamentals&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>beginners</category>
    </item>
    <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>
