<?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: Suliman Mokhtar</title>
    <description>The latest articles on DEV Community by Suliman Mokhtar (@sulimanmukhtar).</description>
    <link>https://dev.to/sulimanmukhtar</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%2F1307777%2F32838a04-093e-419d-8f20-92bb2143ec84.jpeg</url>
      <title>DEV Community: Suliman Mokhtar</title>
      <link>https://dev.to/sulimanmukhtar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sulimanmukhtar"/>
    <language>en</language>
    <item>
      <title>Solana Shred Recovery: When Valid Shreds Form an Invalid Erasure Batch</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:21 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/solana-shred-recovery-when-valid-shreds-form-an-invalid-erasure-batch-4dho</link>
      <guid>https://dev.to/sulimanmukhtar/solana-shred-recovery-when-valid-shreds-form-an-invalid-erasure-batch-4dho</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-shred-recovery-simd-0504" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A Solana block reaches validators as shreds, in batches of 32 data shreds and 32 coding shreds tied together by one Merkle root that the leader signs. Every shred in a batch can carry a valid signature and a valid Merkle proof while the batch itself is wrong, because nothing in the signature says the coding shreds are the parity of the data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/33c75cffdeb38468989313c6381c179a541808f1/proposals/0504-stricter-shred-validation.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0504&lt;/a&gt; says the rules for a batch apply to rebuilt shreds as much as to received ones, and between 24 October and 5 November Agave rebuilt its recovery path to enforce that: every validator now rebuilds every batch, so a batch like this fails everywhere. We modelled the old and new paths with the Reed–Solomon library Agave uses and ran four kinds of malformed batch past simulated validators losing packets. Under the old code, what a validator ended up doing depended on which shreds it happened to receive. Under the new code, every validator that collects 32 shreds rejects the batch: none of 800,000 rebuilds of a malformed batch passed.&lt;/p&gt;




&lt;h2&gt;
  
  
  How an erasure batch is built
&lt;/h2&gt;

&lt;p&gt;The leader cuts its block into data shreds and, for every 32 of them, computes 32 coding shreds with a Reed–Solomon code. Any 32 of the 64 are enough to rebuild the other 32, so a validator can lose half of a batch to the network and still have the block. Then the leader hashes all 64 shreds into a Merkle tree, signs the root and attaches to each shred its path to that root. A validator checks each shred on arrival: the signature over the root, and the path from the shred to the root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shreds per batch&lt;/strong&gt; — 32&amp;nbsp;data&amp;nbsp;+&amp;nbsp;32&amp;nbsp;coding&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Needed to rebuild the batch&lt;/strong&gt; — any&amp;nbsp;32&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload: data · coding shred&lt;/strong&gt; — 1,203&amp;nbsp;·&amp;nbsp;1,228&amp;nbsp;bytes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Erasure-coded part of each&lt;/strong&gt; — 987&amp;nbsp;bytes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merkle tree&lt;/strong&gt; — 64&amp;nbsp;leaves, 6&amp;nbsp;levels&lt;/p&gt;




&lt;h2&gt;
  
  
  What the signature does not cover
&lt;/h2&gt;

&lt;p&gt;The Merkle root commits to the bytes of each of the 64 shreds. It says nothing about how they relate. A leader can compute its coding shreds from different data, or change one byte of a shred after encoding, then build the tree and sign. Each shred still verifies on its own. The batch is not a Reed–Solomon codeword: rebuilding it from different subsets of shreds gives different answers.&lt;/p&gt;

&lt;p&gt;Until 29 October, Agave ran Reed–Solomon only when data shreds were missing and at least 32 shreds of the batch were present, tried again after every new insert of shreds, and logged a failed attempt at info level before moving on. The Merkle check made sure a rebuilt shred was byte-for-byte what the leader had signed, so a bad batch could never smuggle in different block data. But whether a validator noticed anything depended on which shreds it held. In our simulation of 2,000 validators per row, adding one shred per insert, each row counts the validators that completed the batch from data shreds alone · recovered without noticing the bad shred · were left waiting for repair:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First coding shred corrupted, 1% loss&lt;/strong&gt; — 1,492&amp;nbsp;·&amp;nbsp;0&amp;nbsp;·&amp;nbsp;508&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First coding shred corrupted, 5% loss&lt;/strong&gt; — 379&amp;nbsp;·&amp;nbsp;0&amp;nbsp;·&amp;nbsp;1,621&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last coding shred corrupted, 1% loss&lt;/strong&gt; — 62&amp;nbsp;·&amp;nbsp;1,934&amp;nbsp;·&amp;nbsp;4&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last coding shred corrupted, 5% loss&lt;/strong&gt; — 29&amp;nbsp;·&amp;nbsp;1,895&amp;nbsp;·&amp;nbsp;76&lt;/p&gt;

&lt;p&gt;Those in the first group never looked at a coding shred. With the first coding shred corrupted, every recovery that used it failed, about 31 failed attempts per validator even with no loss, and at 5% loss four validators in five could not complete the batch from Turbine. Repair fixes that, because it hands out the shreds the leader signed, but it takes longer. With the last coding shred corrupted, most recoveries never touched it and quietly succeeded. A corrupted data shred, or coding shreds computed from other data, behaved like the first case. Nothing in any row was rejected.&lt;/p&gt;




&lt;h2&gt;
  
  
  SIMD-0504: the batch is the unit of validity
&lt;/h2&gt;

&lt;p&gt;SIMD-0504, written by Philip Taffet of Firedancer, replaces the shred rules each client inferred from Agave’s code with a written list. Sixteen checks apply to each shred: index ranges, the fixed 32:32 layout, a Merkle tree of exactly six levels, flag and size rules. A shred that fails them is dropped. Eight checks apply to the batch as a whole: one slot, one shred version, one chained Merkle root, one parent across the data shreds, padding only at the end. A block containing a batch that fails them is invalid. Under TowerBFT a validator must not vote for it, and under &lt;a href="https://xroot.dev/blog/alpenglow-finality-in-numbers" rel="noopener noreferrer"&gt;Alpenglow&lt;/a&gt; it must vote skip.&lt;/p&gt;

&lt;p&gt;None of the sixteen or eight checks says the coding shreds must be the parity of the data. That follows from the proposal’s note on recovery: the batch checks apply to received and rebuilt shreds alike, so a validator that received all 32 data shreds must still check the coding shreds. The proposal says why: to eliminate any divergence based on whether a validator received or recovered a shred. A rule that only the validators who happened to need recovery could check would split the cluster the first time a leader broke it. Agave enforces the note by rebuilding every batch, and a batch whose coding shreds aren’t the parity of its data cannot be rebuilt to the root the leader signed.&lt;/p&gt;




&lt;h2&gt;
  
  
  “Always recover”: how Agave checks every batch
&lt;/h2&gt;

&lt;p&gt;Agave landed the change in a series of pull requests: &lt;a href="https://github.com/anza-xyz/agave/pull/16194?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16194&lt;/a&gt; wired the rebuild into blockstore on 29 October, &lt;a href="https://github.com/anza-xyz/agave/pull/16378?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16378&lt;/a&gt; added the feature gate and enforcement a day later, and &lt;a href="https://github.com/anza-xyz/agave/pull/16639?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16639&lt;/a&gt; stopped retrying a batch that cannot be rebuilt once SIMD-0504 is active. In &lt;a href="https://github.com/anza-xyz/agave/blob/9eacc06d4515ab46697e109cdf7667b606da6745/ledger/src/shred/merkle.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L819-L1017" rel="noopener noreferrer"&gt;the code at the time of writing&lt;/a&gt;, a batch is rebuilt exactly once, in the &lt;a href="https://github.com/anza-xyz/agave/blob/9eacc06d4515ab46697e109cdf7667b606da6745/ledger/src/blockstore.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L503-L549" rel="noopener noreferrer"&gt;insert&lt;/a&gt; that first brings it to 32 shreds, whether or not any data shred is missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;received exactly 32:  reconstruct the other 32 shards
received more than 32: reconstruct missing data from 32 of them,
                       re-encode all 32 coding shards,
                       compare with the coding shards received
then:                  rebuild the Merkle tree from all 64 leaves,
                       compare with the signed root
fail either check:     with SIMD-0504 active, mark the slot dead
                       (unless it was already full before this insert)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate is &lt;code&gt;stricter_shred_validation&lt;/code&gt;, key &lt;code&gt;s5o4YoA9zUrw9j4v335ncdG5PpznDBsrgM6CvUGrasA&lt;/code&gt;. On 6 November at 03:18 UTC its feature account did not exist on mainnet, testnet or devnet. Until it is activated, Agave runs every check and reports failures to its metrics, but rejects nothing it did not reject before.&lt;/p&gt;




&lt;h2&gt;
  
  
  Same verdict, whatever arrived
&lt;/h2&gt;

&lt;p&gt;Our model follows that code with the same library, version and features (&lt;a href="https://crates.io/crates/reed-solomon-erasure/6.0.0?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;reed-solomon-erasure 6.0.0&lt;/a&gt; with &lt;code&gt;simd-accel&lt;/code&gt;), 987-byte shards and Agave’s Merkle construction. We built four malformed batches, the first or last coding shred flipped by one bit after encoding, one data shred flipped, and all coding shreds computed from different data, and rebuilt each from 200,000 random subsets of every size from 32 to 64 shreds:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest batch, 200,000 subsets&lt;/strong&gt; — all&amp;nbsp;pass&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Malformed batches, 800,000 subsets&lt;/strong&gt; — 0&amp;nbsp;pass&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;…failed as Merkle mismatch&lt;/strong&gt; — 629,095&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;…failed as wrong parity&lt;/strong&gt; — 170,905&lt;/p&gt;

&lt;p&gt;The reason is simple once written down. The rebuild produces one Reed–Solomon codeword from the shreds it used. For the batch to pass, that codeword must equal the signed shreds at every position the validator did not receive (or the Merkle root differs) and at every coding position it did receive (or the parity check fails). The positions it used to rebuild match by construction. So passing requires the codeword to match all 64 signed shreds, which only happens if the batch was a valid codeword to begin with.&lt;/p&gt;

&lt;p&gt;In the packet-loss simulation every validator that reached 32 shreds flagged every malformed batch, at every loss rate from 0 to 45%, and passed the honest one. That held with one shred per insert, and again with inserts of a random 1 to 16 shreds, where about half the failures of the last-coding-shred batch came from the parity comparison rather than the Merkle root. The only validators without a verdict were those that never collected 32 shreds through Turbine, 16 to 19% of them at 45% loss and none at 20% or less; they run the same check when repair brings the batch to 32.&lt;/p&gt;




&lt;h2&gt;
  
  
  What rebuilding every batch costs
&lt;/h2&gt;

&lt;p&gt;Less than it sounds. The old code already rebuilt a batch whenever a data shred was missing when it reached 32 shreds, and that is nearly always. The leader sends a batch’s data and coding shreds together, and Turbine routes each shred down its own tree. If they arrive in random order, the first 32 are exactly the 32 data shreds with probability 1 in C(64, 32), about 5.5 × 10⁻¹⁹, which fits the pull request’s estimate that the old code skipped recovery less than once in a trillion batches. What is new is re-encoding the received coding shreds when an insert pushes a batch past 32 at once, and rebuilding batches made only of data shreds. Timed on our model, per batch:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Old recovery, 16 data + 16 coding&lt;/strong&gt; — 299&amp;nbsp;µs&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New rebuild, same 32 shreds&lt;/strong&gt; — 299&amp;nbsp;µs&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New rebuild, 24 data + 16 coding&lt;/strong&gt; — 325&amp;nbsp;µs&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New rebuild, 32 data and no coding&lt;/strong&gt; — 299&amp;nbsp;µs&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New rebuild, all 64 received&lt;/strong&gt; — 305&amp;nbsp;µs&lt;/p&gt;

&lt;p&gt;Most of that is hashing the 64 leaves of the Merkle tree, about 214 µs here, which both versions do; encoding 32 coding shreds takes about 83 µs. For the same 32 shreds the new path costs exactly what the old one did, and re-encoding the received coding shreds adds about 9% when more than 32 are present. These are timings of our model on an Apple M1 Pro; absolute numbers depend on the hardware, SHA-256 speed above all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Turbine changes around it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rebuilt coding shreds are now retransmitted.&lt;/strong&gt; A validator forwards rebuilt shreds to its children in Turbine, so one dropped packet upstream doesn’t starve everyone below. When an insert brings only data shreds, the rebuild produces coding shreds the validator never received, and the first version of always-recover, keeping Agave’s old output, threw them away. &lt;a href="https://github.com/anza-xyz/agave/pull/16622?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16622&lt;/a&gt; forwards them as well, which its description notes Firedancer already did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leaders no longer rebroadcast.&lt;/strong&gt; When a leader’s own slot had not been seen by enough stake, Agave’s replay stage asked the broadcast stage to send every data and coding shred of that slot again, after 5 seconds and then at doubling intervals up to 320 seconds. On 5 November &lt;a href="https://github.com/anza-xyz/agave/pull/16595?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16595&lt;/a&gt; removed Tower consensus from Agave’s replay stage as part of its Alpenglow cleanup, and the request went with it. Hours later &lt;a href="https://github.com/anza-xyz/agave/pull/16713?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16713&lt;/a&gt; deleted the broadcast worker that served those requests. The issue behind it had called the mechanism redundant if repair works. A validator missing shreds now has two ways to get them: rebuild them from the batch, or ask a peer through repair.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;We ran a model of Agave’s rebuild, not Agave: shred headers, signatures, the per-shred checks and the batch header checks are not in it, and a malformed batch here differs from an honest one only in its erasure coding. Arrival order and loss are random and independent, which real Turbine is not, and inserts are one shred or a random 1 to 16. We had no cluster, so nothing here was measured on a network; the timings are from an Apple M1 Pro, not validator hardware. Firedancer’s implementation of the same rules was out of scope.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shred Recovery: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a Solana erasure batch?
&lt;/h3&gt;

&lt;p&gt;A group of 64 shreds: 32 data shreds carrying the block and 32 coding shreds that are their Reed–Solomon parity, so any 32 of the 64 rebuild the rest. All 64 are leaves of one Merkle tree, and the leader signs its root. Agave and SIMD-0504 also call it an FEC set.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does SIMD-0504 change about shred validation?
&lt;/h3&gt;

&lt;p&gt;It replaces the checks validators inherited from Agave’s code with a written list: sixteen checks on each shred and eight on each erasure batch as a whole. A batch that fails them makes the block invalid: under TowerBFT a validator must not vote for it, under Alpenglow it must vote skip. The checks apply whether a shred was received or rebuilt, so a validator has to check the coding shreds even when all 32 data shreds arrived.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is “always recover” in Agave?
&lt;/h3&gt;

&lt;p&gt;Agave rebuilds every erasure batch once, in the batch of incoming shreds that first brings it to 32 shreds, even when no data shred is missing. It re-encodes the coding shreds from the data, compares them with the coding shreds it received, and rebuilds the Merkle tree from all 64 leaves to compare with the signed root. With the SIMD-0504 feature active, a failure marks the slot dead unless it was already full.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is SIMD-0504 active on Solana mainnet?
&lt;/h3&gt;

&lt;p&gt;No. Agave’s gate, stricter_shred_validation, was merged on 30 October 2026 and had no feature account on mainnet, testnet or devnet on 6 November. Until it is activated, Agave runs the checks and reports failures to metrics without rejecting anything new.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a Solana leader still rebroadcast slots that did not propagate?
&lt;/h3&gt;

&lt;p&gt;Not in Agave’s main branch. The replay code that asked a leader to resend an unpropagated slot was removed on 5 November 2026 along with Tower consensus in replay, and the broadcast worker that served those requests was deleted the same day. Missing shreds are recovered from erasure coding or fetched through repair.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;A leader can sign an erasure batch whose shreds each verify but whose coding shreds aren’t the parity of its data; SIMD-0504 makes that batch invalid, and Agave now rebuilds every batch once it has 32 shreds, so every validator rejects it the same way, whichever shreds reached it.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/33c75cffdeb38468989313c6381c179a541808f1/proposals/0504-stricter-shred-validation.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0504&lt;/a&gt; at commit &lt;code&gt;33c75cff&lt;/code&gt;; Agave &lt;a href="https://github.com/anza-xyz/agave/pull/16194?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16194&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/pull/16378?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16378&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/pull/16639?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16639&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/pull/16622?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16622&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/pull/16595?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16595&lt;/a&gt; and &lt;a href="https://github.com/anza-xyz/agave/pull/16713?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16713&lt;/a&gt;, read at commit &lt;code&gt;9eacc06d&lt;/code&gt;; the old recovery path at &lt;code&gt;dd6f564f&lt;/code&gt;. The model, simulation and timings are our own.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>turbine</category>
      <category>agave</category>
      <category>consensus</category>
    </item>
    <item>
      <title>Solana WebSockets That Stay Connected but Miss Updates</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:19 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/solana-websockets-that-stay-connected-but-miss-updates-21f7</link>
      <guid>https://dev.to/sulimanmukhtar/solana-websockets-that-stay-connected-but-miss-updates-21f7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-websocket-subscriptions-miss-updates" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your indexer subscribes to a program’s logs at &lt;code&gt;finalized&lt;/code&gt;, because it only wants data that can never roll back. The socket stays open. No error arrives, no disconnect, no lag warning. And some transactions that are finalized on chain never reach it.&lt;/p&gt;

&lt;p&gt;We measured how often on 5 November 2026, against the public mainnet RPC endpoint, which reported Agave &lt;code&gt;solana-core 4.4.0&lt;/code&gt;. Over half an hour, a finalized &lt;code&gt;logsSubscribe&lt;/code&gt; stream never looked at about &lt;strong&gt;3% of slots&lt;/strong&gt; and missed &lt;strong&gt;61 of 1,803&lt;/strong&gt; finalized transactions that &lt;code&gt;getSignaturesForAddress&lt;/code&gt; returns for the same slots. A &lt;code&gt;processed&lt;/code&gt; stream on the same connection missed none.&lt;/p&gt;

&lt;p&gt;The cause is a seven-year-old design decision meeting a consensus protocol that has run mainnet for two weeks. It was &lt;a href="https://github.com/anza-xyz/agave/issues/16654?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;reported&lt;/a&gt; on 3 November, and a &lt;a href="https://github.com/anza-xyz/agave/pull/16689?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;fix&lt;/a&gt; was proposed the same day. Here is what the stream reads, why it skips, which subscriptions are exposed, what the fix changes for clients, and the reconciliation loop that catches the gap today.&lt;/p&gt;




&lt;h2&gt;
  
  
  One finalized transaction in thirty never arrived
&lt;/h2&gt;

&lt;p&gt;The setup was three programs and three commitment levels. The Address Lookup Table program and the first Memo program land roughly one transaction every slot or two, so each of their transactions is an independent test of whether a stream looked at its slot. The second Memo program lands a transaction in almost every slot, so the slots a stream reported for it are the slots that stream scanned. Ground truth was &lt;code&gt;getSignaturesForAddress&lt;/code&gt; at &lt;code&gt;finalized&lt;/code&gt;, polled over the same window.&lt;/p&gt;

&lt;p&gt;All nine subscriptions shared one WebSocket connection, so one node served all three commitment levels. The run covered 05:36 to 06:07 UTC, slots 465,589,591 to 465,598,091. The endpoint dropped our connection about once a minute under the high-volume Memo stream (close code 1006, no close frame), so each level is judged only on slots strictly inside a stretch where its connection was live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;finalized — slots never scanned&lt;/strong&gt; — 53&amp;nbsp;of&amp;nbsp;1,664&amp;nbsp;· 3.2%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;finalized — finalized transactions missed&lt;/strong&gt; — 12&amp;nbsp;of&amp;nbsp;438&amp;nbsp;· 2.7%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;confirmed — slots never scanned&lt;/strong&gt; — 4&amp;nbsp;of&amp;nbsp;1,664&amp;nbsp;· 0.24%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;confirmed — finalized transactions missed&lt;/strong&gt; — 4&amp;nbsp;of&amp;nbsp;440 (3&amp;nbsp;in&amp;nbsp;one&amp;nbsp;slot)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;processed — slots never scanned&lt;/strong&gt; — 0&amp;nbsp;of&amp;nbsp;1,664&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;processed — finalized transactions missed&lt;/strong&gt; — 0&amp;nbsp;of&amp;nbsp;440&lt;/p&gt;

&lt;p&gt;A second run, with one connection per commitment level and so most likely three different nodes behind the load balancer, agreed. Finalized never scanned 93 of 3,152 slots (2.95%) and missed 49 of 1,365 transactions; confirmed skipped 8 slots (0.25%) and 2 of 2,415 transactions; processed skipped none. Across both runs, finalized missed 61 of 1,803.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;programSubscribe&lt;/code&gt; behaves the same way. A third connection subscribed to the Lookup Table program’s accounts at all three levels for 30 minutes. In the stretch all three covered, the confirmed stream delivered 37 table updates (3.0% of 1,253) that the finalized stream never sent, and the finalized stream delivered none that confirmed did not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The gaps are not random.&lt;/strong&gt; Almost every jump skipped exactly one slot. Of the 146 slots the finalized streams never scanned across both runs, 135 were the third slot of a leader’s four-slot window (slot number mod 4 = 2, which is how Agave numbers positions in a window), and so were all 12 the confirmed streams skipped. Neither the issue nor the pull request mentions it, and we have not traced the cause. Something deterministic in when that slot becomes rootable is a better guess than thread scheduling.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Each update reads exactly one slot
&lt;/h2&gt;

&lt;p&gt;Agave’s PubSub service is driven by commitment updates. When the processed or finalized slot moves, the notifier thread visits every subscription at that level. For each one, it loads &lt;em&gt;one&lt;/em&gt; bank, the one at the new commitment slot, and runs a query against it. What the query returns depends on the subscription type, and for two of them the answer is “only what happened in this slot”.&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="c1"&gt;// rpc/src/rpc_subscriptions.rs — one bank: the new commitment slot's&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bank_forks&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&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="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// programSubscribe's query against that bank&lt;/span&gt;
&lt;span class="n"&gt;bank&lt;/span&gt;&lt;span class="nf"&gt;.get_program_accounts_modified_since_parent&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;params&lt;/span&gt;&lt;span class="py"&gt;.pubkey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// runtime/src/bank.rs — which scans only that slot's own storage&lt;/span&gt;
&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.rc.accounts&lt;/span&gt;&lt;span class="nf"&gt;.load_by_program_slot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.slot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.bank_id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;program_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// logsSubscribe reads the same bank's log collector: its own transactions only&lt;/span&gt;
&lt;span class="n"&gt;bank&lt;/span&gt;&lt;span class="nf"&gt;.get_transaction_logs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pubkey&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;programSubscribe&lt;/code&gt; sees the accounts written &lt;em&gt;in&lt;/em&gt; the slot it was handed, and &lt;code&gt;logsSubscribe&lt;/code&gt; sees the transactions executed &lt;em&gt;in&lt;/em&gt; it. Nothing records which slots a subscription has already been shown: the “last notified slot” passes through these two filters untouched. So if the finalized slot moves from 5 to 7, slot 7 is scanned and slot 6 is not, by this update or by any later one.&lt;/p&gt;

&lt;p&gt;The one-slot read for program subscriptions dates to &lt;a href="https://github.com/anza-xyz/agave/commit/b053bc2790b81531f3561a1d903b76e0fc3be9e7?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;March 2019&lt;/a&gt;. It is not a regression. The change is in how the commitment slot moves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three ways the commitment slot jumps
&lt;/h2&gt;

&lt;p&gt;The issue names three paths, and each one is in the source.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Coalescing.&lt;/strong&gt; The commitment service drains its queue with &lt;code&gt;try_iter().last()&lt;/code&gt;: if two updates are waiting when it wakes, it keeps the newer one and the older slot is never announced. Both the TowerBFT and the Alpenglow inputs are read this way.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A skip vote the cluster overrules (processed).&lt;/strong&gt; Under Alpenglow, processed follows the node’s own notarize votes. If the node votes to skip a slot the cluster then notarizes, it never notarizes that slot, and a parent check stops it notarizing the rest of that leader window. Processed then jumps to the next window.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;One root for many slots (finalized).&lt;/strong&gt; Votor roots the highest finalized block it can root, and a single &lt;code&gt;Rooted&lt;/code&gt; message moves the node’s finalized and root slots to that slot at once. It is published only after the bank tree has been rooted and pruned, so the banks in between are gone by the time the notifier runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third path is the one our data shows. Alpenglow has decided finality on mainnet since &lt;a href="https://xroot.dev/blog/alpenglow-finality-in-numbers" rel="noopener noreferrer"&gt;21 October 2026&lt;/a&gt;, and under it a block is rootable only once the node has voted on it and replayed it. A finalized block that is not rootable yet when a later one is gets folded into the later root. The processed paths exist in the code, but they did not fire on this node during our half hour.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which subscriptions are exposed, and why confirmed is not a cure
&lt;/h2&gt;

&lt;p&gt;The one-slot read is specific to two subscription types. The others either read through ancestors or walk back over the slots they missed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;programSubscribe · logsSubscribe&lt;/strong&gt; — one slot per update&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;accountSubscribe — reads through ancestors&lt;/strong&gt; — latest state arrives&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;signatureSubscribe — status cache, through ancestors&lt;/strong&gt; — arrives&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;blockSubscribe — walks back to the last slot sent&lt;/strong&gt; — arrives&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;rootSubscribe — every newly rooted slot, in order&lt;/strong&gt; — arrives, twice&lt;/p&gt;

&lt;p&gt;Confirmed takes a different route, which is why it did twelve times better. Under Alpenglow there is no optimistic confirmation in the TowerBFT sense, so votor sends the RPC layer an “optimistically confirmed” notification for each new root, with a comment explaining it is &lt;a href="https://github.com/anza-xyz/agave/blob/32f5b38fd6bd940c025bb38fa3e4edd703fc92b7/votor/src/root_utils.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L67-L80" rel="noopener noreferrer"&gt;critical for compatibility with the RPC API&lt;/a&gt;. The tracker that receives it walks every ancestor above the last confirmed slot and issues one update per slot. On the same node, confirmed and finalized notifications for a slot arrived a median of 9 milliseconds apart: on an Alpenglow Agave node, confirmed is already root-timed.&lt;/p&gt;

&lt;p&gt;It still skipped 0.25% of slots, always the same window position as the finalized gaps. Our reading is a race with pruning. The confirmed walk follows parent links that &lt;a href="https://github.com/anza-xyz/agave/blob/32f5b38fd6bd940c025bb38fa3e4edd703fc92b7/runtime/src/bank.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L3387" rel="noopener noreferrer"&gt;squashing a root clears&lt;/a&gt;, and each slot it queues is then loaded from a bank tree that rooting prunes. Both happen at the moment a slot becomes final. We have not proven which of the two loses in our samples.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix on the table, and what it changes for clients
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/anza-xyz/agave/pull/16689?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Pull request 16689&lt;/a&gt; changes the read from “one slot” to “every slot since the last one this subscription was shown”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Snapshots before rooting.&lt;/strong&gt; Each bank’s account writes and transaction logs are copied into a retained cache before rooting runs, so pruning and accounts cleaning cannot erase an update that has not been delivered yet. Rooted banks are captured from the same function that rooted them.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Delivery in order, once.&lt;/strong&gt; On each commitment update, the notifier walks from the last bank this subscription was shown to the new slot and delivers each in order, deduplicating frozen banks by bank ID. The confirmed path gets the same retained chain.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;A visible change in context.slot.&lt;/strong&gt; Today a notification’s &lt;code&gt;context.slot&lt;/code&gt; is the commitment slot that triggered it. With the fix it is the slot where the change happened, so a jump from 5 to 7 arrives as an update for 6 and then one for 7. Code that treats &lt;code&gt;context.slot&lt;/code&gt; as “the current finalized slot” will see it lag behind.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Bounded, and loud when exceeded.&lt;/strong&gt; The cache is capped at the PubSub queue budget (&lt;code&gt;--rpc-pubsub-queue-capacity-bytes&lt;/code&gt;, 256 MiB by default) and at 512 banks, under two minutes at the 4.7 slots a second we measured. A subscription that needs a bank already evicted is disconnected through the same path a lagging client takes today, instead of being skipped silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As of 5 November the pull request is open with no reviews, and no release contains it. The public endpoint we measured runs 4.4.0, which has the one-slot read, the coalescing and the root-timed confirmed path exactly as described above.&lt;/p&gt;




&lt;h2&gt;
  
  
  Catching the gap in your own pipeline
&lt;/h2&gt;

&lt;p&gt;No subscription setting makes a PubSub stream complete today, so the dependable move is to diff it against the ledger. For a logs stream, &lt;code&gt;getSignaturesForAddress&lt;/code&gt; at finalized is the list of everything that should have arrived. This is the loop we ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ws: an open WebSocket to your RPC node · rpc(): a plain JSON-RPC POST helper&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PROGRAM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AddressLookupTab1e1111111111111111111111111&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GRACE_SLOTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ~30 s for the stream to deliver before we call it missed&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// signatures the stream delivered&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;fromSlot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// first slot the stream was live for&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// newest signature already reconciled&lt;/span&gt;

&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logsSubscribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;mentions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;PROGRAM&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="na"&gt;commitment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finalized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logsNotification&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;fromSlot&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signature&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fromSlot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sigs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getSignaturesForAddress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;PROGRAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;commitment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finalized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cursor&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sigs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Only judge signatures the stream has had time to deliver.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sigs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;sigs&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="nx"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;GRACE_SLOTS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;settled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;settled&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="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;settled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fromSlot&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// fetch with getTransaction and apply it&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;missed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in slot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slot&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="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Against mainnet from 07:57 UTC for three and a half minutes, it checked 239 finalized signatures and flagged 5 that the stream had skipped, every one of them in the third slot of a leader window. The grace period matters: the HTTP call and the WebSocket can land on different nodes, and without it a signature the stream is about to deliver looks missed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Subscribe at confirmed to shrink the backlog.&lt;/strong&gt; On an Alpenglow Agave node it arrived with finalized and skipped a twelfth as often. Gate on &lt;code&gt;rootSubscribe&lt;/code&gt; if you need finality to be explicit rather than a property of today’s implementation: the source says the confirmed notification may be deprecated. Make that gate idempotent. On the same endpoint, &lt;code&gt;rootSubscribe&lt;/code&gt; sent each root twice, once on its own and again just before the next one (127 notifications for 66 roots in 15 seconds).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Treat every disconnect as a gap.&lt;/strong&gt; Reconcile from the last slot you finished. After the fix, falling behind the retention window is exactly how a gap will announce itself.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;For account state, re-read what the missed transactions touched.&lt;/strong&gt; &lt;code&gt;getMultipleAccounts&lt;/code&gt; with &lt;code&gt;minContextSlot&lt;/code&gt;, or &lt;code&gt;accountSubscribe&lt;/code&gt; for a known set, which reads through ancestors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Know which path you are on.&lt;/strong&gt; Geyser plugins are fed from replay, not from this notifier, so a Geyser-based stream does not go through the one-slot read. A provider’s own WebSocket product may or may not be Agave’s PubSub underneath; the reconciliation loop works either way.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;One endpoint, one morning, half an hour per run, and three programs. The rate will move with load and with the node. The logs numbers are checked against the ledger; the &lt;code&gt;programSubscribe&lt;/code&gt; number is a comparison with the confirmed stream on the same connection, because a lookup table can change without an account notification under that owner (a closed table, for one).&lt;/p&gt;

&lt;p&gt;We did not build or run the patched node, so section 05 describes the fix from its code and tests, not from measurement. Processed showed no gaps here, although the paths that would skip it are real. TowerBFT is not measured at all: mainnet no longer runs it. The one-slot read is as old under TowerBFT as under Alpenglow, so the exposure is not new, but we cannot say how often it fired before 21 October. The leader-window clustering is unexplained.&lt;/p&gt;




&lt;h2&gt;
  
  
  Missed WebSocket Updates: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does programSubscribe miss updates on Solana?
&lt;/h3&gt;

&lt;p&gt;At processed and finalized commitment on an Agave node, yes, whenever the commitment slot advances by more than one slot. Each update reads only the accounts written in the one slot it was triggered for, so writes in the slots it jumped over are never sent. On mainnet on 5 November 2026 a finalized stream jumped over about 3% of slots.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is confirmed commitment safe to use instead of finalized?
&lt;/h3&gt;

&lt;p&gt;It is much better but not complete. On an Agave 4.4 node under Alpenglow, confirmed notifications are sent when a block is rooted and walk every slot since the last one, so they arrive within milliseconds of finalized and skipped about 0.25% of slots in our measurement against 3% for finalized. Reconcile against the ledger either way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does accountSubscribe have the same problem?
&lt;/h3&gt;

&lt;p&gt;No. accountSubscribe reads the account through all ancestors of the commitment slot and sends whenever its last-modified slot changes, so the latest state always arrives. signatureSubscribe, blockSubscribe and rootSubscribe also cover the slots in between.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I detect missed logsSubscribe notifications?
&lt;/h3&gt;

&lt;p&gt;Diff the stream against getSignaturesForAddress for the same program at finalized commitment, after giving the stream a grace period to deliver, and fetch anything missing with getTransaction. Treat every disconnect as a gap to reconcile as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Has the fix shipped?
&lt;/h3&gt;

&lt;p&gt;Not as of 5 November 2026. Pull request 16689 in the Agave repository, opened on 3 November, retains the skipped slots and delivers them in order. It is open and unreviewed, and no release contains it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;On Agave today, a finalized &lt;code&gt;programSubscribe&lt;/code&gt; or &lt;code&gt;logsSubscribe&lt;/code&gt; stream is a sample of the finalized slots, not the set. On mainnet it is about 97% of them.&lt;/p&gt;

&lt;p&gt;Subscribe at confirmed, reconcile against the ledger, and treat a disconnect as a gap. When the fix ships, a gap will close your socket instead of passing unnoticed.&lt;/p&gt;

&lt;p&gt;The commitment levels themselves are explained in &lt;a href="https://xroot.dev/blog/solana-deposit-confirmations-how-many" rel="noopener noreferrer"&gt;how many confirmations a deposit needs&lt;/a&gt;, and the certificates behind them in &lt;a href="https://xroot.dev/blog/alpenglow-finality-in-numbers" rel="noopener noreferrer"&gt;Alpenglow finality in numbers&lt;/a&gt;. A pipeline built on these streams is the subject of &lt;a href="https://xroot.dev/blog/solana-pipeline-part-2" rel="noopener noreferrer"&gt;ingesting the Solana firehose with Rust&lt;/a&gt;, and the errors a reconnect can return are in &lt;a href="https://xroot.dev/blog/solana-rpc-error-codes-explained" rel="noopener noreferrer"&gt;the RPC error codes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sources: Agave master at commit &lt;code&gt;32f5b38fd6&lt;/code&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/32f5b38fd6bd940c025bb38fa3e4edd703fc92b7/rpc/src/rpc_subscriptions.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L134-L173" rel="noopener noreferrer"&gt;rpc_subscriptions.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/32f5b38fd6bd940c025bb38fa3e4edd703fc92b7/runtime/src/bank.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L5405-L5412" rel="noopener noreferrer"&gt;bank.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/32f5b38fd6bd940c025bb38fa3e4edd703fc92b7/core/src/commitment_service.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L133-L150" rel="noopener noreferrer"&gt;commitment_service.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/32f5b38fd6bd940c025bb38fa3e4edd703fc92b7/votor/src/event_handler.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L954-L1016" rel="noopener noreferrer"&gt;votor’s event_handler.rs&lt;/a&gt; and &lt;a href="https://github.com/anza-xyz/agave/blob/32f5b38fd6bd940c025bb38fa3e4edd703fc92b7/rpc/src/optimistically_confirmed_bank_tracker.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L246" rel="noopener noreferrer"&gt;optimistically_confirmed_bank_tracker.rs&lt;/a&gt;; the same paths checked in the &lt;a href="https://github.com/anza-xyz/agave/releases/tag/v4.4.0?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;v4.4.0 release&lt;/a&gt;; issue 16654 and pull request 16689 as of commit &lt;code&gt;cfa411b669&lt;/code&gt;. Measurements: our own WebSocket and JSON-RPC calls to the public mainnet endpoint on 5 November 2026, 05:34 to 06:09 and 07:57 to 08:01 UTC.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>rpc</category>
      <category>websockets</category>
      <category>indexing</category>
    </item>
    <item>
      <title>Two Tokens, One Stock: The Weekend Arbitrage Nobody Ran</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:17 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/two-tokens-one-stock-the-weekend-arbitrage-nobody-ran-da5</link>
      <guid>https://dev.to/sulimanmukhtar/two-tokens-one-stock-the-weekend-arbitrage-nobody-ran-da5</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/two-tokens-one-stock-weekend-arbitrage" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Robinhood Chain now has two tokens for NVIDIA, two for Tesla and two for nine other stocks and ETFs: one issued by Robinhood, one by Ondo. Since 26 October, eleven Uniswap v4 pools let anyone swap one for the other at a fixed rate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ondo.finance/blog/ondo-stocks-live-on-robinhood-chain-and-uniswap?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Ondo’s announcement&lt;/a&gt; on 27 October gave a reason the pairing matters. Robinhood’s tokens cannot be minted or redeemed on weekends, so their price can drift. An eligible trader could buy a discounted Robinhood token, swap it for the Ondo token, then bridge and redeem that one, which works on weekends. We read the chain from the pools’ launch to the end of 3 November (UTC): every swap, deposit and bridge mint, plus the Robinhood tokens’ main markets for comparison. The pools held their rate to within a basis point. On the first weekend no one ran the trade, and the discount it needs was not there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two issuers, one ratio per stock
&lt;/h2&gt;

&lt;p&gt;The two tokens are not the same instrument. &lt;a href="https://docs.robinhood.com/chain/stock-tokens?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Robinhood Stock Tokens&lt;/a&gt; are debt securities of Robinhood Assets (Jersey) Limited, minted and burned only by an authorised participant between Monday 02:00 and Saturday 02:00 CET. Ondo’s tokens are issued by Ondo Global Markets (BVI) Limited and minted and redeemed by eligible users on Ethereum, BNB Chain and Solana, and &lt;a href="https://docs.ondo.finance/ondo-stocks/off-hours-trading?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;over the weekend&lt;/a&gt; for all eleven paired tickers. &lt;a href="https://docs.ondo.finance/ondo-stocks/eligibility?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Neither is open to US persons&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Each issuer also folds dividends and corporate actions into its own shares-per-token figure, so the pools do not trade at one for one. Each stores a reference rate, in Ondo tokens per Robinhood token:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SPCX, MSTR, CRCL, TSLA&lt;/strong&gt; — 1.00000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NVDA&lt;/strong&gt; — 0.99906&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GOOGL · META&lt;/strong&gt; — 0.99774 · 0.99772&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AAPL · QQQ&lt;/strong&gt; — 0.99720 · 0.99663&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SPY&lt;/strong&gt; — 0.99232&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GME&lt;/strong&gt; — 0.98315&lt;/p&gt;

&lt;p&gt;Where the numbers can be checked, they are exact. Ondo’s share-multiplier oracle on Ethereum answers for three of these tokens: SPYon, QQQon and TSLAon. For each of those, the pool’s reference equals Robinhood’s multiplier divided by Ondo’s, to six decimal places. For the other eight the oracle returned “asset not found”.&lt;/p&gt;

&lt;p&gt;Ondo listed four more tokens on the chain without a Robinhood counterpart. One of them changed a day later: Robinhood minted its first BMNR token on 28 October at 14:05 UTC. By 3 November there was still no StablePair pool for it.&lt;/p&gt;




&lt;h2&gt;
  
  
  How a StablePair pool holds the rate
&lt;/h2&gt;

&lt;p&gt;The pools run on Uniswap’s &lt;a href="https://github.com/Uniswap/v4-hooks-public/blob/fa163e5c3484dbc2ba8c97c4686fe5327c03569b/docs/technical/StablePairHook.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;StablePair hook&lt;/a&gt;, a dynamic-fee hook for two assets meant to hold a known price. Its reference is not an oracle. It is a number stored per pool when the pool is created, and it changes only when an account with the config-manager role calls &lt;a href="https://github.com/Uniswap/v4-hooks-public/blob/fa163e5c3484dbc2ba8c97c4686fe5327c03569b/src/stable/base/StableFeeConfiguration.sol?utm_source=xroot.dev&amp;amp;utm_medium=referral#L42-L63" rel="noopener noreferrer"&gt;&lt;code&gt;updateFeeConfig&lt;/code&gt;&lt;/a&gt;. Around that number the hook sets every swap’s fee:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inside a ±0.005% band&lt;/strong&gt; — all buys one price, all sells another&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round trip inside the band&lt;/strong&gt; — 1 basis point&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outside, toward the reference&lt;/strong&gt; — decaying fee, ×0.99 per block&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outside, away from the reference&lt;/strong&gt; — no fee&lt;/p&gt;

&lt;p&gt;All eleven pools use the same settings, and no reference changed through 3 November. You can read any pool’s reference yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# feeConfig(bytes32 poolId) on the hook: returns k, optimalFeeE6,&lt;/span&gt;
&lt;span class="c"&gt;# targetMultiplier, referenceSqrtPriceX96. Pool below: NVDA / NVDAon.&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://rpc.mainnet.chain.robinhood.com &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
        "to":"0xd061f665d1c4816787fceaa168fa66c59d3abcc0",
        "data":"0xb9629a414c71fa93d5a448afc030cfec54e1779ed1d2498c905569ee817eb5cadcbdc6fb"
      },"latest"]}'&lt;/span&gt;
&lt;span class="c"&gt;# rate = (referenceSqrtPriceX96 / 2^96)^2 = 0.99906 NVDAon per NVDA&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  One account set up all eleven
&lt;/h2&gt;

&lt;p&gt;Everything happened on Monday 26 October, in about an hour:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;12:27⁠–⁠12:34 UTC&lt;/strong&gt; — Ondo tokens bridged in to one address&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;12:47:52&lt;/strong&gt; — hook deployed, all roles to that address&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;12:51:41⁠–⁠12:58:14&lt;/strong&gt; — eleven pools initialized&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;13:00⁠–⁠13:34&lt;/strong&gt; — one deposit per pool, about $517,000&lt;/p&gt;

&lt;p&gt;The address, &lt;a href="https://robinhoodchain.blockscout.com/address/0x9d9210ea422bcfef44667d3df14d01916154c0e4?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;&lt;code&gt;0x9d9210ea…4c0e4&lt;/code&gt;&lt;/a&gt;, is an ordinary account, not a contract. It received the bridged Ondo tokens, holds the hook’s admin, pool-initializer and config-manager roles, created every pool and seeded each with roughly equal value of both tokens, about $42,000 to $54,000 per pool. At the end of 3 November the pools held between 87.4% (GOOGLon) and 100.0% (GMEon) of each Ondo token’s supply on the chain.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://robinhoodchain.blockscout.com/address/0xd061f665d1c4816787fceaa168fa66c59d3abcc0?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;hook&lt;/a&gt; is an upgradeable proxy, and &lt;a href="https://github.com/Uniswap/hooklist/blob/a55967cb18f8fdc6d1adc50855024f28cad401bd/hooks/robinhood/0xd061f665d1c4816787fceaa168fa66c59d3abcc0.json?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Uniswap’s hook list&lt;/a&gt; records it that way. So the key behind that one address can change any pool’s exchange rate and replace the hook’s code. That says nothing about intent, but anyone providing liquidity or quoting a price off these pools is trusting that key.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first week, swap by swap
&lt;/h2&gt;

&lt;p&gt;Dollar figures below use each Robinhood token’s last price before Friday’s 4pm close on its busiest USDG pool:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swaps, all eleven pools&lt;/strong&gt; — 232, from 127 senders&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume&lt;/strong&gt; — about $47,800&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same tokens, main v3 USDG pools&lt;/strong&gt; — about $99.4 million&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Share through the pegged pools&lt;/strong&gt; — 0.05%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swaps within 1 bp of the reference&lt;/strong&gt; — 179 of 232&lt;/p&gt;

&lt;p&gt;Most swaps executed at the edge of the band, exactly as the design intends. Two pools moved further. In GOOGL, one 17-token trade on 28 October pushed the price 7.1 basis points below the reference, and two trades minutes later pulled it back. On 29 and 30 October a run of one-way trades took it 5.5 basis points below again. While it was out of the band, trades pushing further paid no fee and trades pushing back paid 1 to 1.8 basis points. The pool ended the week 4.1 basis points below.&lt;/p&gt;

&lt;p&gt;GME is the week’s largest trade, and it ran the other way from the announcement’s example. At 05:30 UTC on Wednesday 28 October, one address bridged 838.3 GMEon onto the chain. Four minutes later it swapped 837.9 of them for 851.1 of Robinhood’s GME tokens, a $20,600 trade at prices 13.6 basis points worse than the reference. That emptied the pool’s Robinhood side from 940 tokens to 89 and left its price 26.2 basis points from the reference. Only one later trade, of 0.11 tokens, pushed back, so the gap stayed open through the end of the week: a 25-basis-point edge for anyone holding Robinhood GME tokens who had a use for GMEon.&lt;/p&gt;




&lt;h2&gt;
  
  
  The weekend
&lt;/h2&gt;

&lt;p&gt;Robinhood’s window closed at 01:00 UTC on Saturday 31 October and reopened at 01:00 UTC on Monday 2 November, after Europe had left summer time. That is 48 of the 203 hours we observed. In the pegged pools:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weekend swaps&lt;/strong&gt; — 16 of 232&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Robinhood → Ondo token&lt;/strong&gt; — $2,222&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;…swapped straight back (QQQ)&lt;/strong&gt; — $2,019&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Net, mostly one CRCL trade&lt;/strong&gt; — about $200&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ondo tokens bridged in or out&lt;/strong&gt; — none&lt;/p&gt;

&lt;p&gt;The announcement’s trade ends with a bridge and a redemption. Not a single Ondo token was minted or burned on Robinhood Chain over the weekend. In fact none was after 09:47 UTC on 28 October.&lt;/p&gt;

&lt;p&gt;The likely reason is that the discount never opened. The Robinhood tokens’ busiest USDG markets are Uniswap v3 pools, and the busiest one for each token carried $99.4 million between them in the same week. Over the weekend their volume fell to 3.0% of the week’s total, in 24% of its hours. Excluding trades under $10, whose prices are rounding noise, here is where each token traded relative to its Friday 4pm price:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NVDA&lt;/strong&gt; — avg −0.13% · low −1.13%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;META&lt;/strong&gt; — avg −0.14% · low −0.79%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GME&lt;/strong&gt; — avg −0.22% · low −0.94%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AAPL&lt;/strong&gt; — avg −0.09% · low −0.49%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TSLA · SPY&lt;/strong&gt; — avg +0.05% · +0.14%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QQQ · CRCL&lt;/strong&gt; — avg +0.54% · +0.53%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GOOGL · SPCX&lt;/strong&gt; — avg +1.06% · +1.79%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MSTR&lt;/strong&gt; — avg +2.29% · high +3.26%&lt;/p&gt;

&lt;p&gt;In 8 of the 11, the weekend average sat between Friday’s close and the first hour of Monday’s session: the market pricing weekend news before the open. NVDA, META and GME averaged 0.13% to 0.22% below Friday. That is before the v3 pool’s own fee, the bridge and Ondo’s off-hours spread, which its docs say can be wider. The loop had nothing to capture.&lt;/p&gt;




&lt;h2&gt;
  
  
  What would make the two tokens diverge
&lt;/h2&gt;

&lt;p&gt;Two limits matter when a real gap does open. The first is capacity. The pools hold almost every Ondo token on the chain, so the trade can only take what a pool holds, roughly $21,000 to $27,000 of Ondo tokens per stock at launch. GME shows what happens next: one trade empties a side and the pool stops correcting.&lt;/p&gt;

&lt;p&gt;The second is the stored rate. The hook does not know when either issuer’s shares-per-token figure changes, for example on an ex-dividend date. Until the config manager updates the reference, the pool keeps quoting the old ratio. Because the hook charges nothing to swaps that move the price away from the reference, the pool can be traded to the new ratio for free, and its liquidity providers absorb the difference.&lt;/p&gt;

&lt;p&gt;The token pages for both issuers’ contracts on this chain are in our &lt;a href="https://app.xroot.dev/robinhood/tokens?ref=blog" rel="noopener noreferrer"&gt;Robinhood Chain token list&lt;/a&gt;, and &lt;a href="https://xroot.dev/blog/robinhood-chain-read-directly" rel="noopener noreferrer"&gt;reading the chain directly&lt;/a&gt; covers the RPC and explorer used here.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;This is one week and one weekend. Dollar values come from on-chain USDG prices, not from the stock market, and there is no on-chain fair value for a stock on a Saturday. We could check the reference rate against both issuers for only three of the eleven pools, because Ondo’s oracle did not answer for the other eight. Those eight are reported as configured. Block times were interpolated between samples taken every 2,500 blocks, which is accurate to about ten seconds. We name addresses, not the people or companies behind them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Tokens, One Stock: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the difference between NVDAon and Robinhood’s NVDA token?
&lt;/h3&gt;

&lt;p&gt;They are issued by different companies under different terms. Robinhood Stock Tokens are debt securities issued by Robinhood Assets (Jersey) Limited, minted and redeemed only by an authorised participant during a weekday window. Ondo Stocks are issued by Ondo Global Markets (BVI) Limited and can be minted and redeemed by eligible users on Ethereum, BNB Chain and Solana, including on weekends for the eleven tickers paired on Robinhood Chain. Neither is offered to US persons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Robinhood stock tokens be redeemed on weekends?
&lt;/h3&gt;

&lt;p&gt;No. Robinhood’s tokenization window runs from Monday 02:00 to Saturday 02:00 CET; outside it, no tokens are minted or burned. The tokens still trade on chain over the weekend, but their supply cannot change until Monday.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is the exchange rate between the two tokens not exactly 1?
&lt;/h3&gt;

&lt;p&gt;Each issuer adjusts its own shares-per-token figure for dividends and corporate actions, so one Ondo token and one Robinhood token for the same stock can represent slightly different amounts of it. Each pool stores a reference rate set when it was created: 1.0 for SPCX, MSTR, CRCL and TSLA, and between 0.98315 (GME) and 0.99906 (NVDA) Ondo tokens per Robinhood token for the other seven.&lt;/p&gt;

&lt;h3&gt;
  
  
  Did Robinhood stock tokens trade at a discount on the weekend of 31 October?
&lt;/h3&gt;

&lt;p&gt;Not by enough to matter. On each token’s busiest USDG pool, excluding trades under $10, weekend prices stayed within 1.13% below and 3.26% above Friday’s 4pm price. In 8 of 11 tokens the weekend average sat between Friday’s close and Monday’s first hour, which is the market pricing weekend news. The other three averaged 0.13% to 0.22% below Friday.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who sets the StablePair reference price?
&lt;/h3&gt;

&lt;p&gt;Whoever holds the hook’s config-manager role can change any pool’s reference with updateFeeConfig. On Robinhood Chain all three of the hook’s roles, including the right to upgrade its code, were granted to one ordinary account when it was deployed, and no reference changed through 3 November 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;Eleven pools pin Ondo and Robinhood tokens for the same stock to a rate one account set and can change. In their first week they carried 0.05% of those tokens’ trading, and the weekend discount they were built to absorb never appeared.&lt;/p&gt;

&lt;p&gt;Sources: Ondo’s &lt;a href="https://ondo.finance/blog/ondo-stocks-live-on-robinhood-chain-and-uniswap?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;27 October announcement&lt;/a&gt;, &lt;a href="https://docs.ondo.finance/ondo-stocks/off-hours-trading?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;off-hours trading&lt;/a&gt; and &lt;a href="https://docs.ondo.finance/ondo-stocks/eligibility?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;eligibility&lt;/a&gt; docs; Robinhood’s &lt;a href="https://docs.robinhood.com/chain/stock-tokens?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Stock Tokens&lt;/a&gt; docs; Uniswap’s &lt;a href="https://github.com/Uniswap/v4-hooks-public/blob/fa163e5c3484dbc2ba8c97c4686fe5327c03569b/docs/technical/StablePairHook.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;StablePair specification&lt;/a&gt; at commit &lt;code&gt;fa163e5c&lt;/code&gt; and &lt;a href="https://github.com/Uniswap/hooklist/blob/a55967cb18f8fdc6d1adc50855024f28cad401bd/hooks/robinhood/0xd061f665d1c4816787fceaa168fa66c59d3abcc0.json?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;hook-list entry&lt;/a&gt;. Pool, swap, deposit, transfer and role events were read from &lt;code&gt;rpc.mainnet.chain.robinhood.com&lt;/code&gt;, from the Ondo tokens’ arrival on 26 October to block 105,464,942 (3 November, 23:59:59 UTC). Ondo’s multipliers were read from its oracle on Ethereum.&lt;/p&gt;

</description>
      <category>robinhoodchain</category>
      <category>tokenizedstocks</category>
      <category>uniswapv4</category>
      <category>onchaindata</category>
    </item>
    <item>
      <title>When a Solana Feature Activates but Its Program Migration Fails</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:15 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/when-a-solana-feature-activates-but-its-program-migration-fails-2n4h</link>
      <guid>https://dev.to/sulimanmukhtar/when-a-solana-feature-activates-but-its-program-migration-fails-2n4h</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-program-migration-hash-check-simd-0690" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On Solana, a feature gate activating and the change it carries actually happening are two separate events. For most gates the difference never shows. For a gate that replaces a core program’s bytes, it can: the replacement runs once, at the epoch boundary, and if it fails, the gate stays active anyway.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/eac20f2994dd064482d96f178a4bbc0f4e358fd0/proposals/0690-hash-validation-in-program-upgrades.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0690&lt;/a&gt;, merged on 26 October, adds a deliberate way for that replacement to fail: the runtime hashes the new program bytes and refuses to deploy them unless they match a hash shipped inside the validator. Agave implemented it on 29 October. On 2 November it queued the first migration that relies on it, p-ATA, a rewrite of the Associated Token Account program, and Firedancer followed on 3 November. We pulled the real p-ATA buffer from mainnet, hashed it the way both clients do, and replayed every way the activation can go through the loader Agave uses. The check works. It also depends on something no code enforces: the two clients agree only if the gates activate in the right order.&lt;/p&gt;




&lt;h2&gt;
  
  
  Activated is not the same as replaced
&lt;/h2&gt;

&lt;p&gt;In the first bank of each epoch, Agave works out which feature gates have just activated, writes the activation slot into each gate’s account, and then runs the migrations tied to those gates. The p-ATA one is a single call, and its failure path is a log line:&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;if&lt;/span&gt; &lt;span class="n"&gt;new_feature_activations&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;feature_set&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;replace_ata_with_p_ata&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.upgrade_loader_v2_program_with_loader_v3_program&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;feature_set&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;replace_ata_with_p_ata&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SPL_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;feature_set&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;replace_ata_with_p_ata&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PROGRAM_BUFFER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;feature_set&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;replace_ata_with_p_ata&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BUILD_HASH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"replace_ata_with_p_ata"&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="nd"&gt;warn!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to replace SPL Associated Token Account with p-ATA buffer '{}': {e}"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every check runs before anything is written, so a failure changes nothing except the gate. The gate is now active, so it is never a new activation again and the call never repeats. The proposal says so in one line: on a mismatch, “the feature is still marked as activated, but the program data remains unchanged.” A retry means a new gate in a new release. That leaves four states a migration can be in:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate account does not exist&lt;/strong&gt; — not submitted&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate exists, no activation slot&lt;/strong&gt; — pending, next epoch&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Active, program under Loader v3&lt;/strong&gt; — migrated&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Active, program under its old loader&lt;/strong&gt; — activated, migration failed&lt;/p&gt;




&lt;h2&gt;
  
  
  What the hash check adds
&lt;/h2&gt;

&lt;p&gt;A migration copies a Loader v3 buffer account, uploaded ahead of time, into the target program’s address. Before SIMD-0690, the runtime checked that the buffer existed, was owned by the upgradeable loader and deserialized as a buffer, then deployed whatever it held. Agave’s implementation adds one step between those checks and the deploy:&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;let&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_checked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer_address&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="py"&gt;.buffer_account&lt;/span&gt;&lt;span class="nf"&gt;.data&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;elf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;UpgradeableLoaderState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;size_of_buffer_metadata&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;end_offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elf&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.rposition&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;x&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map_or&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="p"&gt;|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;i&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;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;solana_sha256_hasher&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;hash&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;elf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;end_offset&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;hash&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;expected_hash&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CoreBpfMigrationError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;BuildHashMismatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_hash&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;Skip the 37-byte buffer header, drop trailing zero bytes, take SHA-256, compare. Only then does the deploy step load and verify the ELF in the runtime’s program environment. That step deliberately still accepts SBPF v0 programs &lt;a href="https://github.com/anza-xyz/agave/blob/9260869da1098c7f51d0145567f605027773799c/runtime/src/bank/builtins/core_bpf_migration/mod.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L201-L211" rel="noopener noreferrer"&gt;“for core program migrations”&lt;/a&gt;, which matters here because the p-ATA ELF is v0.&lt;/p&gt;

&lt;p&gt;What came before is the reason for the change. When p-token replaced SPL Token in place on 13 May 2026, the &lt;a href="https://github.com/anza-xyz/agave/blob/a52be3f86f20bd7ae8b7260d3f94487f3efee89b/runtime/src/bank.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L5658-L5672" rel="noopener noreferrer"&gt;function that did it&lt;/a&gt; took a program address, a buffer address, a flag and a log name. There was no hash. A Loader v3 buffer always has a write authority (the loader refuses to remove it), so until the activation slot, any change that authority made to the buffer, as long as it still loaded and verified, would have become SPL Token. Our &lt;a href="https://xroot.dev/blog/spl-token-p-token-replaced-in-place" rel="noopener noreferrer"&gt;p-token explainer&lt;/a&gt; covers that migration. Agave’s &lt;a href="https://github.com/anza-xyz/agave/pull/16077?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0690 change&lt;/a&gt; also turns the expected hash from an optional parameter into a required one, so every future migration must name its bytes, while the check itself waits for its own gate, &lt;a href="https://github.com/anza-xyz/agave/blob/9260869da1098c7f51d0145567f605027773799c/feature-set/src/lib.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L1578-L1580" rel="noopener noreferrer"&gt;&lt;code&gt;core_bpf_hash_validation&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real buffer, hashed
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/93cdf03a368a8bb2743a7f8d458a4cd36d2a904f/proposals/0567-cu-optimized-ata-program.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0567&lt;/a&gt; merged on 1 November at 11:02 UTC. Twenty-seven minutes later, the &lt;a href="https://explorer.solana.com/address/pataAAkF439TTjwRqmxRkTTEfZemRdoriXusez4PQ6S?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;p-ATA buffer&lt;/a&gt; appeared on mainnet: 37 transactions in slots 464,075,848 to 464,075,854, between 11:30:19 and 11:30:21 UTC. No transaction has touched it since. We read its bytes and hashed them both ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Account size&lt;/strong&gt; — 35,869 bytes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loader v3 buffer header&lt;/strong&gt; — 37 bytes, authority set&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ELF&lt;/strong&gt; — 35,832 bytes, SBPF v0&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trailing zero bytes&lt;/strong&gt; — 15&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SHA-256, zeros removed&lt;/strong&gt; — 4e008052⁠…⁠7f7d9e&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agave, Firedancer and SIMD-0567 expect&lt;/strong&gt; — 4e008052⁠…⁠7f7d9e&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SHA-256 of the whole ELF&lt;/strong&gt; — 8dc8d134⁠…⁠3605a4&lt;/p&gt;

&lt;p&gt;The buffer matches. The 15 zero bytes are worth a second look, because they are not padding. The buffer holds exactly one ELF file, and the file itself ends in zeros: its last section header has an alignment of 1 and an entry size of 0. Hash the ELF as a file and you get a different value. The expected hash is defined over the stripped bytes, &lt;a href="https://github.com/Ellipsis-Labs/solana-verifiable-build/blob/ea53dcebc52e6ca6b05e36092517245a268a3fa6/src/main.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L768-L778" rel="noopener noreferrer"&gt;the way solana-verify computes it&lt;/a&gt;, so &lt;code&gt;solana-verify get-buffer-hash&lt;/code&gt; reproduces it straight from the chain.&lt;/p&gt;

&lt;p&gt;The header also shows the buffer’s authority is still set, to &lt;code&gt;8pEc9HbS…7pkzf&lt;/code&gt;. Until the gate activates, whoever holds that key can rewrite or close the buffer. Before SIMD-0690 a rewrite would have been deployed. Now it can only make the activation fail.&lt;/p&gt;




&lt;h2&gt;
  
  
  Seven ways the activation can go
&lt;/h2&gt;

&lt;p&gt;To see the success and failure paths without a cluster, we built a harness around the real buffer bytes. It runs Agave’s check copied verbatim, Firedancer’s check ported from C, and the deploy step through solana-sbpf 0.26.0, the loader version Agave pinned at the commit we read, configured the way Agave’s migration path configures it. With the SIMD-0690 gate active, both clients give the same answer for every case:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Buffer as it is on mainnet&lt;/strong&gt; — migrated&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plus 4,096 zero bytes&lt;/strong&gt; — migrated, same hash&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minus its last 15 zero bytes&lt;/strong&gt; — hash passes, loader rejects&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One .rodata letter changed&lt;/strong&gt; — hash mismatch&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instruction 17 immediate, 83&amp;nbsp;→&amp;nbsp;84&lt;/strong&gt; — hash mismatch&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First opcode set to 0xff&lt;/strong&gt; — hash mismatch&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Buffer closed before activation&lt;/strong&gt; — account not found&lt;/p&gt;

&lt;p&gt;Every failure leaves the gate active and ATA exactly as it was. Two of the rows are worth a closer look.&lt;/p&gt;

&lt;p&gt;The hash pins the content but not the length. A buffer missing its last 15 bytes hashes identically, because those bytes are zeros. It is still a broken ELF: its section header table now runs past the end of the file, and the loader stops with “Offset or value is out of bounds.” The check and the loader cover each other. Neither alone would catch both a substituted program and a truncated one.&lt;/p&gt;

&lt;p&gt;The authority problem has changed shape rather than disappeared. A changed letter in a string constant and a changed constant in an instruction both pass the loader and the verifier. Only the hash notices them. The buffer’s authority can no longer decide what ATA becomes, but closing or rewriting the buffer before activation still decides that ATA does not change, and a retry then needs a new gate.&lt;/p&gt;




&lt;h2&gt;
  
  
  When the order slips, the clients split
&lt;/h2&gt;

&lt;p&gt;The two clients compute the same hash, but they decide differently whether to compute it. Agave hashes only if &lt;code&gt;core_bpf_hash_validation&lt;/code&gt; is already active and &lt;a href="https://github.com/anza-xyz/agave/blob/9260869da1098c7f51d0145567f605027773799c/runtime/src/bank/builtins/core_bpf_migration/mod.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L420-L428" rel="noopener noreferrer"&gt;otherwise skips the check&lt;/a&gt;. Firedancer &lt;a href="https://github.com/firedancer-io/firedancer/blob/37c658197b29deada95ea95a1afb04f7baea28fa/src/flamenco/runtime/fd_runtime.c?utm_source=xroot.dev&amp;amp;utm_medium=referral#L658-L668" rel="noopener noreferrer"&gt;always passes the p-ATA hash&lt;/a&gt; and &lt;a href="https://github.com/firedancer-io/firedancer/blob/37c658197b29deada95ea95a1afb04f7baea28fa/src/flamenco/runtime/fd_core_bpf_migration.c?utm_source=xroot.dev&amp;amp;utm_medium=referral#L464-L480" rel="noopener noreferrer"&gt;checks whenever one is passed&lt;/a&gt;. Its &lt;a href="https://github.com/firedancer-io/firedancer/blob/37c658197b29deada95ea95a1afb04f7baea28fa/src/flamenco/features/feature_map.json?utm_source=xroot.dev&amp;amp;utm_medium=referral#L309-L310" rel="noopener noreferrer"&gt;feature map&lt;/a&gt; says why: it “assumes this feature gate is activated for all migrations”, because the SIMD-0690 gate will activate first. The same harness, with the SIMD-0690 gate inactive when p-ATA activates:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Buffer as it is on mainnet&lt;/strong&gt; — both migrate&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One .rodata letter changed&lt;/strong&gt; — Agave deploys, Firedancer refuses&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instruction 17 immediate, 83&amp;nbsp;→&amp;nbsp;84&lt;/strong&gt; — Agave deploys, Firedancer refuses&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First opcode set to 0xff&lt;/strong&gt; — both refuse, for different reasons&lt;/p&gt;

&lt;p&gt;In the highlighted rows the clients finish the epoch’s first slot with different bytes at the ATA address, so they compute different bank hashes. Validators executing with Firedancer’s runtime would disagree with validators executing with Agave’s about the same block. Frankendancer, the hybrid, executes with Agave’s runtime, so the line falls between runtimes rather than client brands. Which software each validator runs is &lt;a href="https://xroot.dev/blog/which-client-built-each-solana-block" rel="noopener noreferrer"&gt;visible in gossip and in block footers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;None of this is a bug today. The buffer on mainnet matches, so the order makes no difference to the bytes that would be deployed. SIMD-0567 names SIMD-0690 as a dependency, and both clients document the order. Activating both in the same epoch is also safe: Agave &lt;a href="https://github.com/anza-xyz/agave/blob/9260869da1098c7f51d0145567f605027773799c/runtime/src/bank.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L6284-L6288" rel="noopener noreferrer"&gt;installs the new feature set&lt;/a&gt; before it runs any migration, so the check is already on. But the order is a scheduling commitment, not a check in code. Anza’s &lt;a href="https://github.com/anza-xyz/agave/wiki/Feature-Gate-Activation-Guidelines?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;activation guidelines&lt;/a&gt; fix a priority order and say exceptions will occur. The thing to watch is simple: gate &lt;code&gt;FCsQnd4V…zp1u&lt;/code&gt; must be active no later than gate &lt;code&gt;pata6uFa…ksx3&lt;/code&gt;. Neither gate account had been created on mainnet when this was written.&lt;/p&gt;




&lt;h2&gt;
  
  
  Telling the states apart
&lt;/h2&gt;

&lt;p&gt;A failed migration leaves no transaction and no event on chain. The only direct record is a warning in each validator’s log. It can still be detected from three accounts, read in one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FEATURE_OWNER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Feature111111111111111111111111111111111111&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LOADER_V3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BPFLoaderUpgradeab1e11111111111111111111111&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;migrationState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;program&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getMultipleAccounts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nx"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;program&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Feature account: owned by the Feature program, byte 0 = Some(activated_at)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;FEATURE_OWNER&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gate not submitted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gate submitted, pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readBigUInt64LE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;LOADER_V3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`activated at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: migrated`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`activated at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: MIGRATION FAILED (buffer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;still there&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// p-token:  activated at 419472000: migrated&lt;/span&gt;
&lt;span class="c1"&gt;// p-ATA:    gate not submitted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two comments are the script’s output against mainnet, for p-token and p-ATA. Checking the owner of the gate account matters: a gate’s address can hold lamports in an ordinary system account before anyone submits it, and reading those as feature data would report nonsense. Our &lt;a href="https://app.xroot.dev/upgrades?ref=blog" rel="noopener noreferrer"&gt;upgrades tracker&lt;/a&gt; follows which protocol upgrades are live.&lt;/p&gt;

&lt;p&gt;When p-ATA does migrate, the program account at &lt;code&gt;ATokenGP…8knL&lt;/code&gt; changes owner from &lt;code&gt;BPFLoader2111…&lt;/code&gt; to &lt;code&gt;BPFLoaderUpgradeab1e…&lt;/code&gt;, gains a program-data account and has no upgrade authority. SIMD-0567 adds that ATA cannot be invoked during the migration slot, because Loader v3 programs take effect one slot after deployment. Tools that decide whether a program is upgradeable by its loader will see the change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two smaller things in the code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The log reads backwards.&lt;/strong&gt; The mismatch error is built as &lt;code&gt;BuildHashMismatch(hash, expected_hash)&lt;/code&gt;, with the buffer’s hash first, but its &lt;a href="https://github.com/anza-xyz/agave/blob/9260869da1098c7f51d0145567f605027773799c/runtime/src/bank/builtins/core_bpf_migration/error.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L51-L53" rel="noopener noreferrer"&gt;message&lt;/a&gt; labels the first value “Expected”. For our altered-letter buffer, the warning prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invalid build hash. Expected: 7c10bb1d…a3b147e730, Got: 4e008052…674e7f7d9e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value after “Expected” is what the buffer holds, and the value after “Got” is what the validator expected. The labels have been this way since the error was introduced in April 2025. Anyone diagnosing a failed migration from logs should swap them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A panic fixed in passing.&lt;/strong&gt; The 2025 version of this check searched for the last non-zero byte across the whole account, header included. When the program bytes were all zero, as in a buffer created but never written, that search could land inside the 37-byte header and produce a slice that ends before it starts. Our harness reproduces the panic for a header with no authority, and for an authority whose last byte is zero. On a real cluster it would have stopped every validator at the activation slot. The SIMD-0690 &lt;a href="https://github.com/anza-xyz/agave/commit/cee958fd313601de031622dba6e7704284dffeed?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;commit&lt;/a&gt; fixed it under the line “strip trailing zeros from ELF bytes only”. It was never reachable on mainnet: the only migration that ever passed a hash before then was the slashing program’s, whose gate was never created on mainnet, and Agave removed it in June 2026. Firedancer’s version never had the problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;The harness replays the checks and the loader on real bytes. It does not run a cluster through an epoch boundary. The deploy column uses Agave’s loader for both clients; Firedancer has its own loader, which we did not run. The altered buffers are our edits, not anything seen on chain. We checked the on-chain buffer against the published hash but did not rebuild p-ATA from source with solana-verify. We read mainnet only, not testnet or devnet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Program Migrations: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can a Solana feature gate be active while the program it was meant to replace is unchanged?
&lt;/h3&gt;

&lt;p&gt;Yes. A core program migration runs once, inside the first bank of the epoch in which its feature gate activates. If any check fails, the runtime logs a warning and moves on: the feature account records its activation slot and the target program keeps its old bytes. Nothing retries, because the gate is never newly activated again. A second attempt needs a new feature gate in a new validator release.&lt;/p&gt;

&lt;h3&gt;
  
  
  What exactly does SIMD-0690 hash?
&lt;/h3&gt;

&lt;p&gt;The program bytes in the source buffer account: everything after the 37-byte Loader v3 buffer header, with trailing zero bytes removed, hashed with SHA-256. The result must equal a hash compiled into the validator next to the feature gate. solana-verify computes executable hashes the same way, so solana-verify get-buffer-hash reproduces the value from the chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the p-ATA buffer on mainnet the one Agave expects?
&lt;/h3&gt;

&lt;p&gt;Yes. The buffer at pataAAkF439TTjwRqmxRkTTEfZemRdoriXusez4PQ6S hashes to 4e008052…7f7d9e, the value in Agave’s replace_ata_with_p_ata gate, in Firedancer’s constant and in the SIMD-0567 text. It was created and written in 37 transactions on 1 November 2026 and has not been written since.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Agave and Firedancer run the hash check the same way?
&lt;/h3&gt;

&lt;p&gt;They compute the same hash but differ on when they check it. Agave checks only once the SIMD-0690 gate, core_bpf_hash_validation, is active. Firedancer always checks the p-ATA hash and records that it assumes the SIMD-0690 gate activates first. If p-ATA activated before SIMD-0690 with an altered buffer, Agave would deploy the altered bytes, Firedancer would refuse them, and the two clients would compute different bank hashes.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I check whether a migration actually happened?
&lt;/h3&gt;

&lt;p&gt;Read three accounts. The feature account is owned by the Feature program and holds an activation slot once active. The target program is owned by BPFLoaderUpgradeab1e… after a successful migration. The source buffer is consumed on success. An active gate with the program still under its old loader means the migration failed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;SIMD-0690 makes a core program migration deploy the reviewed bytes or nothing. A failure leaves the gate active and the program unchanged, and Agave and Firedancer agree on which happened only if the hash-check gate activates first.&lt;/p&gt;

&lt;p&gt;The last in-place swap is in &lt;a href="https://xroot.dev/blog/spl-token-p-token-replaced-in-place" rel="noopener noreferrer"&gt;SPL Token was replaced in place&lt;/a&gt;, the prefunded-account rule p-ATA builds on is in &lt;a href="https://xroot.dev/blog/solana-create-account-allow-prefund-simd-0312" rel="noopener noreferrer"&gt;SIMD-0312’s CreateAccountAllowPrefund&lt;/a&gt;, and how to tell which client built a block is in &lt;a href="https://xroot.dev/blog/which-client-built-each-solana-block" rel="noopener noreferrer"&gt;reading the block footer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/eac20f2994dd064482d96f178a4bbc0f4e358fd0/proposals/0690-hash-validation-in-program-upgrades.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0690&lt;/a&gt; at commit &lt;code&gt;eac20f29&lt;/code&gt;; &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/93cdf03a368a8bb2743a7f8d458a4cd36d2a904f/proposals/0567-cu-optimized-ata-program.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0567&lt;/a&gt; at &lt;code&gt;93cdf03a&lt;/code&gt;; Agave &lt;a href="https://github.com/anza-xyz/agave/pull/16077?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16077&lt;/a&gt; and &lt;a href="https://github.com/anza-xyz/agave/pull/16524?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16524&lt;/a&gt; (source read at &lt;code&gt;9260869d&lt;/code&gt;); Firedancer &lt;a href="https://github.com/firedancer-io/firedancer/pull/12680?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#12680&lt;/a&gt; (&lt;code&gt;37c65819&lt;/code&gt;); the buffer and gate accounts read from mainnet with &lt;code&gt;getAccountInfo&lt;/code&gt;, &lt;code&gt;getMultipleAccounts&lt;/code&gt; and &lt;code&gt;getSignaturesForAddress&lt;/code&gt;. Scenario results are from our own harness.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>featuregates</category>
      <category>runtime</category>
      <category>rust</category>
    </item>
    <item>
      <title>Why Solana’s getFeeForMessage Is Not a Transaction-Validity Check</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:14 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/why-solanas-getfeeformessage-is-not-a-transaction-validity-check-di</link>
      <guid>https://dev.to/sulimanmukhtar/why-solanas-getfeeformessage-is-not-a-transaction-validity-check-di</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-getfeeformessage-not-validity-check" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;getFeeForMessage&lt;/code&gt; answers one question: what would this message pay? For years Agave also answered &lt;code&gt;null&lt;/code&gt; when the message’s blockhash was gone or its durable nonce did not match, and code learned to read that null as “get a new blockhash”. Agave 4.5 stops doing it. And the null was never a reliable expiry signal in the first place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/anza-xyz/agave/pull/16519?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Agave #16519&lt;/a&gt;, merged on 29 October, removes both checks, because the fee depends on neither. We ran the old behaviour on a local Agave 4.2.1 validator, which has the same check as the 4.4 release, and found a 150-block window in which it priced transactions that the same node was already rejecting with “Blockhash not found”.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the fee is made of
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/anza-xyz/agave/blob/8ffb33c9de24da8bb9f8a90d2b85f41ceb360dad/fee/src/lib.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L29-L56" rel="noopener noreferrer"&gt;fee&lt;/a&gt; has two parts: 5,000 lamports for each signature, counting the signatures inside Ed25519, secp256k1 and secp256r1 precompile instructions, and the priority fee, the compute-unit limit times the compute-unit price in micro-lamports, divided by a million and rounded up. Both come from the message itself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transfer, one signature&lt;/strong&gt; — 5,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two signatures&lt;/strong&gt; — 10,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;200,000 CU at 1,000,000 µlamports&lt;/strong&gt; — 205,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;200,000 CU at 1 µlamport&lt;/strong&gt; — 5,001&lt;/p&gt;

&lt;p&gt;Values in lamports, from our local validator. The last row is the rounding: 0.2 lamports of priority fee costs 1. How that fee ranks a transaction against others is &lt;a href="https://xroot.dev/blog/solana-transaction-priority-formula-compute-limit" rel="noopener noreferrer"&gt;a separate calculation&lt;/a&gt;. Nothing here depends on the recent blockhash or on a durable nonce.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the old version checked first
&lt;/h2&gt;

&lt;p&gt;Up to 4.4, Agave’s bank refused to price a message unless its blockhash was in the bank’s blockhash queue or, failing that, the message was a durable-nonce transaction whose nonce account stored exactly that value. Either check failing meant &lt;code&gt;null&lt;/code&gt;. So did invalid compute-budget instructions, such as two &lt;code&gt;SetComputeUnitLimit&lt;/code&gt; instructions. Three different problems, one answer.&lt;/p&gt;

&lt;p&gt;The catch is in the queue. A bank keeps the last 300 blockhashes, but accepts a transaction only if its blockhash is at most 150 blocks old (&lt;a href="https://github.com/anza-xyz/agave/blob/v4.4.0/accounts-db/src/blockhash_queue.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L90-L131" rel="noopener noreferrer"&gt;blockhash_queue.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/v4.4.0/runtime/src/bank.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L3505-L3508" rel="noopener noreferrer"&gt;is_blockhash_valid&lt;/a&gt;). The fee check asked “is it in the queue?”, not “is it young enough?”. We held one blockhash on the local validator and recorded when each method gave up on it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blockhash issued at block height 360&lt;/strong&gt; — +0&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;lastValidBlockHeight 510&lt;/strong&gt; — +150&amp;nbsp;blocks&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;isBlockhashValid false, at height 511&lt;/strong&gt; — +151&amp;nbsp;blocks&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;getFeeForMessage still&lt;/strong&gt; — 5,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sendTransaction&lt;/strong&gt; — Blockhash not found&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;getFeeForMessage null, at height 661&lt;/strong&gt; — +301&amp;nbsp;blocks&lt;/p&gt;

&lt;p&gt;For 150 blocks, about a minute at mainnet’s 400 ms slots, the old method priced a transaction that the network rejected. Code that waits for a null before refreshing its blockhash spends that minute &lt;a href="https://xroot.dev/blog/solana-transaction-dropped-no-error" rel="noopener noreferrer"&gt;sending transactions that are dropped&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Agave 4.5 changes
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/anza-xyz/agave/blob/8ffb33c9de24da8bb9f8a90d2b85f41ceb360dad/runtime/src/bank.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L3468-L3478" rel="noopener noreferrer"&gt;new function&lt;/a&gt; reads the compute-budget instructions and computes the fee, nothing else. The &lt;a href="https://github.com/anza-xyz/agave/blob/8ffb33c9de24da8bb9f8a90d2b85f41ceb360dad/CHANGELOG.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;changelog&lt;/a&gt; lists it under 4.5.0, which was unreleased at the merge, when the stable line was 4.3 and the beta 4.4. Side by side, with the old column measured on 4.2.1 and the new one read from the merged code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fresh blockhash&lt;/strong&gt; — 5,000&amp;nbsp;→&amp;nbsp;5,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blockhash 151–300 blocks old&lt;/strong&gt; — 5,000&amp;nbsp;→&amp;nbsp;5,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blockhash dropped or never seen&lt;/strong&gt; — null&amp;nbsp;→&amp;nbsp;5,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durable nonce, stored value&lt;/strong&gt; — 5,000&amp;nbsp;→&amp;nbsp;5,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durable nonce, any other value&lt;/strong&gt; — null&amp;nbsp;→&amp;nbsp;5,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicate SetComputeUnitLimit&lt;/strong&gt; — null&amp;nbsp;→&amp;nbsp;null&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v0, lookup table missing&lt;/strong&gt; — error&amp;nbsp;→&amp;nbsp;error&lt;/p&gt;

&lt;p&gt;The last row is a JSON-RPC error, &lt;code&gt;-32602&lt;/code&gt; “invalid transaction message: Attempted to lookup addresses from a table that does not exist”: the RPC method resolves lookup tables before it asks the bank. The &lt;a href="https://github.com/anza-xyz/agave/issues/15876?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;issue behind the change&lt;/a&gt; gives one more reason for it: the nonce check was the only part of the fee calculation that needed resolved addresses, which kept RPC from using a lighter message type. In 4.5 a &lt;code&gt;null&lt;/code&gt; means one thing only: the compute-budget instructions are invalid. Like the &lt;a href="https://xroot.dev/blog/solana-rpc-4-4-what-changed" rel="noopener noreferrer"&gt;4.4 RPC changes&lt;/a&gt;, it will reach clients gradually, as RPC providers upgrade, so code meets both behaviours for a while.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a fee does not promise
&lt;/h2&gt;

&lt;p&gt;A fee is computed from the message alone, in every version. It never checked the things that decide whether a transaction lands. We signed and sent three transactions the local node had just priced at 5,000 lamports:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fee payer with no account: “no record of a prior credit”&lt;/strong&gt; — rejected&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transfer above the payer’s balance: custom program error 0x1&lt;/strong&gt; — rejected&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blockhash past its last valid height: “Blockhash not found”&lt;/strong&gt; — rejected&lt;/p&gt;

&lt;p&gt;All three were rejected in preflight simulation. A fee also says nothing about signatures, account locks, the compute the instructions will use or whether a program succeeds. From 4.5 it says nothing about the blockhash or the nonce either.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to call instead
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is the blockhash still usable?   getLatestBlockhash -&amp;gt; lastValidBlockHeight,
                                 then getBlockHeight &amp;lt;= lastValidBlockHeight
                                 (or isBlockhashValid)
Is the durable nonce current?    getAccountInfo on the nonce account (jsonParsed):
                                 its stored blockhash must equal the message's
Would it execute right now?      simulateTransaction
What will it pay?                getFeeForMessage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our run isBlockhashValid first reported false at block height 511, one past the blockhash’s lastValidBlockHeight of 510, and sendTransaction rejected it there, so the height comparison matches what the node enforces. Simulation is the closest thing to a validity check, but it is one node’s view at one moment: an account can change between simulation and landing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;We measured Agave 4.2.1’s test validator, whose fee check matches the 4.4 release line for line in behaviour; we did not run 4.4 or 4.5, so the 4.5 column comes from the merged code. A local validator makes blocks at its own pace, so the 150-block window is in blocks, not seconds. Every call used processed commitment on a single node.&lt;/p&gt;




&lt;h2&gt;
  
  
  getFeeForMessage: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does getFeeForMessage check whether a Solana blockhash is still valid?
&lt;/h3&gt;

&lt;p&gt;No. From Agave 4.5 it does not look at the blockhash at all. Before that it only checked that the blockhash was still in the bank’s queue of the last 300, while transactions are accepted only up to 150 blocks, so it returned fees for blockhashes the network had already stopped accepting. Use the lastValidBlockHeight returned by getLatestBlockhash, or isBlockhashValid.&lt;/p&gt;

&lt;h3&gt;
  
  
  When does getFeeForMessage return null in Agave 4.5?
&lt;/h3&gt;

&lt;p&gt;Only when the message’s compute-budget instructions are invalid, for example two SetComputeUnitLimit instructions, bad instruction data or an invalid heap size. A message that cannot be read at all, such as a v0 message whose address lookup table does not exist, gets a JSON-RPC error instead of null.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long is a Solana blockhash valid for transactions?
&lt;/h3&gt;

&lt;p&gt;150 blocks after the block that produced it. getLatestBlockhash returns lastValidBlockHeight, the last block height at which a transaction using it can land. In our test isBlockhashValid turned false at lastValidBlockHeight + 1, and sendTransaction rejected the blockhash from then on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does getFeeForMessage include the priority fee?
&lt;/h3&gt;

&lt;p&gt;Yes. It adds the priority fee set by the message’s compute-budget instructions, compute-unit limit times compute-unit price in micro-lamports, divided by a million and rounded up, to 5,000 lamports per signature, counting the signatures in Ed25519, secp256k1 and secp256r1 precompile instructions too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a fee from getFeeForMessage mean the transaction will succeed?
&lt;/h3&gt;

&lt;p&gt;No. It is computed from the message alone. It does not check signatures, whether the fee payer exists or can pay, the blockhash or durable nonce (from Agave 4.5), account locks or whether the instructions succeed. Simulate the transaction to check those.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;getFeeForMessage prices a message and nothing more: from Agave 4.5 it returns a fee for expired blockhashes and wrong nonces, and before that it kept pricing blockhashes for 150 blocks after the network stopped accepting them, so check expiry with lastValidBlockHeight and validity with simulateTransaction.&lt;/p&gt;

&lt;p&gt;Sources: Agave &lt;a href="https://github.com/anza-xyz/agave/pull/16519?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16519&lt;/a&gt;, merged as &lt;code&gt;8ffb33c9&lt;/code&gt;, and &lt;a href="https://github.com/anza-xyz/agave/issues/15876?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;issue #15876&lt;/a&gt;; the 4.4.0 release source for the old check, the blockhash queue and the processing age. Measurements are from our own client against a local solana-test-validator 4.2.1.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>rpc</category>
      <category>agave</category>
      <category>fees</category>
    </item>
    <item>
      <title>Token-2022 v12: What Changed for Rust Developers</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:12 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/token-2022-v12-what-changed-for-rust-developers-1d9d</link>
      <guid>https://dev.to/sulimanmukhtar/token-2022-v12-what-changed-for-rust-developers-1d9d</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/token-2022-v12-rust-breaking-changes" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On 28 October the Token-2022 repository released v12. It rewrites the program on Pinocchio, roughly halving the compute cost of a transfer, and publishes seven crates with breaking version bumps. The program every Solana wallet and exchange calls did not change: all three clusters still run v11.1.0, byte for byte.&lt;/p&gt;

&lt;p&gt;So for now, “v12” means Rust crates. We diffed the &lt;a href="https://github.com/solana-program/token-2022/releases/tag/program%40v11.1.0?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;v11.1.0&lt;/a&gt; and &lt;a href="https://github.com/solana-program/token-2022/releases/tag/program%40v12.0.0?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;v12.0.0&lt;/a&gt; tags, read the deployed program on mainnet, devnet and testnet, and resolved both versions with Cargo. What follows is what breaks for Rust code, what does not, and how to tell when the new program actually ships.&lt;/p&gt;




&lt;h2&gt;
  
  
  Published is not deployed
&lt;/h2&gt;

&lt;p&gt;The deployed Token-2022 binary carries a security.txt that names the release it was built from. Read it from the program-data account and hash the same bytes, and you know exactly what a cluster runs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mainnet&lt;/strong&gt; — &lt;a href="mailto:program@v11.1.0"&gt;program@v11.1.0&lt;/a&gt;, since 23 Oct&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Devnet, testnet&lt;/strong&gt; — &lt;a href="mailto:program@v11.1.0"&gt;program@v11.1.0&lt;/a&gt;, since 23 Sep&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SHA-256, all three&lt;/strong&gt; — 5e888b29⁠…⁠6413d9&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v11.1.0 verified build&lt;/strong&gt; — 5e888b29⁠…⁠6413d9&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v12.0.0 binary on any cluster&lt;/strong&gt; — none&lt;/p&gt;

&lt;p&gt;Mainnet took v11.1.0 on 23 October at 15:39 UTC, a month after devnet and testnet. The v11.1.0 release notes carry a deployment section, a verifiable-build hash and an audit link. The v12.0.0 notes list changes only. Until a v12 build is verified and deployed, every Token-2022 instruction on chain runs the v11.1.0 code, whatever crate version your project depends on. To check for yourself:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;RPC&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://api.mainnet-beta.solana.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PROGRAMDATA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DoU57AYuPFu2QU514RktNPG22QhApEjnKxnBcu4BHDTY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# Token-2022's program-data account
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jsonrpc&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;2.0&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;encode&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;content-type&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;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;getAccountInfo&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;PROGRAMDATA&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;encoding&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;base64&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;value&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;data&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;Q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&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="c1"&gt;# last deploy slot
&lt;/span&gt;&lt;span class="n"&gt;elf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="c1"&gt;# 45-byte header, zero padding stripped
&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_release&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&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;deployed at slot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;# 460497271
&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;source_release &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;elf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;elf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# program@v11.1.0
&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;sha256         &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elf&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;             &lt;span class="c1"&gt;# 5e888b29…6413d9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Pinocchio rewrite, and the savings still waiting
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/solana-program/token-2022/pull/1469?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Pull request #1469&lt;/a&gt; replaces the SDK entrypoint with Pinocchio’s and switches the processor from &lt;code&gt;AccountInfo&lt;/code&gt; to Pinocchio’s &lt;code&gt;AccountView&lt;/code&gt;, the zero-copy view of the input buffer. It is the same move that made &lt;a href="https://xroot.dev/blog/spl-token-p-token-replaced-in-place" rel="noopener noreferrer"&gt;p-token&lt;/a&gt; cheap for the original token program. The pull request reports these instruction counts from its own tests:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;transfer&lt;/strong&gt; — 1,444&amp;nbsp;→&amp;nbsp;719 CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;transfer_checked&lt;/strong&gt; — 1,782&amp;nbsp;→&amp;nbsp;856 CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;initialize_account&lt;/strong&gt; — 1,822&amp;nbsp;→&amp;nbsp;614 CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;initialize_mint&lt;/strong&gt; — 1,347&amp;nbsp;→&amp;nbsp;553 CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mint_to&lt;/strong&gt; — 1,262&amp;nbsp;→&amp;nbsp;545 CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;burn&lt;/strong&gt; — 1,372&amp;nbsp;→&amp;nbsp;638 CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;close_account&lt;/strong&gt; — 1,219&amp;nbsp;→&amp;nbsp;535 CU&lt;/p&gt;

&lt;p&gt;These are the pull request’s measurements from its instruction-count tests, not ours, and extensions add their own cost on top. They become real only when a v12 binary is deployed. Until then, a transfer of any Token-2022 asset on mainnet costs what v11.1.0 charges.&lt;/p&gt;




&lt;h2&gt;
  
  
  What breaks in Rust
&lt;/h2&gt;

&lt;p&gt;Most Rust code depends on &lt;code&gt;spl-token-2022-interface&lt;/code&gt;, the crate with instruction builders and account layouts. Its builders and state types did not change. The breaks are in four places.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The processor signature
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;spl-token-2022&lt;/code&gt; program crate, &lt;a href="https://github.com/solana-program/token-2022/blob/1bc00fa58705dcc948856efe2e2e56fd2693fc4a/program/src/processor.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L1976-L1980" rel="noopener noreferrer"&gt;&lt;code&gt;Processor::process&lt;/code&gt;&lt;/a&gt; changed from &lt;code&gt;&amp;amp;[AccountInfo]&lt;/code&gt; to &lt;code&gt;&amp;amp;mut [AccountView]&lt;/code&gt;. Test suites that run Token-2022 as a native processor, with &lt;code&gt;processor!(spl_token_2022::processor::Processor::process)&lt;/code&gt;, stop compiling on 12.0.0. Load the program’s binary into the test instead, or keep the test on 11.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The zk-ops feature is gone
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/solana-program/token-2022/pull/1524?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#1524&lt;/a&gt; removed &lt;code&gt;zk-ops&lt;/code&gt;, a default feature its own manifest said to remove “once the underlying syscalls are released on all networks”. It gated the confidential-transfer and confidential mint-and-burn processors, which are now always compiled in. A manifest that still names the feature does not build. We asked Cargo to resolve both versions with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# spl-token-2022 = { version = "=11.1.0", default-features = false, features = ["no-entrypoint", "zk-ops"] }
#   resolves

# spl-token-2022 = { version = "=12.0.0", default-features = false, features = ["no-entrypoint", "zk-ops"] }
error: failed to select a version for `spl-token-2022`.
package `featcheck` depends on `spl-token-2022` with feature `zk-ops` but `spl-token-2022` does not have that feature.
help: available features: no-entrypoint, test-sbf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Two interface helpers take AccountView
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;spl-token-2022-interface&lt;/code&gt; 4.0, &lt;a href="https://github.com/solana-program/token-2022/blob/1bc00fa58705dcc948856efe2e2e56fd2693fc4a/interface/src/extension/mod.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L1459-L1555" rel="noopener noreferrer"&gt;&lt;code&gt;alloc_and_serialize&lt;/code&gt; and &lt;code&gt;alloc_and_serialize_variable_len_extension&lt;/code&gt;&lt;/a&gt; now take &lt;code&gt;&amp;amp;mut AccountView&lt;/code&gt;. They grow an account and write an extension into its TLV data; Token-2022 itself uses them for its token-metadata and token-group extensions, and a program that stores TLV data the same way can call them too. Callers holding an &lt;code&gt;AccountInfo&lt;/code&gt; need to move to Pinocchio’s types or stay on interface 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The zero-knowledge types moved a major version
&lt;/h3&gt;

&lt;p&gt;The interface re-exports its proof types from &lt;code&gt;solana-zk-elgamal-proof-interface&lt;/code&gt;, now 1.0.0 instead of 0.1. Its confidential builders take those types, for example &lt;code&gt;ProofLocation&amp;lt;CiphertextCommitmentEqualityProofData&amp;gt;&lt;/code&gt;. Proof data built against the old crate will not type-check. The confidential stack moves together: interface 4.0, proof-generation and proof-extraction 0.7, and solana-zk-sdk 8.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instruction builders&lt;/strong&gt; — unchanged&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mint and account layouts&lt;/strong&gt; — unchanged&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;onchain CPI helpers&lt;/strong&gt; — still AccountInfo&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Processor::process&lt;/strong&gt; — AccountView&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;zk-ops feature&lt;/strong&gt; — removed&lt;/p&gt;




&lt;h2&gt;
  
  
  Confidential transfers: new call, same keys
&lt;/h2&gt;

&lt;p&gt;Changing how confidential keys are derived would be the dangerous kind of break: a wallet that derives different keys can no longer decrypt balances it already holds. zk-sdk 8 changed the &lt;a href="https://docs.rs/solana-zk-sdk/8.0.1/solana_zk_sdk/encryption/derivation/index.html?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;derivation API&lt;/a&gt;, but not the keys. In 7.0.1, &lt;code&gt;derive_confidential_keys(signer, seed)&lt;/code&gt; signs &lt;code&gt;"solana-conf-bal/v1" ‖ seed&lt;/code&gt;. In 8.0.1, &lt;code&gt;STANDARD_DERIVATION_MESSAGE&lt;/code&gt; is defined as that same &lt;code&gt;"solana-conf-bal/v1"&lt;/code&gt;, so the seedless call signs the same bytes the old empty-seed call did.&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="c1"&gt;// zk-sdk 7 (Token-2022 v11 stack)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elgamal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ae&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;derive_confidential_keys&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;signer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;b""&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="c1"&gt;// zk-sdk 8 (v12 stack): the seed parameter is gone; seeded keys use&lt;/span&gt;
&lt;span class="c1"&gt;// derive_confidential_keys_with_seed(&amp;amp;signer, seed)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elgamal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ae&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;derive_confidential_keys&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;signer&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="c1"&gt;// zk-sdk 8 takes a solana-signer v4 Signer. Signers from the Agave 4.3 stack&lt;/span&gt;
&lt;span class="c1"&gt;// are v3, so sign the standard message yourself (same keys):&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signer&lt;/span&gt;&lt;span class="nf"&gt;.try_sign_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STANDARD_DERIVATION_MESSAGE&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elgamal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ae&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;derive_confidential_keys_from_signature&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;signature&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last pattern is the one Token-2022’s own CLI and client tests now use, per &lt;a href="https://github.com/solana-program/token-2022/pull/1584?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#1584&lt;/a&gt;. zk-sdk 8.0.1 depends on &lt;code&gt;solana-signer&lt;/code&gt; 4.0.0, while 7.0.1 used 3.0.0, and a v3 keypair does not satisfy the v4 trait.&lt;/p&gt;

&lt;p&gt;Two smaller fixes ride along. The interface documentation listed the multisig accounts for confidential mint and burn wrongly, naming the mint twice and dropping the token account. The builders and the IDL that generates the JavaScript client were already right, so only hand-written integrations that followed the comments are affected (&lt;a href="https://github.com/solana-program/token-2022/pull/1509?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#1509&lt;/a&gt;). And &lt;code&gt;spl-token-client&lt;/code&gt; 0.21.0 no longer builds proof-record transactions that exceed the packet limit when a memo, nonce or compute-budget instruction is added (&lt;a href="https://github.com/solana-program/token-2022/pull/1550?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#1550&lt;/a&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  An upgrade order that works
&lt;/h2&gt;

&lt;p&gt;Every crate in the release took a breaking version bump, so nothing moves until you change a version requirement: Cargo will not pick 12.0.0 to satisfy &lt;code&gt;"11"&lt;/code&gt;. When you do move:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Builders and account reads only&lt;/strong&gt; — interface 3&amp;nbsp;→&amp;nbsp;4&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token-2022 as a native test processor&lt;/strong&gt; — load the binary&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A manifest that names zk-ops&lt;/strong&gt; — delete the feature&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLV account helpers&lt;/strong&gt; — move to AccountView&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidential transfers&lt;/strong&gt; — upgrade the stack together&lt;/p&gt;

&lt;p&gt;None of this changes what happens on chain. The mint and account layouts did not change, and the instruction builders encode every instruction exactly as interface 3 did.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;We did not build or run v12, so the compute figures are the pull request’s. Our Cargo check covered dependency resolution, not a compile of downstream code. Where we say a signature or type changed, we read it from the tagged source. The deployment facts come from each cluster’s program-data account, and a later upgrade would show up there first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Token-2022 v12: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Token-2022 v12 live on Solana mainnet?
&lt;/h3&gt;

&lt;p&gt;No. Mainnet, devnet and testnet all run the same v11.1.0 binary: its embedded security.txt says source_release &lt;a href="mailto:program@v11.1.0"&gt;program@v11.1.0&lt;/a&gt; and its hash matches the v11.1.0 verified build, 5e888b29…. Mainnet was upgraded to it on 23 October 2026. v12.0.0 was published as Rust crates on 28 October, and its release notes have no deployment or verification section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will upgrading the spl-token-2022 crate to 12.0.0 break my build?
&lt;/h3&gt;

&lt;p&gt;Only if you use the program crate itself. Processor::process now takes &amp;amp;mut [AccountView] instead of &amp;amp;[AccountInfo], so registering Token-2022 as a native processor in tests no longer compiles, and the zk-ops feature no longer exists, so a Cargo.toml that names it fails to resolve. Instruction builders, state parsing and the onchain CPI helpers are unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does solana-zk-sdk 8 change my confidential transfer keys?
&lt;/h3&gt;

&lt;p&gt;No. zk-sdk 8’s derive_confidential_keys(signer) signs STANDARD_DERIVATION_MESSAGE, which is the constant "solana-conf-bal/v1", and zk-sdk 7’s derive_confidential_keys(signer, b"") signed the same bytes. The keys are identical; only the function signature changed, and seeded derivation moved to derive_confidential_keys_with_seed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much cheaper is Token-2022 v12?
&lt;/h3&gt;

&lt;p&gt;According to the instruction-count tests in the pull request that moved the program to Pinocchio, transfer falls from 1,444 to 719 compute units and transfer_checked from 1,782 to 856, roughly half. None of that applies on chain until a v12 binary is deployed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I check which Token-2022 version a cluster runs?
&lt;/h3&gt;

&lt;p&gt;Read the program-data account DoU57AYuPFu2QU514RktNPG22QhApEjnKxnBcu4BHDTY, skip its 45-byte header and trailing zero bytes, and read the source_release field of the embedded security.txt. Hashing the same bytes with SHA-256 gives the value solana-verify reports for a verified build.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;v12 is a Pinocchio rewrite that halves transfer costs, but for now it exists only as crates: it breaks native test processors, the zk-ops feature and two TLV helpers, keeps confidential keys identical, and changes nothing on chain until a verified v12 binary replaces the v11.1.0 that every cluster runs.&lt;/p&gt;

&lt;p&gt;For the extensions themselves, see &lt;a href="https://xroot.dev/blog/token-2022-pausable-permissioned-burn" rel="noopener noreferrer"&gt;pausable and permissioned burn&lt;/a&gt; and &lt;a href="https://xroot.dev/blog/solana-confidential-transfers-native-vs-rings" rel="noopener noreferrer"&gt;how confidential transfers compare&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sources: the &lt;a href="https://github.com/solana-program/token-2022/releases/tag/program%40v12.0.0?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;v12.0.0&lt;/a&gt; and &lt;a href="https://github.com/solana-program/token-2022/releases/tag/program%40v11.1.0?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;v11.1.0&lt;/a&gt; releases and the tagged source (&lt;code&gt;1bc00fa5&lt;/code&gt;, &lt;code&gt;8867f751&lt;/code&gt;); pull requests &lt;a href="https://github.com/solana-program/token-2022/pull/1469?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#1469&lt;/a&gt;, &lt;a href="https://github.com/solana-program/token-2022/pull/1524?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#1524&lt;/a&gt;, &lt;a href="https://github.com/solana-program/token-2022/pull/1584?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#1584&lt;/a&gt;, &lt;a href="https://github.com/solana-program/token-2022/pull/1509?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#1509&lt;/a&gt; and &lt;a href="https://github.com/solana-program/token-2022/pull/1550?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#1550&lt;/a&gt;; the solana-zk-sdk 7.0.1 and 8.0.1 crate sources; and the Token-2022 program-data account on mainnet, devnet and testnet.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>token2022</category>
      <category>rust</category>
      <category>confidentialtransfers</category>
    </item>
    <item>
      <title>One Solana Slot, Multiple Banks: Fork-Aware Geyser Indexing</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:10 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/one-solana-slot-multiple-banks-fork-aware-geyser-indexing-46l6</link>
      <guid>https://dev.to/sulimanmukhtar/one-solana-slot-multiple-banks-fork-aware-geyser-indexing-46l6</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-one-slot-multiple-banks-geyser" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your Geyser plugin buffers account updates by slot and writes a slot to the analytics tables when the slot is rooted. For as long as a slot meant one bank, that was correct. In October, Agave’s accounts database finished a migration whose stated purpose is to let “multiple banks with the same slot” exist side by side.&lt;/p&gt;

&lt;p&gt;A bank is one replay of one block. When a node replays a slot twice, both replays stream their account writes and transactions to every Geyser plugin as they execute, and only one of them is ever finalized. The interface has been naming the bank behind each update since version 4.3.0. An indexer that keys by slot alone merges the two, and the abandoned bank’s data reaches the finalized table.&lt;/p&gt;

&lt;p&gt;Here is where the second bank comes from, what a plugin is and is not told, what a slot-keyed indexer commits wrongly, and the bank-keyed buffer that fixes it, with tests that show both.&lt;/p&gt;




&lt;h2&gt;
  
  
  A slot is not a bank
&lt;/h2&gt;

&lt;p&gt;A slot is a position in time: the window one leader has to produce a block. A bank is a node’s execution of a specific block at that position, with its own account changes, its own transaction results and its own ID. The Solana SDK says it plainly: a &lt;code&gt;BankId&lt;/code&gt; “is unique even if the slot number of two different slots is the same”.&lt;/p&gt;

&lt;p&gt;Agave’s accounts database used to assume the opposite inside its write cache, the in-memory layer that holds a bank’s account writes until its slot is rooted and flushed. Between late September and the end of October, one Agave engineer moved it from slot keys to bank keys:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BankId becomes its own type (#15620)&lt;/strong&gt; — 2 Oct&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ancestors carry (slot, bank ID) (#15857)&lt;/strong&gt; — 8 Oct&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write cache rekeyed by bank ID (#16102)&lt;/strong&gt; — 20 Oct&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Roots added by bank (#16262)&lt;/strong&gt; — 23 Oct&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purge by bank ID (#16360)&lt;/strong&gt; — 27 Oct&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flush and freeze by bank ID (#16447, #16459)&lt;/strong&gt; — 28 Oct&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slot-to-bank map removed (#16497)&lt;/strong&gt; — 28 Oct&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purge deferred to bank drop (#16508)&lt;/strong&gt; — 31 Oct&lt;/p&gt;

&lt;p&gt;The two highlighted changes say what the series is for. &lt;a href="https://github.com/anza-xyz/agave/pull/16360?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16360&lt;/a&gt; switched purging to bank IDs “so multiple banks with the same slot can exist in the write cache”. &lt;a href="https://github.com/anza-xyz/agave/commit/166278df5ab912111c251c090f240515491d8bab?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16497&lt;/a&gt; removed the last lookup by slot, after which the cache “no longer asserts one bank per slot: banks at the same slot each keep their own cache, and removing a bank removes only its own cache.” Before the series, the old bank’s cache had to be purged inline before the slot could be created again. &lt;a href="https://github.com/anza-xyz/agave/pull/16508?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16508&lt;/a&gt; moved that purge to the background and measured it falling from about 2 milliseconds to about 100 microseconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the second bank comes from
&lt;/h2&gt;

&lt;p&gt;Three paths in Agave’s replay stage clear a bank and let the same slot be replayed into a new one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Switching to the cluster’s version of a block.&lt;/strong&gt; A leader can produce more than one version of a block for its slot. Under Alpenglow, live on mainnet since &lt;a href="https://xroot.dev/blog/alpenglow-finality-in-numbers" rel="noopener noreferrer"&gt;21 October&lt;/a&gt;, votor tells replay when the version the cluster certified differs from the one the node replayed. Replay repairs the cluster’s version, then &lt;a href="https://github.com/anza-xyz/agave/blob/5817ebe0092211a1de6db21ae55eb622639982d3/core/src/replay_stage.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L2479-L2640" rel="noopener noreferrer"&gt;clears its own bank for that slot and every descendant&lt;/a&gt; and replays again. The same slots come back with new bank IDs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Fast leader handover.&lt;/strong&gt; The first slot of a leader’s four-slot window may start on an optimistic parent, before the previous leader’s last block is settled. If the leader then switches parents, the block carries an &lt;code&gt;UpdateParent&lt;/code&gt; marker, and replay &lt;a href="https://github.com/anza-xyz/agave/blob/5817ebe0092211a1de6db21ae55eb622639982d3/core/src/replay_stage/update_parent.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L120-L230" rel="noopener noreferrer"&gt;clears the in-progress bank and recreates it&lt;/a&gt; from the marker’s position in the block.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The leader’s own optimistic bank.&lt;/strong&gt; On the producing side, the same handover clears the bank the leader started building and replaces it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second and third paths are fast leader handover, and replay’s restart path checks the &lt;code&gt;FastLeaderHandover11111111111111111111111111&lt;/code&gt; feature, which has not been activated on mainnet. The first needs only a leader that produced two versions of a block, which is rare, but the path is live today. The write-cache series is what lets these replays happen without the old bank’s cache being torn down first.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a Geyser plugin is told, and what it is not
&lt;/h2&gt;

&lt;p&gt;Since &lt;code&gt;agave-geyser-plugin-interface&lt;/code&gt; 4.3.0, the callbacks that carry data name the bank they came from. In version 6.0.0, the current one on 31 October:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;update_account_for_bank&lt;/strong&gt; — slot + bank ID, every write&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;notify_transaction_for_bank&lt;/strong&gt; — slot + bank ID, every transaction&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;update_bank_status · Rooted&lt;/strong&gt; — bank ID + parent, once per rooted slot&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;update_slot_status · Dead&lt;/strong&gt; — slot only, no bank ID&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;notify_entry_update_parent&lt;/strong&gt; — cleared bank ID, entry plugins only&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;bank cleared to switch block versions&lt;/strong&gt; — no callback at all&lt;/p&gt;

&lt;p&gt;Three things in that table decide how an indexer has to be built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discards are mostly silent.&lt;/strong&gt; The only notice of a cleared bank is the &lt;code&gt;UpdateParent&lt;/code&gt; entry notification, and the plugin manager &lt;a href="https://github.com/anza-xyz/agave/blob/5817ebe0092211a1de6db21ae55eb622639982d3/geyser-plugin-manager/src/entry_notifier.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L171-L190" rel="noopener noreferrer"&gt;sends it only to plugins that enable entry notifications&lt;/a&gt;, which most account indexers do not. A bank cleared to switch block versions produces nothing at all. By the time either happens, the bank’s writes and transactions have already been delivered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Finality is per bank, and ordered.&lt;/strong&gt; When votor roots a block, the RPC tracker &lt;a href="https://github.com/anza-xyz/agave/blob/5817ebe0092211a1de6db21ae55eb622639982d3/rpc/src/optimistically_confirmed_bank_tracker.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L266-L289" rel="noopener noreferrer"&gt;sorts the newly rooted chain by slot&lt;/a&gt; and reports each slot once, with its bank ID and with the previous root as its parent. The chain arrives contiguous: a slot between a rooted slot and its reported parent was never rooted. And a bank’s &lt;code&gt;Rooted&lt;/code&gt; arrives after its data. Account writes are delivered synchronously while the bank replays, before it can freeze. Transactions travel through a separate service, but the root notification &lt;a href="https://github.com/anza-xyz/agave/blob/5817ebe0092211a1de6db21ae55eb622639982d3/rpc/src/transaction_status_service.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L272-L326" rel="noopener noreferrer"&gt;waits on that service’s declared work&lt;/a&gt; before it is processed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Old plugins are blind.&lt;/strong&gt; The slot-only callbacks were deprecated in 4.3.0, and each new bank-aware method’s default implementation forwards to its old counterpart, dropping the bank ID on the way. A plugin that still implements only &lt;code&gt;update_account&lt;/code&gt; keeps compiling and keeps receiving data, with no way to tell two banks of one slot apart.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a slot-keyed indexer commits
&lt;/h2&gt;

&lt;p&gt;To see the failure without a validator, we fed the event sequence these paths produce to two small indexers in Rust. One buffers by slot and commits a slot when it is rooted. The other buffers by slot and bank ID. Slot 104 is replayed twice: bank 5 executes transactions &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt;, writing account X to 70 and account Y to 1, and is then cleared. Bank 6 executes transaction &lt;code&gt;c&lt;/code&gt;, writing X to 90, and is rooted.&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="c1"&gt;// bank 5: the version that is abandoned&lt;/span&gt;
&lt;span class="n"&gt;ix&lt;/span&gt;&lt;span class="nf"&gt;.transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0xa&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;ix&lt;/span&gt;&lt;span class="nf"&gt;.account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;w&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;ix&lt;/span&gt;&lt;span class="nf"&gt;.transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0xb&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;ix&lt;/span&gt;&lt;span class="nf"&gt;.account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;w&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&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="c1"&gt;// bank 6: the version the cluster finalizes&lt;/span&gt;
&lt;span class="n"&gt;ix&lt;/span&gt;&lt;span class="nf"&gt;.transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0xc&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;ix&lt;/span&gt;&lt;span class="nf"&gt;.account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;w&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;ix&lt;/span&gt;&lt;span class="nf"&gt;.rooted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;103&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// slot-keyed: Y = 1 is "finalized", slot 104 has 3 transactions&lt;/span&gt;
&lt;span class="c1"&gt;// bank-keyed: no Y, slot 104 has 1 transaction (c), X = 90&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The slot-keyed indexer commits a value for Y that never existed on the finalized chain, and counts three transactions in a block that has one. Volume, fee and activity tables built on it are inflated by exactly the abandoned bank’s work.&lt;/p&gt;

&lt;p&gt;It does get X right, and only by luck. Both banks wrote X, and the higher &lt;code&gt;write_version&lt;/code&gt; happened to belong to bank 6, because the clearing paths remove the old bank before the new one replays. Swap the arrival order and a third test shows the slot-keyed indexer committing the abandoned value. Nothing in the interface promises that order, and the write-cache series exists to make same-slot banks cheaper to keep around.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key by bank, commit on the rooted bank
&lt;/h2&gt;

&lt;p&gt;The fix is a different buffer key and one rule at root time:&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;BankKeyed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BTreeMap&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;Slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BankId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Pending&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;finalized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Finalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;BankKeyed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// update_account_for_bank / notify_transaction_for_bank&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;account&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="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bank_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BankId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AccountWrite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.pending&lt;/span&gt;&lt;span class="nf"&gt;.entry&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bank_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.or_default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// update_bank_status(slot, parent, Rooted, bank_id): one call per newly&lt;/span&gt;
    &lt;span class="c1"&gt;// rooted slot, in slot order, parent = the previous root. Every bank&lt;/span&gt;
    &lt;span class="c1"&gt;// buffered in (parent, slot] is decided now: keep the rooted one.&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;rooted&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="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bank_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BankId&lt;/span&gt;&lt;span class="p"&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;decided&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;
            &lt;span class="py"&gt;.pending&lt;/span&gt;
            &lt;span class="nf"&gt;.range&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;parent&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;span class="nn"&gt;BankId&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MIN&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="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;BankId&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&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;key&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;decided&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.pending&lt;/span&gt;&lt;span class="nf"&gt;.remove&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;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"key was just listed"&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;key&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bank_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="nf"&gt;.commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slot&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="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.finalized&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The range does two jobs. It drops other banks at the rooted slot, the same-slot case, and it drops banks at slots the chain skipped, an ordinary abandoned fork, because a slot between a root and its reported parent will never be rooted. Banks above the rooted slot stay buffered until their own turn. Handling &lt;code&gt;notify_entry_update_parent&lt;/code&gt; is optional: it lets a cleared bank’s buffer go early, but correctness rests on the rooted bank ID, the one signal every plugin receives.&lt;/p&gt;

&lt;p&gt;Six tests pin both behaviours down. Two show the slot-keyed indexer committing the abandoned bank’s data, one of them through arrival order alone. The others show the bank-keyed buffer handling the same-slot case, an abandoned fork at a skipped slot, descendants of the root that must stay buffered, and the early discard. Inside a bank, the highest &lt;code&gt;write_version&lt;/code&gt; per account is its final state, and that rule is still correct. It just has to be applied per bank, not per slot.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;The tests replay event sequences derived from Agave’s source. They are not a captured Geyser stream from a validator, and we have not measured how often a mainnet node clears a bank today. With fast leader handover inactive, only the block-version switch can do it on mainnet, and that needs a leader to have produced two versions of a block.&lt;/p&gt;

&lt;p&gt;The ordering guarantees in section 03 come from reading the notification paths, not from a fault-injection test. And a &lt;code&gt;Dead&lt;/code&gt; status still names only a slot. A plugin that must react to a dead bank, rather than simply never committing it, cannot tell from that call which of a slot’s banks died.&lt;/p&gt;




&lt;h2&gt;
  
  
  Same-Slot Banks: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can two Solana banks have the same slot?
&lt;/h3&gt;

&lt;p&gt;Yes. A bank is one replay of one block, and Agave gives every bank a unique bank ID even when two banks share a slot. Under Alpenglow a node replaces its bank for a slot when the cluster settles on a different version of that block, and fast leader handover adds a second way once its feature activates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Geyser tell a plugin which bank an update came from?
&lt;/h3&gt;

&lt;p&gt;Since agave-geyser-plugin-interface 4.3.0, yes: update_account_for_bank, notify_transaction_for_bank and update_bank_status all carry a bank_id. The older slot-only callbacks are deprecated, and the default bank-aware methods forward to them without the bank ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does an indexer know which bank was finalized?
&lt;/h3&gt;

&lt;p&gt;Agave reports every newly rooted slot through update_bank_status with SlotStatus::Rooted and the rooted bank’s ID, in slot order, with the previous root as the parent. Commit only that bank, and discard every other bank buffered between the parent and the rooted slot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is buffering by slot until the slot is rooted enough?
&lt;/h3&gt;

&lt;p&gt;No. A slot-keyed buffer merges every bank that ever replayed the slot, so accounts written only by an abandoned bank and transactions executed only in it reach the finalized table. Key the buffer by slot and bank ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Agave notify a plugin when it discards a bank?
&lt;/h3&gt;

&lt;p&gt;Not in general. A bank cleared by fast leader handover produces an UpdateParent entry notification, but only to plugins that enable entry notifications. A bank cleared to switch to the cluster’s version of a block produces no notification at all. The rooted bank ID is the only signal every plugin gets.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;A slot can have more than one bank, Geyser streams all of them, and only the bank named in the &lt;code&gt;Rooted&lt;/code&gt; status is the finalized one. Key the buffer by slot and bank ID, and implement the &lt;code&gt;_for_bank&lt;/code&gt; callbacks.&lt;/p&gt;

&lt;p&gt;The pipeline this buffer belongs in is the subject of &lt;a href="https://xroot.dev/blog/solana-pipeline-part-2" rel="noopener noreferrer"&gt;ingesting the Solana firehose with Rust&lt;/a&gt;, and the certificates that decide which bank is rooted are in &lt;a href="https://xroot.dev/blog/alpenglow-finality-in-numbers" rel="noopener noreferrer"&gt;Alpenglow finality in numbers&lt;/a&gt;. What a block says about its own producer is in &lt;a href="https://xroot.dev/blog/which-client-built-each-solana-block" rel="noopener noreferrer"&gt;which client built each block&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sources: Agave master at commit &lt;code&gt;5817ebe009&lt;/code&gt;, the merge of #16508: &lt;a href="https://github.com/anza-xyz/agave/blob/5817ebe0092211a1de6db21ae55eb622639982d3/core/src/replay_stage.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L2479-L2640" rel="noopener noreferrer"&gt;replay_stage.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/5817ebe0092211a1de6db21ae55eb622639982d3/core/src/replay_stage/update_parent.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L120-L230" rel="noopener noreferrer"&gt;update_parent.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/5817ebe0092211a1de6db21ae55eb622639982d3/rpc/src/optimistically_confirmed_bank_tracker.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L266-L289" rel="noopener noreferrer"&gt;optimistically_confirmed_bank_tracker.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/5817ebe0092211a1de6db21ae55eb622639982d3/geyser-plugin-manager/src/entry_notifier.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L171-L190" rel="noopener noreferrer"&gt;entry_notifier.rs&lt;/a&gt; and &lt;a href="https://github.com/anza-xyz/agave/blob/5817ebe0092211a1de6db21ae55eb622639982d3/rpc/src/transaction_status_service.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L272-L326" rel="noopener noreferrer"&gt;transaction_status_service.rs&lt;/a&gt;; pull requests &lt;a href="https://github.com/anza-xyz/agave/pull/16360?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16360&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/pull/16497?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16497&lt;/a&gt; and &lt;a href="https://github.com/anza-xyz/agave/pull/16508?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16508&lt;/a&gt;; the &lt;a href="https://docs.rs/agave-geyser-plugin-interface/6.0.0/agave_geyser_plugin_interface/geyser_plugin_interface/trait.GeyserPlugin.html?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;GeyserPlugin trait in agave-geyser-plugin-interface 6.0.0&lt;/a&gt;; and &lt;a href="https://docs.rs/solana-clock/4.0.0/solana_clock/type.BankId.html?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;solana_clock::BankId&lt;/a&gt;. The feature account &lt;code&gt;FastLeaderHandover11111111111111111111111111&lt;/code&gt; does not exist on mainnet.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>geyser</category>
      <category>rust</category>
      <category>indexing</category>
    </item>
    <item>
      <title>One Lamport Can Break Consensus: Solana’s Integer Inflation</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:09 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/one-lamport-can-break-consensus-solanas-integer-inflation-58am</link>
      <guid>https://dev.to/sulimanmukhtar/one-lamport-can-break-consensus-solanas-integer-inflation-58am</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-integer-inflation-simd-0607" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once an epoch, every Solana validator computes how many lamports of inflation to mint, adds them to the chain’s capitalization, and folds the result into its bank hash. Today that calculation runs in floating point, with a &lt;code&gt;powf&lt;/code&gt; in the middle of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/2934dbeb1c8a7dfecc4f70a93a6b152e525b1698/proposals/0607-remove-runtime-float-ops.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0607&lt;/a&gt;, written by engineers from both Anza and Firedancer, replaces every floating-point step with integer arithmetic specified down to the order of its divisions. Agave merged the implementation on 30 October behind a feature gate. We copied the specification’s Rust into a test harness to check its claims and to measure the problem it fixes. Two mathematically identical float formulas for the same reward disagreed by one lamport in 62 of 60,000 epochs. The integer path cannot disagree with itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why one lamport is a consensus failure
&lt;/h2&gt;

&lt;p&gt;Capitalization is part of the state every validator hashes. If two clients compute the same epoch’s reward and land one lamport apart, their capitalizations differ, their bank hashes differ, and the network has a disagreement no vote can fix. The proposal’s motivation is exactly that: these values “must be computed identically by all validator clients, without relying on IEEE-754 behavior or platform math-library functions such as &lt;code&gt;powf&lt;/code&gt;.”&lt;/p&gt;

&lt;p&gt;Basic float operations are deterministic. Exponentiation is not specified to the last bit, and the inflation rate is &lt;code&gt;0.08 × (1 − taper)^year&lt;/code&gt;. To see how much that matters, we computed the legacy epoch reward two ways that are equal on paper, &lt;code&gt;(1 − taper).powf(year)&lt;/code&gt; and &lt;code&gt;exp(year × ln(1 − taper))&lt;/code&gt;, for 6,000 epochs in each of five slot-time regimes and both tapers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capitalization 10¹⁸ lamports&lt;/strong&gt; — 62 of 60,000 rewards differ&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capitalization 6.2 × 10¹⁷ lamports&lt;/strong&gt; — 47 of 60,000 rewards differ&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Size of every difference&lt;/strong&gt; — 1 lamport&lt;/p&gt;

&lt;p&gt;About one epoch in a thousand, the truncation that turns a float reward into lamports lands on the other side of an integer. That is not a claim that Agave and Firedancer compute different rewards today. It is the reason neither should depend on how a math library rounds the last bit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The integer schedule
&lt;/h2&gt;

&lt;p&gt;Rates are integers over &lt;code&gt;RATE_SCALE = 10^15&lt;/code&gt;, so 8% is &lt;code&gt;80_000_000_000_000&lt;/code&gt;. Decay factors are integers over &lt;code&gt;2^64&lt;/code&gt;, a finer scale because a factor very close to one is multiplied by itself across hundreds of millions of slots. The per-slot factor is derived, not tabulated: a 32-term series for the logarithm of the taper and a cubic approximation of the exponential, then a binary exponentiation whose order and flooring are fixed:&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;fn&lt;/span&gt; &lt;span class="nf"&gt;decay_per_slot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ns&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="n"&gt;taper_num&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taper_den&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&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;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u128&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// -ln(1 - taper) = ln(taper_den / (taper_den - taper_num)), scaled by 2^64&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;annual_exponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nf"&gt;approximate_logarithm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taper_den&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taper_den&lt;/span&gt;&lt;span class="nf"&gt;.checked_sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;taper_num&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="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;slot_exponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;annual_exponent&lt;/span&gt;&lt;span class="nf"&gt;.checked_mul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;YEAR_NS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;approximate_exponential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slot_exponent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 2^64 * exp(-x), cubic&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;pow_decay_floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;base&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;mut&lt;/span&gt; &lt;span class="n"&gt;n&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;-&amp;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;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DECAY_SCALE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;n&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mul_decay_floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&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;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mul_decay_floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&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;result&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// rate   = max(TERMINAL_RATE, floor(anchor_rate * decay / 2^64))&lt;/span&gt;
&lt;span class="c1"&gt;// annual = floor(capitalization * rate / 10^15)&lt;/span&gt;
&lt;span class="c1"&gt;// reward = floor(annual * slots_in_epoch * ns_per_slot / YEAR_NS)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every step floors, every multiplication is checked, and an overflow is an error the validator must turn into a panic. The specification forbids wrapping, saturating or substituting a value, and it forbids falling back to floating point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Its numbers, reproduced to the lamport
&lt;/h2&gt;

&lt;p&gt;The proposal publishes how far the integer schedule drifts from the floating-point one: the largest difference in a single epoch’s reward over 6,000 epochs, at a capitalization of 10¹⁸ lamports, across the 400, 350, 300, 250 and 200 ms slot regimes. Running its functions verbatim against its stated float baseline:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;15% taper, largest difference&lt;/strong&gt; — 5,923 lamports (spec: 5,923)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;30% taper, largest difference&lt;/strong&gt; — 1,061 lamports (spec: 1,061)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the 15% worst case falls&lt;/strong&gt; — 350 ms, epoch 1,282&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the 30% worst case falls&lt;/strong&gt; — 400 ms, epoch 509&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;32-term logarithm vs 200 terms&lt;/strong&gt; — identical, both tapers&lt;/p&gt;

&lt;p&gt;Both drift figures match exactly. So does the claim that 32 terms of the logarithm series are enough: for the 15% and 30% tapers, the 32-term sum and a 200-term sum are the same integer, to the last of 2⁶⁴ units. The drift is the price of the change, at most a few thousand lamports of an epoch’s reward. A relative difference below 7 × 10⁻¹¹ buys a reward every client computes identically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Overflow, and why it panics
&lt;/h2&gt;

&lt;p&gt;With 128-bit intermediates, the reward arithmetic stays valid up to a capitalization of about 18.4 trillion SOL, a thousand times beyond the 18.4 billion SOL a 64-bit lamport count can hold at all. The checks that can fail in practice are the invalid inputs: a zero slot duration, or a taper of 100% or more, return an error before any arithmetic.&lt;/p&gt;

&lt;p&gt;The panic rule is deliberate. A validator that cannot compute the reward exactly as specified is safer stopped than running on a number the rest of the cluster did not compute. That is the same reasoning as the proposal itself, applied to its own failure path.&lt;/p&gt;




&lt;h2&gt;
  
  
  The activation order with SIMD-0550
&lt;/h2&gt;

&lt;p&gt;SIMD-0550 doubles the yearly disinflation from 15% to 30%. Its original design kept issuance continuous by recomputing the stored &lt;code&gt;initial&lt;/code&gt; rate at activation, &lt;code&gt;initial = anchor / (1 − 0.30)^year&lt;/code&gt;, with &lt;code&gt;powf&lt;/code&gt;. Our &lt;a href="https://xroot.dev/blog/solana-inflation-double-disinflation-simd-0550" rel="noopener noreferrer"&gt;earlier post on SIMD-0550&lt;/a&gt; described that design. &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/pull/704?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;An amendment merged on 29 October&lt;/a&gt; replaced it: SIMD-0607 must activate first or at the same boundary, the anchor is computed with the integer 15% schedule including its terminal clamp, and the 30% integer taper applies from the activation slot. Agave rekeyed the SIMD-0550 gate in the 4.4.0 release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anchor rate, activation at year 6 (illustrative)&lt;/strong&gt; — 3.0172%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate zero slots after activation&lt;/strong&gt; — 3.0172%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate one year later&lt;/strong&gt; — 2.1120%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Naive governor formula, 0.08 × 0.70⁶&lt;/strong&gt; — 1.5% (terminal floor)&lt;/p&gt;

&lt;p&gt;The anchor works: at zero elapsed slots the decay factor is exactly 2⁶⁴, so the rate does not move. The trap is for anyone reading the stored governor. After activation it reports &lt;code&gt;initial = 0.08&lt;/code&gt; and &lt;code&gt;taper = 0.30&lt;/code&gt;, and the textbook formula built from those two numbers, which measures decay from genesis, lands on the 1.5% floor while the chain is minting at about 3%. Forward curves should start from &lt;code&gt;getInflationRate&lt;/code&gt;, not from &lt;code&gt;getInflationGovernor&lt;/code&gt;.&lt;/p&gt;




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

&lt;p&gt;Agave’s implementation, &lt;a href="https://github.com/anza-xyz/agave/pull/16355?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16355&lt;/a&gt;, merged on 30 October behind the &lt;a href="https://github.com/anza-xyz/agave/blob/9bcf93e368b5fb52c21677ed8c7f0360eb93b5b7/feature-set/src/lib.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;&lt;code&gt;remove_runtime_float_ops&lt;/code&gt;&lt;/a&gt; gate. Neither that gate nor SIMD-0550’s has been activated on mainnet. When SIMD-0607 does activate at the boundary into epoch E+1, rewards earned in epoch E are still paid with the legacy calculation, and the integer path starts with rewards earned in E+1.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;We ran the specification’s code, not Agave’s implementation of it, and compared against the float baseline the specification states rather than against a running validator. The two float paths in section 01 are a test of sensitivity, not a measurement of any client. The year-6 activation in section 05 is illustrative, at constant 400 ms slots; the real anchor depends on the slot-time history up to the activation slot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Integer Inflation: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does Solana want to remove floating point from inflation?
&lt;/h3&gt;

&lt;p&gt;Inflation rewards are added to capitalization, which is part of the bank hash. If two validator clients compute an epoch’s reward one lamport apart, their bank hashes differ. Floating-point exponentiation depends on the math library, and mathematically identical float formulas can round to different lamport amounts. SIMD-0607 specifies the whole calculation in integers so every client gets the same number.&lt;/p&gt;

&lt;h3&gt;
  
  
  How large is the difference between the integer and floating-point schedules?
&lt;/h3&gt;

&lt;p&gt;At most 5,923 lamports per epoch for the 15% taper and 1,061 for the 30% taper, over 6,000 epochs and five slot-time regimes at a capitalization of 10^18 lamports, a relative difference below 7.03 × 10^-11. We reproduced both figures exactly from the specification’s code.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does SIMD-0607 have to do with SIMD-0550?
&lt;/h3&gt;

&lt;p&gt;SIMD-0550 doubles the disinflation rate from 15% to 30% a year. Since an amendment merged on 29 October 2026, it must activate after or together with SIMD-0607: the anchor rate at activation is computed with the integer 15% schedule, and the 30% integer taper applies from there. The stored inflation governor keeps initial 0.08 and switches taper to 0.30.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I compute Solana’s inflation rate from getInflationGovernor?
&lt;/h3&gt;

&lt;p&gt;Not after SIMD-0550 activates. The governor will report initial 0.08 and taper 0.30, but the rate decays from the activation anchor, not from genesis, so initial × (1 − taper)^years gives the wrong answer. Read getInflationRate, which the node computes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is SIMD-0607 active on mainnet?
&lt;/h3&gt;

&lt;p&gt;Not as of 30 October 2026. Agave merged the implementation that day behind the remove_runtime_float_ops feature gate, which has not been activated on mainnet.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;Floating point lets two correct implementations disagree by a lamport about once in a thousand epochs; SIMD-0607 trades a few thousand lamports of drift per epoch for a reward every client computes identically, and SIMD-0550 now waits for it.&lt;/p&gt;

&lt;p&gt;The faster disinflation itself is in &lt;a href="https://xroot.dev/blog/solana-inflation-double-disinflation-simd-0550" rel="noopener noreferrer"&gt;Solana’s inflation will decay twice as fast&lt;/a&gt;, how those rewards reach stake accounts is in &lt;a href="https://xroot.dev/blog/solana-staking-rewards-when-and-why-zero" rel="noopener noreferrer"&gt;when staking rewards arrive&lt;/a&gt;, and why two independent clients matter at all is in &lt;a href="https://xroot.dev/blog/which-client-built-each-solana-block" rel="noopener noreferrer"&gt;which client built each block&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/2934dbeb1c8a7dfecc4f70a93a6b152e525b1698/proposals/0607-remove-runtime-float-ops.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0607&lt;/a&gt; as amended at commit &lt;code&gt;2934dbeb&lt;/code&gt;; &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/pull/704?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;the SIMD-0550 amendment&lt;/a&gt;; Agave &lt;a href="https://github.com/anza-xyz/agave/pull/16355?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16355&lt;/a&gt; (commit &lt;code&gt;9bcf93e368&lt;/code&gt;) and &lt;a href="https://github.com/anza-xyz/agave/pull/16499?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16499&lt;/a&gt;. Measurements are from the specification’s functions run in our own harness.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>inflation</category>
      <category>consensus</category>
      <category>rust</category>
    </item>
    <item>
      <title>Agave CLI Transaction V1: Which Signing Workflows Break?</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:08 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/agave-cli-transaction-v1-which-signing-workflows-break-2j80</link>
      <guid>https://dev.to/sulimanmukhtar/agave-cli-transaction-v1-which-signing-workflows-break-2j80</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-cli-transaction-v1-signing-workflows" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On 29 October, Agave merged a change that makes the &lt;code&gt;solana&lt;/code&gt; command-line tool build every transaction as a version 1 message. Not optionally and not behind a flag: there is no longer a code path in the CLI that compiles a legacy message.&lt;/p&gt;

&lt;p&gt;Its &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/CHANGELOG.md?utm_source=xroot.dev&amp;amp;utm_medium=referral#L84-L85" rel="noopener noreferrer"&gt;changelog entry&lt;/a&gt; carries one warning: hardware wallets that cannot parse v1 “may no longer work with the CLI”. That is the visible break. There are three quieter ones, plus a list of things that look like they should break and do not. We traced each through the code at the merge commit and measured the ones with numbers by compiling the same messages the new CLI compiles.&lt;/p&gt;




&lt;h2&gt;
  
  
  One builder, one version
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/anza-xyz/agave/pull/16448?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Pull request 16448&lt;/a&gt; replaces hand-built message assembly across eleven files with a &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/cli/src/message_builder.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;&lt;code&gt;MessageBuilder&lt;/code&gt;&lt;/a&gt;, whose &lt;code&gt;build&lt;/code&gt; returns &lt;code&gt;VersionedMessage::V1&lt;/code&gt; and nothing else. Three things change in what the CLI emits, all because &lt;a href="https://xroot.dev/blog/solana-transaction-v1" rel="noopener noreferrer"&gt;the v1 format&lt;/a&gt; moves resource requests out of ComputeBudget instructions and into the message itself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First byte of a transfer message&lt;/strong&gt; — 0x01 → 0x81&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same transfer, message size&lt;/strong&gt; — 150 → 164 bytes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compute limit when not simulated&lt;/strong&gt; — 200,000 per instruction, max 1.4M&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loaded-accounts data limit&lt;/strong&gt; — always 64 MiB&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Priority fee&lt;/strong&gt; — flat: ⌈price × limit ÷ 10⁶⌉ lamports&lt;/p&gt;

&lt;p&gt;The explicit limits are not optional either. A v1 transaction that requests no compute limit and no loaded-accounts size gets zero of both and fails at execution, so the builder always writes them. The rest of this post is what those three facts do to the ways people sign.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Ledger app refuses them, and fails safe
&lt;/h2&gt;

&lt;p&gt;The CLI side of Ledger signing does not care about message versions: it &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/remote-wallet/src/ledger.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L478-L490" rel="noopener noreferrer"&gt;chunks the message bytes into sign APDUs&lt;/a&gt; and lets the device decide. The device runs the Solana app, version 1.16.1 on the day of the merge, and its parser &lt;a href="https://github.com/LedgerHQ/app-solana/blob/af6ed454f0ed39cad8a5e1bca544ced18350a403/libsol/parser.c?utm_source=xroot.dev&amp;amp;utm_medium=referral#L132-L168" rel="noopener noreferrer"&gt;accepts any first byte with the high bit set&lt;/a&gt; as “a versioned message”, then reads the rest with the v0 layout. Walk the CLI’s v1 transfer through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;81            version byte          → versioned, version 1
01 00 01      header                → 1 signer, 0 + 1 read-only
0c            [v1: config mask]     → read as "12 account keys"
              12 × 32 = 384 bytes needed, 159 left
                                    → check_buffer_length fails
device → 0x6a80  CLI → "Ledger received invalid Solana message"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a small message the misread account count runs off the end of the buffer. A large v1 message can survive that read, but the next step looks for your public key among the first “accounts”, and in a v1 message those bytes are the config mask and the lifetime, never your key. Depending on where the misread runs out, the device answers 0x6a80 or 0x6a81. The app’s &lt;a href="https://github.com/LedgerHQ/app-solana/blob/af6ed454f0ed39cad8a5e1bca544ced18350a403/src/handle_sign_message.c?utm_source=xroot.dev&amp;amp;utm_medium=referral#L509-L520" rel="noopener noreferrer"&gt;sign handler returns on both errors&lt;/a&gt; before it reaches the blind-signing branch, so this fails closed: the device refuses rather than showing wrong amounts or offering to sign a hash.&lt;/p&gt;

&lt;p&gt;A reviewer on the pull request put the cost plainly: Ledger has not implemented v1, and when it does, the change will “force all users to update their Ledger firmware”. The suggested workaround is to install an earlier CLI. We read the Ledger app’s source rather than signing on a device, and did not check Trezor or Keystone firmware, which the CLI also supports.&lt;/p&gt;




&lt;h2&gt;
  
  
  Offline signing across CLI versions
&lt;/h2&gt;

&lt;p&gt;Offline signing with &lt;code&gt;--sign-only&lt;/code&gt; works because every party rebuilds the same message from the same arguments and signs the same bytes. The submitter attaches the collected signatures as &lt;code&gt;--signer PUBKEY=SIGNATURE&lt;/code&gt;, and each is checked against the message it compiles.&lt;/p&gt;

&lt;p&gt;A cold signer on a 4.4 CLI compiles the legacy encoding; a submitter on a build with this change compiles v1. Same instructions, same blockhash, different bytes. We signed the legacy encoding of a transfer and checked the signature against both: it verifies the legacy message and fails the v1 one. Multi-party flows, treasury setups and any procedure that moves an unsigned transaction between machines need every participant on the same side of the change.&lt;/p&gt;

&lt;p&gt;Durable nonces, the usual companion of offline signing, keep working on their own. The builder inserts &lt;code&gt;AdvanceNonceAccount&lt;/code&gt; as the first instruction and uses the nonce value as the message lifetime, and the runtime finds a durable nonce the same way for every message version: instruction zero, the System program, the advance-nonce discriminant. The problem is not the nonce. It is that the workflows nonces exist for are the ones that cross machines, and often end on a hardware wallet.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 44× priority fee on nonce transactions
&lt;/h2&gt;

&lt;p&gt;Online commands simulate first: when you pass &lt;code&gt;--with-compute-unit-price&lt;/code&gt;, the CLI &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/cli/src/compute_budget.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L64-L105" rel="noopener noreferrer"&gt;simulates the transaction and sets the limit to what it consumed&lt;/a&gt;, then recomputes the v1 priority fee from that. Those fees barely move. A transaction that cannot be simulated, because it uses a durable nonce, takes the &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/cli/src/wallet.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L1013-L1017" rel="noopener noreferrer"&gt;default limit&lt;/a&gt;, and that is where the two versions part.&lt;/p&gt;

&lt;p&gt;A legacy message with no limit instruction gets the runtime’s default, which gives each &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/builtins-default-costs/src/lib.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L108-L110" rel="noopener noreferrer"&gt;non-migrating builtin&lt;/a&gt; instruction 3,000 compute units. The System program and the ComputeBudget program are both builtins, so a nonce transfer with a price, three instructions, defaulted to 9,000. The v1 builder has no builtin discount: 200,000 per instruction for the two it emits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nonce transfer, legacy limit&lt;/strong&gt; — 9,000 CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nonce transfer, v1 limit&lt;/strong&gt; — 400,000 CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Priority fee at 1,000,000 micro-lamports/CU&lt;/strong&gt; — 9,000 → 400,000 lamports&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Priority fee at 100,000 micro-lamports/CU&lt;/strong&gt; — 900 → 40,000 lamports&lt;/p&gt;

&lt;p&gt;Same flag, 44 times the fee. &lt;code&gt;solana transfer&lt;/code&gt; does not take a compute-limit flag, so the only lever is the price: divide it by the ratio of the limits to pay what you paid before. Without a price there is no priority fee at all, but the declared limit still grows. An online transfer without a price is never simulated, so its v1 message declares the 1,400,000-unit maximum where the legacy one defaulted to 3,000. That costs nothing in fees. It does raise the cost the leader divides by when it &lt;a href="https://xroot.dev/blog/solana-transaction-priority-formula-compute-limit" rel="noopener noreferrer"&gt;ranks transactions&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What looks like it should break, and does not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Address lookup tables.&lt;/strong&gt; v1 has none, but the CLI never used them for its own messages, and the &lt;code&gt;address-lookup-table&lt;/code&gt; subcommands are ordinary instructions to the lookup-table program, so they build and send as v1 like anything else.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Program deploys get no faster.&lt;/strong&gt; v1 allows 4,096-byte transactions, but the loader &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/cli/src/program.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L2550-L2563" rel="noopener noreferrer"&gt;limits write instruction data to the 1,232-byte packet size for every version&lt;/a&gt;. v1’s larger header costs each write 13 bytes of program data, 1,012 down to 999 when one keypair pays for and owns the buffer, so an 800 KB program takes 801 writes instead of 791. With a price set, deploy writes are simulated in both versions and their fees stay close.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Memos.&lt;/strong&gt; &lt;code&gt;--memo&lt;/code&gt; already used the Memo v4 program before the change, and still does.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Durable nonces themselves&lt;/strong&gt;, as section 03 shows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more downstream effect belongs to everyone who reads transactions rather than signing them. CLI transactions now carry their compute settings in the v1 config rather than in ComputeBudget instructions, so fee dashboards that scan for those instructions will record nothing for them. That is &lt;a href="https://xroot.dev/blog/solana-transaction-v1" rel="noopener noreferrer"&gt;the general v1 reader problem&lt;/a&gt;, arriving through a tool many teams treat as a reference client.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to do before it ships
&lt;/h2&gt;

&lt;p&gt;No release contained the change when it merged; it is on master, on the way to the next minor line. Until Ledger ships v1 parsing, pin a 4.4 CLI on any machine that signs with a hardware wallet. Upgrade every machine in an offline-signing procedure together, never one at a time. And if you set a price on nonce or other unsimulated transactions, recompute it, because the limit it multiplies is no longer the runtime default.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;The Ledger result comes from reading the app’s parser and walking the CLI’s bytes through it, not from signing on a device. The sizes, signature check and limits come from compiling the same messages with the &lt;code&gt;solana-message&lt;/code&gt; 6.0.0 crate the merge commit uses; the legacy defaults come from the runtime’s builtin table. Which other CLI commands use default rather than simulated limits varies by subcommand, and only the transfer path is worked through here.&lt;/p&gt;




&lt;h2&gt;
  
  
  CLI Transaction V1: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does the Solana CLI still create legacy transactions?
&lt;/h3&gt;

&lt;p&gt;Not on Agave master after 29 October 2026. Pull request 16448 routes every CLI message through a MessageBuilder that compiles version 1 messages only. Released CLIs up to 4.4 still build legacy messages, and no release contained the change when it merged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I sign v1 transactions with a Ledger?
&lt;/h3&gt;

&lt;p&gt;Not with Ledger Solana app 1.16.1. Its parser reads a v1 message with the v0 layout and rejects it, so the CLI reports “Ledger received invalid Solana message”. It fails closed: the device refuses rather than displaying wrong details or offering blind signing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do durable nonce transactions still work in v1?
&lt;/h3&gt;

&lt;p&gt;Yes. The builder puts AdvanceNonceAccount first and uses the nonce value as the message lifetime, and the runtime detects durable nonces the same way for every message version. What changes is the priority fee: nonce transactions use the default compute limit, which the v1 builder sets at 200,000 per instruction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did my priority fee go up with the new CLI?
&lt;/h3&gt;

&lt;p&gt;v1 carries a flat lamport priority fee, which the CLI computes as price × limit ÷ 1,000,000. Commands that do not simulate, such as transfers with a durable nonce, now declare 200,000 compute units per instruction instead of the runtime’s 3,000 for each builtin instruction, so the same --with-compute-unit-price costs 44 times more on a nonce transfer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I sign offline with one CLI version and submit with another?
&lt;/h3&gt;

&lt;p&gt;Not across this change. Offline signing only works if every signer and the submitter compile byte-identical messages. A signature over the legacy encoding does not verify the v1 encoding of the same instructions, so everyone in the flow has to run a CLI on the same side of the change.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;The CLI change is a format change, and formats break at the edges: the device that parses them, the other machine that must produce the same bytes, and the fee that is now computed from a different number. Keep hardware-wallet machines on 4.4, move offline-signing setups together, and recompute prices on nonce transactions.&lt;/p&gt;

&lt;p&gt;The v1 wire format itself is in &lt;a href="https://xroot.dev/blog/solana-transaction-v1" rel="noopener noreferrer"&gt;Solana’s 4,096-byte transactions&lt;/a&gt;, durable nonces are in &lt;a href="https://xroot.dev/blog/solana-durable-nonce-pre-signed-transactions" rel="noopener noreferrer"&gt;the pre-signed transaction that never expires&lt;/a&gt;, and what the limit does to landing is in &lt;a href="https://xroot.dev/blog/solana-transaction-priority-formula-compute-limit" rel="noopener noreferrer"&gt;the leader’s ranking formula&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sources: Agave at the merge of &lt;a href="https://github.com/anza-xyz/agave/pull/16448?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#16448&lt;/a&gt;, commit &lt;code&gt;a4f4c5ea9e&lt;/code&gt;: &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/cli/src/message_builder.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;message_builder.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/cli/src/compute_budget.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L64-L105" rel="noopener noreferrer"&gt;compute_budget.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/cli/src/wallet.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L1013-L1017" rel="noopener noreferrer"&gt;wallet.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/cli/src/program.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L2550-L2563" rel="noopener noreferrer"&gt;program.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/remote-wallet/src/ledger.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L478-L490" rel="noopener noreferrer"&gt;remote-wallet/ledger.rs&lt;/a&gt;, &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/compute-budget-instruction/src/compute_budget_instruction_details.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L196-L220" rel="noopener noreferrer"&gt;compute_budget_instruction_details.rs&lt;/a&gt; and &lt;a href="https://github.com/anza-xyz/agave/blob/a4f4c5ea9e309ad09e2765954195fb10cf730f89/builtins-default-costs/src/lib.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L108-L110" rel="noopener noreferrer"&gt;builtins-default-costs&lt;/a&gt;; LedgerHQ/app-solana at &lt;code&gt;af6ed454f0&lt;/code&gt;: &lt;a href="https://github.com/LedgerHQ/app-solana/blob/af6ed454f0ed39cad8a5e1bca544ced18350a403/libsol/parser.c?utm_source=xroot.dev&amp;amp;utm_medium=referral#L132-L168" rel="noopener noreferrer"&gt;parser.c&lt;/a&gt; and &lt;a href="https://github.com/LedgerHQ/app-solana/blob/af6ed454f0ed39cad8a5e1bca544ced18350a403/src/handle_sign_message.c?utm_source=xroot.dev&amp;amp;utm_medium=referral#L509-L520" rel="noopener noreferrer"&gt;handle_sign_message.c&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>cli</category>
      <category>transactions</category>
      <category>hardwarewallets</category>
    </item>
    <item>
      <title>Agave 4.4 RPC Compatibility: What Actually Changed?</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:07 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/agave-44-rpc-compatibility-what-actually-changed-137c</link>
      <guid>https://dev.to/sulimanmukhtar/agave-44-rpc-compatibility-what-actually-changed-137c</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-rpc-4-4-what-changed" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Agave 4.4.0 shipped as the stable release on 28 October. For the weeks it takes operators to upgrade, an RPC endpoint behind a load balancer is a mix of 4.3 and 4.4 nodes, and each request can land on either. Most of what changed in 4.4 is a new parameter, and a 4.3 node does not reject a parameter it does not know. It ignores it.&lt;/p&gt;

&lt;p&gt;The release’s &lt;a href="https://github.com/anza-xyz/agave/blob/v4.4.0/CHANGELOG.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt; lists four RPC changes. Diffing the RPC crates from v4.3.2 to v4.4.0 finds six behavior changes that reach clients. One parameter and one notification fix are not in the changelog, and two other RPC fixes on the 4.4 line were backported to 4.3.x, so they are not differences at all. For each change below: what a 4.4.0 node returns, what a 4.3.2 node returns for the same request, and what to send so both give you a usable answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Six changes, and what a 4.3 node does with each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;getTransaction · minContextSlot&lt;/strong&gt; — 4.3 ignores it&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;getSignatureStatuses · commitment, minContextSlot&lt;/strong&gt; — 4.3 rejects or ignores&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;accountSubscribe, programSubscribe · dataSlice&lt;/strong&gt; — 4.3 ignores it&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;getLeaderSchedule · unknown identity&lt;/strong&gt; — {} on 4.3, -32022 on 4.4&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;getLeaderSchedule · keyByVoteAccount (not in the changelog)&lt;/strong&gt; — 4.3 ignores it&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;confirmed notifications that could be skipped (not in the changelog)&lt;/strong&gt; — fixed in 4.4 only&lt;/p&gt;

&lt;p&gt;The 4.3 column is not a guess. The request-config structs below are copied verbatim from &lt;code&gt;rpc-client-types/src/config.rs&lt;/code&gt; at both tags and parsed the way a node parses parameters, with &lt;code&gt;jsonrpc_core::Params::parse&lt;/code&gt; into the method’s argument tuple. Neither version sets &lt;code&gt;deny_unknown_fields&lt;/code&gt;, so a field the struct does not have is dropped without an error. The 4.4 column was checked against a node running 4.4.0.&lt;/p&gt;




&lt;h2&gt;
  
  
  getTransaction: null used to mean two things
&lt;/h2&gt;

&lt;p&gt;An indexer that learns of a transaction from one node and fetches it from another has always had a race: the second node may not have reached that slot yet, and &lt;code&gt;getTransaction&lt;/code&gt; answers &lt;code&gt;null&lt;/code&gt; both for “no such transaction” and for “not yet”. Since &lt;a href="https://github.com/anza-xyz/agave/pull/15091?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#15091&lt;/a&gt;, a 4.4.0 node takes &lt;code&gt;minContextSlot&lt;/code&gt; and fails fast when it is behind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;signature&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"commitment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"finalized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"maxSupportedTransactionVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"minContextSlot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;999999999999&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;←&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-32016&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Minimum context slot has not been reached"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"contextSlot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;node's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;finalized&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;slot&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 4.3.2 node parses the same request, keeps &lt;code&gt;encoding&lt;/code&gt;, &lt;code&gt;commitment&lt;/code&gt; and &lt;code&gt;maxSupportedTransactionVersion&lt;/code&gt;, and drops &lt;code&gt;minContextSlot&lt;/code&gt;, because &lt;a href="https://github.com/anza-xyz/agave/blob/v4.3.2/rpc-client-types/src/config.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L304-L312" rel="noopener noreferrer"&gt;its config struct has no such field&lt;/a&gt;. It answers &lt;code&gt;null&lt;/code&gt; exactly as before. A client that added &lt;code&gt;minContextSlot&lt;/code&gt; and now treats &lt;code&gt;null&lt;/code&gt; as “definitely not there” is wrong whenever a 4.3 node answers.&lt;/p&gt;




&lt;h2&gt;
  
  
  getSignatureStatuses: the config that fails on 4.3
&lt;/h2&gt;

&lt;p&gt;The same pull request gave &lt;code&gt;getSignatureStatuses&lt;/code&gt; a &lt;code&gt;commitment&lt;/code&gt; and a &lt;code&gt;minContextSlot&lt;/code&gt;, and made &lt;code&gt;searchTransactionHistory&lt;/code&gt; optional. That last part is what bites. In 4.3.2 the config struct’s only field had no default, so the natural 4.4-style request is a parse error on a 4.3 node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;signature&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"commitment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"finalized"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="mf"&gt;4.4&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;←&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"apiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"slot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
                      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confirmationStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"finalized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mf"&gt;4.3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;←&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-32602&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invalid params: missing field `searchTransactionHistory`."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;"searchTransactionHistory": false&lt;/code&gt; and the request parses on both, but a 4.3 node then drops &lt;code&gt;commitment&lt;/code&gt; and &lt;code&gt;minContextSlot&lt;/code&gt; and answers from its processed bank, which is the only bank this method used before 4.4. On 4.4 without a commitment it still defaults to processed, so leaving the parameter out gives the same answer on both versions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Subscriptions: dataSlice now does something
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;accountSubscribe&lt;/code&gt; and &lt;code&gt;programSubscribe&lt;/code&gt; have accepted a &lt;code&gt;dataSlice&lt;/code&gt; for a long time, because they share the account-config type with &lt;code&gt;getAccountInfo&lt;/code&gt;. In 4.3.2 the notifier &lt;a href="https://github.com/anza-xyz/agave/blob/v4.3.2/rpc/src/rpc_subscriptions.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L388" rel="noopener noreferrer"&gt;passed no slice to the encoder&lt;/a&gt;, so the full account went out regardless. Since &lt;a href="https://github.com/anza-xyz/agave/pull/15241?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#15241&lt;/a&gt; the slice is applied. Subscribing to the Clock sysvar on a 4.4.0 node:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;no dataSlice&lt;/strong&gt; — 40 bytes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;{ offset: 0, length: 8 }&lt;/strong&gt; — 8 bytes: the slot&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;{ offset: 16, length: 0 }&lt;/strong&gt; — 0 bytes&lt;/p&gt;

&lt;p&gt;A 4.3.2 node sends 40 bytes for all three. That makes this the one change that can break a client that changed nothing: a subscription config copied from a &lt;code&gt;getProgramAccounts&lt;/code&gt; call, slice included, used to stream full accounts and now streams the slice. If you set &lt;code&gt;dataSlice&lt;/code&gt; on a subscription, check the length of what arrives, because the answer depends on the node.&lt;/p&gt;




&lt;h2&gt;
  
  
  getLeaderSchedule: a new error and a new key
&lt;/h2&gt;

&lt;p&gt;Filtering the schedule by an identity that has no slots this epoch used to return &lt;code&gt;{}&lt;/code&gt;. Since &lt;a href="https://github.com/anza-xyz/agave/pull/12197?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#12197&lt;/a&gt; it is an error with a code that did not exist before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"identity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"11111111111111111111111111111112"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;←&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-32022&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Node 11111111111111111111111111111112 was not in the leader schedule for specified epoch"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code that reads an empty map as “not a leader this epoch” now throws on 4.4 nodes, and code written for the error never sees it from a 4.3 node. Handle both.&lt;/p&gt;

&lt;p&gt;The second change is missing from the changelog. &lt;a href="https://github.com/anza-xyz/agave/pull/13796?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#13796&lt;/a&gt; added &lt;code&gt;keyByVoteAccount&lt;/code&gt;, which keys the schedule by vote account instead of validator identity, while the &lt;code&gt;identity&lt;/code&gt; filter keeps matching on identity. On a 4.4.0 node, a validator’s leader slots come back unchanged under its vote account, the one &lt;code&gt;getVoteAccounts&lt;/code&gt; lists for that identity. A 4.3.2 node drops the flag and returns the identity-keyed map. Both are valid base-58 public keys, so nothing fails: the keys are simply not the ones the client asked for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Confirmed notifications that never came
&lt;/h2&gt;

&lt;p&gt;The last change is a fix, and it is also not in the changelog. When a slot’s confirmation arrived before its bank finished replaying, the node parked it until the bank froze. If the next slot’s confirmation was processed first, the node’s “last notified” mark moved past the parked slot, and the deferred notification &lt;a href="https://github.com/anza-xyz/agave/pull/14936?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;emitted nothing&lt;/a&gt;. The slot was later announced rooted without ever being announced confirmed, to &lt;code&gt;confirmed&lt;/code&gt; WebSocket subscribers and on the Geyser slot-status stream, and its prioritization-fee entry was never finalized.&lt;/p&gt;

&lt;p&gt;The pull request reports this “several times per day per node at mainnet volume” on the 4.2 line. 4.4.0 delivers the parked notification, and &lt;a href="https://github.com/anza-xyz/agave/pull/15068?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;a follow-up&lt;/a&gt; keeps it from being delivered twice. Neither was backported to 4.3. A consumer that waits for “confirmed” before it will accept “rooted” for a slot can stall on 4.3 nodes; treat rooted as implying confirmed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Writing requests that work on both
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Always send &lt;code&gt;searchTransactionHistory&lt;/code&gt;&lt;/strong&gt; in a &lt;code&gt;getSignatureStatuses&lt;/code&gt; config. It costs nothing on 4.4 and is required on 4.3.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Check &lt;code&gt;context.apiVersion&lt;/code&gt; before trusting a new parameter.&lt;/strong&gt; Every HTTP method that returns a context reports the answering node’s version, and behind a load balancer it can change from one request to the next. An answer from a 4.3 node was given without your &lt;code&gt;commitment&lt;/code&gt; or &lt;code&gt;minContextSlot&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Keep retrying a &lt;code&gt;null&lt;/code&gt; from &lt;code&gt;getTransaction&lt;/code&gt;&lt;/strong&gt; when you know the transaction exists. &lt;code&gt;getTransaction&lt;/code&gt; returns no context, so a 4.3 answer cannot be told apart; on 4.4, retry -32016 until the node’s &lt;code&gt;contextSlot&lt;/code&gt; catches up.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Treat &lt;code&gt;{}&lt;/code&gt; and -32022 as the same answer&lt;/strong&gt;, and verify vote-account keys against &lt;code&gt;getVoteAccounts&lt;/code&gt; before relying on &lt;code&gt;keyByVoteAccount&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Measure subscription payloads&lt;/strong&gt; if you pass &lt;code&gt;dataSlice&lt;/code&gt;, and drop it if you were relying on the full account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Rust clients, the 4.4 &lt;code&gt;solana-rpc-client&lt;/code&gt; adds &lt;code&gt;get_signature_statuses_with_config&lt;/code&gt;, and &lt;a href="https://github.com/anza-xyz/agave/pull/15309?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#15309&lt;/a&gt; lets &lt;code&gt;get_fee_for_message&lt;/code&gt; take a v1 message. One more 4.4 change, a bound on the root scan in &lt;code&gt;getBlocks&lt;/code&gt;, makes the method faster without changing what it returns.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;The 4.3 behavior is reproduced from its source, by parsing requests with the exact structs a 4.3.2 node uses, not by sending them to a 4.3.2 node. How long mainnet stays mixed, and how a given provider routes requests, are not measured here. Rejecting base58 encoding for v1 transactions and the camelCase fields of &lt;code&gt;getAgGenesisCert&lt;/code&gt; were both backported to 4.3.x, so they are left out as changes between these two versions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agave 4.4 RPC: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does getTransaction support minContextSlot?
&lt;/h3&gt;

&lt;p&gt;From Agave 4.4.0, yes. A node whose context slot at the requested commitment is below the minimum returns error -32016 with the node’s contextSlot in the error data, instead of null. A 4.3 node accepts the parameter and ignores it, so it can still return null for a transaction it simply has not seen yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does getSignatureStatuses fail with “missing field searchTransactionHistory”?
&lt;/h3&gt;

&lt;p&gt;The node is running 4.3 or earlier. Before 4.4.0 the config object’s searchTransactionHistory field had no default, so a config that only sets commitment or minContextSlot fails to parse with error -32602. Always send searchTransactionHistory explicitly and the request parses on both versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is RPC error -32022?
&lt;/h3&gt;

&lt;p&gt;New in Agave 4.4.0: getLeaderSchedule returns -32022 when the identity you filter by is not in the leader schedule for the epoch. Earlier versions returned an empty object for the same request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does accountSubscribe support dataSlice?
&lt;/h3&gt;

&lt;p&gt;From Agave 4.4.0, accountSubscribe and programSubscribe apply dataSlice to binary encodings, and a zero-length slice returns empty data. Earlier versions accepted dataSlice on subscriptions and ignored it, always sending full account data.&lt;/p&gt;

&lt;h3&gt;
  
  
  How can a client tell which version answered?
&lt;/h3&gt;

&lt;p&gt;HTTP methods that return a context object include context.apiVersion, the answering node’s version; WebSocket notifications do not. Behind a load balancer each request can reach a different node, so check it per response, not once per connection.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;Agave 4.4 mostly adds parameters, and 4.3 nodes ignore parameters they do not know. Until the fleet behind your endpoint has upgraded, the new protections are only as good as the version that answered, so read &lt;code&gt;apiVersion&lt;/code&gt; and write requests that are valid on both.&lt;/p&gt;

&lt;p&gt;The full list of custom error codes, which -32022 now extends, is in &lt;a href="https://xroot.dev/blog/solana-rpc-error-codes-explained" rel="noopener noreferrer"&gt;Solana RPC error codes explained&lt;/a&gt;, and what each commitment level means is in &lt;a href="https://xroot.dev/blog/solana-deposit-confirmations-how-many" rel="noopener noreferrer"&gt;how many confirmations a deposit needs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sources: the &lt;a href="https://github.com/anza-xyz/agave/releases/tag/v4.4.0?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;v4.4.0 release&lt;/a&gt; and its &lt;a href="https://github.com/anza-xyz/agave/blob/v4.4.0/CHANGELOG.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;; &lt;code&gt;rpc-client-types/src/config.rs&lt;/code&gt; at &lt;a href="https://github.com/anza-xyz/agave/blob/v4.3.2/rpc-client-types/src/config.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;v4.3.2&lt;/a&gt; and &lt;a href="https://github.com/anza-xyz/agave/blob/v4.4.0/rpc-client-types/src/config.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;v4.4.0&lt;/a&gt;; &lt;code&gt;rpc/src/rpc_subscriptions.rs&lt;/code&gt; at &lt;a href="https://github.com/anza-xyz/agave/blob/v4.3.2/rpc/src/rpc_subscriptions.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L388" rel="noopener noreferrer"&gt;v4.3.2&lt;/a&gt; and &lt;a href="https://github.com/anza-xyz/agave/blob/v4.4.0/rpc/src/rpc_subscriptions.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L388-L394" rel="noopener noreferrer"&gt;v4.4.0&lt;/a&gt;; and pull requests #12197, #13796, #14936, #15068, #15091, #15131, #15241 and #15309 in anza-xyz/agave.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>rpc</category>
      <category>agave</category>
      <category>compatibility</category>
    </item>
    <item>
      <title>Solana P-256 Syscalls: Agave and Firedancer, Edge Case by Edge Case</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:05 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/solana-p-256-syscalls-agave-and-firedancer-edge-case-by-edge-case-12c</link>
      <guid>https://dev.to/sulimanmukhtar/solana-p-256-syscalls-agave-and-firedancer-edge-case-by-edge-case-12c</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-p256-syscalls-simd-0565" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On 27 October both of Solana’s independent runtimes merged the same new feature, written separately. Agave added P-256 curve operations to its syscalls at 02:38 UTC; Firedancer added its own sixteen hours later. Different authors, different languages, different arithmetic, and consensus needs them to return the same bytes for every input a program can send, including every input the specification rejects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/f13f5d657ccae32cb19cae84ccb6c781cd125ad7/proposals/0565-secp256r1-syscalls.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0565&lt;/a&gt; extends Solana’s curve syscalls to P-256, also called secp256r1: the curve behind passkeys, WebAuthn and the secure chips in phones. We wrote our own reference implementation from the curve’s published constants, generated 626 test vectors aimed at the edges of the specification, ran them through the code Agave’s syscalls call, and checked Firedancer’s tests and source for the same cases. We found no case where the two differ. We also built a small program that verifies a P-256 signature with three of the new syscalls and no modular inverse. It costs 22,760 compute units.&lt;/p&gt;




&lt;h2&gt;
  
  
  What SIMD-0565 adds
&lt;/h2&gt;

&lt;p&gt;No new syscall names. The proposal adds two curve ids, 7 for little-endian and 0x87 for big-endian, to the four curve syscalls programs already use for curve25519 and BLS12-381: point validation, decompression, group operations (add, subtract, multiply by a scalar) and multiscalar multiplication. Points are 64 bytes, x then y, and the point at infinity is 64 zero bytes. Compressed points are 33 bytes: a &lt;code&gt;02&lt;/code&gt; or &lt;code&gt;03&lt;/code&gt; parity prefix, then x. Every call has a fixed price, charged before its inputs are read, whether it succeeds or not:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validate a point&lt;/strong&gt; — 20&amp;nbsp;CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decompress&lt;/strong&gt; — 610&amp;nbsp;CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add or subtract&lt;/strong&gt; — 520&amp;nbsp;CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiply by a scalar&lt;/strong&gt; — 9,000&amp;nbsp;CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiscalar, n points&lt;/strong&gt; — 9,000&amp;nbsp;+&amp;nbsp;3,800&amp;nbsp;×&amp;nbsp;(n&amp;nbsp;−&amp;nbsp;1)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most points within 1.4M CU&lt;/strong&gt; — 367&lt;/p&gt;

&lt;p&gt;A multiscalar call with more than 512 points fails outright, before anything is charged. The SIMD’s authors set the prices on a private ten-validator cluster with 200 ms slots, so that a block packed with any one of these operations still replays within its slot.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two implementations of one rulebook
&lt;/h2&gt;

&lt;p&gt;Agave’s version, &lt;a href="https://github.com/anza-xyz/agave/pull/13918?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#13918&lt;/a&gt;, opened in July, calls &lt;a href="https://crates.io/crates/solana-secp256r1/0.1.0?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;solana-secp256r1 0.1.0&lt;/a&gt;, a pure-Rust P-256 crate Anza published in September. Firedancer’s, &lt;a href="https://github.com/firedancer-io/firedancer/pull/12498?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#12498&lt;/a&gt;, opened on 22 October, is C built on AWS’s &lt;a href="https://github.com/awslabs/s2n-bignum?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;s2n-bignum&lt;/a&gt; routines where the build has them. What the two share is everything consensus can see: the same feature-gate key, the same seven compute prices, and five test vectors. Firedancer’s syscall tests reuse the expected outputs from Agave’s, in both byte orders.&lt;/p&gt;

&lt;p&gt;Agreement matters more than either implementation being right by its own tests. If one client returns 0 where the other returns 1, a program that branches on the result writes different account data on each, the two compute different bank hashes for the same block, and the network splits along client lines. The dangerous inputs are the ones nobody thought to try.&lt;/p&gt;




&lt;h2&gt;
  
  
  The edge cases, and what both clients do
&lt;/h2&gt;

&lt;p&gt;These are the inputs where a reasonable implementation could choose differently. In every row the two give the same answer: Agave on our vectors through the crate its syscalls call, Firedancer in its own tests or, where its tests don’t reach, in its source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;64 zero bytes&lt;/strong&gt; — identity, valid&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;x&amp;nbsp;=&amp;nbsp;0, y&amp;nbsp;=&amp;nbsp;√b, a real point&lt;/strong&gt; — valid&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(0,&amp;nbsp;1)&lt;/strong&gt; — rejected&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;x or y written as value&amp;nbsp;+&amp;nbsp;p&lt;/strong&gt; — rejected&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefix other than 02 or 03&lt;/strong&gt; — rejected&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;33 zero bytes, SEC1’s identity&lt;/strong&gt; — rejected&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P&amp;nbsp;+&amp;nbsp;P, or P&amp;nbsp;−&amp;nbsp;(−P)&lt;/strong&gt; — 2P&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P&amp;nbsp;−&amp;nbsp;P, or k·P&amp;nbsp;+&amp;nbsp;(n&amp;nbsp;−&amp;nbsp;k)·P&lt;/strong&gt; — identity&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;0·P, or k times the identity&lt;/strong&gt; — identity&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scalar n, n&amp;nbsp;+&amp;nbsp;1 or 2²⁵⁶&amp;nbsp;−&amp;nbsp;1&lt;/strong&gt; — rejected&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiscalar, 0 points&lt;/strong&gt; — identity&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiscalar, bad last scalar or point&lt;/strong&gt; — rejected&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiscalar, over 512 points&lt;/strong&gt; — InvalidLength&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Any P-256 call before activation&lt;/strong&gt; — InvalidAttribute&lt;/p&gt;

&lt;p&gt;“Rejected” means the call returns 1 and writes nothing; the last two rows are errors that abort the transaction. Firedancer’s &lt;a href="https://github.com/firedancer-io/firedancer/blob/8e7388dbcbb05735f1b162976e9ff1d6affe76ab/src/ballet/secp256r1/test_secp256r1.c?utm_source=xroot.dev&amp;amp;utm_medium=referral#L426-L539" rel="noopener noreferrer"&gt;tests&lt;/a&gt; cover the identity, off-curve and out-of-range points, the &lt;code&gt;04&lt;/code&gt; prefix, both byte orders, P + P, P − P, P + (−P), adding and subtracting the identity, 0·P, k times the identity, n·P, multiscalar calls of 0, 1, 2 and 70 points including a bad scalar or point, running out of compute and the gate being off. The x = 0 point, the value-plus-p encodings, the other prefixes, n + 1, doubling through subtraction and cancellation inside a multiscalar call we confirmed in &lt;a href="https://github.com/firedancer-io/firedancer/blob/8e7388dbcbb05735f1b162976e9ff1d6affe76ab/src/ballet/secp256r1/fd_secp256r1.c?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;its source&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  626 vectors, and three things to know before you call them
&lt;/h2&gt;

&lt;p&gt;Our reference is about 160 lines of Python written from the curve constants in &lt;a href="https://csrc.nist.gov/pubs/sp/800/186/final?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;NIST SP 800-186&lt;/a&gt; and the SIMD text, without reading either client’s arithmetic. It reproduces all five shared vectors. From it we generated 313 vectors per byte order: 73 hand-picked cases and 240 random operations. The hand-picked ones include everything in the table above, 64-point multiscalar calls whose two 32-point halves are equal or cancel (Firedancer works in batches of 32 points, so these land on its batch boundary), and calls of 367 and 512 points. Run through solana-secp256r1 0.1.0, called exactly as &lt;a href="https://github.com/anza-xyz/agave/blob/cf58f070111534ce5bf490578ba2bf4dea79a7a7/syscalls/src/lib.rs?utm_source=xroot.dev&amp;amp;utm_medium=referral#L1736-L1835" rel="noopener noreferrer"&gt;Agave’s syscalls&lt;/a&gt; call it at the merge commit, all 626 match, including all 127 rejections.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;x = 0 is on the curve.&lt;/strong&gt; The identity’s encoding has x = 0, and so does a real point, (0, √b). Both clients treat only the exact all-zero encoding as the identity, so (0, √b) is an ordinary point, decompression of x = 0 succeeds, and (0, 1) is rejected.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Scalars are never reduced.&lt;/strong&gt; A scalar of n, n + 1 or 2²⁵⁶ − 1 makes the call return 1, even when the point is the identity. A SHA-256 hash is a valid scalar only if it is below n, so reduce it first: any 256-bit value is below 2n, so one comparison and one subtraction are enough.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Little-endian is per coordinate.&lt;/strong&gt; With curve id 7, x and y are each byte-reversed in place, and a compressed point keeps its prefix byte first with only x reversed, which is not SEC1. The generator written in the other byte order fails validation.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The one place the code differs
&lt;/h2&gt;

&lt;p&gt;Read side by side, the clients take different paths in one place: an unknown curve id or group operation in validation, group operations or multiscalar multiplication. Firedancer’s &lt;a href="https://github.com/firedancer-io/firedancer/blob/8e7388dbcbb05735f1b162976e9ff1d6affe76ab/src/flamenco/vm/syscall/fd_vm_syscall_curve.c?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;syscalls&lt;/a&gt; always abort with &lt;code&gt;InvalidAttribute&lt;/code&gt;. Agave aborts only when the feature from &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/f13f5d657ccae32cb19cae84ccb6c781cd125ad7/proposals/0137-curve25519-error.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0137&lt;/a&gt; is active, and otherwise returns 1. That feature has been active on mainnet since slot 311,904,000, the first slot of epoch 722, so the result is the same there. Nothing on the P-256 path itself branches this way.&lt;/p&gt;




&lt;h2&gt;
  
  
  A signature check from three syscalls
&lt;/h2&gt;

&lt;p&gt;The obvious use is checking passkey signatures inside a program, which today goes through the &lt;a href="https://xroot.dev/blog/solana-passkeys-secp256r1-webauthn" rel="noopener noreferrer"&gt;secp256r1 precompile&lt;/a&gt;. ECDSA verification normally needs s⁻¹ mod n, and no syscall computes it. It can be avoided. The signature is valid when s·R equals z·G + r·Q up to sign, where R is the point whose x-coordinate is r, z the message hash and Q the public key. Decompression gives R, one multiplication gives s·R, one two-point multiscalar call gives z·G + r·Q, and comparing x-coordinates ignores the sign:&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;use&lt;/span&gt; &lt;span class="nn"&gt;solana_define_syscall&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;
    &lt;span class="nn"&gt;curve_constants&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;GROUP_OP_MUL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SECP256R1_BE&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nn"&gt;definitions&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;sol_curve_decompress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sol_curve_group_op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sol_curve_multiscalar_mul&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cd"&gt;/// ECDSA check: s·R == ±(z·G + r·Q), where R is the point with x = r.&lt;/span&gt;
&lt;span class="cd"&gt;/// Same answer as the textbook check except for x(R) = r + n, which has&lt;/span&gt;
&lt;span class="cd"&gt;/// probability about 2^-128 and is not tried here.&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;verify_p256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_zero&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;is_zero&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&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;span class="c1"&gt;// The syscalls reject scalars &amp;gt;= n instead of reducing them.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&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;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;c&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="mi"&gt;0x02&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// either parity: s·(−R) = −(s·R) has the same x&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.copy_from_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;t&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="mi"&gt;0u8&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="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&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="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;scalars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r&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;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// R: 610 CU&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;sol_curve_decompress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SECP256R1_BE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="nf"&gt;.as_ptr&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="nf"&gt;.as_mut_ptr&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&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;span class="c1"&gt;// U = s·R: 9,000 CU&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;sol_curve_group_op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SECP256R1_BE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GROUP_OP_MUL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.as_ptr&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="nf"&gt;.as_ptr&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="nf"&gt;.as_mut_ptr&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&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;span class="c1"&gt;// T = z·G + r·Q: 9,000 + 3,800 CU; returns 1 if Q is not a valid point&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;sol_curve_multiscalar_mul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;SECP256R1_BE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;scalars&lt;/span&gt;&lt;span class="nf"&gt;.as_ptr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="nf"&gt;.as_ptr&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;const&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;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="nf"&gt;.as_mut_ptr&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="mi"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&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;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&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;==&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&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;span class="c1"&gt;// N and G are the group order and generator, big-endian; is_zero and sub are byte helpers.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We built it with platform-tools v1.57 and ran it in &lt;a href="https://crates.io/crates/solana-sbpf/0.26.0?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;solana-sbpf 0.26.0&lt;/a&gt;, the VM version Agave pinned at the merge, set up as in our &lt;a href="https://xroot.dev/blog/solana-cpi-invoke-signed-v2-simd-0670" rel="noopener noreferrer"&gt;CPI measurement&lt;/a&gt;, with the three curve syscalls implemented the way Agave implements them on top of the same crate. OpenSSL signed 20 messages, and the program got each signature, the same signature with the other half of S, a hash with one bit flipped, and a different public key. All 80 verdicts match textbook ECDSA:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Curve syscalls: 610&amp;nbsp;+&amp;nbsp;9,000&amp;nbsp;+&amp;nbsp;12,800&lt;/strong&gt; — 22,410&amp;nbsp;CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory syscalls&lt;/strong&gt; — 30&amp;nbsp;CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Program instructions&lt;/strong&gt; — 320&amp;nbsp;CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One signature check&lt;/strong&gt; — 22,760&amp;nbsp;CU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Precompile, cost-model charge&lt;/strong&gt; — 4,800&amp;nbsp;CU&lt;/p&gt;

&lt;p&gt;Two differences from the precompile. Cost: a check through the syscalls takes about 4.7 times the 4,800 compute units the cost model charges per precompile signature, before the message is hashed. Policy: this check accepts both halves of S, where the precompile rejects the high one, the reason half of raw WebAuthn signatures fail it. A program that wants the precompile’s rule adds s ≤ n/2; one that doesn’t can skip normalising S altogether. The SIMD gives that freedom as the reason for curve operations instead of a single verify syscall, which would have had to fix one malleability rule for everyone.&lt;/p&gt;




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

&lt;p&gt;Both pull requests introduce the gate &lt;code&gt;secp256r1_syscall_enabled&lt;/code&gt;, key &lt;code&gt;SeRjtX7Abiq5hQYbzdCny266oveDseHZh9tnrs2cdVC&lt;/code&gt;, so the syscalls were off on every cluster when they merged, and a program calling them gets &lt;code&gt;InvalidAttribute&lt;/code&gt; until the feature is activated. The SIMD itself was still marked Draft, with its compute prices added a week earlier. The program side is ready: &lt;a href="https://crates.io/crates/solana-define-syscall/5.2.0?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;solana-define-syscall 5.2.0&lt;/a&gt;, published in July, already declares the curve ids and all four syscalls, and our program imports them from there.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;We did not build or run Firedancer. Its side rests on reading its source and on its own &lt;a href="https://github.com/firedancer-io/firedancer/actions/runs/40559696236/job/122385083035?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;CI run&lt;/a&gt;, which passed &lt;code&gt;test_secp256r1&lt;/code&gt; and &lt;code&gt;test_vm_syscall_curve&lt;/code&gt; on the pull request’s final commit, whose P-256 files are identical to the merged ones. On Agave’s side we ran the crate its syscalls call, not the syscalls: memory translation, compute charging and the gate we read rather than executed. Vectors find disagreements; they cannot prove there are none. The compute prices are the SIMD’s calibration, not our measurement, and 22,760 is one program built with one toolchain.&lt;/p&gt;




&lt;h2&gt;
  
  
  P-256 Syscalls: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What does SIMD-0565 add to Solana?
&lt;/h3&gt;

&lt;p&gt;It extends the four existing curve syscalls (point validation, decompression, group operations and multiscalar multiplication) to P-256, also called secp256r1, with curve id 7 for little-endian and 0x87 for big-endian encoding. Points are 64 bytes, x then y, with 64 zero bytes for the point at infinity. Agave and Firedancer both merged an implementation on 27 October 2026, behind a new feature gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does it cost to verify a P-256 signature with the new syscalls?
&lt;/h3&gt;

&lt;p&gt;Our test program verified an ECDSA P-256 signature for 22,760 compute units: 22,410 for one decompression, one scalar multiplication and one two-point multiscalar multiplication, 30 for memory syscalls and about 320 program instructions. Hashing the message is extra. The secp256r1 precompile is charged 4,800 compute units per signature in the cost model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do the secp256r1 syscalls reduce scalars modulo the group order?
&lt;/h3&gt;

&lt;p&gt;No. A scalar of n or more makes the call return 1 in both Agave and Firedancer, including n + 1 and 2^256 − 1. A program that uses a 256-bit hash as a scalar must reduce it first; since any 256-bit value is below 2n, one comparison and one subtraction are enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is the point at infinity encoded for Solana’s P-256 syscalls?
&lt;/h3&gt;

&lt;p&gt;As 64 zero bytes. Only that exact encoding is the identity: (0, √b), which also has x = 0, is an ordinary point on the curve, and (0, 1) is rejected. Decompression cannot produce the identity, and 33 zero bytes are rejected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are the P-256 syscalls active on mainnet?
&lt;/h3&gt;

&lt;p&gt;Not when they were merged. Both pull requests introduced the feature gate secp256r1_syscall_enabled, so it was inactive on every cluster, and every P-256 call fails with InvalidAttribute until it is activated.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;Agave and Firedancer implemented SIMD-0565’s P-256 syscalls independently and agree on every edge case we could find, from the x = 0 point to unreduced scalars; with them, a program verifies a P-256 signature for 22,760 compute units and sets its own rule for S.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/blob/f13f5d657ccae32cb19cae84ccb6c781cd125ad7/proposals/0565-secp256r1-syscalls.md?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0565&lt;/a&gt; at commit &lt;code&gt;f13f5d65&lt;/code&gt;; Agave &lt;a href="https://github.com/anza-xyz/agave/pull/13918?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#13918&lt;/a&gt;, merged as &lt;code&gt;cf58f070&lt;/code&gt;; Firedancer &lt;a href="https://github.com/firedancer-io/firedancer/pull/12498?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;#12498&lt;/a&gt;, merged as &lt;code&gt;8e7388db&lt;/code&gt;; solana-secp256r1 0.1.0, solana-define-syscall 5.2.0 and solana-sbpf 0.26.0. The reference, vectors, harnesses and test program are our own.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>secp256r1</category>
      <category>firedancer</category>
      <category>computeunits</category>
    </item>
    <item>
      <title>Solana Geo Leader Schedule: Location Pays, Splitting Doesn’t</title>
      <dc:creator>Suliman Mokhtar</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:24:03 +0000</pubDate>
      <link>https://dev.to/sulimanmukhtar/solana-geo-leader-schedule-location-pays-splitting-doesnt-3g3m</link>
      <guid>https://dev.to/sulimanmukhtar/solana-geo-leader-schedule-location-pays-splitting-doesnt-3g3m</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://xroot.dev/blog/solana-geo-aware-leader-schedule-simd-0675" rel="noopener noreferrer"&gt;xroot.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Under Alpenglow’s fast leader handover, the gap between two leaders is mostly the time a block takes to cross from one to the next. Today’s schedule picks leaders at random, so that crossing is often an ocean. &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/pull/675?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0675&lt;/a&gt; proposes to reorder the schedule so that leaders near each other go one after another.&lt;/p&gt;

&lt;p&gt;In review, Dean Little raised the obvious worry: that the reordering “appears to incentivize sybils”, letting validators use geography, “or even just a self-attestation of geography”, to line up their leader rotations “with better-than-random odds”. Two days later, the author of the proposal’s parameter analysis answered that splitting stake barely moves the odds, 7.146% to 7.147% in one example, because “the majority of the ‘upside’ for extractors (compared to random) is already given by the schedule design.”&lt;/p&gt;

&lt;p&gt;We rebuilt the reordering in Rust from the specification to check both statements. The reviewer is right about splitting: it changes nothing, not even one window. But the second half of the sentence is the finding. The upside the schedule hands out depends on where an operator’s stake sits, and an honest operator in the right region gets back-to-back windows five to seven times as often as today, without splitting or lying about anything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bins, radii and runs
&lt;/h2&gt;

&lt;p&gt;The base schedule is computed exactly as today, one stake-weighted random leader per four-slot window. SIMD-0675 then walks it in order and drops each leader into a bin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Radius.&lt;/strong&gt; Every validator has a radius, the smallest distance around it that holds &lt;code&gt;STAKE_FLOOR&lt;/code&gt; (10%) of all stake. Dense places get small radii, isolated ones large radii. Locations are self-reported, through &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/pull/674?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0674&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Bins.&lt;/strong&gt; A leader joins a random open bin whose anchor is within the larger of the two radii, or opens a new bin if none is.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Runs.&lt;/strong&gt; When a bin holds &lt;code&gt;RUN_LENGTH&lt;/code&gt; leaders, it is shuffled and written out as a block of consecutive windows. The proposal says 3; reviewers have argued for 7.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every validator keeps exactly the windows it would have had. Only their order changes. With 200 ms slots, a run of three windows is 2.4 seconds of block production from one area of the map.&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;for&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;base&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;near&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;bins&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.filter&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;i&lt;/span&gt;&lt;span class="p"&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bins&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="py"&gt;.anchor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nf"&gt;dist2&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;vs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&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;vs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;near&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;bins&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="n"&gt;bins&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&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;span class="k"&gt;else&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;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;near&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="nf"&gt;.roll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;near&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;())];&lt;/span&gt; &lt;span class="c1"&gt;// one roll per join&lt;/span&gt;
        &lt;span class="n"&gt;bins&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="py"&gt;.members&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;i&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;bins&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bi&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="py"&gt;.members&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;run_length&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bins&lt;/span&gt;&lt;span class="nf"&gt;.remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="nf"&gt;.shuffle&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;b&lt;/span&gt;&lt;span class="py"&gt;.members&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Fisher-Yates&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="nf"&gt;.extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="py"&gt;.members&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="c1"&gt;// end of epoch: shuffle the open bins, shuffle each one, append&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The model, and a check against the proposal
&lt;/h2&gt;

&lt;p&gt;Our implementation follows the specification’s pseudocode line for line: the radius, the nearness test, one random draw per join, Fisher-Yates within a run and a shuffled flush at the end of the epoch. Two simplifications are deliberate. The random generator is SplitMix64 rather than the specified ChaCha20, which changes which schedule comes out but not its statistics. And the network is stylized: about 800 validators with lognormal stake across 19 metros. Asia holds 10.4% of stake, the figure in &lt;a href="https://solana.com/news/geo-aware-leader-schedule?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;the parameter analysis published on solana.com&lt;/a&gt;; the European and North American split and the metro mix are our assumptions. Latency between leaders uses the analysis’ estimate, 1.5 times the great-circle distance at 200 km per millisecond. Each scenario is 108,000 windows, an epoch, averaged over five seeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mean handover, random schedule (SIMD’s figure)&lt;/strong&gt; — 37.3 ms (36.2)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RUN_LENGTH 3&lt;/strong&gt; — 18.2 ms (17.0)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RUN_LENGTH 7&lt;/strong&gt; — 11.6 ms (10.8)&lt;/p&gt;

&lt;p&gt;The proposal’s simulation, on the real stake distribution of epoch 1038 and measured round-trip times, is in brackets. Our stylized network lands within about a millisecond at every run length, which is close enough to trust it for the question it was built for: who gets consecutive windows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Splitting stake changes nothing
&lt;/h2&gt;

&lt;p&gt;We gave one operator 3% of stake as one validator in Frankfurt, then as six validators of 0.5% each in the same place. The two schedules were identical, window for window, at every run length.&lt;/p&gt;

&lt;p&gt;That is not a statistical tie, it is structural. The base schedule gives the six the same windows the one would have had. The radius of a location is computed from all the stake there, so it is the same. And a bin only ever asks about locations and radii, never identities. Sybils at one address are invisible to the reorder, which is the reviewer’s 7.146% to 7.147% taken to its limit.&lt;/p&gt;

&lt;p&gt;Splitting across regions is worse than useless for an extractor. The same 3% spread over Frankfurt, Ashburn and Singapore led two windows in a row 4.2% of the time at &lt;code&gt;RUN_LENGTH&lt;/code&gt; 3, against 7.6% for the operator that stayed in one place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Location changes a lot
&lt;/h2&gt;

&lt;p&gt;Consecutive windows under one operator are the precondition for multi-block MEV: two windows are eight slots in which one party orders every transaction. We measured how often an operator’s window is followed by another of its own, under today’s random schedule and under the reorder. Columns are random, then &lt;code&gt;RUN_LENGTH&lt;/code&gt; 3, then 7.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1% operator, Frankfurt&lt;/strong&gt; — 1.06%&amp;nbsp;→ 2.56%&amp;nbsp;→ 3.31%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1% operator, Tokyo&lt;/strong&gt; — 1.06%&amp;nbsp;→ 5.31%&amp;nbsp;→ 7.66%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3% operator, Frankfurt&lt;/strong&gt; — 3.15%&amp;nbsp;→ 7.56%&amp;nbsp;→ 9.22%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3% operator, Tokyo&lt;/strong&gt; — 3.15%&amp;nbsp;→ 15.10%&amp;nbsp;→ 19.10%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3% split over three regions&lt;/strong&gt; — 3.15%&amp;nbsp;→ 4.18%&amp;nbsp;→ 4.90%&lt;/p&gt;

&lt;p&gt;Under random, the chance is the operator’s stake share, wherever it sits. Under the reorder, a run is filled from nearby stake, so the chance tracks the operator’s share of its region instead. Three percent of all stake is a modest slice of Europe and a large slice of Asia, the smallest region that still forms bins of its own at a 10% floor. The same operator in Tokyo gets back-to-back windows 4.8 times as often as today at &lt;code&gt;RUN_LENGTH&lt;/code&gt; 3, and 6.1 times at 7, twice what it would get in Frankfurt.&lt;/p&gt;

&lt;p&gt;Nobody lied and nobody split stake. This is the schedule working as designed, and it is the reviewer’s point stated with a location attached: the upside comes from the design, and the design pays most to the largest operator in the smallest self-contained region. The solana.com analysis already notes that Asia sits just above the floor, at 10.4%, “with very little margin”. Our numbers say the margin is also where consecutive control is cheapest.&lt;/p&gt;




&lt;h2&gt;
  
  
  And lying about it
&lt;/h2&gt;

&lt;p&gt;The attack the proposal’s authors model is a validator that declares an isolated location, so that no one else joins its bins. The solana.com analysis uses the most isolated point it could find, in the South Pacific, and reports that at &lt;code&gt;RUN_LENGTH&lt;/code&gt; 7 lying is worth 0.28 extra consecutive windows to a validator with 4.07% of stake, the largest today, and 1.69 to one with 10%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.07% operator, back-to-back, truthful in Frankfurt&lt;/strong&gt; — 4.23%&amp;nbsp;→ 10.13%&amp;nbsp;→ 12.15%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.07% operator, back-to-back, declares South Pacific&lt;/strong&gt; — 4.23%&amp;nbsp;→ 32.34%&amp;nbsp;→ 41.59%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10% operator, back-to-back, truthful in Frankfurt&lt;/strong&gt; — 10.05%&amp;nbsp;→ 21.72%&amp;nbsp;→ 25.98%&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10% operator, back-to-back, declares South Pacific&lt;/strong&gt; — 10.05%&amp;nbsp;→ 65.11%&amp;nbsp;→ 83.19%&lt;/p&gt;

&lt;p&gt;Measured the analysis’ way, as the expected run remaining from one of the liar’s windows, our model puts the gain from lying, against the same operator in Frankfurt, at 0.58 windows for the 4.07% validator and 2.43 for the 10% one, both at &lt;code&gt;RUN_LENGTH&lt;/code&gt; 7. That is higher than the analysis’ figures, which come from the real map, where a few validators in Argentina and Australia can still reach the isolated point. The direction is the same: lying pays a little below the floor and a lot at it, which is why &lt;code&gt;STAKE_FLOOR&lt;/code&gt; is pinned at 10%.&lt;/p&gt;

&lt;p&gt;The proposal’s own location-fibbing table shows lying rarely improves a validator’s latency. It was never meant to. The reason to lie is the run, not the handover.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it means for the parameters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;RUN_LENGTH&lt;/code&gt; moves every number above. Going from 3 to 7 buys 6.6 milliseconds of mean handover in our model and raises back-to-back control for every operator we measured. The proposal’s authors prefer 3 for exactly this reason; the analysis finds a knee at 7 and calls 3 “the conservative choice”. Reviewers suggested handling Sybils outside the protocol, by raising the &lt;a href="https://xroot.dev/blog/alpenglow-validator-admission-2000-seats" rel="noopener noreferrer"&gt;validator admission ticket&lt;/a&gt; or by detecting misreported locations, and the authors agreed that detection is probably possible.&lt;/p&gt;

&lt;p&gt;Neither remedy touches the effect in section 04. A ticket prices the number of validators, and the location-driven gain needs only one. Detection catches false locations, and the Tokyo operator’s location is true. If the concern is consecutive control by honest operators, the levers are the run length and the floor, and the floor is already pressed against Asia’s share.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this does not show
&lt;/h2&gt;

&lt;p&gt;The network is stylized, so the exact percentages belong to it; the mechanism, that consecutive control tracks local rather than global share, does not depend on the split we chose. Back-to-back windows are a precondition for multi-block MEV, not a measure of its value, which the analysis also stresses. The random generator differs from the specified one. And SIMD-0675 and SIMD-0674 are open proposals, last amended on 22 October, so the constants may still change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Geo-Aware Leader Schedule: Questions People Actually Ask
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Solana’s geo-aware leader schedule?
&lt;/h3&gt;

&lt;p&gt;SIMD-0675, an open proposal, keeps today’s stake-weighted random leader schedule and then reorders it: leaders that are geographically close are grouped into runs of RUN_LENGTH consecutive leader windows, so the handover between them is short. Every validator keeps exactly as many windows as before.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does splitting stake into several validators help under SIMD-0675?
&lt;/h3&gt;

&lt;p&gt;Not at one location. The reorder sees a validator’s location and stake, not its identity, so six validators in one city with a combined stake behave exactly like one. In our model they produced identical schedules, window for window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where does the geo-aware schedule give an operator more consecutive windows?
&lt;/h3&gt;

&lt;p&gt;Where its share of the local stake is large. A run is filled from nearby stake, so an operator’s chance of leading two windows in a row tracks its share of its region. In our model a 3% operator in Tokyo got back-to-back windows 15% of the time at RUN_LENGTH 3, against 3% under today’s random schedule and 7.6% in Frankfurt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a validator lie about its location to get consecutive leader slots?
&lt;/h3&gt;

&lt;p&gt;Yes, and the proposal’s parameter analysis measures it: a validator that declares an isolated location ends up alone in its bins. The analysis puts the gain at 0.28 extra consecutive windows for a 4.07% validator at RUN_LENGTH 7, rising sharply once the liar holds STAKE_FLOOR, 10% of stake, which is why STAKE_FLOOR sits at 10%.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is SIMD-0675 live on mainnet?
&lt;/h3&gt;

&lt;p&gt;No. As of 26 October 2026 it is an open proposal, as is SIMD-0674, the location registration it depends on. Its parameters, especially RUN_LENGTH, are still being debated between 3 and 7.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;SIMD-0675 halves handover latency and does not reward Sybils, but it turns local stake share into consecutive leader windows, and the smallest region that forms its own bins is where a large operator gains the most.&lt;/p&gt;

&lt;p&gt;The consensus this schedule feeds is in &lt;a href="https://xroot.dev/blog/alpenglow-finality-in-numbers" rel="noopener noreferrer"&gt;Alpenglow finality in numbers&lt;/a&gt;, the admission ticket reviewers proposed raising is in &lt;a href="https://xroot.dev/blog/alpenglow-validator-admission-2000-seats" rel="noopener noreferrer"&gt;the 2,000 validator seats&lt;/a&gt;, and the stake-weighted QoS that follows the leader is in &lt;a href="https://xroot.dev/blog/solana-stake-weighted-qos-formula" rel="noopener noreferrer"&gt;stake-weighted QoS, with the formula&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/pull/675?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0675&lt;/a&gt; and &lt;a href="https://github.com/solana-foundation/solana-improvement-documents/pull/674?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;SIMD-0674&lt;/a&gt; as amended on 22 October 2026, with their review threads to 26 October; &lt;a href="https://solana.com/news/geo-aware-leader-schedule?utm_source=xroot.dev&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;the parameter analysis on solana.com&lt;/a&gt;, by Umberto Natale, 14 October 2026. Results are from our Rust model of the reorder, described in section 02.&lt;/p&gt;

</description>
      <category>solana</category>
      <category>consensus</category>
      <category>mev</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
