<?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: Sipho Yawe</title>
    <description>The latest articles on DEV Community by Sipho Yawe (@siphoyawe).</description>
    <link>https://dev.to/siphoyawe</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%2F3985577%2Fa4606a4c-f3a6-4c81-817f-9bdb5da4fb06.jpeg</url>
      <title>DEV Community: Sipho Yawe</title>
      <link>https://dev.to/siphoyawe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/siphoyawe"/>
    <language>en</language>
    <item>
      <title>How I shrank a Solana deadline guard from 148 compute units to 4.</title>
      <dc:creator>Sipho Yawe</dc:creator>
      <pubDate>Mon, 15 Jun 2026 12:46:11 +0000</pubDate>
      <link>https://dev.to/siphoyawe/how-i-shrank-a-solana-deadline-guard-from-148-compute-units-to-4-18d5</link>
      <guid>https://dev.to/siphoyawe/how-i-shrank-a-solana-deadline-guard-from-148-compute-units-to-4-18d5</guid>
      <description>&lt;p&gt;An arbitrage bot signs a transaction at slot N. The spread is good at slot N. Then the network hiccups, the transaction waits in a forwarding queue, and it lands 200 slots later, about 80 seconds, in a market where the spread has inverted. The transaction is still valid, so it executes. The bot loses money on a trade it priced ninety seconds ago.&lt;/p&gt;

&lt;p&gt;Solana's built-in defense is &lt;code&gt;recentBlockhash&lt;/code&gt; expiry, but that window runs roughly 150 slots and you can't tune it per transaction. What you want is a programmable deadline: if this lands after slot X, fail everything. That is the subject of Blueshift's &lt;a href="https://learn.blueshift.gg/en/challenges/assembly-timeout" rel="noopener noreferrer"&gt;Assembly Timeout challenge&lt;/a&gt;: a guard program, written in raw sBPF assembly, that you bundle into a transaction. It reads an 8-byte little-endian &lt;code&gt;u64&lt;/code&gt; from instruction data (&lt;code&gt;max_slot_height&lt;/code&gt;), compares it to the current slot, and returns nonzero when the transaction is late. A nonzero return aborts the whole transaction, every instruction in it. Stale arbitrage, delayed execution, replayed instructions: dead on arrival.&lt;/p&gt;

&lt;p&gt;I wrote this program three times. The verifier rejected it twice. The version that passed is 5 instructions, a 320-byte &lt;code&gt;.so&lt;/code&gt;, and 4 compute units on the success path. Getting there meant debugging the specification, not my code. Twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The input buffer, since you have probably never seen it
&lt;/h2&gt;

&lt;p&gt;Write Anchor and you never look at how a Solana program receives its inputs, because Anchor looks for you. The runtime serializes everything (account count, accounts, instruction data) into one memory region and hands your entrypoint a single pointer in &lt;code&gt;r1&lt;/code&gt;. Offset 0 holds the account count as a &lt;code&gt;u64&lt;/code&gt;. Each account follows, fully serialized. Then an 8-byte instruction-data length, then the instruction data.&lt;/p&gt;

&lt;p&gt;That layout is the whole story here. Every bug and every fix below is an offset into that buffer.&lt;/p&gt;

&lt;p&gt;Why assembly? This program runs inside someone else's transaction, and every compute unit it burns comes out of their budget. A guard that costs 10,000 CUs gets deleted under load. A guard that costs 4 gets left in. The &lt;a href="https://github.com/blueshift-gg/sbpf" rel="noopener noreferrer"&gt;blueshift-gg/sbpf&lt;/a&gt; toolchain builds the &lt;code&gt;.so&lt;/code&gt; in about 2 ms, so the edit-test loop beats &lt;code&gt;anchor build&lt;/code&gt;'s dependency check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Program 1: test-driven against the wrong spec
&lt;/h2&gt;

&lt;p&gt;The challenge page teaches a solution built on the &lt;code&gt;sol_get_clock_sysvar&lt;/code&gt; syscall: load the account count, load the deadline from offset &lt;code&gt;0x10&lt;/code&gt;, fetch the Clock struct into a stack buffer, compare slots, exit.&lt;/p&gt;

&lt;p&gt;I wrote the tests first. &lt;a href="https://github.com/buffalojoec/mollusk" rel="noopener noreferrer"&gt;Mollusk&lt;/a&gt; 0.7.2, pointed at the &lt;code&gt;sbpf init&lt;/code&gt; scaffold, so every test failed before I wrote a line of assembly. Then I implemented the documented contract, and my with-accounts tests stayed red.&lt;/p&gt;

&lt;p&gt;The canonical code has what looks like an elegant account veto. It loads the account count into &lt;code&gt;r0&lt;/code&gt;, reasoning that &lt;code&gt;r0&lt;/code&gt; is the exit-code register, so a nonzero count fails the program at exit with no branch needed. The next instruction calls &lt;code&gt;sol_get_clock_sysvar&lt;/code&gt;. Syscalls return their status in &lt;code&gt;r0&lt;/code&gt;. The syscall succeeds, returns 0, and erases the veto. The check is dead code.&lt;/p&gt;

&lt;p&gt;It gets worse. With an account serialized in front of the instruction data, offset &lt;code&gt;0x10&lt;/code&gt; no longer holds the deadline. It holds the first 8 bytes of that account's pubkey. Pubkeys are random 32-byte values, so those 8 bytes read as an astronomically large number. The deadline check degenerates to "always pass." A guard whose one job is rejecting bad transactions waves them through.&lt;/p&gt;

&lt;p&gt;The fix is one instruction of reordering: &lt;code&gt;jne r0, 0, end&lt;/code&gt; right after loading the count, before the syscall, while &lt;code&gt;r0&lt;/code&gt; still holds the truth. Fail closed, 3 CUs on the veto path, never even pays for the syscall. Tests green. I uploaded the &lt;code&gt;.so&lt;/code&gt; feeling clever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rejection 1: three compute units is a fingerprint
&lt;/h2&gt;

&lt;p&gt;The verifier came back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Custom program error: 0x1, consumed 3 of 1400000 compute units
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three CUs is not a number. It is a stack trace: &lt;code&gt;ldxdw&lt;/code&gt;, &lt;code&gt;jne&lt;/code&gt; taken, &lt;code&gt;exit&lt;/code&gt;. My account veto fired on the verifier's success vector. The verifier invokes the program with one account attached and expects it to succeed.&lt;/p&gt;

&lt;p&gt;Read that twice, because I had to. The canonical code's &lt;code&gt;r0&lt;/code&gt;-clobber is not a bug to the verifier. The dead veto and the garbage-pubkey "deadline" are exactly what its success vector exercises. The bug was load-bearing. I had test-driven my way to a correct implementation of the wrong contract.&lt;/p&gt;

&lt;p&gt;The lesson started to form: the spec is the verifier's behavior. The page's comments are commentary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rejection 2: the verifier rejects its own course page
&lt;/h2&gt;

&lt;p&gt;So I reverted to byte-for-byte canonical. I even kept the two-slot &lt;code&gt;lddw r0, 1&lt;/code&gt; instead of the cheaper &lt;code&gt;mov64&lt;/code&gt;, on the theory that deviation got me rejected last time. Uploaded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exceeded compute units: used 148, max 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The log shows the program succeeded, at 148 CUs. The verifier caps the success path at 4.&lt;/p&gt;

&lt;p&gt;Here is the problem with that cap. &lt;code&gt;sol_get_clock_sysvar&lt;/code&gt; alone costs 140 CUs. I measured it from both directions. The success path runs 8 instructions and totals 148, so the syscall is 148 minus 8, or 140. The failure path runs 9 and totals 149: 149 minus 9, again 140. Both decompositions agree. Any solution that touches the syscall starts 136 CUs over budget.&lt;/p&gt;

&lt;p&gt;So the verifier rejects the exact solution its own course page teaches. I re-scraped the page expecting an update I had missed. Same content, same commit hash. The page and the verifier had diverged, and only one of them grades your submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deduction
&lt;/h2&gt;

&lt;p&gt;A 4-CU budget admits very few designs. Four CUs is four executed instructions. You cannot call anything. You can load, compare, and exit. And the verifier passes one account in.&lt;/p&gt;

&lt;p&gt;That forces a single conclusion: the account is the Clock sysvar, and you read the slot straight out of the input buffer. The cheapest syscall is the one you skip. The runtime already serialized the data you need into memory you already hold.&lt;/p&gt;

&lt;p&gt;Now the offset arithmetic. One account, 40 bytes of data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0x0000&lt;/code&gt;: account count, 8 bytes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0x0008&lt;/code&gt;: account header (dup flag, is_signer, is_writable, executable, 4-byte orig_data_len), 8 bytes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0x0010&lt;/code&gt;: pubkey, 32 bytes (the same bytes Program 1 mistook for a deadline)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0x0030&lt;/code&gt;: owner, 32 bytes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0x0050&lt;/code&gt;: lamports, 8 bytes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0x0058&lt;/code&gt;: data length, 8 bytes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0x0060&lt;/code&gt;: account data, the Clock struct, with &lt;code&gt;slot&lt;/code&gt; as the first &lt;code&gt;u64&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Past the data sit 40 bytes of Clock, 10,240 bytes of realloc padding, 8 bytes of &lt;code&gt;rent_epoch&lt;/code&gt;, and 8 bytes of instruction-data length. Add those from &lt;code&gt;0x0060&lt;/code&gt; and you land at &lt;code&gt;0x2898&lt;/code&gt;, where the caller's deadline lives.&lt;/p&gt;

&lt;p&gt;I checked the arithmetic against a prior graduate's public solution. Identical offsets: &lt;code&gt;0x0060&lt;/code&gt; and &lt;code&gt;0x2898&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Program 3: five instructions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.equ CLOCK_SLOT, 0x0060
.equ MAX_SLOT_HEIGHT, 0x2898

.globl entrypoint
entrypoint:
    ldxdw r2, [r1+MAX_SLOT_HEIGHT]
    ldxdw r1, [r1+CLOCK_SLOT]
    jle r1, r2, end
    lddw r0, 1
end:
    exit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load the deadline, load the slot, compare, exit. The &lt;code&gt;.so&lt;/code&gt; is 320 bytes. Measured at 4 CUs on success, 5 on failure. The verifier passed it 2 for 2.&lt;/p&gt;

&lt;p&gt;One detail earns its own paragraph. The success path never writes &lt;code&gt;r0&lt;/code&gt;. The SVM zero-initializes registers before entry, and that free zero is the entire success exit code. Four instructions run on success (&lt;code&gt;ldxdw&lt;/code&gt;, &lt;code&gt;ldxdw&lt;/code&gt;, &lt;code&gt;jle&lt;/code&gt;, &lt;code&gt;exit&lt;/code&gt;) and not one of them spends a cycle saying "OK." The runtime said it at initialization.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jle&lt;/code&gt; is inclusive, which is a decision rather than an accident. A transaction landing exactly on the deadline slot passes. I test that boundary on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test suite
&lt;/h2&gt;

&lt;p&gt;Rewritten each round, always red before green:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;passes_when_current_slot_below_deadline&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;passes_when_current_slot_equals_deadline&lt;/code&gt;, the &lt;code&gt;jle&lt;/code&gt; boundary&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fails_when_current_slot_exceeds_deadline&lt;/code&gt;, asserting &lt;code&gt;ProgramError::Custom(1)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;passes_with_max_u64_deadline&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fails_when_max_slot_is_zero_and_current_nonzero&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cu_budget_in_success_path&lt;/code&gt;, at most 4, the verifier's cap, pinned as a regression test&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cu_budget_in_failure_path&lt;/code&gt;, at most 5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/REPLACE_WITH_UPLOADED_tests.png_URL" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/REPLACE_WITH_UPLOADED_tests.png_URL" alt="cargo test: 7 of 7 green, 4 CUs on success and 5 on failure"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tests build the Clock account by hand: 40 bytes of bincode (slot, epoch_start_timestamp, epoch, leader_schedule_epoch, unix_timestamp, each 8-byte little-endian) passed as account #1, matching the verifier's shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am keeping
&lt;/h2&gt;

&lt;p&gt;A test harness tells you what your code does. Only the integration target tells you what it must do. Mollusk found the &lt;code&gt;r0&lt;/code&gt;-clobber in milliseconds. No amount of local testing could have found the real contract, because the real contract lived only in the verifier's two live invocations.&lt;/p&gt;

&lt;p&gt;Compute-unit counts are fingerprints. "Consumed 3" named which branch fired, down to the instruction. "Max 4" named the entire intended design, ruling out every syscall solution in a single number.&lt;/p&gt;

&lt;p&gt;The cheapest syscall is the one you skip. With &lt;code&gt;sol_get_clock_sysvar&lt;/code&gt;, the floor was 140 CUs no matter how tight the assembly around it. Reading the sysvar from the input buffer removed the floor. The data was already there. I had been paying 140 CUs to ask for a copy.&lt;/p&gt;

&lt;p&gt;One production caveat: the 4-CU version trusts the caller to pass the real Clock sysvar at account #1. For untrusted callers, check the account key against the Clock sysvar address. A few more CUs, well spent.&lt;/p&gt;

&lt;p&gt;Next I want a &lt;code&gt;slot-deadline&lt;/code&gt; helper: &lt;code&gt;tx.with_deadline(secs)&lt;/code&gt; that resolves to a &lt;code&gt;max_slot_height&lt;/code&gt;, so arb bots, DAO vote windows, and timed airdrops can use this without doing slot arithmetic by hand. If that would be useful to you, the repo below is where it will land.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Challenge: &lt;a href="https://learn.blueshift.gg/en/challenges/assembly-timeout" rel="noopener noreferrer"&gt;learn.blueshift.gg/en/challenges/assembly-timeout&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Toolchain: &lt;a href="https://github.com/blueshift-gg/sbpf" rel="noopener noreferrer"&gt;github.com/blueshift-gg/sbpf&lt;/a&gt; by @deanmlittle and Blueshift, about 2 ms builds&lt;/li&gt;
&lt;li&gt;Test harness: &lt;a href="https://github.com/buffalojoec/mollusk" rel="noopener noreferrer"&gt;github.com/buffalojoec/mollusk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;My code and full test suite: &lt;a href="https://github.com/SiphoYawe/blueshift-assembly-timeout" rel="noopener noreferrer"&gt;github.com/SiphoYawe/blueshift-assembly-timeout&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
  </channel>
</rss>
