<?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>Forem: NOVAInetwork</title>
    <description>The latest articles on Forem by NOVAInetwork (@0xdevc).</description>
    <link>https://forem.com/0xdevc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3898802%2Fe797d15e-1e95-4646-8ada-b97a915c30a8.png</url>
      <title>Forem: NOVAInetwork</title>
      <link>https://forem.com/0xdevc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/0xdevc"/>
    <language>en</language>
    <item>
      <title>AI Entities as Protocol Primitives: Why I Didn't Use Smart Contracts</title>
      <dc:creator>NOVAInetwork</dc:creator>
      <pubDate>Mon, 04 May 2026 15:12:22 +0000</pubDate>
      <link>https://forem.com/0xdevc/ai-entities-as-protocol-primitives-why-i-didnt-use-smart-contracts-343d</link>
      <guid>https://forem.com/0xdevc/ai-entities-as-protocol-primitives-why-i-didnt-use-smart-contracts-343d</guid>
      <description>&lt;p&gt;I've been building an L1 blockchain called NOVAI. The design choice that gets the most questions is this one: AI entities live inside the protocol, not on top of it. There is no VM. No deployable contracts. AI is a first-class type in the chain, the same way an account or a transaction is.&lt;/p&gt;

&lt;p&gt;This post is about what that means in practice, why I made the call, and what it costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  How AI on chain usually works
&lt;/h3&gt;

&lt;p&gt;If you've looked at AI-on-chain projects in the last while, the pattern is roughly this. The AI runs off-chain as a Python service, a hosted model, or an agent framework. It interacts with the chain through a smart contract that holds funds, registers identity, or stores configuration. Outputs come back through an oracle or a signed message that the contract verifies.&lt;/p&gt;

&lt;p&gt;This works in the sense that you can ship something. But it leaves the chain blind. From the chain's point of view, there is no such thing as "an AI." There is an address. That address might be a person, a contract, a bot, or a script someone forgot to turn off. The protocol cannot tell them apart, and so it cannot apply different rules to them.&lt;/p&gt;

&lt;p&gt;The off-chain AI also has no native identity, no native memory, and no native economic agency. Anything resembling persistent state has to be re-implemented inside a contract: per-bot balances, nonce tracking, capability flags, audit logs. Every project does this slightly differently. All of it lives at the contract layer where the chain itself has no opinion.&lt;/p&gt;

&lt;p&gt;That is the square-peg-round-hole problem. Smart contracts were built for arbitrary user logic. Bolting AI on top means the chain treats AI like any other untyped caller, and developers carry the weight of inventing identity, memory, and economic primitives every time they ship a new agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  The decision
&lt;/h3&gt;

&lt;p&gt;I went the other direction. NOVAI does not have a VM. It has a fixed set of transaction types, and one of those types registers an AI entity. Here is the struct from &lt;code&gt;crates/ai_entities/src/lib.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;AiEntity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AiEntityId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;code_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CodeHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;creator&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="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;autonomy_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AutonomyMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Capabilities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;economic_balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;pubkey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;memory_root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;params_root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;registered_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;last_active_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&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;What each field means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt; is a 32-byte identifier computed as &lt;code&gt;blake3("NOVAI_AI_ENTITY_ID_V1" || code_hash || creator)&lt;/code&gt;. Same code and same creator produce the same id by design. Different creators get different ids even when running the same code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;code_hash&lt;/code&gt; is the hash of the module code or weights. The chain does not run the model. It records what model is supposed to be running.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;creator&lt;/code&gt; is the account that registered the entity.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;autonomy_mode&lt;/code&gt; is &lt;code&gt;Advisory&lt;/code&gt;, &lt;code&gt;Gated&lt;/code&gt;, or &lt;code&gt;Autonomous&lt;/code&gt; (reserved). Advisory entities can only emit signals. Gated entities can request actions that go through approval gates.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;capabilities&lt;/code&gt; is a bitfield with five flags: read public chain, read memory objects, emit proposals, request execution, read NNPX derived views.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;economic_balance&lt;/code&gt; is the entity's own balance, in a &lt;code&gt;u128&lt;/code&gt;. The entity pays its own fees from this. It is not the creator's wallet.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nonce&lt;/code&gt; increments per entity-signed transaction, like an account nonce.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pubkey&lt;/code&gt; is the entity's ed25519 public key. The entity signs its own transactions with the matching secret.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory_root&lt;/code&gt; and &lt;code&gt;params_root&lt;/code&gt; are roots over the entity's persistent on-chain memory and learned parameters.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_active&lt;/code&gt; flips to false if a governance rollback removes the entity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thing to land on: an AI entity has its own keypair and pays its own fees. It is not a function call dispatched by a user wallet. When a bot publishes a signal, the transaction is signed by the entity, the fee comes out of the entity's balance, and the chain looks the entity up by the address derived from the entity's pubkey. The chain knows an AI is talking. It applies AI-specific rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signals and memory
&lt;/h3&gt;

&lt;p&gt;Two more types matter. Signals are the AI's output to the chain. Memory objects are the AI's persistent storage on the chain.&lt;/p&gt;

&lt;p&gt;The signal commitment, from &lt;code&gt;crates/ai_entities/src/signals.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;SignalCommitment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;commitment_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;signal_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AiSignalType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;32&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;AiSignalType&lt;/code&gt; has seven variants: &lt;code&gt;Anomaly&lt;/code&gt;, &lt;code&gt;Optimization&lt;/code&gt;, &lt;code&gt;Prediction&lt;/code&gt;, &lt;code&gt;RiskScore&lt;/code&gt;, &lt;code&gt;AuditReport&lt;/code&gt;, &lt;code&gt;SpamRisk&lt;/code&gt;, &lt;code&gt;CongestionForecast&lt;/code&gt;. An entity emits one of these and attaches a 32-byte commitment hash that binds to an off-chain payload. The chain indexes the signal by issuer and height. Other entities, wallets, or the explorer can query &lt;code&gt;getSignalsByIssuer&lt;/code&gt; and read every signal an entity ever produced.&lt;/p&gt;

&lt;p&gt;Memory objects, from &lt;code&gt;crates/ai_entities/src/memory.rs&lt;/code&gt;, have five types: &lt;code&gt;ChainSummary&lt;/code&gt;, &lt;code&gt;LabelIndex&lt;/code&gt;, &lt;code&gt;EmbeddingCommitment&lt;/code&gt;, &lt;code&gt;AnomalyLog&lt;/code&gt;, &lt;code&gt;StatisticsSnapshot&lt;/code&gt;. The size of each object is capped at &lt;code&gt;MAX_MEMORY_OBJECT_SIZE = 65536&lt;/code&gt; bytes. The number of objects per entity is capped at &lt;code&gt;MAX_MEMORY_OBJECTS_PER_ENTITY = 100&lt;/code&gt;. These are protocol constants, not contract logic. Every entity has the same bounds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 10 transaction types
&lt;/h3&gt;

&lt;p&gt;Because there is no VM, the transaction surface is finite. Every transaction in every block is one of ten types, defined as constants in &lt;code&gt;crates/execution/src/lib.rs&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Transfer&lt;/td&gt;
&lt;td&gt;Send tokens between accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;SignalCommitment&lt;/td&gt;
&lt;td&gt;An AI entity publishes a signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;CreateMemoryObject&lt;/td&gt;
&lt;td&gt;Entity stores a memory object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;UpdateMemoryObject&lt;/td&gt;
&lt;td&gt;Entity updates a memory object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;DeleteMemoryObject&lt;/td&gt;
&lt;td&gt;Entity deletes a memory object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;SubmitProposal&lt;/td&gt;
&lt;td&gt;Submit a governance proposal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;ExecuteProposal&lt;/td&gt;
&lt;td&gt;Execute a passed proposal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;RegisterAiEntity&lt;/td&gt;
&lt;td&gt;Register an entity (no key)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;CreditAiEntity&lt;/td&gt;
&lt;td&gt;Top up an entity's balance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;RegisterAiEntityWithKey&lt;/td&gt;
&lt;td&gt;Register an entity with its own ed25519 key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dispatcher routes by the first byte of the payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;dispatch_tx&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KvBatch&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;K&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;TxV1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;current_height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ExecError&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;K&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...fee check...&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ai_entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_ai_entity_sender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&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="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="py"&gt;.payload&lt;/span&gt;&lt;span class="nf"&gt;.first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.copied&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.ok_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ExecError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;UnknownPayloadVersion&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;TRANSFER_PAYLOAD_V1&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;apply_tx_v1_transfer_inner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&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="n"&gt;ai_entity&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;SIGNAL_COMMITMENT_PAYLOAD_V1&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ai_entity&lt;/span&gt;&lt;span class="nf"&gt;.ok_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ExecError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;IssuerNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nf"&gt;apply_signal_commitment_tx_inner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&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="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_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;// ... and so on for the other eight ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line that does the AI-specific work is &lt;code&gt;check_ai_entity_sender&lt;/code&gt;. Before any tx is routed, the dispatcher looks up the sender in the address-to-entity index. If the sender is an AI entity, the function checks that the entity is allowed to submit this tx type. Signal commitments require the &lt;code&gt;emit_proposals&lt;/code&gt; capability. Memory writes require the &lt;code&gt;read_memory_objects&lt;/code&gt; capability. Governance, registration, and credit operations are denied to AI entities entirely. A normal account is unaffected by these checks.&lt;/p&gt;

&lt;p&gt;That is the protocol-level distinction the smart-contract approach cannot make. The chain knows.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this costs
&lt;/h3&gt;

&lt;p&gt;The trade-off, plainly. No VM means no arbitrary code. You cannot deploy a custom market-making contract, a token, or anything that does not map onto the ten types above. If your application needs that, NOVAI is the wrong chain.&lt;/p&gt;

&lt;p&gt;What you get in exchange:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Determinism by construction.&lt;/strong&gt; No floats anywhere in execution. Iteration over state is sorted. Two nodes with the same starting state and the same block produce the same state root, every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No gas surprises.&lt;/strong&gt; Every tx type has a minimum fee constant and a fixed worst-case cost. There are no out-of-gas reverts halfway through a tx. There is no quadratic blowup hidden inside a contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The chain understands every operation.&lt;/strong&gt; Indexing, audit logs, capability checks, and per-entity quotas are enforced at the protocol level. Memory objects capped at 100 per entity. Each object capped at 64 KiB. Transactions capped at 128 KiB. These are not conventions. They are invariants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI is a typed thing.&lt;/strong&gt; The chain can answer "is this address an AI?" with a state lookup. Every signal it ever published is indexed by issuer and height. Every memory object it owns is indexed by type. None of this requires a third-party indexer.&lt;/p&gt;

&lt;p&gt;That last point is the design payoff. AI on-chain identity is a primitive, not an afterthought.&lt;/p&gt;

&lt;h3&gt;
  
  
  A demo entity
&lt;/h3&gt;

&lt;p&gt;The repo has two demo bots in TypeScript. They are small enough to read in one sitting.&lt;/p&gt;

&lt;p&gt;The anomaly bot in &lt;code&gt;demos/anomaly-bot/&lt;/code&gt; registers itself as a Gated entity with its own ed25519 key, polls &lt;code&gt;novai_getLatestBlock&lt;/code&gt; every 1.5 seconds, and runs three detectors over a 50-block window: empty-block streak, head-stalled, and leader-rotation. Registration looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;registerAiEntityWithKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;creator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;REGISTER_FEE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;CODE_HASH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;AutonomyMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Gated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;readPublicChain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;readMemoryObjects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;emitProposals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;ENTITY_INITIAL_BALANCE&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;When a detector fires, the bot publishes a &lt;code&gt;SignalType.Anomaly&lt;/code&gt; signal and an &lt;code&gt;AnomalyLog&lt;/code&gt; memory object. Both are signed by the entity, not the creator. Both deduct fees from the entity's balance. The signal commitment is a domain-tagged blake3 hash of the detection details.&lt;/p&gt;

&lt;p&gt;The multi-entity demo in &lt;code&gt;demos/multi-entity/&lt;/code&gt; runs two of these. Bot A (the predictor) publishes a &lt;code&gt;Prediction&lt;/code&gt; signal and a &lt;code&gt;LabelIndex&lt;/code&gt; memory object every ten seconds. Bot B (the risk-scorer) reads Bot A's signals and memory objects via RPC, compares Bot A's predictions to actual block data, and publishes its own &lt;code&gt;RiskScore&lt;/code&gt; signal in response.&lt;/p&gt;

&lt;p&gt;The thing worth pointing at: Bot B never makes an HTTP call to Bot A. It calls &lt;code&gt;getSignalsByIssuer&lt;/code&gt; and &lt;code&gt;getMemoryObjects&lt;/code&gt; against the chain. The chain is the only integration surface. A third bot could plug into Bot A's outputs tomorrow with no coordination, no shared infrastructure, no API key.&lt;/p&gt;

&lt;p&gt;That is composability without contracts. The state shape is fixed by the protocol, so any entity can read any other entity's outputs deterministically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consensus integration
&lt;/h3&gt;

&lt;p&gt;There is one more piece worth showing. Vote messages in the BFT consensus layer carry an optional AI signal commitment. From &lt;code&gt;crates/consensus_types/src/lib.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Vote&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;block_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;voter&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="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="cd"&gt;/// Optional AI signal commitment (hash only, advisory).&lt;/span&gt;
    &lt;span class="cd"&gt;/// Does not affect vote validity.&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;ai_signal_commitment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;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;The comment matters. "Advisory" and "does not affect vote validity." The signal commitment does not gate consensus. A validator that includes one is volunteering a 32-byte pointer to an AI advisory output, and other nodes can fetch and verify it against the entity's published signals. Consensus is still consensus. The AI layer rides alongside it.&lt;/p&gt;

&lt;p&gt;The point is that this field exists at all. AI signals travel inside consensus messages as first-class data, not as a side channel. That is the kind of thing you can only do when AI is part of the protocol.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's next
&lt;/h3&gt;

&lt;p&gt;The chain runs. The four-node devnet boots from one script. The two demos run end to end.&lt;/p&gt;

&lt;p&gt;Next up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public testnet.&lt;/li&gt;
&lt;li&gt;More entity types and richer capability constraints.&lt;/li&gt;
&lt;li&gt;More demo entities, including ones that consume each other's memory objects in non-trivial ways.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/0x-devc/NOVAI-node" rel="noopener noreferrer"&gt;github.com/0x-devc/NOVAI-node&lt;/a&gt;. The architecture doc walks crate by crate. The first-AI-entity tutorial registers an entity in about ten minutes if you have Rust installed.&lt;/p&gt;

&lt;p&gt;Twitter: @NOVAInetwork&lt;/p&gt;

</description>
      <category>ai</category>
      <category>blockchain</category>
      <category>opensource</category>
      <category>rust</category>
    </item>
    <item>
      <title>I'm 18 and I built a Layer 1 blockchain from scratch in Rust</title>
      <dc:creator>NOVAInetwork</dc:creator>
      <pubDate>Mon, 27 Apr 2026 13:42:36 +0000</pubDate>
      <link>https://forem.com/0xdevc/im-18-and-i-built-a-layer-1-blockchain-from-scratch-in-rust-2e6n</link>
      <guid>https://forem.com/0xdevc/im-18-and-i-built-a-layer-1-blockchain-from-scratch-in-rust-2e6n</guid>
      <description>&lt;p&gt;I'm 18 and I'm building NOVAI, an AI-native Layer 1 blockchain written from scratch in Rust. This week I went open source and shipped a full set of developer docs. Here's everything that landed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The project
&lt;/h2&gt;

&lt;p&gt;NOVAI is a Layer 1 blockchain where AI entities are protocol primitives, not smart contracts. Most "AI blockchains" bolt AI onto an existing VM through oracle calls or contract wrappers. NOVAI does it differently. AI entities exist at the same level as accounts and validators. They have on-chain identity, persistent memory, economic balance, and capability flags. All enforced at the protocol layer.&lt;/p&gt;

&lt;p&gt;There is no smart contract VM. No WASM runtime. Every transaction type is a native protocol operation.&lt;/p&gt;

&lt;p&gt;The entire codebase is clean-room. No code from Substrate, Tendermint, Cosmos SDK, or any other implementation. 65,000+ lines of Rust across 16 crates, 1,100+ tests, zero unsafe code.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/0x-devc/NOVAI-node" rel="noopener noreferrer"&gt;github.com/0x-devc/NOVAI-node&lt;/a&gt;&lt;br&gt;
Website: &lt;a href="https://novai.network" rel="noopener noreferrer"&gt;novai.network&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What makes NOVAI different
&lt;/h2&gt;

&lt;p&gt;On most blockchains, "AI integration" means an off-chain model that pokes the chain through oracle calls or contract wrappers. The AI runs somewhere else. The chain just stores the result.&lt;/p&gt;

&lt;p&gt;NOVAI puts AI entities inside the protocol. An entity is a first-class on-chain identity that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Holds its own balance and pays its own fees&lt;/li&gt;
&lt;li&gt;Has its own Ed25519 signing key and signs its own transactions&lt;/li&gt;
&lt;li&gt;Publishes signal commitments (anomaly, prediction, risk-score, and 4 more types)&lt;/li&gt;
&lt;li&gt;Owns persistent memory objects (chain summaries, statistics snapshots, anomaly logs)&lt;/li&gt;
&lt;li&gt;Has governance-controlled autonomy modes and capability flags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The chain doesn't need to interpret bytecode to understand what an entity is doing. Every operation has known semantics at the protocol layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What shipped this week
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Open source launch
&lt;/h3&gt;

&lt;p&gt;The full codebase went public under Apache 2.0. Git history was cleaned. CI is green on GitHub Actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Developer docs - 5 deliverables
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Quick Start Tutorial&lt;/strong&gt; - "Build Your First AI Entity on NOVAI in 10 Minutes"&lt;/p&gt;

&lt;p&gt;Step-by-step CLI walkthrough. Generate keys, fund from faucet, register an AI entity with its own signing key, publish a signal, create a memory object, query everything back. Every command and output block is real captured data from a live 4-node devnet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/0x-devc/NOVAI-node/blob/main/docs/tutorials/FIRST_AI_ENTITY.md" rel="noopener noreferrer"&gt;Read it on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. TypeScript SDK Tutorial&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;170-line working example. Connect to a node, fund an account, transfer tokens, register an AI entity, verify it on chain. Self-contained npm project. Just run &lt;code&gt;npm install &amp;amp;&amp;amp; npm start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/0x-devc/NOVAI-node/tree/main/sdk/novai-sdk-ts/examples/quick-start" rel="noopener noreferrer"&gt;See the example&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Rust SDK Tutorial&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same flow in idiomatic async Rust on tokio. Single file, runs with &lt;code&gt;cargo run --example quick-start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/0x-devc/NOVAI-node/tree/main/sdk/novai-sdk/examples/quick-start" rel="noopener noreferrer"&gt;See the example&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. RPC Reference&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;777 lines covering all 13 JSON-RPC endpoints. Each one has a description, parameter table, response shape, error table, and a real curl command with captured output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/0x-devc/NOVAI-node/blob/main/docs/RPC_REFERENCE.md" rel="noopener noreferrer"&gt;Read it on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Architecture Deep Dive&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Crate-by-crate walkthrough of all 16 crates organized by dependency layer. Mermaid diagrams for the consensus flow and the transaction lifecycle. Three guided reading paths for newcomers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/0x-devc/NOVAI-node/blob/main/docs/ARCHITECTURE.md" rel="noopener noreferrer"&gt;Read it on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Block explorer
&lt;/h3&gt;

&lt;p&gt;React + Vite + Tailwind single-page app that calls the node's RPC endpoints. Live block list with 2-second polling, block detail page, account lookup, AI entity page with memory objects and signals, and a network stats dashboard. Developers run it locally against their devnet.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI entity demos
&lt;/h3&gt;

&lt;p&gt;Three runnable demos showing the AI-entity-as-protocol-primitive pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anomaly bot&lt;/strong&gt; - A TypeScript bot that registers itself as an on-chain entity, polls chain activity every 1.5 seconds, runs three heuristic detectors (empty block streaks, round spikes, stalled chains), and publishes an anomaly signal plus a memory object whenever one fires. Cooldowns prevent re-firing on the same condition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-entity demo&lt;/strong&gt; - Two bots interacting purely through the chain. Bot A (predictor) publishes prediction signals guessing future block tx counts. Bot B (risk-scorer) reads those predictions via on-chain memory objects, waits for the target block, compares predicted vs actual, and publishes a risk-score signal with the delta. No shared database. No API calls between them. Just on-chain data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI demo script&lt;/strong&gt; - Full entity lifecycle in bash with banner sections for blog posts or video recordings. Keygen, faucet, register, credit, signal publish, memory CRUD, query.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bug fix that unblocked everything
&lt;/h3&gt;

&lt;p&gt;While building the tutorials I found that entity-signed signal and memory transactions were silently failing through the RPC path. The root cause was four handlers using the wrong lookup key. They did a primary-key lookup with an address value instead of using the reverse index that maps address to entity ID. The entity record was never found so every signal and memory transaction quietly returned an error that got swallowed.&lt;/p&gt;

&lt;p&gt;The fix was refactoring all four handlers into inner functions that take a pre-resolved entity. Added 7 regression tests that exercise the full dispatch path. Verified end-to-end on a live devnet.&lt;/p&gt;

&lt;p&gt;I wrote about a similar silent-failure bug in my first blog post: &lt;a href="https://dev.to/0xdevc/the-bug-that-silently-broke-my-entire-blockchain-how-a-single-function-rejected-trailing-bytes-4fij"&gt;The Bug That Silently Broke My Entire Blockchain&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;65,000+ lines of Rust&lt;/li&gt;
&lt;li&gt;16 crates in the workspace&lt;/li&gt;
&lt;li&gt;1,100+ tests passing&lt;/li&gt;
&lt;li&gt;30M+ blocks committed on the private testnet&lt;/li&gt;
&lt;li&gt;Zero unsafe code&lt;/li&gt;
&lt;li&gt;10 native transaction types&lt;/li&gt;
&lt;li&gt;4-validator private testnet running since early 2026&lt;/li&gt;
&lt;li&gt;HotStuff BFT consensus with 3-chain commit rule&lt;/li&gt;
&lt;li&gt;Sparse Merkle Tree state with deterministic 32-byte roots&lt;/li&gt;
&lt;li&gt;Ed25519 signatures, Blake3 hashing, Noise XX transport encryption&lt;/li&gt;
&lt;li&gt;Apache 2.0 licensed&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Public testnet. The private testnet runs on a shared VPS that causes state root divergence under sustained load. The fix is a dedicated CPU server. Once that's in place we'll have a public RPC with SSL, validator onboarding, and the block explorer deployed at explorer.novai.network.&lt;/p&gt;

&lt;p&gt;I'm also looking for a technical co-founder. I'm building this solo. If you're a Rust engineer interested in BFT consensus, on-chain AI primitives, or clean-room blockchain development, the codebase is open and PRs are welcome.&lt;/p&gt;




&lt;p&gt;Website: &lt;a href="https://novai.network" rel="noopener noreferrer"&gt;novai.network&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/0x-devc/NOVAI-node" rel="noopener noreferrer"&gt;github.com/0x-devc/NOVAI-node&lt;/a&gt;&lt;br&gt;
Twitter: &lt;a href="https://x.com/NOVAInetwork" rel="noopener noreferrer"&gt;@NOVAInetwork&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>blockchain</category>
      <category>rust</category>
      <category>showdev</category>
    </item>
    <item>
      <title>The Bug That Silently Broke My Entire Blockchain How a single function rejected "trailing bytes" and made every block commit with zero transactions</title>
      <dc:creator>NOVAInetwork</dc:creator>
      <pubDate>Sun, 26 Apr 2026 13:29:55 +0000</pubDate>
      <link>https://forem.com/0xdevc/the-bug-that-silently-broke-my-entire-blockchain-how-a-single-function-rejected-trailing-bytes-4fij</link>
      <guid>https://forem.com/0xdevc/the-bug-that-silently-broke-my-entire-blockchain-how-a-single-function-rejected-trailing-bytes-4fij</guid>
      <description>&lt;p&gt;I spent two days debugging why my from-scratch Layer 1 blockchain committed every block with zero transactions, despite the mempool accepting them perfectly.&lt;/p&gt;

&lt;p&gt;This is the story of how I found it, what it taught me, and why silent failures are the hardest bugs in distributed systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I'm building &lt;a href="https://github.com/0x-devc/NOVAI-node" rel="noopener noreferrer"&gt;NOVAI&lt;/a&gt;, a Layer 1 blockchain in Rust with HotStuff BFT consensus. No forks, no frameworks. Every crate written from scratch. Four validators running on a local devnet, producing blocks at around 75 per second.&lt;/p&gt;

&lt;p&gt;The chain worked perfectly. Blocks committed. QCs formed. Validators voted. Everything was green.&lt;/p&gt;

&lt;p&gt;Then I added transaction support. And every single block committed with tx_count=0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptoms
&lt;/h2&gt;

&lt;p&gt;The RPC endpoint accepted transactions. Submitted, accepted, zero rejected. The mempool inserted them. &lt;code&gt;drain_ready&lt;/code&gt; pulled them into proposed blocks. But every committed block: zero transactions.&lt;/p&gt;

&lt;p&gt;No error logs. No panics. No warnings. The chain just kept producing empty blocks as if nothing was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The investigation
&lt;/h2&gt;

&lt;p&gt;I started with the obvious: is the mempool shared between the RPC thread and the consensus loop? I compared Arc pointer addresses. Same instance. Same mempool.&lt;/p&gt;

&lt;p&gt;Then I checked timing. Maybe the leader's block was being replaced by an empty one from a different leader after a timeout. I found a race condition in the timeout handler where &lt;code&gt;round_start_time&lt;/code&gt; could be read before the state lock was acquired. Fixed it. Blocks still empty.&lt;/p&gt;

&lt;p&gt;Next hypothesis: only node 0 had the RPC endpoint. The other three validators had empty mempools. When they were leader (75% of the time), they proposed empty blocks. I added transaction gossip so all validators share transactions over P2P. Still empty.&lt;/p&gt;

&lt;p&gt;I added diagnostic logging at every stage of the pipeline. &lt;code&gt;PROPOSE_DIAG&lt;/code&gt;, &lt;code&gt;COMMIT_DIAG&lt;/code&gt;, &lt;code&gt;VERIFY_DIAG&lt;/code&gt;, &lt;code&gt;QC_DIAG&lt;/code&gt;. Every block showed tx_count=0 at the proposal stage. Transactions were being drained from the mempool into the proposed block, but somehow vanishing before the block reached other validators.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Then I noticed something. The chain ran perfectly at around 75 blocks per second with empty blocks. But the moment a block contained even one transaction, the chain stalled. Timeouts fired. Round numbers escalated. No recovery.&lt;/p&gt;

&lt;p&gt;This wasn't a "transactions get lost" bug. This was a "transactions kill consensus" bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause
&lt;/h2&gt;

&lt;p&gt;Deep in the codec layer, there was a function called &lt;code&gt;decode_tx_v1_signed()&lt;/code&gt;. It decoded a single transaction from a byte buffer, then checked if there were any remaining bytes. If so, it rejected the input as "trailing bytes."&lt;/p&gt;

&lt;p&gt;This is correct behavior when decoding a standalone transaction from an RPC call. One transaction, one buffer, no leftovers.&lt;/p&gt;

&lt;p&gt;But inside &lt;code&gt;decode_block_v1()&lt;/code&gt;, the buffer contains multiple transactions concatenated together. The decoder would parse the first transaction, see the remaining transactions as "trailing bytes," and silently return an error.&lt;/p&gt;

&lt;p&gt;Every block with one or more transactions failed to decode at the network layer. Validators never received the proposal. They never voted. No quorum certificate formed. The chain stalled.&lt;/p&gt;

&lt;p&gt;The fix was one new function: &lt;code&gt;decode_tx_v1_signed_streaming()&lt;/code&gt;. It advances the cursor without checking for trailing bytes. Used exclusively inside block decoding. The original function is preserved for standalone transaction decoding where the trailing bytes check is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened after the fix
&lt;/h2&gt;

&lt;p&gt;The chain immediately started committing blocks with transactions. All four validators reaching consensus. Transaction gossip working across the network. The chain has since committed over 16 million blocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Silent failures are the hardest bugs in distributed systems. There were no error logs, no panics, no stack traces. The chain just produced empty blocks and looked healthy. Every metric was green except the one that mattered.&lt;/p&gt;

&lt;p&gt;Systematic elimination is the only approach that works. I ruled out dual mempool instances, lock contention, timeout races, leader rotation, cache eviction, and codec round-trip failures, one by one. Each hypothesis was tested, disproved, and crossed off.&lt;/p&gt;

&lt;p&gt;The fix was 20 lines of code. The investigation was two days. The ratio between understanding and implementation is always lopsided in distributed systems, and that's fine. The understanding is the hard part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The technical details
&lt;/h2&gt;

&lt;p&gt;For anyone working on custom binary codecs in Rust: be careful with "trailing bytes" checks in your decoders. They're correct for standalone message parsing but catastrophically wrong when the same decoder is reused inside a container format where multiple messages are concatenated. The streaming pattern (advance cursor, don't check for leftovers) is the right approach for container decoding.&lt;/p&gt;

&lt;p&gt;The codebase is on GitHub: &lt;a href="https://github.com/0x-devc/NOVAI-node" rel="noopener noreferrer"&gt;github.com/0x-devc/NOVAI-node&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;65,000+ lines of Rust. 4,000+ tests. Zero unsafe code.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>blockchain</category>
    </item>
  </channel>
</rss>
