<?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: Arnaud Briche</title>
    <description>The latest articles on DEV Community by Arnaud Briche (@arnaudbriche).</description>
    <link>https://dev.to/arnaudbriche</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3192120%2Fda79f5d8-a86e-438d-afec-425426023584.png</url>
      <title>DEV Community: Arnaud Briche</title>
      <link>https://dev.to/arnaudbriche</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arnaudbriche"/>
    <language>en</language>
    <item>
      <title>From Go to Rust: Supercharging Our ClickHouse UDFs with Alloy</title>
      <dc:creator>Arnaud Briche</dc:creator>
      <pubDate>Mon, 14 Jul 2025 11:47:51 +0000</pubDate>
      <link>https://dev.to/arnaudbriche/from-go-to-rust-supercharging-our-clickhouse-udfs-with-alloy-376l</link>
      <guid>https://dev.to/arnaudbriche/from-go-to-rust-supercharging-our-clickhouse-udfs-with-alloy-376l</guid>
      <description>&lt;p&gt;At &lt;a href="https://agnostic.dev/" rel="noopener noreferrer"&gt;Agnostic&lt;/a&gt;, we build open-source infrastructure for collaborative blockchain data platforms. One of our flagship tools is &lt;strong&gt;&lt;a href="https://github.com/agnosticeng/clickhouse-evm" rel="noopener noreferrer"&gt;clickhouse-evm&lt;/a&gt;&lt;/strong&gt;, a suite of high-performance User Defined Functions (UDFs) that brings native Ethereum decoding and querying capabilities directly into &lt;a href="https://clickhouse.com/" rel="noopener noreferrer"&gt;ClickHouse&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While our Go-based implementation has served us well, we've been exploring whether Rust—with its rapidly maturing Ethereum ecosystem—could take us even further. The potential benefits are compelling: better performance, enhanced safety, and improved portability that could make it easier to bring these UDFs to other analytical engines like &lt;a href="https://datafusion.apache.org/" rel="noopener noreferrer"&gt;DataFusion&lt;/a&gt; or &lt;a href="https://duckdb.org/" rel="noopener noreferrer"&gt;DuckDB&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At the heart of this exploration is &lt;strong&gt;&lt;a href="https://github.com/alloy-rs" rel="noopener noreferrer"&gt;Alloy&lt;/a&gt;&lt;/strong&gt;, a promising Rust library offering composable, well-designed primitives for Ethereum data. Its type system and tooling make it an ideal candidate for building cleaner, more robust decoding logic and ABI handling.&lt;/p&gt;

&lt;p&gt;To put this theory to the test, we reimplemented a core piece of our stack in Rust: the &lt;a href="https://github.com/agnosticeng/clickhouse-evm/blob/main/docs/functions/evm_decode_event.md" rel="noopener noreferrer"&gt;&lt;code&gt;evm_decode_event&lt;/code&gt;&lt;/a&gt; UDF. The results were encouraging enough that we wanted to share our findings.&lt;/p&gt;

&lt;p&gt;This post walks through our benchmarking methodology, compares the Rust and Go implementations, and explores the performance gains, developer experience improvements, and future opportunities this migration unlocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding &lt;code&gt;evm_decode_event&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before diving into the benchmarks, let's clarify what &lt;code&gt;evm_decode_event&lt;/code&gt; actually does. Here's a practical example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;evm_decode_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;evm_hex_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;evm_hex_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'0x00000000000000000000000063dfe4e34a3bfc00eb0220786238a7c6cef8ffc4'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;evm_hex_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'0x000000000000000000000000936c700adf05d1118d6550a3355f66e93c9476c6'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FixedString&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;evm_hex_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'0x0000000000000000000000000000000000000000000000000000000252e9f940'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'event Transfer(address indexed,address indexed,uint256)'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;
&lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="n"&gt;output_format_arrow_string_as_string&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&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="err"&gt;arg&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="mi"&gt;63&lt;/span&gt;&lt;span class="err"&gt;DFE&lt;/span&gt;&lt;span class="mi"&gt;4e34&lt;/span&gt;&lt;span class="err"&gt;A&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;bFC&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;eB&lt;/span&gt;&lt;span class="mi"&gt;0220786238&lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="err"&gt;C&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;cEF&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="err"&gt;Ffc&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="s2"&gt;",
    "&lt;/span&gt;&lt;span class="err"&gt;arg&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="mi"&gt;936&lt;/span&gt;&lt;span class="err"&gt;C&lt;/span&gt;&lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="err"&gt;Adf&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;1118&lt;/span&gt;&lt;span class="err"&gt;D&lt;/span&gt;&lt;span class="mi"&gt;6550&lt;/span&gt;&lt;span class="err"&gt;A&lt;/span&gt;&lt;span class="mi"&gt;3355&lt;/span&gt;&lt;span class="err"&gt;f&lt;/span&gt;&lt;span class="mi"&gt;66e93&lt;/span&gt;&lt;span class="err"&gt;C&lt;/span&gt;&lt;span class="mi"&gt;9476&lt;/span&gt;&lt;span class="err"&gt;C&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="s2"&gt;", 
    "&lt;/span&gt;&lt;span class="err"&gt;arg&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="mi"&gt;9981000000&lt;/span&gt;&lt;span class="s2"&gt;"
  },
  "&lt;/span&gt;&lt;span class="err"&gt;fullsig&lt;/span&gt;&lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="err"&gt;event&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Transfer(address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;indexed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;indexed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;uint&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;",
  "&lt;/span&gt;&lt;span class="err"&gt;signature&lt;/span&gt;&lt;span class="s2"&gt;": "&lt;/span&gt;&lt;span class="err"&gt;Transfer(address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;uint&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UDF takes raw bytes from a log's topics and data, along with a list of &lt;strong&gt;fullsig&lt;/strong&gt; strings—compact, human-readable representations of ABI event signatures containing all the information needed for decoding. It attempts to decode the log using each fullsig in sequence, returning a JSON representation of the decoded event on success, or an error if no signatures match.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rust Implementation Journey
&lt;/h2&gt;

&lt;p&gt;ClickHouse communicates with UDF processes via standard input/output, streaming blocks of rows back and forth. The first challenge in porting &lt;code&gt;evm_decode_event&lt;/code&gt; was selecting an efficient serialization format for data exchange between ClickHouse and our Rust binary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Serialization Strategy
&lt;/h3&gt;

&lt;p&gt;In our Go implementation, we leveraged the excellent &lt;a href="https://github.com/ClickHouse/ch-go" rel="noopener noreferrer"&gt;&lt;strong&gt;ch-go&lt;/strong&gt;&lt;/a&gt; library, which provides low-level serialization directly in ClickHouse's native binary format—the most efficient and tightly integrated option available.&lt;/p&gt;

&lt;p&gt;Unfortunately, we couldn't find an equivalent implementation of the ClickHouse native format in Rust. As an alternative, we opted for the &lt;strong&gt;Arrow IPC format&lt;/strong&gt;, which offered two key advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High performance&lt;/strong&gt; with minimal overhead when converting to/from ClickHouse native blocks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mature Rust ecosystem support&lt;/strong&gt; through libraries like &lt;a href="https://github.com/apache/arrow-rs" rel="noopener noreferrer"&gt;arrow-rs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Alloy Advantage
&lt;/h3&gt;

&lt;p&gt;For the core decoding logic, &lt;strong&gt;Alloy&lt;/strong&gt; proved to be a game-changer. It provides ergonomic and efficient abstractions for working with Ethereum data, including crucial functionality to parse fullsig strings into typed Event objects. Once parsed, decoding logs from topics and data becomes straightforward.&lt;/p&gt;

&lt;p&gt;The porting experience was surprisingly smooth—even for someone relatively new to Rust. Alloy handled much of the heavy lifting, and Arrow proved to be a practical bridge to ClickHouse integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarking Setup: Real-World Scale
&lt;/h2&gt;

&lt;p&gt;To ensure meaningful results, we designed our benchmark around substantial real-world data: &lt;strong&gt;1 million blocks&lt;/strong&gt; of raw log data from our open-source dataset, extracted using ClickHouse's native format for efficient processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data extraction query:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;iceberg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://data.agnostic.dev/agnostic-ethereum-mainnet/logs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
             &lt;span class="n"&gt;SETTINGS&lt;/span&gt; &lt;span class="n"&gt;iceberg_use_version_hint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;block_number&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;21000000&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;21099999&lt;/span&gt; 
&lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;OUTFILE&lt;/span&gt; &lt;span class="s1"&gt;'./tmp/ethereum_mainnet_logs_sample_21000000_21099999.bin'&lt;/span&gt; 
&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;Native&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dataset characteristics:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;count&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;as&lt;/span&gt; &lt;span class="n"&gt;total_logs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;start_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;end_block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_size&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;file_size&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'./benchmark/ethereum_mainnet_logs_sample_21000000_21099999.bin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Native&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Total Logs&lt;/th&gt;
&lt;th&gt;Start Block&lt;/th&gt;
&lt;th&gt;End Block&lt;/th&gt;
&lt;th&gt;File Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;40.87 million&lt;/td&gt;
&lt;td&gt;21,000,000&lt;/td&gt;
&lt;td&gt;21,099,999&lt;/td&gt;
&lt;td&gt;11.40 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This dataset provides a rich, realistic snapshot of Ethereum mainnet activity—large enough to meaningfully stress-test our decoding pipeline while remaining manageable for iterative experimentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  ABI Signature Dictionary
&lt;/h3&gt;

&lt;p&gt;We also needed a comprehensive collection of fullsig definitions to interpret the raw log data. We maintain a regularly updated set sourced from excellent daily &lt;strong&gt;Sourcify&lt;/strong&gt; dumps, providing broad coverage of known Ethereum event signatures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DICTIONARY&lt;/span&gt; &lt;span class="n"&gt;evm_abi_decoding&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;selector&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fullsigs&lt;/span&gt; &lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;
&lt;span class="k"&gt;SOURCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="s1"&gt;'./benchmark/sourcify_20250519.parquet'&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt; &lt;span class="s1"&gt;'Parquet'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;LIFETIME&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;LAYOUT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashed&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dictionary stats:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signatures:&lt;/strong&gt; 1.62 million
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory usage:&lt;/strong&gt; 223.98 MiB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ClickHouse's dictionary engine enables efficient querying of this rich signature set with minimal memory overhead—perfect for high-throughput benchmarking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark Results: A Performance Journey
&lt;/h2&gt;

&lt;p&gt;Our benchmark query processes each log row through &lt;code&gt;evm_decode_event&lt;/code&gt;, attempting to decode it into structured JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;decoded_logs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;JSONExtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;evm_decode_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FixedString&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;data&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;dictGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evm_abi_decoding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fullsigs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topics&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="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'JSON'&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'./tmp/ethereum_mainnet_logs_sample_21000000_21099999.bin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Native'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;formatReadableQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;count&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;as&lt;/span&gt; &lt;span class="n"&gt;total_logs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;countIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;decoded_logs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;countIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;undecoded_logs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;formatReadableQuantity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;countIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;count&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;as&lt;/span&gt; &lt;span class="n"&gt;decoding_ratio&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;decoded_logs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Round 1: The Debug Build Embarrassment
&lt;/h3&gt;

&lt;p&gt;Initial results were... disappointing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Decoded Logs&lt;/th&gt;
&lt;th&gt;Events/s&lt;/th&gt;
&lt;th&gt;Decoding Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;clickhouse-evm (Go)&lt;/td&gt;
&lt;td&gt;63s&lt;/td&gt;
&lt;td&gt;39.87M&lt;/td&gt;
&lt;td&gt;632.86K&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ch-evm (Rust)&lt;/td&gt;
&lt;td&gt;344s&lt;/td&gt;
&lt;td&gt;40.16M&lt;/td&gt;
&lt;td&gt;116.74K&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After some head-scratching, I discovered a classic mistake: &lt;strong&gt;I was running a debug build&lt;/strong&gt;! 🤦‍♂️&lt;/p&gt;

&lt;p&gt;Sometimes the biggest performance bottleneck is your own oversight. Always double-check your build mode before blaming the code!&lt;/p&gt;

&lt;h3&gt;
  
  
  Round 2: Release Build Reality Check
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Decoded Logs&lt;/th&gt;
&lt;th&gt;Events/s&lt;/th&gt;
&lt;th&gt;Decoding Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;clickhouse-evm (Go)&lt;/td&gt;
&lt;td&gt;63s&lt;/td&gt;
&lt;td&gt;39.87M&lt;/td&gt;
&lt;td&gt;632.86K&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ch-evm (debug)&lt;/td&gt;
&lt;td&gt;344s&lt;/td&gt;
&lt;td&gt;40.16M&lt;/td&gt;
&lt;td&gt;116.74K&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ch-evm (release)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;54s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40.16M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;738.64K&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.98&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Much better! The Rust implementation now &lt;strong&gt;outperforms Go by 17%&lt;/strong&gt;, even with the overhead of native ⇄ Arrow conversions. The slightly improved decoding ratio suggests Alloy's ABI decoder handles more edge cases than our custom Go implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Round 3: Eliminating serde_arrow
&lt;/h3&gt;

&lt;p&gt;Profiling with &lt;a href="https://github.com/flamegraph-rs/flamegraph" rel="noopener noreferrer"&gt;flamegraph-rs&lt;/a&gt; revealed that ~10% of CPU time was spent on &lt;code&gt;serde_arrow&lt;/code&gt; conversions. In our Go implementation, we operate directly on columnar data for maximum performance.&lt;/p&gt;

&lt;p&gt;We removed the &lt;code&gt;serde_arrow&lt;/code&gt; layer and worked directly with Arrow arrays:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Decoded Logs&lt;/th&gt;
&lt;th&gt;Events/s&lt;/th&gt;
&lt;th&gt;Decoding Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;clickhouse-evm (Go)&lt;/td&gt;
&lt;td&gt;63s&lt;/td&gt;
&lt;td&gt;39.87M&lt;/td&gt;
&lt;td&gt;632.86K&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ch-evm (no serde_arrow)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;41s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40.16M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;990.68K&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.98&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Significant improvement!&lt;/strong&gt; We're now decoding at nearly &lt;strong&gt;1 million logs per second&lt;/strong&gt;—a 57% improvement over the original Go implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Round 4: JSON Serialization Optimization
&lt;/h3&gt;

&lt;p&gt;Further profiling showed substantial time spent in &lt;code&gt;serde_json&lt;/code&gt;. Instead of converting Alloy's &lt;code&gt;DynSolValue&lt;/code&gt; enum to &lt;code&gt;serde_json::Value&lt;/code&gt; and then serializing, we implemented a direct formatter that visits &lt;code&gt;DynSolValue&lt;/code&gt; recursively, writing JSON chunks directly.&lt;/p&gt;

&lt;p&gt;This eliminated a whole layer of allocations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Decoded Logs&lt;/th&gt;
&lt;th&gt;Events/s&lt;/th&gt;
&lt;th&gt;Decoding Ratio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;clickhouse-evm (Go)&lt;/td&gt;
&lt;td&gt;63s&lt;/td&gt;
&lt;td&gt;39.87M&lt;/td&gt;
&lt;td&gt;632.86K&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ch-evm (final optimized)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;34s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40.16M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.21M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.98&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Final Results: 91% Performance Improvement
&lt;/h2&gt;

&lt;p&gt;Here is a nice chart that summarizes our journey:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F75gtanyojhu885xtm9rt.png" 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/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F75gtanyojhu885xtm9rt.png" alt=" " width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our optimized Rust implementation achieved &lt;strong&gt;1.21 million events per second&lt;/strong&gt;—a &lt;strong&gt;91% performance improvement&lt;/strong&gt; over the original Go version while maintaining the same high decoding accuracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Performance: Strategic Advantages
&lt;/h2&gt;

&lt;p&gt;This exploration delivered more than just speed improvements:&lt;/p&gt;

&lt;h3&gt;
  
  
  Developer Experience
&lt;/h3&gt;

&lt;p&gt;Alloy's ergonomic design makes complex Ethereum operations feel natural and safe. The type system catches errors at compile time that might only surface during runtime in other languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Portability
&lt;/h3&gt;

&lt;p&gt;The Rust implementation positions us well for expansion beyond ClickHouse. We can now more easily bring these UDFs to DataFusion, DuckDB, and other analytical engines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ecosystem Access
&lt;/h3&gt;

&lt;p&gt;Rust opens doors to blockchain ecosystems where it's the dominant language—particularly &lt;strong&gt;Solana&lt;/strong&gt;. This natural fit could significantly expand Agnostic's reach in blockchain data analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Forward
&lt;/h2&gt;

&lt;p&gt;This proof-of-concept has been so successful that we've decided to rewrite all UDFs in clickhouse-evm using Rust. The new repository, &lt;a href="https://github.com/agnosticeng/ch-evm" rel="noopener noreferrer"&gt;&lt;strong&gt;ch-evm&lt;/strong&gt;&lt;/a&gt;, will be our primary development focus going forward, with all new UDFs developed in Rust.&lt;/p&gt;

&lt;p&gt;The combination of Rust's performance characteristics, Alloy's powerful abstractions, and the broader ecosystem opportunities make this transition a strategic win for Agnostic's data infrastructure evolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The Rust + Alloy combination has proven to be a powerful force multiplier for blockchain data processing. What started as an exploration of potential performance gains has evolved into a comprehensive strategy for expanding our capabilities across the blockchain ecosystem.&lt;/p&gt;

&lt;p&gt;For teams working with blockchain data at scale, this experience demonstrates that modern Rust tooling—particularly Alloy—has matured to the point where it can deliver both superior performance and developer productivity. The future of blockchain data infrastructure is looking increasingly Rust-colored.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Interested in blockchain data infrastructure? Check out our open-source tools at &lt;a href="https://github.com/agnosticlines" rel="noopener noreferrer"&gt;Agnostic&lt;/a&gt; or explore our datasets at &lt;a href="https://data.agnostic.dev" rel="noopener noreferrer"&gt;data.agnostic.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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