Hero

Blockchain in Zig - PART 1


Disclaimers

This series is inspired by the incredible and super well-done blog by Ivan Kuznetsov. I highly recommend checking out his work and the original “Making a Blockchain in Go” series.

I’ll be skipping the “you should know” introductory blockchain concepts. This is not a generic blockchain tutorial, it’s all about Zig, and how something like a Go program translates into Zig.

This is only my second blog post about Zig, so expect some not-so-good practices here and there (I’m improving though 🙂).

For those interested in keeping up with development before the blog posts are published, you can check the repo here. Give it a star ⭐ if you like it, it really motivates a fellow small dev like me hehe.

Enough talk, let’s begin, we have a lot to cover ⚡


The Block

The first obvious step when building a Blockchain (wink, wink) is to think about the two main pieces: the Block and the Chain (a list of Blocks).

Here’s the initial info a Block will hold:

timestamp: i64,
data: []const u8,
prevblockhash: []const u8,
hash: []const u8,

This was literally my first attempt. If you’re an experienced Zig developer, you can already spot some smelly code (yep, mistakes right out of the gate 😅).

We’re in Zig. Even though the above could work, it can be done way better:

timestamp: i64,
data: []const u8,
prevblockhash: [32]u8,
hash: [32]u8,

Much better, right? Doesn’t that just feel more satisfying? I know it did for me.


The Chain

Great, so a Blockchain should look something like this:

// Take this as pseudo-code
const Blockchain = struct {
    blocks: std.ArrayList(Block),
}

You get the idea: translating the Go code that Ivan shows into Zig isn’t too hard at this stage.

But wait, this is Zig. If a Blockchain is going to have an unknown number of Blocks that grow at runtime… you guessed it, we need to allocate them. So let’s enhance our Blockchain data model:

const Blockchain = struct {
    blocks: std.ArrayListUnmanaged(Block),
    arena: std.heap.ArenaAllocator,
}

OCD left the chat.

Nice. Let’s go deeper with the Block now.


Building Blocks

const Block = struct {
    const Self = @This();

    timestamp: i64,
    data: []const u8,
    prevblockhash: [32]u8,
    hash: [32]u8,

    pub fn init(data: []const u8, prevblockhash: [32]u8) Self {
        var block: Self = .{
            .timestamp = std.time.timestamp(),
            .data = data,
            .prevblockhash = prevblockhash,
            .hash = undefined,
        };
        block.set_hash();

        return block;
    }

    pub fn set_hash(self: *Self) void {
        var h = sha256.init(.{});

        h.update(self.prevblockhash[0..]);
        h.update(self.data);

        var ts_buf: [32]u8 = undefined;
        const ts_bytes = std.fmt.bufPrint(&ts_buf, "{d}", .{self.timestamp}) catch unreachable;
        h.update(ts_bytes);

        h.final(self.hash[0..]);
    }
};

Some patterns

I personally like this OOP-ish style of coding in Zig.

You usually see something like:

const Block = struct {
    const Self = @This();
}

so you can easily return Self and do similar things.

Another common pattern is having an init() function (like a constructor). If init() allocates memory (as we’ll see in the Blockchain), then you should also provide a deinit() function. This makes cleanup explicit and easy: deinit after you init.


Some notes

My first approach to the set_hash function was much less clean. It looked like this:

pub fn set_hash(self: *Self) void {
    var h = sha256.init(.{});

    h.update(self.prevblockhash[0..]);
    h.update(self.data);
    h.update(self.timestamp);
}

I was just shoving everything into h so it would compute the sha256 hash. The problem: timestamp is an i64, but update() expects a []const u8.

So we need a buffer to hold a []const u8 version of the timestamp:

var ts_buf: [32]u8 = undefined;
const ts_bytes = std.fmt.bufPrint(&ts_buf, "{d}", .{self.timestamp}) catch unreachable;
h.update(ts_bytes);

I had to ask AI how to properly convert the i64 into []const u8. I’m sure there are better ways, I’ll keep exploring and maybe update this in future parts of the series.


Building the Chain

The Block is a fairly simple data structure. The Blockchain, on the other hand, requires some actual functionality:

const Blockchain = struct {
    const Self = @This();

    blocks: std.ArrayListUnmanaged(Block),
    arena: std.heap.ArenaAllocator,

    pub fn init(alloc: Allocator) !Self {
        const arena = std.heap.ArenaAllocator.init(alloc);
        return .{
            .arena = arena,
            .blocks = .{},
        };
    }

    pub fn deinit(self: *Self) void {
        self.arena.deinit();
    }

    pub fn mine(self: *Self, data: []const u8) !void {
        const a = self.arena.allocator();

        // own the payload in the arena
        const owned_data = try a.dupe(u8, data);

        const prev = self.lastHash();
        const blk = Block.init(owned_data, prev);

        try self.blocks.append(a, blk);
    }

    pub fn lastHash(self: *const Self) [32]u8 {
        if (self.blocks.items.len == 0) return [_]u8{0} ** 32;
        return self.blocks.items[self.blocks.items.len - 1].hash;
    }
};

First of all, init() now allocates memory, so we need a deinit() to clean up. Zig is all about being explicit, and this makes ownership clear.

Why does the Blockchain allocate? Because it’s responsible for mining Blocks, and those Blocks need to be stored somewhere. For now, we’re keeping them in memory using an ArenaAllocator.

Finally, the only real functionality we’ve added so far is the mine() method, which appends a new Block to the Chain, effectively creating a BLOCKCHAIN!

Putting It All Together

Now that we have our Block and Blockchain structs, here’s a small main() to test it:

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer _ = gpa.deinit();

    var chain = try Blockchain.init(allocator);
    defer chain.deinit();

    try chain.mine("alice->bob: 5");
    try chain.mine("bob->carol: 2");

    for (chain.blocks.items, 0..) |b, i| {
        const hex_hash = std.fmt.bytesToHex(b.hash, .lower);
        const prev_hex = std.fmt.bytesToHex(b.prevblockhash, .lower);

        std.debug.print(
            \\* Block #{d}
            \\  ├─ Timestamp : {d}
            \\  ├─ Data      : {s}
            \\  ├─ Hash      : {s}
            \\  └─ PrevHash  : {s}
            \\
        ,
            .{ i, b.timestamp, b.data, hex_hash, prev_hex },
        );

        if (i < chain.blocks.items.len - 1) {
            std.debug.print("    │\n\n", .{});
        }
    }
}

And here’s the output you’ll get:

* Block #0
  ├─ Timestamp : 1724511120
  ├─ Data      : alice->bob: 5
  ├─ Hash      : 4d2f...
  └─ PrevHash  : 0000...



* Block #1
  ├─ Timestamp : 1724511121
  ├─ Data      : bob->carol: 2
  ├─ Hash      : 93a8...
  └─ PrevHash  : 4d2f...

Final words

So, in this first part, we now have a kinda V0.1 of a blockchain in Zig, which hey, if you are using this to learn more zig and you understood all of it, it is great improvement so congrats on that, learning new stuff is hard, and staying consistant is even harder.

Thanks a lot for reading 🫡