DEV Community

Cover image for A Raspberry Pi 3 as a mainnet validator — what a 1 GB board teaches a blockchain node
Malek
Malek

Posted on

A Raspberry Pi 3 as a mainnet validator — what a 1 GB board teaches a blockchain node

I run a small Layer-1 called Janzeer: stake-free BFT, 15-second slots, two voting rounds, finality in one block. The four
founding validators live on one 2 GB VPS, and I wrote about that box last week. This post is about the fifth validator.
It is a Raspberry Pi 3 Model B from 2016 — four Cortex-A53 cores at 1.2 GHz, 1 GB of RAM of which 905 MB are usable, a
USB disk, and 2.4 GHz Wi-Fi in a flat with thick walls. It has produced blocks on mainnet since 2026-10-15, and finding
out what it could not do fixed seven bugs in the node that four comfortable servers had never shown. No token talk, as
before; the coin is not on any exchange.

Why bother

The founding validators are meant to phase out. A chain whose only validators are the project's own servers is a
database with extra steps, so the active set has to be reachable for people who own ordinary hardware. If the node cannot
run on the cheapest 64-bit board in the drawer, over the Wi-Fi it happens to have, the "anyone can validate" line is
marketing. The Pi was the cheapest way to find out whether it was true. It was not, at first.

Getting a native image onto arm64

The node is a Spring Boot application in Kotlin, shipped as a GraalVM native image: no JDK on the board, one 80 MB
tarball, a 30-second boot on the Pi (3 seconds on the VPS). Building that image for arm64 was the first lesson.
native-image needs about 9 GB of heap for this project, which rules out building on the Pi itself. I built it in an
emulated aarch64 virtual machine on my desktop: 2 h 45 min per build, against 6 minutes native on x86-64. It works, and
it is the reason arm64 releases ship a few hours after the others. A rented arm64 VM would do it in minutes; I have not
needed one yet.

The image needs the same discipline on arm64 as anywhere else: every class the wire codec instantiates by reflection is
registered by hand, Hibernate's bytecode provider is off, the heap is capped. On this board the cap is -Xmx384m, and the
process sits at about 370 MB resident with the chain fully synced.

What the board found in the node

Each of these is a numbered finding in the project's test log. All are fixed in the current release, 0.1.7, and the
public nodes run it.

A restart during the first sync killed the sync thread. Restarting a node that had been staging blocks for an hour
left it "PROCESSING" for ever: the new sync session cleared the staging table through the ORM, which loaded all 287 MB of
staged rows as entities into a 256 MB heap. The sync thread died of the out-of-memory error and nothing noticed. The
clear is one bulk DELETE now, every task on the sync thread runs under a guard that turns an escaped error into a
restart of the sync, and an out-of-memory error ends the process so the service manager brings it back.

H2 scales its in-memory row limit with the heap. With 1 GiB of heap, H2 keeps 40,000 rows of a result in memory
before spilling to disk; at -Xmx256m that is 10,000 rows. The node's tip lookup, run on every commit, was a union over
all blocks. The moment the chain passed block 10,000, every commit wrote a temporary file first. Sync pace on a 1 GB
board fell from 400–660 blocks per minute to 28, at exactly that height. Nobody had seen it, because the servers had the
heap. The lookup is now an indexed top-row read per concrete table instead of the union, and the board commits at the
pace of its disk again.

The staging-table wipe spilled 405 MB into /tmp, which on Raspberry Pi OS is RAM. A 1 GB board with 405 MB of
temporary database file in its RAM does not survive. The launcher now points the JVM's temp directory at the data disk.

Pages that took longer than 10 seconds to arrive were thrown away. Sync fetches blocks in pages of 200, about 330 KB
each. On this Wi-Fi, one page from one peer takes 15–20 seconds. The decoder treated any message older than 10 seconds
as stale and dropped it, a 10-second timer asked a second peer for the same page, and each late duplicate triggered yet
another request. Three servers streamed the same pages to the Pi at 25 KB/s each, and it staged one page a minute.
Block pages are now exempt from the staleness check, a request is not repeated while bytes are still arriving, and a
late duplicate is ignored quietly. Staging went from 100 to 800–880 blocks per minute on the same link.

A validator one block behind downloaded 200 blocks to get it back. Slot ownership rotates, so this node's turn
comes every 75 seconds. Missing one live block, which happens on Wi-Fi, flipped it to catch-up mode, and catch-up asked
for the top page of the epoch: 330 KB, 15 seconds here, exactly one slot. By the time the page landed, the node was one
block behind again. It could sit in that loop for twenty minutes, producing nothing while reporting itself healthy. The
server now remembers the requester's tip from the availability handshake that precedes every catch-up and serves only the
blocks above it. A one-block catch-up is now 2–10 seconds, and the Pi makes its next slot.

A lost page request could leave a node "processing" for ever, silently. The waiting logic treated any bytes on the
channel as "the page is still streaming". Live blocks and votes are bytes too. A page request lost when a connection
dropped and redialed was never re-asked, and the node sat there for 75 minutes without a single log line. The wait is now
bounded whatever else flows on the channel, the request is re-sent to another peer, and the node says so in the log.

A lagging validator proposing with a stale tip showed up as an ERROR on everybody else's node, and paged the
operator. It is a normal event on a slow link: the proposal is dropped, the slot stays empty. It is logged as what it is now.

None of these were visible on four servers with 1 Gbit links and 2 GB each. The Pi found all of them in nine days.

What the board taught the operator

The code was half of it. The other half had nothing to do with the node.

Power. The first week's reboots were brown-outs. A phone charger runs a Pi 3 at idle and drops it under sync load.
The kernel counts under-voltage events; mine logged 2,191 of them in twelve hours before I bought the official supply with
its own short cable. Any mystery reboot on a Pi is the power until proven otherwise.

No real-time clock. After a power cut the Pi boots with its clock minutes behind until NTP steps it. The node will
not produce without a trusted clock, which is correct, but it also means the log timestamps around a boot are wrong, and
I lost an hour to that once.

Wi-Fi. Today I changed the Wi-Fi password for two hours and changed it back. The Pi never rejoined. NetworkManager
treats a run of authentication failures as wrong secrets and stops auto-connecting until something re-activates the
connection; a reboot does, three hours later. The validator missed every one of its slots meanwhile, and the chain carried
on without it, which is the point of a 2f+1 quorum. The board now has a two-minute watchdog timer that re-activates the
connection when it is not up, and a persistent journal, because the volatile one took the evidence with it.

Unclean stops cost 20 minutes. The node skips its boot-time validation of the last two epochs when it finds the
marker it writes on a clean shutdown. A reboot that kills the process leaves no marker, and this board takes 22 minutes
to re-validate 34,000 blocks before it dials anyone. Stop the service before you pull the plug.

Being late is not being slashed. The Pi's seat became active while it was still syncing, so it was silent for its
first epoch and was dropped from the active set for the next one. It came back automatically the epoch after, deposit
untouched. A validator that misses a whole epoch sits one out; it is not punished for being slow, only for signing twice.

Where it stands

The Pi is one of five active validators. In the first 2,300 blocks of its first full epoch it produced 443 of its 460
slots. After today's incident it caught up 560 blocks in two minutes and produced the next slot it owned. Its block
reward goes to the wallet that registered it, like anyone else's.

The arm64 bundle is a signed download alongside Linux x86-64, Windows and an Arch package; installing it is a service
file and one tar. The deposit for a validator seat is 2,000 JNZ, and the project currently covers it for the first ten
outside operators (details on the validators page). If you have a 64-bit board, a proper power supply and a port you can
forward, you can hold a seat on a live BFT chain from your shelf. I would rather have your Pi in the set than my fifth server.

Explorer: https://explorer.janzeer.org
Validators: https://janzeer.org/validators
SDKs and wallet: https://github.com/janzeerorg

Top comments (0)