<?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: byhashdong</title>
    <description>The latest articles on DEV Community by byhashdong (@byhashdong).</description>
    <link>https://dev.to/byhashdong</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%2F4246020%2F0e5bfbb4-92ea-4488-9227-37bc78be088e.png</url>
      <title>DEV Community: byhashdong</title>
      <link>https://dev.to/byhashdong</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/byhashdong"/>
    <language>en</language>
    <item>
      <title>Why a masked precompile must stay warm</title>
      <dc:creator>byhashdong</dc:creator>
      <pubDate>Sun, 15 Nov 2026 13:48:33 +0000</pubDate>
      <link>https://dev.to/byhashdong/why-a-masked-precompile-must-stay-warm-40e1</link>
      <guid>https://dev.to/byhashdong/why-a-masked-precompile-must-stay-warm-40e1</guid>
      <description>&lt;p&gt;A code override can return the expected result and still charge the wrong gas. In a proposed fix for Reth’s precompile overrides, an empty-code replacement worked, but a nested call to it cost an extra 2,500 gas.&lt;/p&gt;

&lt;p&gt;The original report concerned replacement code being ignored. The gas error came from my first version of the fix, which changed native dispatch and accidentally changed address warming with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the replacement code to run
&lt;/h2&gt;

&lt;p&gt;State overrides let an RPC request simulate a call against temporary account state. They do not change the chain's state or the network's precompile rules.&lt;/p&gt;

&lt;p&gt;The report in &lt;a href="https://github.com/paradigmxyz/reth/issues/27806" rel="noopener noreferrer"&gt;Reth #27806&lt;/a&gt; concerned a &lt;code&gt;code&lt;/code&gt; override at &lt;code&gt;0x04&lt;/code&gt;, the identity precompile. Identity normally returns its input unchanged. My reproduction used replacement bytecode that returns the byte &lt;code&gt;0x2a&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;Expected output&lt;/th&gt;
&lt;th&gt;Observed output before the fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ordinary address, replacement code&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x2a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x2a&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity, no code override, input &lt;code&gt;0x1234&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x1234&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x1234&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity, replacement code, input &lt;code&gt;0x1234&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x2a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0x1234&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The database contained the replacement code, but the EVM dispatched to the native identity implementation first.&lt;/p&gt;

&lt;p&gt;My patch wraps the installed precompile provider—the component that identifies and executes native precompiles—and masks addresses with an explicit &lt;code&gt;code&lt;/code&gt; override. That includes empty bytecode. The proposed behavior leaves balance-, nonce- and storage-only overrides unchanged; their cross-client semantics are a &lt;a href="https://github.com/ethereum/execution-apis/issues/934" rel="noopener noreferrer"&gt;separate discussion&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Wrapping the installed provider avoids rebuilding it and losing custom precompiles or cached results. Calls to masked addresses fall through to account code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrapper also changed gas
&lt;/h2&gt;

&lt;p&gt;The wrapper filtered the provider's address enumeration as well as its execution methods. I had left &lt;code&gt;warm_addresses&lt;/code&gt; using the trait default, which derived the warm set from that filtered list. A masked address therefore stopped being prewarmed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/paradigmxyz/reth/pull/28440#issuecomment-6802202067" rel="noopener noreferrer"&gt;MysticRyuujin pointed this out in review&lt;/a&gt;, with cross-client results showing that code overrides preserve the fork's precompile warm set.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://eips.ethereum.org/EIPS/eip-2929" rel="noopener noreferrer"&gt;EIP-2929&lt;/a&gt; initializes the transaction's accessed-address set with the sender, destination and precompiles. A warm account access costs 100 gas; a cold one costs 2,600. Suppressing native dispatch should not remove the address from that initial set.&lt;/p&gt;

&lt;p&gt;The correction in my wrapper was to forward warming to the original provider:&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;warm_addresses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="kd"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;evm2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;evm&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;PrewarmSet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warm_addresses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warm&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;Membership checks, native execution and address/ID enumeration remain filtered. Warming follows the underlying provider's contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the gas separately
&lt;/h2&gt;

&lt;p&gt;Calling &lt;code&gt;0x04&lt;/code&gt; directly would hide this mistake: the transaction destination already starts warm. The regression test instead calls an ordinary probe contract, which makes a nested &lt;code&gt;STATICCALL&lt;/code&gt; to &lt;code&gt;0x04&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The probe uses empty input and output buffers under Prague rules. It records &lt;code&gt;GAS&lt;/code&gt; before and after the call; the measured difference includes 25 gas of probe overhead.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Measured gas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Empty-code override with my initial filter&lt;/td&gt;
&lt;td&gt;2625&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty-code override after forwarding warming&lt;/td&gt;
&lt;td&gt;125&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;STOP&lt;/code&gt; replacement after forwarding warming&lt;/td&gt;
&lt;td&gt;125&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native identity, no override&lt;/td&gt;
&lt;td&gt;140&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Empty code and &lt;code&gt;STOP&lt;/code&gt; require no execution gas here, so the corrected result is 100 for warm access plus 25 for the probe. Native identity adds its 15-gas charge for empty input. My initial filter charged 2,600 for cold access instead: the extra 2,500 gas came from the wrapper, not the original report.&lt;/p&gt;

&lt;p&gt;I also added a custom-provider test whose warm set includes an address absent from its precompile list. This checks that the wrapper preserves the provider's actual warming behavior rather than reconstructing it from another address list.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/paradigmxyz/reth/blob/6d931e49b8c1e2721b517ce61d747cd57204d730/crates/rpc/rpc-eth-types/src/simulate.rs#L1312" rel="noopener noreferrer"&gt;gas regression&lt;/a&gt; and &lt;a href="https://github.com/paradigmxyz/reth/commit/b4e780243b896d4dc13d91e95a02c987dd7abe46" rel="noopener noreferrer"&gt;correction commit&lt;/a&gt; are available for inspection. These numbers measure gas accounting in a test, not throughput or a reduction in fees paid on the live network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related work
&lt;/h2&gt;

&lt;p&gt;The ownership hook used by this wrapper was merged in &lt;a href="https://github.com/alloy-rs/evm2/pull/1067" rel="noopener noreferrer"&gt;evm2 #1067&lt;/a&gt;. As of 15 November 2026, the &lt;a href="https://github.com/paradigmxyz/reth/pull/28440" rel="noopener noreferrer"&gt;Reth patch&lt;/a&gt; uses that upstream hook but is still under review and has not been merged. The tests discussed here exercise execution and RPC helpers, rather than a full-node setup.&lt;/p&gt;

&lt;p&gt;Thanks to MysticRyuujin for identifying the warming problem and providing cross-client checks. Their separate &lt;a href="https://github.com/alloy-rs/evm2/pull/1068" rel="noopener noreferrer"&gt;evm2 #1068&lt;/a&gt; addresses warming when precompiles are relocated.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>testing</category>
      <category>ethereum</category>
    </item>
  </channel>
</rss>
