<?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: Dmytro Svynarenko</title>
    <description>The latest articles on DEV Community by Dmytro Svynarenko (@dmytro_svynarenko).</description>
    <link>https://dev.to/dmytro_svynarenko</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3605778%2F7945f34f-882a-4b43-a087-744644f20ed5.JPG</url>
      <title>DEV Community: Dmytro Svynarenko</title>
      <link>https://dev.to/dmytro_svynarenko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmytro_svynarenko"/>
    <language>en</language>
    <item>
      <title>Designing Blockchain #4: Merkle Trees and State Verification</title>
      <dc:creator>Dmytro Svynarenko</dc:creator>
      <pubDate>Thu, 15 Jan 2026 18:16:10 +0000</pubDate>
      <link>https://dev.to/dmytro_svynarenko/designing-blockchain-4-merkle-trees-and-state-verification-15i8</link>
      <guid>https://dev.to/dmytro_svynarenko/designing-blockchain-4-merkle-trees-and-state-verification-15i8</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/dmytro_svynarenko/designing-blockchain-3-cryptography-and-wallets-153h"&gt;previous article&lt;/a&gt;, we secured our Fleming blockchain by implementing cryptographic signatures and wallets, ensuring that only legitimate account owners can authorise transactions. However, if you remember from the &lt;a href="https://dev.to/dmytro_svynarenko/designing-blockchain-2-accounts-and-state-c82"&gt;second article&lt;/a&gt;, we left a critical architectural problem unresolved: &lt;strong&gt;storing the full state in every block is extremely wasteful and doesn't scale well.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's understand the magnitude of this problem. In our current implementation, each block contains a complete copy of the entire blockchain state — a &lt;code&gt;HashMap&amp;lt;Address, u64&amp;gt;&lt;/code&gt; where Address is a String ("0x" prefix + 40 hex characters = 42 bytes) and balance is a &lt;code&gt;u64&lt;/code&gt; (8 bytes). That's 50 bytes per account just for the data itself, not counting HashMap's internal overhead. If our blockchain has 1,000,000 accounts, each block stores at least 50 MB of state data. Now imagine we have 10,000 blocks in our chain. That's 500 GB just for storing redundant state copies! And the problem compounds exponentially as both the number of accounts and blocks grow.&lt;/p&gt;

&lt;p&gt;This memory inefficiency makes our blockchain impractical for any real-world application. We need a way to represent the entire state compactly while still being able to verify individual account balances. The solution comes from cryptographic data structures called Merkle Trees.&lt;/p&gt;

&lt;p&gt;In this article, we'll explore how Merkle Trees work, discover why a simple binary tree isn't enough for our needs, and implement a data structure that allows us to represent millions of accounts with just a single 32-byte hash. We'll also see how to cryptographically prove that a specific account exists in the state without revealing the entire state data. By the end, instead of storing megabytes of redundant state in each block, we'll store just 32 bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tree Data Structures
&lt;/h2&gt;

&lt;p&gt;Before diving into Merkle Trees, let's establish what tree data structures are and why they're useful for blockchain.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;tree&lt;/strong&gt; is a hierarchical data structure consisting of nodes connected by edges. Each tree has a &lt;strong&gt;root node&lt;/strong&gt; at the top, and every node can have zero or more &lt;strong&gt;child nodes&lt;/strong&gt;. Nodes without children are called leaf nodes. Trees enable efficient O(log N) operations — with 1,000,000 elements, you might only need to traverse 20 levels instead of checking all million elements linearly.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;trie&lt;/strong&gt; (pronounced "try", from retrieval) is a specialized type of tree where the path from root to a node represents a key. The key difference from regular trees: in a trie, keys are broken down into individual components (characters, nibbles, or bits), and nodes share common prefixes.&lt;/p&gt;

&lt;p&gt;Here's a simple example storing words "cat", "car", and "dog":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    root
   /    \
  c      d
  |      |
  a      o
 / \     |
t   r    g
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how "cat" and "car" share the path root→c→a, saving space. This prefix-sharing property makes tries perfect for blockchain addresses.&lt;/p&gt;

&lt;p&gt;For blockchain, we use &lt;strong&gt;hexadecimal tries&lt;/strong&gt; where each level of the tree represents one hex character (0-9, a-f). Since our addresses are hex strings like "0x3a7f...", we can naturally traverse the trie by following the hex characters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Address: 0x3a7f...

     root
    / | \
   3  a  f  ...
   |
   a
   |
   7
   |
   f
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each node can have up to 16 children (one for each hex digit). This structure is perfect for organizing blockchain addresses efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merkle Trees
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Merkle Tree&lt;/strong&gt; (also known as a hash tree) is a tree data structure where every leaf node contains a hash of some data, and every non-leaf node contains a hash of its child nodes. This structure was invented by Ralph Merkle in 1979 and has become fundamental to blockchain technology.&lt;/p&gt;

&lt;p&gt;Let's build up the concept step by step. Imagine we have four accounts in our blockchain state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Account A: balance = 100
Account B: balance = 50
Account C: balance = 75
Account D: balance = 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of storing this data directly, we create a binary tree of hashes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             ROOT HASH
            /          \
           /            \
       H(AB)            H(CD)
      /    \           /    \
     /      \         /      \
  H(A)    H(B)     H(C)    H(D)
   |       |        |        |
A:100    B:50     C:75    D:200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the bottom level (leaf nodes), we hash each account's data. For example, &lt;code&gt;H(A) = SHA256("A" + "100")&lt;/code&gt;. Then we move up the tree, combining pairs of hashes: &lt;code&gt;H(AB) = SHA256(H(A) + H(B))&lt;/code&gt;. We continue this process until we reach a single hash at the top — the &lt;strong&gt;Merkle Root&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Merkle Root is a single 32-byte hash that uniquely represents all the data in the tree. If any account balance changes — even by a single coin — the root hash changes completely. This is the cryptographic property we need: a compact fingerprint of the entire state.&lt;/p&gt;

&lt;p&gt;Now here's the powerful part: we can prove that a specific account exists in the state without revealing all the other accounts. This is called a &lt;strong&gt;Merkle Proof&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merkle Proof
&lt;/h3&gt;

&lt;p&gt;Let's say someone asks: "Does account B with balance 50 exist in this state?" Instead of showing them all four accounts, we provide a Merkle Proof — just the minimum set of hashes needed to verify the data.&lt;/p&gt;

&lt;p&gt;For account B, the proof would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Proof = {
  leaf_hash: H(B),
  siblings: [H(A), H(CD)]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier can then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with &lt;code&gt;H(B)&lt;/code&gt; — the hash of account B's data&lt;/li&gt;
&lt;li&gt;Combine with &lt;code&gt;H(A)&lt;/code&gt; to get &lt;code&gt;H(AB)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Combine &lt;code&gt;H(AB)&lt;/code&gt; with &lt;code&gt;H(CD)&lt;/code&gt; to get the ROOT HASH&lt;/li&gt;
&lt;li&gt;Compare this computed root with the root stored in the block header&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If they match, the proof is valid — account B definitely exists in this state. If the account data was tampered with, or if it doesn't exist in the state, the computed root wouldn't match.&lt;/p&gt;

&lt;p&gt;The beauty is that the proof size grows logarithmically with the number of accounts. For 1,000,000 accounts in a binary tree, you only need about 20 hashes (log₂ 1,000,000 ≈ 20) to prove any single account — that's just 640 bytes instead of 50 MB!&lt;/p&gt;

&lt;h3&gt;
  
  
  Merkle Tree vs Merkle Trie
&lt;/h3&gt;

&lt;p&gt;Before we continue, it's important to clarify the terminology, as these terms are often used interchangeably but refer to different structures:&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Merkle Tree&lt;/strong&gt; is a general hash tree where the structure can be arbitrary. The classic example is Bitcoin's binary Merkle Tree — transactions are grouped pairwise, hashed together, and combined bottom-up until reaching a single root. The tree structure doesn't encode any information about the data itself; it's just an efficient way to hash a list of items.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Merkle Trie&lt;/strong&gt; (or Merkle Patricia Trie) is a specific type of Merkle Tree where the &lt;strong&gt;path from root to leaf encodes the key&lt;/strong&gt;. In a trie, you traverse the tree by following the components of the key (bits, hex characters, etc.) to find the value. This makes it perfect for key-value storage like account balances, where the address itself determines the path through the tree.&lt;/p&gt;

&lt;p&gt;The key difference: in a Merkle Tree, the structure is determined by how you group the data. In a Merkle Trie, the structure is determined by the keys themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Merkle Trees for Blockchain
&lt;/h3&gt;

&lt;p&gt;Merkle Trees (and Merkle Tries) solve several critical problems for blockchain:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compact state representation&lt;/strong&gt; — Instead of storing the full state (50+ MB) in each block, we store just the Merkle Root (32 bytes). This makes blocks dramatically smaller and the blockchain more scalable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Efficient verification&lt;/strong&gt; — Anyone can verify that a specific account exists in the state with a small proof, without needing to download and validate the entire state. This enables light clients — nodes that don't store the full blockchain but can still verify data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tamper detection&lt;/strong&gt; — Any change to any account instantly changes the root hash. This makes it cryptographically impossible to modify historical state without detection, as the root hash is included in the block hash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic&lt;/strong&gt; — For the same set of accounts and balances, the Merkle Root is always the same. Different nodes can independently compute the state and verify they're in sync by comparing just 32 bytes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merkle Trees in Production Blockchains
&lt;/h3&gt;

&lt;p&gt;Different blockchains use Merkle Trees in different ways, depending on their architecture and requirements:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bitcoin&lt;/strong&gt; uses a simple binary Merkle Tree to organize transactions within each block. All transactions in a block are hashed pairwise and combined bottom-up until reaching a single Merkle Root, which is included in the block header. This enables Simplified Payment Verification (SPV) — light clients can verify that a transaction exists in a block by downloading just the block headers and a Merkle Proof, without downloading all transactions. Bitcoin doesn't use Merkle Trees for state because it follows the UTXO model, where state is simply the set of unspent outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ethereum&lt;/strong&gt; takes a more sophisticated approach with three separate Patricia Merkle Tries for each block: a State Trie (account balances and contract storage), a Transaction Trie (transactions in the block), and a Receipt Trie (transaction execution results). The Patricia Trie is a modified hexadecimal trie with several optimizations — it uses extension nodes to compress long paths without branching, and branch nodes that can have up to 16 children (one for each hex digit 0-f). These optimizations dramatically reduce tree depth compared to a naive implementation. The structure allows efficient incremental updates: when an account balance changes, only the path from that leaf to the root needs to be recalculated, not the entire tree. The State Trie enables light clients to verify account balances without storing Ethereum's entire state (over 200 million accounts).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solana&lt;/strong&gt; uses Merkle Trees for its accounts database. The blockchain maintains a Merkle Tree of all account states, and the root is included in each block. Solana's high-performance architecture processes thousands of transactions per second, and Merkle Trees enable efficient state verification without sacrificing speed. Light clients can query account data and verify it against the Merkle Root without trusting full nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cardano&lt;/strong&gt; also employs Merkle Trees for transaction verification within blocks, similar to Bitcoin. This allows light wallets to verify transactions efficiently without downloading the entire blockchain.&lt;/p&gt;

&lt;p&gt;For our Fleming blockchain, we'll implement a &lt;strong&gt;hexadecimal Merkle Trie&lt;/strong&gt; similar to Ethereum's approach — using hex characters from our addresses to traverse the trie. Since the path through the tree is determined by the address itself (each hex character decides which of the 16 children to follow), this is a trie structure, not a simple tree. This gives us a good balance between implementation simplicity and real-world applicability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sparse Merkle Trie
&lt;/h2&gt;

&lt;p&gt;Now that we understand Merkle Trees and Merkle Tries, let's address a critical challenge: &lt;strong&gt;how do we handle dynamic state updates efficiently?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine we implement a naive hexadecimal Merkle Trie for our blockchain state. Our addresses are 40 hex characters long, which means a full trie would have a depth of 40 levels. Here's the problem: if we only have 3 accounts in our blockchain, we'd still need to maintain the entire tree structure with potentially millions of empty branches. Even worse, every time we add a new account, we'd need to traverse up to 40 levels to insert it and recalculate hashes along the path.&lt;/p&gt;

&lt;p&gt;This is where the &lt;strong&gt;Sparse Merkle Trie&lt;/strong&gt; comes in. The key insight is that most of the trie is empty — with millions of possible addresses but only thousands or millions actually in use, the vast majority of the tree nodes don't exist. A Sparse Merkle Trie takes advantage of this sparseness.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Sparse Merkle Tries Work
&lt;/h3&gt;

&lt;p&gt;A Sparse Merkle Trie has a &lt;strong&gt;fixed depth&lt;/strong&gt; determined by the key length. For our 40-character hex addresses, the tree has exactly 40 levels. However, instead of actually creating all possible nodes, we only store the nodes that exist along paths to actual data.&lt;/p&gt;

&lt;p&gt;Here's the clever part: we use a &lt;strong&gt;default hash&lt;/strong&gt; for empty branches. If a subtree contains no data, we don't store it at all — we just use a pre-computed empty hash. When we need to compute a node's hash and one or more of its children are empty, we use the default hash for those children.&lt;/p&gt;

&lt;p&gt;Let's visualize this with a simple example. Suppose we have only two accounts with addresses starting with "0x3..." and "0xf...":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;           root
          /    \
         /      \
      [3]       [f]
       |         |
     actual    actual
      data      data

All other children (0,1,2,4,5,6,7,8,9,a,b,c,d,e) use default empty hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Sparse Merkle Tries Solve Our Problem
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Efficient storage&lt;/strong&gt; — We only store nodes that lead to actual accounts. If we have 1,000 accounts in a tree with 40 levels, we store roughly 1,000 × 40 = 40,000 nodes maximum, not the theoretical 16^40 nodes of a full trie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incremental updates&lt;/strong&gt; — When we add a new account or update a balance, we only need to update the nodes along one path from the leaf to the root (40 nodes). We don't need to rebuild the entire tree. This is O(depth) = O(40) for our addresses — a constant-time operation regardless of how many accounts exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic root&lt;/strong&gt; — For any given set of accounts and balances, the root hash is always the same. Empty parts of the tree always hash to the default value, so two nodes with identical state will compute identical roots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Efficient proofs&lt;/strong&gt; — To prove an account exists, we provide the hashes along the path from leaf to root (40 hashes). To prove an account doesn't exist, we can show that a path leads to an empty node (default hash).&lt;/p&gt;

&lt;h3&gt;
  
  
  Our Implementation Approach
&lt;/h3&gt;

&lt;p&gt;For our Fleming blockchain, we'll implement a &lt;strong&gt;hexadecimal Sparse Merkle Trie&lt;/strong&gt; with these characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed depth of 40 levels&lt;/strong&gt; (one per hex character in our addresses)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;16 children per node&lt;/strong&gt; (one for each hex digit 0-f)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default hash for empty nodes&lt;/strong&gt; (pre-computed to avoid redundant calculations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HashMap-based storage&lt;/strong&gt; (only storing non-empty nodes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach gives us a good balance: simple enough to understand and implement, but efficient enough to handle millions of accounts with incremental updates.&lt;/p&gt;

&lt;p&gt;Let's implement it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;Before implementing the Sparse Merkle Trie, we need to refactor our hash types. Currently, we have &lt;code&gt;BlockHash&lt;/code&gt; as a wrapper, but we'll be using 32-byte hashes in multiple places: blocks, Merkle Tree nodes, and state roots. Instead of having different hash types for different purposes, let's introduce a single consistent hash type.&lt;/p&gt;

&lt;p&gt;Since we're using SHA-256 everywhere (which produces 256-bit = 32-byte hashes), let's rename &lt;code&gt;BlockHash&lt;/code&gt; to &lt;code&gt;Hash256&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// core/hash256.rs
use std::fmt::{Debug, Formatter};

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Hash256(pub [u8; 32]);

impl Debug for Hash256 {
    fn fmt(&amp;amp;self, f: &amp;amp;mut Formatter) -&amp;gt; std::fmt::Result {
        write!(f, "\"0x{}\"", hex::encode(self.0))
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can update our &lt;code&gt;Block&lt;/code&gt; structure to use &lt;code&gt;Hash256&lt;/code&gt; instead of &lt;code&gt;BlockHash&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// core/block.rs
use crate::core::Hash256;

#[derive(Debug, Clone)]
pub struct Block {
    number: u64,
    transactions: Vec&amp;lt;Transaction&amp;gt;,
    state: State,
    previous_hash: Hash256,  // was BlockHash
    hash: Hash256,           // was BlockHash
    timestamp: u64,
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This refactoring gives us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt; — all 32-byte hashes use the same type across the codebase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type safety&lt;/strong&gt; — &lt;code&gt;Hash256&lt;/code&gt; is a distinct type, not just an alias&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusability&lt;/strong&gt; — we can now use &lt;code&gt;Hash256&lt;/code&gt; for Merkle Trie hashes without creating yet another hash type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this foundation in place, we're ready to implement the Sparse Merkle Trie.&lt;/p&gt;

&lt;p&gt;Let's extend our existing &lt;code&gt;core/state.rs&lt;/code&gt; module with the Sparse Merkle Trie implementation. First, add the new data structures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use crate::core::Hash256;
use crate::core::address::Address;
use std::collections::HashMap;

const TREE_DEPTH: usize = 40;
const HEX_RADIX: usize = 16;

pub type State = HashMap&amp;lt;Address, u64&amp;gt;;

struct TrieNode {
    hash: Hash256,
    value: Option&amp;lt;u64&amp;gt;, // just for leafs
}

pub struct SparseMerkleTrie {
    nodes: HashMap&amp;lt;String, TrieNode&amp;gt;, // path -&amp;gt; node
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;TrieNode&lt;/code&gt; stores the hash of a node and optionally a balance value (only for leaf nodes). The &lt;code&gt;SparseMerkleTrie&lt;/code&gt; uses a HashMap to store nodes indexed by their path — for example, "3a7f" represents the node at depth 4 reached by following hex characters 3, a, 7, and f from the root. We use a HashMap-based approach rather than recursive node structures to avoid Rust's ownership complexities with &lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt; and self-referential types.&lt;/p&gt;

&lt;p&gt;Now let's implement the helper functions for hashing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;impl SparseMerkleTrie {
    fn hash_leaf(balance: Option&amp;lt;u64&amp;gt;) -&amp;gt; Hash256 {
        let mut hasher = Sha256::new();
        if let Some(balance) = balance {
            hasher.update(balance.to_le_bytes());
        }
        Hash256(hasher.finalize().into())
    }

    fn hash_internal_node(children: &amp;amp;[Hash256; HEX_RADIX]) -&amp;gt; Hash256 {
        let mut hasher = Sha256::new();
        for child_hash in children {
            hasher.update(child_hash.0);
        }
        Hash256(hasher.finalize().into())
    }

    fn address_to_path(address: &amp;amp;Address) -&amp;gt; &amp;amp;str {
        address
            .as_str()
            .strip_prefix("0x")
            .unwrap_or(address.as_str())
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;hash_leaf&lt;/code&gt; function hashes a balance value (or an empty value for non-existent accounts). The &lt;code&gt;hash_internal_node&lt;/code&gt; concatenates the hashes of all 16 children and hashes the result. The &lt;code&gt;address_to_path&lt;/code&gt; function strips the "0x" prefix from addresses to get the path through the trie.&lt;/p&gt;

&lt;p&gt;Next, implement the constructor and basic operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn new() -&amp;gt; Self {
    SparseMerkleTrie {
        nodes: HashMap::new(),
    }
}

pub fn get(&amp;amp;self, address: &amp;amp;Address) -&amp;gt; Option&amp;lt;u64&amp;gt; {
    let path = Self::address_to_path(address);
    self.nodes.get(path).and_then(|node| node.value)
}

pub fn insert(&amp;amp;mut self, address: &amp;amp;Address, balance: u64) {
    let path = Self::address_to_path(address);

    // Create leaf node with balance
    let leaf_hash = Self::hash_leaf(Some(balance));
    self.nodes.insert(
        path.to_string(),
        TrieNode {
            hash: leaf_hash,
            value: Some(balance),
        },
    );

    // Update hashes of all parent nodes up to root
    self.update_path(path);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The get method simply looks up the node in the HashMap and returns its balance. The &lt;code&gt;insert&lt;/code&gt; method creates or updates a leaf node with the account balance, then updates all ancestor hashes up to the root.&lt;/p&gt;

&lt;p&gt;Now implement the core logic for updating hashes along a path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fn update_path(&amp;amp;mut self, path: &amp;amp;str) {
    // Update hashes from leaf to root
    // Walk backwards: parent_of_leaf -&amp;gt; parent_of_parent -&amp;gt; ... -&amp;gt; root
    for i in (0..path.len()).rev() {
        let parent_path = &amp;amp;path[..i]; // trim last character
        let parent_hash = self.compute_node_hash(parent_path);

        // Create/update parent node
        self.nodes.insert(
            parent_path.to_string(),
            TrieNode {
                hash: parent_hash,
                value: None, // only leaves have values
            },
        );
    }
}

fn compute_node_hash(&amp;amp;self, path: &amp;amp;str) -&amp;gt; Hash256 {
    let mut children_hashes = [Hash256([0u8; 32]); HEX_RADIX];

    // Check all 16 possible children (0-f)
    for (i, hex_char) in "0123456789abcdef".chars().enumerate() {
        let child_path = format!("{}{}", path, hex_char);

        // If a child exists, use its hash; otherwise use zero hash
        children_hashes[i] = if let Some(child_node) = self.nodes.get(&amp;amp;child_path) {
            child_node.hash
        } else {
            Hash256([0u8; 32]) // empty branch
        };
    }

    // Hash all 16 children hashes together
    Self::hash_internal_node(&amp;amp;children_hashes)
}

pub fn get_root_hash(&amp;amp;self) -&amp;gt; Hash256 {
    // Root has empty path
    self.compute_node_hash("")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;update_path&lt;/code&gt; method walks from the leaf up to the root, recalculating hashes for each ancestor node. The &lt;code&gt;compute_node_hash&lt;/code&gt; method computes a node's hash by gathering the hashes of all 16 possible children — using the stored hash if the child exists, or a zero hash if it doesn't. The &lt;code&gt;get_root_hash&lt;/code&gt; method returns the Merkle Root by computing the hash of the root node (empty path).&lt;/p&gt;

&lt;p&gt;Finally, let's update the Block structure to use the Merkle Root instead of storing the full state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// core/block.rs

#[derive(Debug, Clone)]
pub struct Block {
    number: u64,                    // just a serial number of block
    transactions: Vec&amp;lt;Transaction&amp;gt;, // array of transactions
    state_root: Hash256,            // Merkle root of the state trie (32 bytes)
    previous_hash: Hash256,         // SHA-256 of previous block
    hash: Hash256,                  // SHA-256 of current block
    timestamp: u64,                 // block creation timestamp
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now update the block hash calculation to include the state root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fn calculate_hash(&amp;amp;self) -&amp;gt; Hash256 {
    let mut hasher = Sha256::new();
    hasher.update(self.number.to_le_bytes());
    hasher.update(&amp;amp;self.previous_hash.0);
    hasher.update(self.timestamp.to_le_bytes());

    for tx in &amp;amp;self.transactions {
        hasher.update(tx.as_bytes());
    }

    hasher.update(&amp;amp;self.state_root.0);

    Hash256(hasher.finalize().into())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And update the blockchain to maintain the trie and compute state roots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// core/blockchain.rs

pub struct Blockchain {
    chain: Vec&amp;lt;Block&amp;gt;, // chain of blocks
    state: SparseMerkleTrie, // global state trie
}

pub fn append_block(&amp;amp;mut self, transactions: Vec&amp;lt;Transaction&amp;gt;) {
    let previous_block = self.chain.last().unwrap();
    let previous_hash = previous_block.hash().clone();
    let block_number = previous_block.number() + 1;

    // apply all transactions to the state
    for tx in &amp;amp;transactions {
        // check transaction
        if !tx.is_valid() {
            panic!("Invalid transaction");
        }

        // check balances
        let from_balance = self.state.get(&amp;amp;tx.from).unwrap_or(0);
        if from_balance &amp;lt; tx.amount {
            // will handle correctly later
            panic!("Insufficient balance!");
        }

        let to_balance = self.state.get(&amp;amp;tx.to).unwrap_or(0);

        self.state.insert(&amp;amp;tx.from, from_balance - tx.amount);
        self.state.insert(&amp;amp;tx.to, to_balance + tx.amount);
    }

    let state_root = self.state.get_root_hash();
    let new_block = Block::new(block_number, transactions, state_root, previous_hash);
    println!("Appending block: {:#?}", new_block);
    self.chain.push(new_block);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of storing the full state in each block (which was 50+ MB for 1,000,000 accounts), we now store only the 32-byte Merkle Root. The blockchain maintains a single &lt;code&gt;SparseMerkleTrie&lt;/code&gt; that gets updated with each block, and only the root hash is included in the block. This dramatically reduces storage requirements while maintaining cryptographic verifiability of the entire state.&lt;/p&gt;

&lt;p&gt;Now let's implement Merkle Proofs to verify account balances without storing the full state. We'll need this later when we add peer-to-peer connections — a light client can request a proof from a full node and verify it against the state root in the block header.&lt;/p&gt;

&lt;p&gt;First, define the proof structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// core/state.rs

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MerkleProof {
    pub siblings: Vec&amp;lt;Hash256&amp;gt;,
}

impl SparseMerkleTrie {
    pub fn get_proof(&amp;amp;self, address: &amp;amp;Address) -&amp;gt; Option&amp;lt;MerkleProof&amp;gt; {
        let path = Self::address_to_path(address);

        // Check if account exists
        if !self.nodes.contains_key(path) {
            return None;
        }

        let mut siblings = Vec::new();

        // Collect sibling hashes along the path
        for i in (0..path.len()).rev() {
            let parent_path = &amp;amp;path[..i];
            let current_char = path.chars().nth(i).unwrap();

            // For each level, we need the hashes of all 15 siblings
            // (all children except the one we're traversing)
            for (j, hex_char) in "0123456789abcdef".chars().enumerate() {
                if hex_char != current_char {
                    let sibling_path = format!("{}{}", parent_path, hex_char);
                    let sibling_hash = if let Some(node) = self.nodes.get(&amp;amp;sibling_path) {
                        node.hash
                    } else {
                        Hash256([0u8; 32])
                    };
                    siblings.push(sibling_hash);
                }
            }
        }

        Some(MerkleProof { siblings })
    }

    pub fn verify_proof(
        address: &amp;amp;Address,
        balance: u64,
        proof: &amp;amp;MerkleProof,
        expected_root: &amp;amp;Hash256,
    ) -&amp;gt; bool {
        let path = Self::address_to_path(address);
        let mut current_hash = Self::hash_leaf(Some(balance));
        let mut sibling_idx = 0;

        // Rebuild the path from leaf to root using the proof
        for i in (0..path.len()).rev() {
            let current_char = path.chars().nth(i).unwrap();
            let char_idx = "0123456789abcdef".find(current_char).unwrap();

            let mut children_hashes = [Hash256([0u8; 32]); HEX_RADIX];

            // Place current hash at correct position
            children_hashes[char_idx] = current_hash;

            // Fill in siblings from proof
            for (j, _) in "0123456789abcdef".chars().enumerate() {
                if j != char_idx {
                    children_hashes[j] = proof.siblings[sibling_idx];
                    sibling_idx += 1;
                }
            }

            current_hash = Self::hash_internal_node(&amp;amp;children_hashes);
        }

        // Compare computed root with expected root
        current_hash.0 == expected_root.0
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;get_proof&lt;/code&gt; method collects all sibling hashes along the path from the leaf to the root. For our hexadecimal trie, each level contributes 15 sibling hashes (all 16 children except the one we traverse). The &lt;code&gt;verify_proof&lt;/code&gt; method reconstructs the root hash using only the address, balance, and proof — if the computed root matches the expected root, the proof is valid. This allows light clients to verify account balances with just ~19 KB of data instead of downloading the entire state.&lt;/p&gt;

&lt;p&gt;We've now transformed our blockchain to use Merkle Trie for state management. However, this refactoring requires updating all existing tests — changing assertions from checking HashMap state to using the &lt;code&gt;get()&lt;/code&gt; method, updating genesis block initialization, and adjusting validation logic. Rather than showing dozens of test updates in this article, you can find the complete test suite in the &lt;a href="https://github.com/dsvynarenko/fleming-blockchain" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. The tests verify trie operations, proof generation and verification, and ensure backward compatibility with previous functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we've solved the memory inefficiency problem by implementing a Sparse Merkle Trie for state management in our Fleming blockchain. We explored how Merkle Trees work, understood the difference between Merkle Trees and Merkle Tries, and implemented a hexadecimal Sparse Merkle Trie that efficiently handles millions of accounts.&lt;/p&gt;

&lt;p&gt;We successfully implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hash type refactoring with &lt;code&gt;Hash256&lt;/code&gt; for consistency across the codebase&lt;/li&gt;
&lt;li&gt;Sparse Merkle Trie with HashMap-based storage for efficient sparse data handling&lt;/li&gt;
&lt;li&gt;Hashing functions for leaf nodes and internal nodes&lt;/li&gt;
&lt;li&gt;Incremental hash updates from leaf to root&lt;/li&gt;
&lt;li&gt;Merkle Proof generation and verification for light client support&lt;/li&gt;
&lt;li&gt;Block structure update to store only the 32-byte Merkle Root instead of the full state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The impact is dramatic: instead of storing 50+ MB of state data in each block for 1,000,000 accounts, we now store just 32 bytes. The Merkle Root provides cryptographic verification of the entire state — any change to any account balance instantly changes the root hash, making tampering detectable. With Merkle Proofs, light clients can verify account balances using only ~19 KB of data instead of downloading the entire blockchain state, enabling efficient verification without trusting full nodes.&lt;/p&gt;

&lt;p&gt;However, our blockchain still stores everything in memory, which means we lose all data when the program stops. For a real-world blockchain, we need persistent storage that survives restarts and can handle massive amounts of data efficiently.&lt;/p&gt;

&lt;p&gt;In the next article, we'll tackle persistent storage by integrating &lt;strong&gt;Rocks DB&lt;/strong&gt; — a high-performance embedded database used by production blockchains. We'll explore how to store blocks and state on disk, implement efficient queries, and ensure data integrity across restarts. This will transform our blockchain from a learning prototype into something that could actually handle real-world workloads.&lt;/p&gt;

&lt;p&gt;As usual, all the source code from the article can be found &lt;a href="https://github.com/dsvynarenko/fleming-blockchain/releases/tag/v0.0.4" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

</description>
      <category>rust</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Designing Blockchain #3: Cryptography and Wallets</title>
      <dc:creator>Dmytro Svynarenko</dc:creator>
      <pubDate>Tue, 11 Nov 2025 14:12:28 +0000</pubDate>
      <link>https://dev.to/dmytro_svynarenko/designing-blockchain-3-cryptography-and-wallets-153h</link>
      <guid>https://dev.to/dmytro_svynarenko/designing-blockchain-3-cryptography-and-wallets-153h</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/dmytro_svynarenko/designing-blockchain-2-accounts-and-state-c82"&gt;previous article&lt;/a&gt;, we explored how blockchain manages accounts and state transitions through transactions. However, we left a critical security gap: anyone could create a transaction claiming to be from any account. This article addresses that fundamental problem by introducing cryptographic signatures and wallets, the mechanisms that prove ownership and secure transactions on the blockchain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cryptographic Primitives
&lt;/h2&gt;

&lt;p&gt;Let's dive into the cryptographic foundations. From &lt;a href="https://dev.to/dmytro_svynarenko/designing-blockchain-1-introduction-401j"&gt;the first article&lt;/a&gt;, we learned about hash functions. To recap: a &lt;strong&gt;hash function&lt;/strong&gt; is a cryptographic algorithm that transforms any input into a fixed-size output (digest). Any change to the input produces a completely different output. But hashing is a one-way process — we need something we can encrypt and decrypt, or sign and verify. This is where &lt;strong&gt;asymmetric cryptography&lt;/strong&gt; comes in. Unlike symmetric encryption, where the same key encrypts and decrypts data, asymmetric cryptography uses two mathematically related keys: a &lt;strong&gt;private key&lt;/strong&gt; (kept secret) and a &lt;strong&gt;public key&lt;/strong&gt; (shared openly).&lt;/p&gt;

&lt;p&gt;Here's how asymmetric cryptography solves our account ownership problem: when you create an account, you generate two linked keys — a private key (like a secret password only you know) and a &lt;strong&gt;public key&lt;/strong&gt; (like your account number that everyone can see). To prove you own an account, you sign a transaction with your &lt;strong&gt;private key&lt;/strong&gt;. This creates a unique signature that anyone can verify using your &lt;strong&gt;public key&lt;/strong&gt;. If the signature checks out, it proves only the private key holder could have created it. Think of it like signing a check: anyone can see your signature and verify it's yours, but only you can actually create it.&lt;/p&gt;

&lt;p&gt;We have a public key, but blockchains need addresses. The transformation involves several steps. First, we hash the public key to create a shorter identifier. This hashing serves multiple purposes: it makes addresses easier to work with, adds security (even if quantum computers break the key derivation, they'd still need to reverse the hash), and creates a uniform address format regardless of the cryptographic algorithm used.&lt;/p&gt;

&lt;p&gt;Traditional algorithms like RSA produce very large keys — often 2048 or 4096 bits. This creates problems for blockchain: larger keys mean larger transactions, which means more data to store and transmit. Remember, every node stores a complete copy of the blockchain, so size matters enormously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Elliptic Curve Cryptography&lt;/strong&gt; (ECC) provides the same security as RSA but with much smaller keys. A 256-bit ECC key offers equivalent security to a 3072-bit RSA key — more than 10 times smaller! This efficiency is crucial for blockchain, where we process thousands of transactions, each containing signatures. Smaller keys mean faster verification, less storage, and lower network bandwidth. ECC achieves this through the mathematical properties of elliptic curves, which make certain computational problems extremely hard to solve even with smaller key sizes.&lt;/p&gt;

&lt;p&gt;The most widely used elliptic curves in blockchain are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;secp256k1&lt;/strong&gt; — standardized by the Standards for Efficient Cryptography Group. Bitcoin chose it for its efficiency and security, and Ethereum followed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Curve25519&lt;/strong&gt; — designed by Daniel J. Bernstein with a focus on security and performance. It's used in modern protocols and newer blockchains.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Elliptic curves support different signature schemes, each with distinct trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ECDSA (Elliptic Curve Digital Signature Algorithm)&lt;/strong&gt; is the most common scheme, used in Bitcoin and Ethereum. It produces variable-length signatures and requires careful implementation to avoid vulnerabilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schnorr signatures&lt;/strong&gt;, introduced in Bitcoin's Taproot upgrade, offer key advantages: they're more efficient, support signature aggregation (combining multiple signatures into one), and have simpler security proofs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EdDSA (Edwards-curve Digital Signature Algorithm)&lt;/strong&gt; uses twisted Edwards curves like Ed25519. It provides deterministic signatures — no random number generation needed — making it less prone to implementation errors. EdDSA is faster and simpler than ECDSA.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Blockchain implementations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bitcoin&lt;/strong&gt;: Uses secp256k1 curve with ECDSA signatures. Taproot upgrade (2021) added Schnorr signature support for improved privacy and efficiency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ethereum&lt;/strong&gt;: Uses secp256k1 curve with ECDSA signatures. Addresses are derived by taking the Keccak-256 hash of the public key and using the last 20 bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solana&lt;/strong&gt;: Uses Ed25519 (EdDSA) with Curve25519. This choice prioritizes performance — signature verification is significantly faster than ECDSA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cardano&lt;/strong&gt;: Uses Ed25519 for transaction signatures, emphasizing security through formal verification and deterministic signatures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polkadot&lt;/strong&gt;: Uses Schnorrkel/Ristretto, a variant of Schnorr signatures built on Curve25519, enabling efficient multi-signature schemes for its nominated proof-of-stake consensus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For our learning purposes, we'll use the more efficient and modern Ed25519 with Curve25519 for the Fleming blockchain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wallet
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;wallet&lt;/strong&gt; in blockchain is software that manages your cryptographic keys — it doesn't actually store cryptocurrency. The coins exist on the blockchain; the wallet stores the private keys that prove you own them. Think of it like a keychain: your house (the account) exists independently, but you need the key (private key) to access it.&lt;/p&gt;

&lt;p&gt;Modern wallets use &lt;strong&gt;Hierarchical Deterministic (HD)&lt;/strong&gt; wallets, which generate unlimited key pairs from a single seed. This seed is typically represented as a &lt;strong&gt;mnemonic phrase&lt;/strong&gt; — a sequence of 12 or 24 human-readable words (like "army van defense carry jealous true garbage claim echo media make crunch"). This phrase, following the &lt;strong&gt;BIP-39&lt;/strong&gt; standard, can regenerate all your private keys, making backup and recovery much simpler than managing individual keys.&lt;/p&gt;

&lt;p&gt;The beauty of this system: you only need to safely store those 12–24 words to recover access to all your accounts, even if you lose your device.&lt;/p&gt;

&lt;p&gt;There's much more to say about wallets, but that's not the focus of this article. For the Fleming blockchain, I'll use BIP39 to generate key pairs from a seed phrase — it's simpler than managing raw private keys. I'll skip implementing full HD wallets for the same reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;Let's start with the Wallet implementation. First, we need to install new dependencies (&lt;strong&gt;ed25519-dalek&lt;/strong&gt;, &lt;strong&gt;bip39&lt;/strong&gt; and &lt;strong&gt;rand&lt;/strong&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[dependencies]
sha2 = "0.10.9"
hex = "0.4.3"
ed25519-dalek = "2.2.0"
bip39 = "2.2.0"
rand = "0.9.2"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, add a couple of type aliases for simplicity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use ed25519_dalek::{SigningKey as Ed25519SigningKey, VerifyingKey as Ed25519VerifyingKey};

pub type PrivateKey = Ed25519SigningKey;
pub type PublicKey = Ed25519VerifyingKey;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, out Wallet definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use crate::core::address::Address;
use crate::core::crypto::{PrivateKey, PublicKey};

pub struct Wallet {
    private_key: PrivateKey,   // private key
    pub public_key: PublicKey, // public key
    pub address: Address,      // address (hash of public key)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we'll create a wallet from a mnemonic phrase using BIP-39:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;impl Wallet {
    pub fn from_mnemonic(mnemonic_phrase: &amp;amp;str) -&amp;gt; Self {
        let mnemonic = Mnemonic::parse(mnemonic_phrase).expect("Invalid mnemonic phrase");
        let seed = mnemonic.to_seed(""); // don't use passphrase for our simple wallet

        // private key should be 256 bits (32 bytes) so slicing seed
        let private_key = PrivateKey::from_bytes(&amp;amp;seed[..32].try_into().expect("Invalid seed"));
        let public_key = private_key.verifying_key();
        let address = Address::from_public_key(public_key);

        Wallet {
            private_key,
            public_key,
            address,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Fleming blockchain, we derive the address by taking the SHA-256 hash of the public key and using the &lt;strong&gt;first 20 bytes&lt;/strong&gt; (&lt;code&gt;ADDRESS_LENGTH&lt;/code&gt;), whereas Ethereum uses Keccak-256 and takes the &lt;strong&gt;last 20 bytes&lt;/strong&gt;, Bitcoin uses a more complex process with RIPEMD-160 after SHA-256 and adds checksums for error detection, and Solana simply uses the raw 32-byte Ed25519 public key as the address without hashing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub const ADDRESS_LENGTH: usize = 20;

#[derive(Hash, Clone, PartialEq, Eq)]
pub struct Address(String);

impl Address {
    pub fn from_public_key(public_key: &amp;amp;PublicKey) -&amp;gt; Self {
        let mut hasher = Sha256::new();
        hasher.update(public_key.as_bytes());
        let hash = hasher.finalize();

        // Like Ethereum but just first 20 bytes, not last
        Address(format!("0x{}", hex::encode(&amp;amp;hash[..ADDRESS_LENGTH])))
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let's add the signer's public key and signature fields to the transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#[derive(Debug, Clone)]
pub struct Transaction {
    pub from: Address,
    pub to: Address,
    pub amount: u64,
    pub signer: Option&amp;lt;PublicKey&amp;gt;,
    pub signature: Option&amp;lt;Signature&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Ed25519, the public key cannot be recovered from the signature alone, unlike ECDSA where public key recovery is possible through additional parameters. Therefore, we must explicitly include the signer's public key in the transaction so verifiers can validate the signature against it.&lt;/p&gt;

&lt;p&gt;Now, add a sign method to the Wallet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn sign(&amp;amp;self, transaction: &amp;amp;mut Transaction) {
    let message = transaction.as_bytes();

    // add signer's public key and signature
    transaction.signer = Some(self.public_key);
    transaction.signature = Some(self.private_key.sign(message.as_slice()));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Wallet's sign method serialises the transaction data, attaches the signer's public key to the transaction, and creates a cryptographic signature using the private key.&lt;/p&gt;

&lt;p&gt;Now, let's implement signature verification. We'll add a verify method to check if a transaction was actually signed by the owner of the from address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn verify_signature(&amp;amp;self) -&amp;gt; bool {
    if let (Some(public_key), Some(signature)) = (&amp;amp;self.signer, &amp;amp;self.signature) {
        let message = self.as_bytes();
        public_key.verify(&amp;amp;message.as_slice(), signature).is_ok()
    } else {
        false
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verification process reconstructs the original message from the transaction data and uses the public key to verify the signature matches. If verification succeeds, we know the transaction was signed by someone with the corresponding private key.&lt;/p&gt;

&lt;p&gt;Now add all checks to the &lt;code&gt;is_valid&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn is_valid(&amp;amp;self) -&amp;gt; bool {
    if self.signer.is_none() || self.signature.is_none() {
        return false;
    }

    if !self.verify_signature() {
        return false;
    }

    if let Some(signer) = &amp;amp;self.signer {
        if self.from != Address::from_public_key(signer) {
            return false;
        }
    }

    true
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we need to update our blockchain validation to verify transaction signatures before accepting them into blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn append_block(&amp;amp;mut self, transactions: Vec&amp;lt;Transaction&amp;gt;) {

    ...

    // apply all transactions to the state
    for tx in &amp;amp;transactions {
        // check transaction
        if !tx.is_valid() {
            panic!("Invalid transaction");
        }

        ...
    }

    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last step is updating the tests, but that's routine work. You can find the complete implementation on &lt;a href="https://github.com/dsvynarenko/fleming-blockchain/releases/tag/v0.0.3" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we've secured our Fleming blockchain by implementing cryptographic signatures and wallets using &lt;strong&gt;Ed25519&lt;/strong&gt; with &lt;strong&gt;Curve25519&lt;/strong&gt;. We explored how public-key cryptography works, implemented wallet generation from &lt;strong&gt;mnemonic phrases&lt;/strong&gt; using &lt;strong&gt;BIP-39&lt;/strong&gt;, and added signature verification to ensure only the legitimate owner of an account can authorise transactions. With cryptographic security in place, our blockchain now prevents unauthorised transactions, but we still need to address the memory inefficiency problem of storing full state in every block — a challenge we'll tackle in the next article.&lt;/p&gt;

&lt;p&gt;As usual all the source code from the article can be found &lt;a href="https://github.com/dsvynarenko/fleming-blockchain/releases/tag/v0.0.3" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

</description>
      <category>rust</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Designing Blockchain #2: Accounts and State</title>
      <dc:creator>Dmytro Svynarenko</dc:creator>
      <pubDate>Tue, 11 Nov 2025 13:56:11 +0000</pubDate>
      <link>https://dev.to/dmytro_svynarenko/designing-blockchain-2-accounts-and-state-c82</link>
      <guid>https://dev.to/dmytro_svynarenko/designing-blockchain-2-accounts-and-state-c82</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/dmytro_svynarenko/designing-blockchain-1-introduction-401j"&gt;the previous article&lt;/a&gt;, we created a simple blockchain from scratch, covering the fundamentals of blocks, hashing, and chain validation, but we left our transactions in a very primitive way. So, today we'll fix them, but before we do, we need to dive deeper into one of the most critical components of any blockchain system: accounts and state management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accounting models
&lt;/h2&gt;

&lt;p&gt;There are two most popular accounting models for blockchains: UTXO and account-based.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's worth mentioning that Sui blockchain implements a different approach — an object-centric data model, where everything is an object, but to my taste it's more like a customised UTXO model than truly something new.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  UTXO
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;UTXO&lt;/strong&gt; (UTxO, Unspent Transaction Output) is an accounting model where transactions consume previous outputs as inputs and create new outputs, with no concept of account balances — only unspent outputs that can be used in future transactions. I know it sounds difficult to understand, but I'll show an example and it'll make things clear.&lt;/p&gt;

&lt;p&gt;Imagine you have a wallet with paper money and coins. These are your UTXOs — you can't divide them (just like in real life, you can't cut a $5 bill and claim it's now $4.85). You can only use them whole for transactions. How do you know your balance? Simply sum up all your paper money and coins — or in blockchain terms, your UTXOs. Now, you want to buy a newspaper (create a transaction) that costs $2, but you only have a $5 bill, so you'll receive change — for example, three $1 bills.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1bfbvrsonuovyhcbao3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv1bfbvrsonuovyhcbao3.png" alt="Buying a newspaper UTXO example" width="762" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations — now you understand how Bitcoin works! In the UTXO model, there are no accounts with balances. Instead, each transaction consumes previous outputs and creates new ones. Here are some blockchains that use this model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bitcoin&lt;/strong&gt; and similar blockchains like &lt;strong&gt;Litecoin&lt;/strong&gt; (forked from Bitcoin's source code), &lt;strong&gt;Bitcoin Cash&lt;/strong&gt; (a hard fork), &lt;strong&gt;Dogecoin&lt;/strong&gt; (based on Litecoin), &lt;strong&gt;ZCash&lt;/strong&gt; (forked from Bitcoin's source code) and others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cardano&lt;/strong&gt; (extended UTXO model with smart contract support)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monero&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let's explore the pros and cons of the UTXO model.&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High privacy&lt;/strong&gt; — each transaction generates a new address, making it harder to trace wallet activity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High scalability&lt;/strong&gt; — parallel transaction processing significantly increases efficiency and throughput&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy validation&lt;/strong&gt; — simply check whether a UTXO exists in the unspent set&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smart contracts&lt;/strong&gt; — difficult to implement, though Cardano's extended UTXO model (eUTXO) provides smart contract support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Balance fragmentation&lt;/strong&gt; — wallets with many small UTXOs can struggle to transfer certain amounts in a single transaction due to input limits or this transaction will . For example, if I have 1000 UTXOs worth 0.001 COIN each and want to send 1 COIN, but can only include 900 UTXOs in a transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard to maintain wallet state&lt;/strong&gt; — requires storing and indexing all unspent UTXOs to manage the wallet's balance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Account-Based
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Account-based&lt;/strong&gt; is an accounting model where transactions directly modify account balances, maintaining a global state of all accounts with their current balances — similar to traditional banking systems.&lt;/p&gt;

&lt;p&gt;Think of it like your bank account. When you receive your salary, the bank simply adds that amount to your account balance. When you buy something, they subtract the cost. Your balance is always stored and updated directly — there's no need to track individual bills or coins.&lt;/p&gt;

&lt;p&gt;Here is our example with a newspaper:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsce3yeijo5j6nej3kfh2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsce3yeijo5j6nej3kfh2.png" alt="Buying a newspaper Account-based example" width="702" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the account-based model, each account has a persistent balance that's updated with every transaction. Here are some blockchains that use this model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ethereum&lt;/strong&gt; and EVM-compatible chains like &lt;strong&gt;Polygon&lt;/strong&gt;, &lt;strong&gt;Binance Smart Chain&lt;/strong&gt;, &lt;strong&gt;Avalanche C-Chain&lt;/strong&gt;, and many others&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Solana&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tezos&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now let's explore the pros and cons of the account-based model.&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt; — easy to understand and implement, similar to traditional banking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart contracts&lt;/strong&gt; — naturally supports complex smart contract logic and state management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficient for frequent transactions&lt;/strong&gt; — no need to track multiple outputs, just update the balance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lower privacy&lt;/strong&gt; — all transactions are directly linked to account addresses, making activity easier to trace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay attack vulnerability&lt;/strong&gt; — requires nonces or sequence numbers to prevent transaction replay&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequential processing&lt;/strong&gt; — transactions from the same account must be processed in order, limiting parallelisation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  State
&lt;/h2&gt;

&lt;p&gt;Now that we understand the two main accounting models, let's talk about state — one of the most fundamental concepts in blockchain architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State&lt;/strong&gt; represents the current snapshot of all data in the blockchain at a given moment. Think of it as a photograph of the entire system — all account balances, smart contract storage, and any other data that exists at a specific block height.&lt;/p&gt;

&lt;p&gt;The way state is managed differs significantly between UTXO and account-based models.&lt;/p&gt;

&lt;h3&gt;
  
  
  UTXO
&lt;/h3&gt;

&lt;p&gt;In UTXO-based blockchains, the state is the set of all unspent transaction outputs (the UTXO set). Each block modifies this set by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consuming (removing) UTXOs used as transaction inputs&lt;/li&gt;
&lt;li&gt;Creating (adding) new UTXOs as transaction outputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current state can be calculated by starting from the genesis block and applying all transactions in sequence. However, most implementations maintain the UTXO set in memory or a database for quick access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Account-Based
&lt;/h3&gt;

&lt;p&gt;In account-based blockchains, the state is a mapping of account addresses to their current data, which typically includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Balance&lt;/strong&gt; — the amount of cryptocurrency the account holds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code&lt;/strong&gt; — smart contract bytecode (if the account is a contract)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt; — persistent data used by smart contracts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each transaction modifies the state by updating account balances or changing smart contract storage. The state is somehow stored in the block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;For our Fleming blockchain, we'll use the account-based model because it's easier to understand and offers more interesting concepts to explore.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm continuing to update the codebase from the previous article, which you can find &lt;a href="https://github.com/dsvynarenko/fleming-blockchain/releases/tag/v0.0.1" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First, define a type alias for the Address type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub type Address = String;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's update our Transaction type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#[derive(Debug, Clone)]
pub struct Transaction {
    pub from: Address,
    pub to: Address,
    pub amount: u64,
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done! Now we're ready to create state for our blockchain. To keep things simple, let's define our state as a hashmap where the key is an address and the value is the balance of that address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub type State = HashMap&amp;lt;Address, u64&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then let’s add state to a block. For now, let’s store a full state in each block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#[derive(Debug, Clone)]
pub struct Block {
    number: u64,                    // just a serial number of block
    transactions: Vec&amp;lt;Transaction&amp;gt;, // array of transactions
    state: State,                   // FULL state of blockchain
    previous_hash: BlockHash,       // SHA-256 of previous block
    hash: BlockHash,                // SHA-256 of current block
    timestamp: u64,                 // block creation timestamp
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;calculate_hash&lt;/code&gt; method to include state in block hash calculations (this prevents compromising the blockchain by tampering with state):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fn calculate_hash(&amp;amp;self) -&amp;gt; BlockHash {
    let mut hasher = Sha256::new();
    hasher.update(self.number.to_le_bytes());
    hasher.update(&amp;amp;self.previous_hash.0);
    hasher.update(self.timestamp.to_le_bytes());

    for tx in &amp;amp;self.transactions {
        hasher.update(tx.as_bytes());
    }

    // Since HashMap has indeterministic order, lets convert it to an array and sort by keys
    let mut state_entries: Vec&amp;lt;_&amp;gt; = self.state.iter().collect();
    state_entries.sort_by(|a, b| a.0.cmp(b.0));

    for (address, balance) in state_entries {
        hasher.update(address.as_bytes());
        hasher.update(balance.to_le_bytes());
    }

    BlockHash(hasher.finalize().into())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we need to process transactions by checking the blockchain's state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn append_block(&amp;amp;mut self, transactions: Vec&amp;lt;Transaction&amp;gt;) {
    let previous_block = self.chain.last().unwrap();
    let previous_hash = previous_block.hash().clone();
    let block_number = previous_block.number() + 1;

    // clone full state
    let mut new_state = previous_block.state().clone();

    // apply all transactions to the state
    for tx in &amp;amp;transactions {
        let from_balance = new_state.get(&amp;amp;tx.from).unwrap_or(&amp;amp;0);
        if *from_balance &amp;lt; tx.amount {
            // will handle correctly later
            panic!("Insufficient balance!");
        }

        *new_state.get_mut(&amp;amp;tx.from).unwrap() -= tx.amount;
        *new_state.entry(tx.to.clone()).or_insert(0) += tx.amount;
    }

    let new_block = Block::new(block_number, transactions, new_state, previous_hash);
    println!("Appending block: {:#?}", new_block);
    self.chain.push(new_block);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transaction processing logic iterates through each transaction and updates the state. First, it verifies that the sender has sufficient balance to cover the transaction amount and panicking if they don't. I know this isn't a good way to handle such cases, and we'll fix it later, but for now it's fine. Then it deducts the amount from the sender's balance and adds it to the recipient's balance, creating a new entry if the recipient doesn't exist in the state yet. To validate that our new logic works correctly, let's update our unit tests and add a couple more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_blockchain_creation_with_state() {
        let blockchain = Blockchain::new(vec![(String::from("A"), 10), (String::from("B"), 5)]);
        assert_eq!(blockchain.chain.len(), 1);
        assert!(blockchain.is_valid());

        let genesis_block = &amp;amp;blockchain.chain.last().unwrap();
        assert_eq!(genesis_block.number(), 0);
        assert_eq!(*genesis_block.state().get("A").unwrap(), 10);
        assert_eq!(*genesis_block.state().get("B").unwrap(), 5);
    }

    #[test]
    fn test_valid_blockchain_and_state_after_transactions() {
        let mut blockchain = Blockchain::new(vec![(String::from("A"), 10), (String::from("C"), 5)]);
        blockchain.append_block(vec![Transaction::new(
            String::from("A"),
            String::from("B"),
            10,
        )]);
        blockchain.append_block(vec![Transaction::new(
            String::from("C"),
            String::from("D"),
            5,
        )]);

        assert_eq!(blockchain.chain.len(), 3);
        assert!(blockchain.is_valid());

        // initial: A=10, C=5
        // A -(10)-&amp;gt; B: A=0, B=10
        // C -(5)-&amp;gt; D: C=0, D=5
        let last_block = &amp;amp;blockchain.chain.last().unwrap();
        assert_eq!(last_block.number(), 2);
        assert_eq!(*last_block.state().get("A").unwrap(), 0);
        assert_eq!(*last_block.state().get("B").unwrap(), 10);
        assert_eq!(*last_block.state().get("C").unwrap(), 0);
        assert_eq!(*last_block.state().get("D").unwrap(), 5);
    }

    #[test]
    #[should_panic(expected = "Insufficient balance")]
    fn test_insufficient_balance_panics() {
        let mut blockchain = Blockchain::new(vec![(String::from("A"), 10)]);

        blockchain.append_block(vec![Transaction::new(
            String::from("A"),
            String::from("B"),
            15,
        )]);
    }

    #[test]
    fn test_tampered_transaction_blockchain_invalid() {
        let mut blockchain = Blockchain::new(vec![(String::from("A"), 10), (String::from("C"), 5)]);
        blockchain.append_block(vec![Transaction::new(
            String::from("A"),
            String::from("B"),
            10,
        )]);
        blockchain.append_block(vec![Transaction::new(
            String::from("C"),
            String::from("D"),
            5,
        )]);

        blockchain.chain[1]
            .tamper_transaction(0, Transaction::new(String::from("A"), String::from("B"), 2));

        assert!(!blockchain.is_valid());
    }

    #[test]
    fn test_tampered_state_blockchain_invalid() {
        let mut blockchain = Blockchain::new(vec![(String::from("A"), 10), (String::from("C"), 5)]);
        blockchain.append_block(vec![Transaction::new(
            String::from("A"),
            String::from("B"),
            10,
        )]);
        blockchain.append_block(vec![Transaction::new(
            String::from("C"),
            String::from("D"),
            5,
        )]);

        blockchain.chain[1].tamper_state(String::from("F"), 15);

        assert!(!blockchain.is_valid());
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test suite validates the blockchain's state management and transaction processing capabilities. Tests verify that the genesis block initialises with the correct state, transactions properly update account balances, and the blockchain maintains validity after multiple transactions. The suite also ensures that insufficient balance scenarios are caught and that any tampering with either transactions or state makes the blockchain invalid. Finally, tests confirm that the cryptographic hashing properly detects modifications to both transaction data and state data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we've taken a significant step forward by implementing accounts and state management in our Fleming blockchain. We explored the two main accounting models used in blockchain systems — UTXO and account-based — and chose the account-based model for its simplicity and flexibility with smart contracts.&lt;/p&gt;

&lt;p&gt;We successfully implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Account addresses and transaction structure with sender, recipient, and amount&lt;/li&gt;
&lt;li&gt;State management as a hashmap of account balances&lt;/li&gt;
&lt;li&gt;Transaction processing logic that validates balances and updates state&lt;/li&gt;
&lt;li&gt;Comprehensive tests to ensure our blockchain maintains validity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, our current implementation has two critical problems that we need to address:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory inefficiency:&lt;/strong&gt; Storing the full state in every block is extremely wasteful and doesn't scale well. As the blockchain grows and the number of accounts increases, each block becomes larger and larger, consuming excessive memory and making the blockchain impractical for real-world use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security vulnerability:&lt;/strong&gt; Currently, anyone can create transactions from any account without proving ownership. There's no authentication mechanism to verify that the person sending a transaction actually owns the account they're sending from. This is a massive security flaw that would allow anyone to steal funds from any account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next article, we'll tackle the second problem by implementing &lt;strong&gt;wallets and cryptographic signatures&lt;/strong&gt;. We'll explore how public-key cryptography works, how to generate wallet addresses, and most importantly, how to ensure that only the legitimate owner of an account can authorise transactions from that account. This will make our blockchain secure and ready for more advanced features.&lt;/p&gt;

&lt;p&gt;As usual all the source code from the article can be found &lt;a href="https://github.com/dsvynarenko/fleming-blockchain/releases/tag/v0.0.2" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

</description>
      <category>rust</category>
      <category>blockchain</category>
      <category>tutorial</category>
      <category>web3</category>
    </item>
    <item>
      <title>Designing Blockchain #1: Introduction</title>
      <dc:creator>Dmytro Svynarenko</dc:creator>
      <pubDate>Tue, 11 Nov 2025 13:32:26 +0000</pubDate>
      <link>https://dev.to/dmytro_svynarenko/designing-blockchain-1-introduction-401j</link>
      <guid>https://dev.to/dmytro_svynarenko/designing-blockchain-1-introduction-401j</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;In today's rapidly growing Web3 world of blockchains, understanding the fundamental technology behind cryptocurrencies, smart contracts, and decentralized applications has become increasingly important. To build a strong general knowledge of how blockchain works, I've decided to create a cycle of articles about creating a blockchain from scratch in Rust. This hands-on approach will provide deeper insights into the inner workings of blockchain technology while also offering practical programming experience with a powerful systems language.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Blockchain?
&lt;/h2&gt;

&lt;p&gt;From Wikipedia:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The blockchain is a distributed ledger with growing lists of records (blocks) that are securely linked together via cryptographic hashes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From ChatGPT:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Blockchain is essentially a digital, decentralised ledger that records transactions in a way that is secure, transparent, and tamper-resistant. Think of it as a chain of “blocks,” where each block contains a list of transactions. Once a block is added, it’s linked to the previous one, forming a chain—hence the name blockchain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, from both definitions we have the common keywords:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distributed&lt;/li&gt;
&lt;li&gt;Ledger&lt;/li&gt;
&lt;li&gt;Records/Block&lt;/li&gt;
&lt;li&gt;Security&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Distributed&lt;/strong&gt; – the blockchain's data isn't stored in just one central location or controlled by a single entity. Instead, it's copied and spread across many computers (called nodes) in different locations. This distribution makes the blockchain more resilient because if one node fails or is compromised, the data still exists on many others. This is a bit complicated thing to implement, so we’ll talk about it in later articles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ledger&lt;/strong&gt; – a digital record-keeping system that tracks all transactions that have occurred on the blockchain. Unlike traditional ledgers that might be controlled by a single entity (like a bank), blockchain ledgers are distributed (our previous word) across the network, with each node having their own copy. This ensures transparency and makes it extremely difficult to alter past records, as any change would need to be approved by the majority of the network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Block&lt;/strong&gt; – a fundamental unit of a blockchain that contains a collection of transactions and metadata. When transactions occur on the blockchain, they are verified and grouped together into blocks. Each block contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Block Number&lt;/li&gt;
&lt;li&gt;List of validated transactions&lt;/li&gt;
&lt;li&gt;Timestamp showing when the block was created&lt;/li&gt;
&lt;li&gt;Link to the previous block&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first block in the blockchain called master-block. So, now we have a ledger in a form of a chain of blocks, let’s move to the final keyword.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt; – comes from using cryptographic methods (mainly hash and asymmetric crypto functions). Each block has its own hash (like a digital fingerprint) created from what's inside the block. If someone changes anything in the block, the hash completely changes too. Because each block contains the previous block's hash, any changes are easy to spot, making the chain secure and hard to tamper with.&lt;/p&gt;

&lt;p&gt;Okay, for now we have the fundamental definition of the blockchain, and to start our Rust simple implementation we just need to find out what a hash function is (we'll talk about asymmetric crypto functions later).&lt;/p&gt;

&lt;h2&gt;
  
  
  Hash function
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Hash function&lt;/strong&gt; – is like a digital fingerprint machine for data. It takes any information (like a transaction or block details) and converts it into a fixed-size string of characters that looks random. The main crucial properties of hash functions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any tiny change to the data completely changes the hash output&lt;/li&gt;
&lt;li&gt;You can't recreate the original data from just the hash&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not a trivial task to create a secure and efficient hash function by yourself, and you need at least a PhD in cryptography, so we'll use existing ones.&lt;/p&gt;

&lt;p&gt;For example, usage of hash functions by popular blockchains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bitcoin – SHA-256&lt;/li&gt;
&lt;li&gt;Ethereum – Keccak-256&lt;/li&gt;
&lt;li&gt;Solana – SHA-256 / BLAKE3&lt;/li&gt;
&lt;li&gt;Litecoin – Scrypt&lt;/li&gt;
&lt;li&gt;Cardano – Blake2b-256&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SHA-256&lt;/strong&gt; (256 means that the hash length is 256 bits) – is very popular and time-tested algorithm in cryptography that provides strong security guarantees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SHA-3 (Keccak)&lt;/strong&gt; – a newer member of the SHA family, standardized in 2015. Unlike SHA-256, it uses a different internal structure (sponge construction) making it resistant to attacks that might eventually threaten SHA-256. Ethereum chose Keccak-256 (a variant of SHA-3) for its blockchain to differentiate from Bitcoin and provide additional security margins.&lt;/p&gt;

&lt;p&gt;Also, it's important to use a verified or even audited version of the crypto library, so for our case I'll choose the sha2 crate (since it's battle-proven with the Solana blockchain).&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial implementation
&lt;/h2&gt;

&lt;p&gt;Our blockchain project is named &lt;strong&gt;Fleming&lt;/strong&gt;, after Ian Fleming, the creator of James Bond. The word "Bond" is similar-sounding to the financial instrument "bond" — a wordplay I find particularly interesting in the context of blockchain technology.&lt;/p&gt;

&lt;p&gt;Since we decided to use &lt;strong&gt;sha2&lt;/strong&gt; as the library for hashing, let's install it as well (&lt;strong&gt;hex&lt;/strong&gt; is used for encoding byte arrays to more human-readable strings):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[package]
name = "fleming"
version = "0.1.0"
edition = "2021"

[dependencies]
sha2 = "0.10.9"
hex = "0.4.3"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First of all, let's define the Transaction type (in this article it'll be just a string alias, we'll come back to it in the next articles):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub type Transaction = String;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's add a more user-friendly type alias for hashes and use the hex crate to encode it to a string for debug purposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#[derive(Clone)]
pub struct BlockHash(pub [u8; 32]); //  SHA-256 -&amp;gt; 32 bytes = 256 bits

impl Debug for BlockHash {
    fn fmt(&amp;amp;self, f: &amp;amp;mut Formatter) -&amp;gt; fmt::Result {
        write!(f, "\\"0х{}\\"", hex::encode(self.0))
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the Block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#[derive(Debug, Clone)]
pub struct Block {
    number: u64,                    // just a serial number of block
    transactions: Vec&amp;lt;Transaction&amp;gt;, // array of transactions
    previous_hash: BlockHash,       // SHA-256 of previous block
    hash: BlockHash,                // SHA-256 of current block
    timestamp: u64,                 // block creation timestamp
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And, finally, the Blockchain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub struct Blockchain {
    chain: Vec&amp;lt;Block&amp;gt;, // chain of blocks
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we've defined our data structures, let's implement them. We'll start with the Block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn new(
    block_number: u64,
    transactions: Vec&amp;lt;Transaction&amp;gt;,
    previous_hash: BlockHash,
) -&amp;gt; Self {
    let mut block = Block {
        number: block_number,
        transactions,
        timestamp: get_timestamp(),
        previous_hash,
        hash: BlockHash([0; 32]),
    };

    block.hash = block.calculate_hash();
    block
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To create a new block, we need three things: a block number, an array of transactions, and the previous block's hash. Once we have these, we calculate the block's hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fn calculate_hash(&amp;amp;self) -&amp;gt; BlockHash {
    let mut hasher = Sha256::new();
    hasher.update(self.number.to_le_bytes());
    hasher.update(&amp;amp;self.previous_hash.0);
    hasher.update(self.timestamp.to_le_bytes());

    for tx in &amp;amp;self.transactions {
        hasher.update(tx.as_bytes());
    }

    BlockHash(hasher.finalize().into())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All components must be included in the hash. To verify the block, we can recalculate its hash and compare it to the stored one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn is_valid(&amp;amp;self) -&amp;gt; bool {
    let current_hash = self.calculate_hash();
    self.hash.0 == current_hash.0
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're done with the Block. Let's move on to the Blockchain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn new() -&amp;gt; Self {
    let master_block = Block::new(0, vec![], BlockHash([0; 32]));
    println!("Master block: {:#?}", master_block);

    Blockchain {
        chain: vec![master_block],
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To create a new blockchain, we initialize it with a master-block (block number 0) that has an empty transaction list and a zero hash as its previous hash, since there's no block before it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn append_block(&amp;amp;mut self, transactions: Vec&amp;lt;Transaction&amp;gt;) {
    let previous_block = self.chain.last().unwrap();
    let previous_hash = previous_block.hash().clone();
    let block_number = previous_block.number() + 1;

    let new_block = Block::new(block_number, transactions, previous_hash);
    println!("Appending block: {:#?}", new_block);
    self.chain.push(new_block);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To append a new block to the blockchain, we retrieve the last block from the chain, get its hash and block number, then create a new block with an incremented number and the previous block's hash. And, finally, to validate the whole blockchain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn is_valid(&amp;amp;self) -&amp;gt; bool {
    for block_index in 1..self.chain.len() {
        let current_block = &amp;amp;self.chain[block_index];
        let previous_block = &amp;amp;self.chain[block_index - 1];

        if !current_block.is_valid() {
            println!("Block {} has invalid hash", current_block.number());
            return false;
        }

        if current_block.previous_hash().0 != previous_block.hash().0 {
            println!("Block {} has invalid previous_hash", current_block.number());
            return false;
        }
    }
    true
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To validate the entire blockchain, we iterate through each block (starting from block 1, since the master block has no previous block to verify against), check if each block's hash is valid, and verify that each block's previous_hash matches the actual hash of the preceding block. And that's it. We've got a basic blockchain implementation in a couple of hundred lines of code. Don’t forget to write a couple unit tests to make sure our logic is fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_blockchain_creation() {
        let blockchain = Blockchain::new();
        assert_eq!(blockchain.chain.len(), 1);
        assert!(blockchain.is_valid());
    }

    #[test]
    fn test_valid_blockchain() {
        let mut blockchain = Blockchain::new();
        blockchain.append_block(vec![String::from("A -&amp;gt; B: 10 FLMG")]);
        blockchain.append_block(vec![String::from("C -&amp;gt; D: 5 FLMG")]);

        assert_eq!(blockchain.chain.len(), 3);
        assert!(blockchain.is_valid());
    }

    #[test]
    fn test_tampered_blockchain_invalid() {
        let mut blockchain = Blockchain::new();
        blockchain.append_block(vec![String::from("A -&amp;gt; B: 10 FLMG")]);
        blockchain.append_block(vec![String::from("C -&amp;gt; D: 5 FLMG")]);

        blockchain.chain[1].tamper_transaction(0, String::from("A -&amp;gt; B: 1000 FLMG"));

        assert!(!blockchain.is_valid());
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we've built a foundational blockchain implementation in Rust from scratch. We explored the core concepts of blockchain technology — distributed ledgers, blocks, and cryptographic security — and implemented basic data structures for transactions, blocks, and the blockchain itself. Using the SHA-256 hash function via the sha2 crate, we created a system where each block contains transactions, a timestamp, and a cryptographic link to the previous block. We also implemented validation mechanisms to ensure the integrity of individual blocks and the entire chain, demonstrating how blockchain's immutability works in practice.&lt;/p&gt;

&lt;p&gt;In the next article, we'll talk about accounting models, states, and make our transactions more functional, so stay tuned!&lt;/p&gt;

&lt;p&gt;All code from this article can be found &lt;a href="https://github.com/dsvynarenko/fleming-blockchain" rel="noopener noreferrer"&gt;here&lt;/a&gt; or in the release &lt;a href="https://github.com/dsvynarenko/fleming-blockchain" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>blockchain</category>
      <category>tutorial</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
