<?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: Malek</title>
    <description>The latest articles on DEV Community by Malek (@syriandeveloper).</description>
    <link>https://dev.to/syriandeveloper</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F156048%2F56d1ff84-a7fa-49dd-9567-df04eedde356.png</url>
      <title>DEV Community: Malek</title>
      <link>https://dev.to/syriandeveloper</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/syriandeveloper"/>
    <language>en</language>
    <item>
      <title>Turn a callback WebSocket feed into a Kotlin Flow with callbackFlow (with a live feed to try it on)</title>
      <dc:creator>Malek</dc:creator>
      <pubDate>Fri, 13 Nov 2026 04:08:49 +0000</pubDate>
      <link>https://dev.to/syriandeveloper/turn-a-callback-websocket-feed-into-a-kotlin-flow-with-callbackflow-with-a-live-feed-to-try-it-on-lj0</link>
      <guid>https://dev.to/syriandeveloper/turn-a-callback-websocket-feed-into-a-kotlin-flow-with-callbackflow-with-a-live-feed-to-try-it-on-lj0</guid>
      <description>&lt;p&gt;Most APIs that push data hand you a callback. Kotlin code would rather have a &lt;code&gt;Flow&lt;/code&gt;. &lt;code&gt;callbackFlow&lt;/code&gt; is the bridge, and&lt;br&gt;
it is short, but two details decide whether your version leaks sockets: where the cleanup goes, and whether the Flow is&lt;br&gt;
cold.&lt;/p&gt;

&lt;p&gt;To have something real to wrap, this article uses a public blockchain network that emits a block every 15 seconds over&lt;br&gt;
a WebSocket. It needs no API key and no account, so the code runs as written. You do not need to know anything about&lt;br&gt;
the chain; treat it as a server that pushes one event every 15 seconds.&lt;/p&gt;
&lt;h3&gt;
  
  
  The project
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;build.gradle.kts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;kotlin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"jvm"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="s2"&gt;"2.2.0"&lt;/span&gt;
    &lt;span class="n"&gt;application&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;repositories&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mavenCentral&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"org.janzeer:janzeer-sdk-client:0.1.7"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;kotlin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;jvmToolchain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;application&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;mainClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MainKt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The callback API
&lt;/h3&gt;

&lt;p&gt;The SDK's WebSocket client looks like most push clients:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JanzeerRpcWs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://explorer.janzeer.org"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribeNewBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NewBlocksParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// … later&lt;/span&gt;
&lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to manage by hand: the socket, the subscription, and the moment you stop caring.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;awaitClose&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Flow&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;callbackFlow&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runBlocking&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;withTimeout&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;janzeer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BlockView&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;janzeer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;JanzeerRpcWs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;janzeer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NewBlocksParams&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="no"&gt;NODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://explorer.janzeer.org"&lt;/span&gt;

&lt;span class="cm"&gt;/** Every newly committed block as a cold Flow: the socket opens on collect and closes when the collector stops. */&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;newBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Flow&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BlockView&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;callbackFlow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JanzeerRpcWs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribeNewBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NewBlocksParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;trySend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;awaitClose&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runBlocking&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;90_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;newBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1000.0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;
                &lt;span class="s2"&gt;"#${block.height}  ${block.hash.take(12)}…  gap=${gap ?: "&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="s2"&gt;"}s"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;println&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;./gradlew run&lt;/code&gt; printed this on a real run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#239312  126c6f6b7094…  gap=-s
#239313  c93fbe16e199…  gap=15.012s
#239314  02364077a56f…  gap=14.999s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The two details
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cleanup belongs in &lt;code&gt;awaitClose&lt;/code&gt;, and nowhere else.&lt;/strong&gt; &lt;code&gt;awaitClose&lt;/code&gt; suspends the builder until the collector goes&lt;br&gt;
away, then runs its block. "Goes away" covers every case at once: &lt;code&gt;take(3)&lt;/code&gt; finished, the collecting coroutine was&lt;br&gt;
cancelled, &lt;code&gt;withTimeout&lt;/code&gt; fired, or an exception came from downstream. Here it unsubscribes first, so the server is told,&lt;br&gt;
and then closes the socket. If you forget &lt;code&gt;awaitClose&lt;/code&gt;, &lt;code&gt;callbackFlow&lt;/code&gt; throws at runtime to tell you so, which is one of&lt;br&gt;
the kinder errors in the coroutines library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open the resource inside the builder, so the Flow is cold.&lt;/strong&gt; &lt;code&gt;JanzeerRpcWs.connect&lt;/code&gt; is called inside &lt;code&gt;callbackFlow&lt;/code&gt;,&lt;br&gt;
not outside it. Calling &lt;code&gt;newBlocks(NODE)&lt;/code&gt; therefore opens nothing; the socket exists only while somebody collects, and&lt;br&gt;
two collectors get two sockets. If you want one shared socket for many collectors, add &lt;code&gt;shareIn&lt;/code&gt; at the call site, where&lt;br&gt;
the scope that owns the socket is visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;newBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shareIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SharingStarted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WhileSubscribed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WhileSubscribed&lt;/code&gt; closes the socket when the last collector leaves, through the same &lt;code&gt;awaitClose&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;trySend&lt;/code&gt;, not &lt;code&gt;send&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The callback runs on the WebSocket's thread, which is not a coroutine, so it cannot call the suspending &lt;code&gt;send&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;trySend&lt;/code&gt; never blocks that thread. With the default buffer it drops an element only if the collector is 64 events&lt;br&gt;
behind; at one event every 15 seconds that does not happen. For a fast feed, pick the policy on purpose:&lt;br&gt;
&lt;code&gt;.buffer(Channel.CONFLATED)&lt;/code&gt; to keep only the newest, or a larger buffer to keep all.&lt;/p&gt;

&lt;h3&gt;
  
  
  A timeout around the collector
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;withTimeout(90_000)&lt;/code&gt; is there because a push feed can go quiet without closing. Six block intervals without an event&lt;br&gt;
means something is wrong, and a timeout turns "hangs for ever" into an exception you can log. The timeout cancels the&lt;br&gt;
collector, and &lt;code&gt;awaitClose&lt;/code&gt; still runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where to go next
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The same wrapper works for the address feed: &lt;code&gt;ws.subscribeAddressActivity(listOf(address)) { event -&amp;gt; trySend(event) }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The SDK is on Maven Central as &lt;code&gt;org.janzeer:janzeer-sdk-client&lt;/code&gt;, Apache-2.0; source and examples:
&lt;a href="https://github.com/janzeerOrg/janzeer-sdk-kotlin" rel="noopener noreferrer"&gt;https://github.com/janzeerOrg/janzeer-sdk-kotlin&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The same feed drawn live in a browser: &lt;a href="https://explorer.janzeer.org/workflow" rel="noopener noreferrer"&gt;https://explorer.janzeer.org/workflow&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kotlin</category>
      <category>coroutines</category>
      <category>flow</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Follow a live blockchain from Python with asyncio and 20 lines of code</title>
      <dc:creator>Malek</dc:creator>
      <pubDate>Thu, 12 Nov 2026 21:50:21 +0000</pubDate>
      <link>https://dev.to/syriandeveloper/follow-a-live-blockchain-from-python-with-asyncio-and-20-lines-of-code-390</link>
      <guid>https://dev.to/syriandeveloper/follow-a-live-blockchain-from-python-with-asyncio-and-20-lines-of-code-390</guid>
      <description>&lt;p&gt;Most &lt;code&gt;asyncio&lt;/code&gt; tutorials fake their data source with &lt;code&gt;asyncio.sleep&lt;/code&gt;. This one uses a real feed: a public blockchain&lt;br&gt;
network that produces a block every 15 seconds and pushes each one over a WebSocket. There is no API key, no account&lt;br&gt;
and no rate plan, so everything below runs as written.&lt;/p&gt;

&lt;p&gt;The network is Janzeer, a small Layer-1 chain. You do not need to know anything about it to follow along; treat it as&lt;br&gt;
a server that emits one JSON event every 15 seconds.&lt;/p&gt;
&lt;h3&gt;
  
  
  Set up
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python -m venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; . .venv/bin/activate
pip install &lt;span class="s2"&gt;"janzeer-sdk[ws]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Python 3.10 or newer. The &lt;code&gt;[ws]&lt;/code&gt; extra pulls in &lt;code&gt;websockets&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Print every new block
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;

&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;janzeer&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;JanzeerRpcWs&lt;/span&gt;

&lt;span class="no"&gt;NODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://explorer.janzeer.org"&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxlen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;last_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;JanzeerRpcWs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe_new_blocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_nowait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_ts&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;last_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;
                &lt;span class="n"&gt;avg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gaps&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
                &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"#{block['height']}  {block['hash'][:12]}…  txs={block['transactionsCount']}  avg gap={avg:.1f}s"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output from a real run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#239213  4e0054fc98db…  txs=1  avg gap=0.0s
#239214  f92c4915328c…  txs=1  avg gap=15.0s
#239215  4e5cf64b2332…  txs=1  avg gap=15.0s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things in there are worth copying into your own code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A queue as the callback sink.&lt;/strong&gt; &lt;code&gt;subscribe_new_blocks&lt;/code&gt; takes a plain callback. Passing &lt;code&gt;blocks.put_nowait&lt;/code&gt; turns the&lt;br&gt;
push feed into something you can &lt;code&gt;await&lt;/code&gt;, and keeps your processing out of the WebSocket reader. If your handler is&lt;br&gt;
slow, the queue absorbs the burst; the socket keeps being read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A timeout on every wait.&lt;/strong&gt; &lt;code&gt;asyncio.wait_for(blocks.get(), timeout=60)&lt;/code&gt; is the difference between a script that&lt;br&gt;
reports "the feed went quiet" and one that hangs for ever. Sixty seconds is four block intervals here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;try&lt;/code&gt; / &lt;code&gt;finally&lt;/code&gt; around the subscription.&lt;/strong&gt; The &lt;code&gt;async with&lt;/code&gt; closes the socket, and the &lt;code&gt;finally&lt;/code&gt; unsubscribes&lt;br&gt;
first, so the server is told before the connection drops. On Ctrl-C, &lt;code&gt;asyncio.run&lt;/code&gt; cancels &lt;code&gt;main&lt;/code&gt;, and both still run.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Watch an address
&lt;/h3&gt;

&lt;p&gt;The same feed can filter by address. A validator's wallet is a convenient target because it receives a block reward&lt;br&gt;
every few minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;janzeer&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;JanzeerRpcWs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_jnz&lt;/span&gt;

&lt;span class="no"&gt;NODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://explorer.janzeer.org"&lt;/span&gt;
&lt;span class="no"&gt;WATCH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0x038cba8510852bd5ce07166427d16cf81b6fb849"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;JanzeerRpcWs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe_address_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;WATCH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_nowait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"transaction"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"block {event['blockHeight']}: {tx['type']} of {format_jnz(tx['amount'])} ({tx['status']})"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;block 239222: reward of 10 JNZ (FINAL)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status is &lt;code&gt;FINAL&lt;/code&gt; in the very first notification. On this network a committed block cannot be reorganised, so&lt;br&gt;
there is no "wait for six confirmations" loop to write. That removes a whole class of state from the client.&lt;/p&gt;

&lt;p&gt;Amounts arrive as decimal strings and &lt;code&gt;format_jnz&lt;/code&gt; prints them. The SDK never converts money to &lt;code&gt;float&lt;/code&gt;; if you do&lt;br&gt;
arithmetic on amounts, use its helpers or &lt;code&gt;decimal.Decimal&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Run several things in one loop
&lt;/h3&gt;

&lt;p&gt;Because the clients are ordinary &lt;code&gt;asyncio&lt;/code&gt; objects, combining them is &lt;code&gt;asyncio.gather&lt;/code&gt; or a &lt;code&gt;TaskGroup&lt;/code&gt;. This reads the&lt;br&gt;
chain's current height over HTTP while the WebSocket subscription is open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;janzeer&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;AsyncJanzeerRpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;JanzeerRpcWs&lt;/span&gt;

&lt;span class="no"&gt;NODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://explorer.janzeer.org"&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;AsyncJanzeerRpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;JanzeerRpcWs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;NODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe_new_blocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_nowait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"tip over HTTP:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"| next block over WebSocket:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both async HTTP clients accept an &lt;code&gt;http=&lt;/code&gt; argument, so several clients can share one &lt;code&gt;httpx.AsyncClient&lt;/code&gt; and its&lt;br&gt;
connection pool. The SDK's &lt;a href="https://github.com/janzeerOrg/janzeer-sdk-python/blob/master/docs/asyncio.md" rel="noopener noreferrer"&gt;asyncio guide&lt;/a&gt;&lt;br&gt;
covers that, and was written by an outside contributor last week.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when the connection drops
&lt;/h3&gt;

&lt;p&gt;The WebSocket client reconnects by itself and resumes a block subscription from the last block it delivered, so a&lt;br&gt;
restart of your Wi-Fi does not lose or repeat blocks. If you need the same guarantee across restarts of your own&lt;br&gt;
process, store the last height you handled and pass it back as &lt;code&gt;from_height&lt;/code&gt; when you subscribe; the node replays from&lt;br&gt;
there before following the tip.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where to go next
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sending a signed transfer and waiting for it to become final is 15 more lines:
&lt;a href="https://github.com/janzeerOrg/janzeer-sdk-python/blob/master/examples/07_async_quickstart_transfer.py" rel="noopener noreferrer"&gt;&lt;code&gt;examples/07_async_quickstart_transfer.py&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The package is on PyPI as &lt;a href="https://pypi.org/project/janzeer-sdk/" rel="noopener noreferrer"&gt;&lt;code&gt;janzeer-sdk&lt;/code&gt;&lt;/a&gt;, typed (&lt;code&gt;py.typed&lt;/code&gt;), Apache-2.0.&lt;/li&gt;
&lt;li&gt;The live view of the same feed, drawn in a browser: &lt;a href="https://explorer.janzeer.org/workflow" rel="noopener noreferrer"&gt;https://explorer.janzeer.org/workflow&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>asyncio</category>
      <category>websockets</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>أرسل أول معاملة على بلوكتشين «جنزير» بلغة بايثون في أقل من عشرين سطرًا</title>
      <dc:creator>Malek</dc:creator>
      <pubDate>Sat, 07 Nov 2026 14:07:16 +0000</pubDate>
      <link>https://dev.to/syriandeveloper/rsl-wl-mml-l-blwktshyn-blg-bythwn-fy-ql-mn-shryn-stran-2kl3</link>
      <guid>https://dev.to/syriandeveloper/rsl-wl-mml-l-blwktshyn-blg-bythwn-fy-ql-mn-shryn-stran-2kl3</guid>
      <description>&lt;h2&gt;
  
  
  ⁧لماذا هذا المقال؟⁩
&lt;/h2&gt;

&lt;p&gt;⁧معظم سلاسل الكتل العامة توثيقها بالإنجليزية فقط، ومعظم أدوات التطوير فيها تفترض أنك تعرف الإيثيريوم مسبقًا. «جنزير» (Janzeer) سلسلة كتل من الطبقة الأولى أطلقتُ شبكتها الرئيسية في الأول من تشرين الأول/أكتوبر 2026، وفيها أمران يهمّان المطوّر العربي تحديدًا: التوثيق الرسمي كله بالعربية والإنجليزية معًا (الورقة البيضاء، دليل المطوّر، مرجع الواجهة)، ومجموعات تطوير رسمية بخمس لغات: بايثون، وتايب سكريبت، ودارت، وكوتلن، وغو، كلها مفتوحة المصدر برخصة Apache-2.0.⁩&lt;/p&gt;

&lt;p&gt;⁧في هذا المقال نستخدم بايثون. في النهاية ستكون قد قرأت بيانات السلسلة، وأنشأت محفظة على جهازك، وأرسلت معاملة، وانتظرت حتى صارت نهائية — كل ذلك من سطر الأوامر.⁩&lt;/p&gt;

&lt;h2&gt;
  
  
  ⁧ما الذي يميّز «جنزير» بكلمات قليلة؟⁩
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⁧لا تجميد (staking) ولا تفويض: من يريد التحقق من الكتل يشغّل عقدة ويدفع وديعة لمرة واحدة قدرها 2000 JNZ، ثم يكسب مكافأة الكتل التي ينتجها. لا تصويت ولا لجان.⁩&lt;/li&gt;
&lt;li&gt;⁧نهائية في كتلة واحدة: الكتلة كل 15 ثانية، وتُعتمد بتوافق BFT من مرحلتين (&lt;code&gt;PREPARE&lt;/code&gt; ثم &lt;code&gt;COMMIT&lt;/code&gt;). الكتلة المعتمدة نهائية؛ لا يوجد «عدد تأكيدات».⁩&lt;/li&gt;
&lt;li&gt;⁧معروض ثابت: 100 مليون JNZ، لا سكّ بعدها. مكافآت الكتل تُدفع من الخزانة.⁩&lt;/li&gt;
&lt;li&gt;⁧غير حافظ (non-custodial) بالتصميم: المفاتيح لا تغادر جهازك أبدًا، والعقدة لا تفعل سوى التحقق.⁩&lt;/li&gt;
&lt;li&gt;⁧الشبكة صغيرة وجديدة: سبعة مدقّقين نشطين اليوم، وقرابة مئتي ألف كتلة. أقول هذا بوضوح حتى لا يتوقع أحد شبكة بحجم الكبار.⁩&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⁧ما الذي تحتاج إليه؟⁩
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⁧بايثون 3.10 أو أحدث على لينكس أو ماك أو ويندوز (العجلات جاهزة، لا حاجة إلى مترجم).⁩&lt;/li&gt;
&lt;li&gt;⁧حزمة &lt;code&gt;janzeer-sdk&lt;/code&gt; من PyPI.⁩&lt;/li&gt;
&lt;li&gt;⁧لقراءة بيانات السلسلة لا تحتاج إلى شيء آخر. لإرسال معاملة تحتاج إلى بضع وحدات JNZ في محفظتك؛ أسهل طريقة اليوم هي لعبة jPong داخل تيليغرام (التي تدفع لمن يلعب) أو أن يحوّل لك أحدهم. لا يوجد صنبور (faucet) على الشبكة الرئيسية.⁩&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⁧الخطوة 0: التثبيت⁩
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"janzeer-sdk[ws]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⁧اللاحقة [ws] تضيف دعم &lt;code&gt;WebSocket&lt;/code&gt; للاشتراكات الحيّة؛ احذفها إن لم تكن تحتاج إليها.⁩&lt;/p&gt;

&lt;h2&gt;
  
  
  ⁧الخطوة 1: اقرأ السلسلة (بلا مفاتيح وبلا رصيد)⁩
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;janzeer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JanzeerClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JanzeerRpc&lt;/span&gt;

&lt;span class="n"&gt;NODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://explorer.janzeer.org&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JanzeerClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# REST تحت ‎/api/v1
&lt;/span&gt;&lt;span class="n"&gt;rpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JanzeerRpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# JSON-RPC تحت ‎/rpc
&lt;/span&gt;
&lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;إصدار العقدة:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;| الشبكة:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;networkId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;آخر كتلة:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⁧العقدة العامة &lt;code&gt;explorer.janzeer.org&lt;/code&gt; هي نفسها التي يعمل عليها المستكشف. كل نتيجة تعود كقاموس أو قائمة بأسماء الحقول التي تستخدمها العقدة نفسها (&lt;code&gt;blockHeight&lt;/code&gt; و&lt;code&gt;senderAddress&lt;/code&gt; وهكذا)، فلا توجد طبقة أسماء ثانية عليك حفظها.⁩&lt;/p&gt;

&lt;h2&gt;
  
  
  ⁧الخطوة 2: أنشئ محفظة على جهازك⁩
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;janzeer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mnemonic&lt;/span&gt;

&lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mnemonic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# 12 كلمة وفق BIP39
&lt;/span&gt;&lt;span class="n"&gt;me&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_mnemonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                              &lt;span class="c1"&gt;# احفظها في مكان آمن؛ من يملكها يملك المحفظة
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checksum_address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⁧هذا كل ما يحدث: اشتقاق محلي. لم تُرسل أي كلمة إلى أي خادم. (العنوان الناتج يبدأ بـ 0x وطوله 20 بايتًا، لكنه ليس عنوان إيثيريوم: الاشتقاق مختلف.)⁩&lt;/p&gt;

&lt;h2&gt;
  
  
  ⁧الخطوة 3: أرسل معاملة وانتظر النهائية⁩
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;janzeer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JanzeerClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JanzeerRpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TxBuilder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_jnz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_for_finality&lt;/span&gt;

&lt;span class="n"&gt;NODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://explorer.janzeer.org&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JanzeerClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NODE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;JanzeerRpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NODE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;me&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_mnemonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;…الكلمات الاثنتا عشرة من الخطوة السابقة…&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checksum_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;format_jnz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JNZ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign_tx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TxBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0x598b1301acef3baba6ce25e38dd17b723f7b98b1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.25&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;# نص عشري، ليس float
&lt;/span&gt;    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;مرحبا من بايثون&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;network_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;networkId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;# HTTP 201 أو خطأ ذو نوع محدد
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;أُرسلت:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wait_for_finality&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# فتحة أو فتحتان من 15 ثانية
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;نهائية في الكتلة&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blockHeight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://explorer.janzeer.org/tx/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⁧هذه هي عملية التكامل كاملة: اشتقاق ← رقم تسلسلي (&lt;code&gt;nonce&lt;/code&gt;) ← بناء ← توقيع ← إرسال ← انتظار. حين تعود &lt;code&gt;wait_for_finality&lt;/code&gt; تكون المعاملة في كتلة معتمدة، أي نهائية. افتح الرابط الأخير في المستكشف وسترى المعاملة ورسالتها.⁩&lt;/p&gt;

&lt;p&gt;⁧لاحظ ثلاثة أشياء:⁩&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;⁧المبلغ نص "1.25" وليس 1.25. المكتبة ترفض &lt;code&gt;float&lt;/code&gt; في كل مكان وترمي &lt;code&gt;TypeError&lt;/code&gt;، لأن 0.1 لا يُمثَّل تمثيلًا دقيقًا بالثنائي، والدفع يجب ألّا يعتمد على طريقة طباعة الأعداد العشرية. المقبول: نص عشري، أو &lt;code&gt;int&lt;/code&gt; للوحدات الكاملة، أو decimal.Decimal.⁩&lt;/li&gt;
&lt;li&gt;⁧&lt;code&gt;network_id&lt;/code&gt; يدخل في بصمة المعاملة، فمعاملة موقّعة لشبكة الاختبار لا يمكن إعادة تشغيلها على الشبكة الرئيسية.⁩&lt;/li&gt;
&lt;li&gt;⁧الأخطاء مصنّفة: &lt;code&gt;TxRejectedError&lt;/code&gt; و&lt;code&gt;NonceMismatchError&lt;/code&gt; و&lt;code&gt;RateLimitedError&lt;/code&gt; وغيرها، فبرنامجك يعرف ماذا حدث بدل أن يفسّر نصًا.⁩&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  ⁧الخطوة 4 (اختيارية): شاهد الكتل الجديدة حيّة⁩
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;janzeer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JanzeerRpcWs&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;JanzeerRpcWs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wss://explorer.janzeer.org/rpc/ws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe_new_blocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fromHeight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
        &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;كتلة&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;معاملات:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transactionsCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⁧(المثال الكامل مع المعالجة الصحيحة للإنهاء موجود في &lt;code&gt;examples/06_watch_new_blocks.py&lt;/code&gt; داخل المستودع، وقد كتبه مساهم من خارج المشروع.)⁩&lt;/p&gt;

&lt;h2&gt;
  
  
  ⁧ماذا تقرأ بعد ذلك؟⁩
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⁧دليل المطوّر بالعربية: &lt;a href="https://janzeer.org/docs" rel="noopener noreferrer"&gt;https://janzeer.org/docs&lt;/a&gt; — كل نقطة نهاية REST وكل دالة JSON-RPC، بالعربية والإنجليزية.⁩&lt;/li&gt;
&lt;li&gt;⁧المستودع: &lt;a href="https://github.com/janzeerOrg/janzeer-sdk-python" rel="noopener noreferrer"&gt;https://github.com/janzeerOrg/janzeer-sdk-python&lt;/a&gt; — ستة أمثلة جاهزة، وقضايا موسومة &lt;code&gt;good first issue&lt;/code&gt; لمن يريد المساهمة.⁩&lt;/li&gt;
&lt;li&gt;⁧المجموعات الأخرى: تايب سكريبت (npm: &lt;code&gt;janzeer-sdk&lt;/code&gt;)، دارت (pub.dev: &lt;code&gt;janzeer_sdk&lt;/code&gt;)، كوتلن (Maven Central: &lt;code&gt;org.janzeer&lt;/code&gt;)، غو.⁩&lt;/li&gt;
&lt;li&gt;⁧المستكشف: &lt;a href="https://explorer.janzeer.org" rel="noopener noreferrer"&gt;https://explorer.janzeer.org&lt;/a&gt; — وفيه صفحة «كيف تعمل السلسلة» برسم متحرك حيّ.⁩&lt;/li&gt;
&lt;li&gt;⁧القناة: &lt;a href="https://t.me/janzeerorg" rel="noopener noreferrer"&gt;https://t.me/janzeerorg&lt;/a&gt; — كل إعلان بالعربية والإنجليزية.⁩&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>blockchain</category>
      <category>python</category>
      <category>arabic</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Three ways a BFT chain goes quiet — a week of incidents on a small Layer-1</title>
      <dc:creator>Malek</dc:creator>
      <pubDate>Wed, 28 Oct 2026 04:36:36 +0000</pubDate>
      <link>https://dev.to/syriandeveloper/three-ways-a-bft-chain-goes-quiet-a-week-of-incidents-on-a-small-layer-1-315k</link>
      <guid>https://dev.to/syriandeveloper/three-ways-a-bft-chain-goes-quiet-a-week-of-incidents-on-a-small-layer-1-315k</guid>
      <description>&lt;p&gt;I run a small Layer-1 called Janzeer: stake-free BFT, 15-second slots, two voting rounds (PREPARE and COMMIT), finality in one block, a 2f+1 quorum over the epoch's validator set. Mainnet started on 2026-10-01. In the last week of October the network had six validators: four project nodes on one 2 GB VPS, one Raspberry Pi 3 at my home, one balenaFin at a friend's. Quorum was five of six.&lt;/p&gt;

&lt;p&gt;That week the chain stopped twice for about four hours each, and two validators went silent for four and thirteen hours without a single log line. None of it lost or reverted a block. All of it was our own code. Here is what happened, in the order we understood it, with block heights and times so you can check the explorer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incident 1 — one validator never gives up a question (F-052)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;24 October, 23:20 UTC, tip #135,855, four hours.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Finality needs 5 of 6 votes. The Pi had been quiet since 19:06 UTC: it had seen a live block ahead of its tip, flipped itself to PROCESSING and asked its peers "do you know my tip?". Nobody's answer arrived, and the code had no bound on that wait. Eight hours in PROCESSING, during which every block, vote and certificate is dropped by design (a node that does not trust its own ledger must not vote). When the balenaFin went offline at 23:20 UTC, four voters remained and no block could reach the COMMIT quorum.&lt;/p&gt;

&lt;p&gt;The four VPS nodes were healthy and proposed on every slot. A restart of the Pi restored the fifth vote; the chain resumed within one slot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause.&lt;/strong&gt; The availability round had a retry timer that was &lt;em&gt;replaced&lt;/em&gt; instead of &lt;em&gt;cancelled and replaced&lt;/em&gt; when a new request went out, so the re-check's own request was never re-sent, and nothing ever returned the node to SYNCHRONIZED.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix (0.1.9).&lt;/strong&gt; A &lt;code&gt;TipCheckWatch&lt;/code&gt; bounds the round: log the first retry, then once a minute, and after 120 seconds give up, return to SYNCHRONIZED, keep asking quietly. The next live block ahead of the tip re-triggers a clean check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incident 2 — one vote freezes everybody (F-053)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;25 October, 06:21 UTC, tip #136,455, four hours.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one was different, and it took longer to see because every node looked fine. All six validators were SYNCHRONIZED at the same tip, no node logged anything, restarting the Pi changed nothing, rebooting it changed nothing. The chain simply did not move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause.&lt;/strong&gt; Slot bookkeeping. A vote or certificate carried a slot number, and the handler moved the node's "current slot" counter to that number &lt;em&gt;before&lt;/em&gt; checking the signature or the sender's membership. One vote with a slot number far in the future, from a validator whose clock had jumped at boot, moved every project node's counter forward. From then on each node built its own block, called &lt;code&gt;enterSlot(now)&lt;/code&gt;, got &lt;code&gt;false&lt;/code&gt; because "now" was below the poisoned counter, and dropped its own block before broadcasting it. No gossip, no log line, poison only in memory. The fix was already written the night before from code reading; the live network proved it the next morning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix (0.1.9).&lt;/strong&gt; Verify first, then move: a vote or certificate must carry a valid signature from an active validator and must sit within one slot of the local clock; a node whose counter ran ahead of its clock heals itself. Implausible votes are logged with their sender.&lt;/p&gt;

&lt;p&gt;Our diagnostic rule from this day: &lt;em&gt;every validator SYNCHRONIZED and silent&lt;/em&gt; means slot poisoning, restart the proposers; &lt;em&gt;one board PROCESSING&lt;/em&gt; means a stuck re-check, restart that board.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incident 3 — a race that only fast links can lose (F-056)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;25 October, 14:02 UTC, node five, four hours. 26 October, 18:10 UTC, the Pi, thirteen hours.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We had added a fifth project node that week. At 14:02 UTC I registered it as a validator. The block carrying its registration, #137,271, was the last block it committed for four hours. It stayed healthy by every health check and wrote nothing to its log. A &lt;code&gt;systemctl restart&lt;/code&gt; did not help. A &lt;code&gt;stop&lt;/code&gt;, a pause, and a &lt;code&gt;start&lt;/code&gt; did.&lt;/p&gt;

&lt;p&gt;Two defects chained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sweep.&lt;/strong&gt; When a node commits a block that registers its &lt;em&gt;own&lt;/em&gt; key, it switches to full sync mode and re-validates the last two epochs, about 34,000 blocks. It did this on the consensus thread, inside the ledger write lock, even though it already was in full mode. Eighty-six seconds frozen, every peer channel stalled. Every validator we had ever registered had paid this at its registration; on a 1 GB board it is twenty minutes, and nobody had connected the dots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The race.&lt;/strong&gt; After the sweep the node was behind, so it ran a tip re-check: send "do you know my tip?" to the peers, then arm the retry loop. On the same VPS the peers answer in under a millisecond. The sync thread had already started a catch-up session and armed the session's own page timer when the caller finally executed &lt;code&gt;future = startRequestScheduler(...)&lt;/code&gt;: that assignment cancelled the session's timer and installed a loop that returns silently while a session exists. The first page request had gone out on a channel still re-handshaking after the stall and was lost. Nothing would ever re-send it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before (0.1.9): request first, arm second — an answer can overtake the arming&lt;/span&gt;
&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PROCESSING&lt;/span&gt;
&lt;span class="nf"&gt;checkBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loud&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                   &lt;span class="c1"&gt;// sends to every promoter peer&lt;/span&gt;
&lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requestRetryScheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startRequestScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;   &lt;span class="c1"&gt;// cancels whatever `future` is NOW —&lt;/span&gt;
    &lt;span class="nf"&gt;tipCheckRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                          &lt;span class="c1"&gt;// including a session's page timer&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// after (0.1.10): the whole round runs on the sync thread, arm first, send second.&lt;/span&gt;
&lt;span class="c1"&gt;// Responses are handled on that same single thread in arrival order, so none can overtake it.&lt;/span&gt;
&lt;span class="n"&gt;syncExecutor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tipCheck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requestRetryScheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startRequestScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;tipCheckRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;checkBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loud&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why had this never shown on the home boards? Their answers take tens of milliseconds, so the arming always won. It finally bit the Pi on 26 October for a different reason: a bad power supply. The board reported under-voltage thousands of times per boot, the CPU was throttled, and a garbage-collection pause of a few tens of milliseconds between those two statements was enough. The Pi lost a block in a power glitch, re-checked its tip, lost the race, lost the page on a flaky link, and sat for thirteen hours. Healthy, silent, not producing.&lt;/p&gt;

&lt;p&gt;And the restart-versus-stop mystery? Pure timing. &lt;code&gt;restart&lt;/code&gt; ran the first re-check nine seconds after boot, when the peer channels were still handshaking, and lost the race again. &lt;code&gt;stop&lt;/code&gt; then &lt;code&gt;start&lt;/code&gt; ran it fourteen seconds after boot on settled channels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix (0.1.10).&lt;/strong&gt; Arm on the sync thread before sending; a retry period that finds a live session re-requests the outstanding page instead of returning; the self-registration switch is a no-op when the mode is unchanged and otherwise runs after the commit, off the consensus thread; every catch-up session start logs one line. The last point matters most: the thing that made this bug expensive was not the race, it was the silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed in how we work
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Silence is a symptom.&lt;/strong&gt; Every wait in the sync path now has a bound and a log line at the bound. A node that is PROCESSING must be able to explain why in its own journal within two minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serialize anything that can lose a race against a sub-millisecond peer.&lt;/strong&gt; The sync thread already existed as the single-flight owner of catch-ups; the re-check's arming simply was not on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash, do not download.&lt;/strong&gt; GitHub's release API now returns a server-side &lt;code&gt;digest&lt;/code&gt; per asset; we compare it with the signed &lt;code&gt;SHA256SUMS&lt;/code&gt; instead of pulling 320 MB back over a 1 Mbit/s uplink.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the hardware.&lt;/strong&gt; &lt;code&gt;vcgencmd get_throttled&lt;/code&gt; on a Raspberry Pi should read &lt;code&gt;0x0&lt;/code&gt;. Ours read &lt;code&gt;0x50005&lt;/code&gt;. Half of incident 3 was a power supply.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything above is public: the releases with signed checksums are on &lt;a href="https://github.com/janzeerOrg/janzeer-node/releases" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, the chain is on the &lt;a href="https://explorer.janzeer.org" rel="noopener noreferrer"&gt;explorer&lt;/a&gt;, the numbers on the &lt;a href="https://janzeer.org/transparency" rel="noopener noreferrer"&gt;transparency page&lt;/a&gt; come straight from the ledger, and every incident, including these four, is kept on the &lt;a href="https://janzeer.org/incidents" rel="noopener noreferrer"&gt;incidents page&lt;/a&gt; with its finding and the release that fixed it.&lt;br&gt;
If you run distributed systems and see a hole in the reasoning, I would like to hear it.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>kotlin</category>
      <category>distributedsystems</category>
      <category>postmortem</category>
    </item>
    <item>
      <title>A Raspberry Pi 3 as a mainnet validator — what a 1 GB board teaches a blockchain node</title>
      <dc:creator>Malek</dc:creator>
      <pubDate>Mon, 19 Oct 2026 05:06:53 +0000</pubDate>
      <link>https://dev.to/syriandeveloper/a-raspberry-pi-3-as-a-mainnet-validator-what-a-1-gb-board-teaches-a-blockchain-node-3oap</link>
      <guid>https://dev.to/syriandeveloper/a-raspberry-pi-3-as-a-mainnet-validator-what-a-1-gb-board-teaches-a-blockchain-node-3oap</guid>
      <description>&lt;p&gt;I run a small Layer-1 called Janzeer: stake-free BFT, 15-second slots, two voting rounds, finality in one block. The four&lt;br&gt;
founding validators live on one 2 GB VPS, and I wrote about that box &lt;a href="https://dev.to/syriandeveloper/running-a-bft-blockchain-as-one-native-binary-on-a-2-gb-vps-what-broke-and-what-held-4ff1"&gt;last week&lt;/a&gt;. This post is about the fifth validator.&lt;br&gt;
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&lt;br&gt;
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&lt;br&gt;
out what it could not do fixed seven bugs in the node that four comfortable servers had never shown. No token talk, as&lt;br&gt;
before; the coin is not on any exchange.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  Getting a native image onto arm64
&lt;/h2&gt;

&lt;p&gt;The node is a Spring Boot application in Kotlin, shipped as a GraalVM native image: no JDK on the board, one 80 MB&lt;br&gt;
tarball, a 30-second boot on the Pi (3 seconds on the VPS). Building that image for arm64 was the first lesson.&lt;br&gt;
&lt;code&gt;native-image&lt;/code&gt; needs about 9 GB of heap for this project, which rules out building on the Pi itself. I built it in an&lt;br&gt;
emulated aarch64 virtual machine on my desktop: 2 h 45 min per build, against 6 minutes native on x86-64. It works, and&lt;br&gt;
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&lt;br&gt;
needed one yet.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What the board found in the node
&lt;/h2&gt;

&lt;p&gt;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&lt;br&gt;
public nodes run it.&lt;/p&gt;

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

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

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

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

&lt;p&gt;&lt;strong&gt;A validator one block behind downloaded 200 blocks to get it back.&lt;/strong&gt; Slot ownership rotates, so this node's turn&lt;br&gt;
comes every 75 seconds. Missing one live block, which happens on Wi-Fi, flipped it to catch-up mode, and catch-up asked&lt;br&gt;
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&lt;br&gt;
block behind again. It could sit in that loop for twenty minutes, producing nothing while reporting itself healthy. The&lt;br&gt;
server now remembers the requester's tip from the availability handshake that precedes every catch-up and serves only the&lt;br&gt;
blocks above it. A one-block catch-up is now 2–10 seconds, and the Pi makes its next slot.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;A lagging validator proposing with a stale tip showed up as an ERROR on everybody else's node&lt;/strong&gt;, and paged the&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the board taught the operator
&lt;/h2&gt;

&lt;p&gt;The code was half of it. The other half had nothing to do with the node.&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;Wi-Fi.&lt;/strong&gt; Today I changed the Wi-Fi password for two hours and changed it back. The Pi never rejoined. NetworkManager&lt;br&gt;
treats a run of authentication failures as wrong secrets and stops auto-connecting until something re-activates the&lt;br&gt;
connection; a reboot does, three hours later. The validator missed every one of its slots meanwhile, and the chain carried&lt;br&gt;
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&lt;br&gt;
connection when it is not up, and a persistent journal, because the volatile one took the evidence with it.&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;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&lt;br&gt;
slots. After today's incident it caught up 560 blocks in two minutes and produced the next slot it owned. Its block&lt;br&gt;
reward goes to the wallet that registered it, like anyone else's.&lt;/p&gt;

&lt;p&gt;The arm64 bundle is a signed download alongside Linux x86-64, Windows and an Arch package; installing it is a service&lt;br&gt;
file and one &lt;code&gt;tar&lt;/code&gt;. The deposit for a validator seat is 2,000 JNZ, and the project currently covers it for the first ten&lt;br&gt;
outside operators (details on the validators page). If you have a 64-bit board, a proper power supply and a port you can&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;Explorer: &lt;a href="https://explorer.janzeer.org" rel="noopener noreferrer"&gt;https://explorer.janzeer.org&lt;/a&gt;&lt;br&gt;
Validators: &lt;a href="https://janzeer.org/validators" rel="noopener noreferrer"&gt;https://janzeer.org/validators&lt;/a&gt;&lt;br&gt;
SDKs and wallet: &lt;a href="https://github.com/janzeerorg" rel="noopener noreferrer"&gt;https://github.com/janzeerorg&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>raspberrypi</category>
      <category>kotlin</category>
      <category>graalvm</category>
    </item>
    <item>
      <title>Running a BFT blockchain as one native binary on a 2 GB VPS — what broke and what held</title>
      <dc:creator>Malek</dc:creator>
      <pubDate>Sat, 10 Oct 2026 21:09:42 +0000</pubDate>
      <link>https://dev.to/syriandeveloper/running-a-bft-blockchain-as-one-native-binary-on-a-2-gb-vps-what-broke-and-what-held-4ff1</link>
      <guid>https://dev.to/syriandeveloper/running-a-bft-blockchain-as-one-native-binary-on-a-2-gb-vps-what-broke-and-what-held-4ff1</guid>
      <description>&lt;p&gt;I run a small Layer-1 called Janzeer: stake-free BFT, 15-second slots, two voting rounds, finality in one block. Mainnet went live on 2026-10-01. The whole network — four validator nodes, PostgreSQL, and a Telegram mini-game that pays players — runs on one 2 GB, single-core VPS. This post is about how that is possible and what went wrong on the way. No token talk; the coin is not on any exchange and that is not what this is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint
&lt;/h2&gt;

&lt;p&gt;The node is a Spring Boot application in Kotlin: Netty for P2P, JPA on PostgreSQL, a REST and JSON-RPC API, and it serves the explorer SPA. Four of those as JVMs would never fit in 2 GB with a database next to them. So every process is a GraalVM native image: a ~250 MB binary that boots in 3 seconds and is capped at &lt;code&gt;-Xmx256m&lt;/code&gt;. The binary's pages are shared by the four processes, so the cgroup charge is about 200 MB per node. The game backend is a native image too. Peak memory of the whole box during a 6-hour rehearsal, with load: 933 MB.&lt;/p&gt;

&lt;p&gt;Three rules keep the native image working, and each one was learned the hard way:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;@Lazy&lt;/code&gt; injections.&lt;/strong&gt; A lazy CGLIB proxy cannot be generated in the image. &lt;code&gt;ObjectProvider&lt;/code&gt; does the same job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every class that is instantiated reflectively must be registered.&lt;/strong&gt; The wire codec builds messages by reflection. The tracing agent only sees the messages that happen to flow during the trace, so the hints are written by hand for every message type. The first image pushed no WebSocket notifications because the notification envelope was missing from the hints — Jackson serialized it fine on the JVM and silently failed in the image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hibernate's bytecode provider off&lt;/strong&gt; (&lt;code&gt;hibernate.bytecode.provider=none&lt;/code&gt;) and a capped DB pool. An image otherwise grows towards a share of physical RAM per process.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The 7-day acceptance test
&lt;/h2&gt;

&lt;p&gt;Before going live, the public test network ran for a week with a monitor that scanned the logs for consensus errors, killed and relaunched nodes, and pushed load. Thirty-seven findings, all fixed before launch. The ones worth retelling:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four nodes booting together all hit &lt;code&gt;pool.ntp.org&lt;/code&gt; in the same instant.&lt;/strong&gt; Two got no answer and waited the full 60-second retry before asking again. Slot ownership is clock-based, so a node without a trusted clock does not produce: 75 seconds of empty slots at every joint start. The fix is boring and effective: a jittered exponential retry, 5 s → 10 s → 20 s, capped at 60.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A node relaunched on its own database started two sync sessions at once&lt;/strong&gt; when two peers answered its availability request, and committed a duplicated epoch genesis. Sync is now single-flight on one thread: the first answer starts the session, later answers are ignored, and a uniqueness constraint on block hash, height and epoch makes the duplicate impossible at the database level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The periodic tip check, when it ran on validators, kept a stall alive forever.&lt;/strong&gt; The check flips the node to PROCESSING, and a PROCESSING node drops every vote. Four anchors doing that in lock-step during a stall could never reach quorum again. Validators now catch up only through the live block that proves they are behind; the periodic check runs on watch-only nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL enforces read-only transactions; H2 does not.&lt;/strong&gt; The JSON-RPC &lt;code&gt;send*&lt;/code&gt; methods ran inside a read-only transaction template. Every test passed on H2. PostgreSQL refused the first real write. Anything that touches transaction boundaries is now tried on PostgreSQL before it ships, and a test pins the transaction attributes of those methods.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first live upgrades
&lt;/h2&gt;

&lt;p&gt;Shipping to the box is one script: it stages the native binaries, launchers, the game and the website into a folder, verifies that no secret ever enters it, and a second script on the VPS installs files, renders systemd units and nginx vhosts, and restarts what changed.&lt;/p&gt;

&lt;p&gt;The first upgrades restarted all four validators together. That cost about a minute of no blocks at first, then four minutes last week. The log showed why: at boot the node re-validated the last two epochs on disk — 17,000 blocks per epoch, 2.5 minutes on one core — while reporting itself as PROCESSING and not dialing anyone. Every restart paid that.&lt;/p&gt;

&lt;p&gt;Two changes fixed it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A clean-shutdown marker.&lt;/strong&gt; At &lt;code&gt;@PreDestroy&lt;/code&gt; the node writes the tip hash next to its config file. At boot it reads and deletes the marker; if the stored tip still matches, the blocks on disk were validated when they were committed and nothing changed since, so the sweep is skipped. A crash, an older build or a moved tip leaves no matching marker and the sweep runs as before. Restart time went from 2.5 minutes to 11 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling restarts.&lt;/strong&gt; The quorum is 3 of 4, so one node may be down. Slot ownership is round-robin, so a node restarted right after its own block has 60 seconds before its next turn. The deploy script now restarts one validator at a time, each right after it produced, and moves on only when it is back, synchronized and at the tip. It falls back to a joint restart only when the protocol version changed, because a node refuses a peer that speaks another version. The last upgrade: four nodes rolled, the chain never stopped, largest gap between blocks 15 seconds — one slot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A detail that bit us once: right after a restart, the other nodes can report one peer fewer for a few seconds, because a fresh connection is registered on one side before the other. The first production roll stopped before the fourth node on that. The pre-check now waits up to 90 seconds for the network to settle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone doing the same
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The native image is worth it for small boxes, but treat the tracing agent's output as a supplement. Write the reflection hints for everything your codec or your JSON envelope touches.&lt;/li&gt;
&lt;li&gt;Test your database semantics on the database you deploy. H2 in PostgreSQL mode is not PostgreSQL.&lt;/li&gt;
&lt;li&gt;Measure boot. A node that is "up" in 3 seconds but busy for 3 minutes is down for 3 minutes.&lt;/li&gt;
&lt;li&gt;Never restart a quorum together if you can avoid it. With round-robin slots and a 2f+1 quorum, a rolling restart timed after each node's own block costs zero blocks.&lt;/li&gt;
&lt;li&gt;An alert bot that reports the symptoms of your own planned restart trains you to ignore it. Mute the transient symptoms for the planned window, never the real dangers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The explorer has an animated page of the whole cycle, driven by the live chain, if you want to see the slots and votes move: &lt;a href="https://explorer.janzeer.org/workflow" rel="noopener noreferrer"&gt;https://explorer.janzeer.org/workflow&lt;/a&gt;. The SDKs (TypeScript, Dart, Kotlin, Python, Go) and the wallet are open source at &lt;a href="https://github.com/janzeerorg" rel="noopener noreferrer"&gt;https://github.com/janzeerorg&lt;/a&gt;; the node is a public download with a closed source for now.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>kotlin</category>
      <category>graalvm</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
