<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>detoo</title>
        <link>https://paragraph.com/@detoo-2</link>
        <description>undefined</description>
        <lastBuildDate>Wed, 08 Apr 2026 11:17:47 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>detoo</title>
            <url>https://storage.googleapis.com/papyrus_images/021151a4e5c6a5c3bd9fdb8fb080d91da3f632756de9c7f0d5b3906e26e160ba.png</url>
            <link>https://paragraph.com/@detoo-2</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[Multi-dimensional Strong-enumeration of JSON Messages with Rust]]></title>
            <link>https://paragraph.com/@detoo-2/multi-dimensional-strong-enumeration-of-json-messages-with-rust</link>
            <guid>HUuAwxk0cS3QPoVawYWd</guid>
            <pubDate>Fri, 06 Sep 2024 23:18:14 GMT</pubDate>
            <description><![CDATA[Often among streaming API calls we encounter structures like the one shown below. There may be many message variants, defining scopes across multiple dimensions such as aspect of the system (topic) and purpose of the changes (event):// Snapshot of all markets { "topic": "market", "event": "snapshot", "markets": [ { "ticker": "BTC/USD", "price": 1000000, "volume": 1000 }, { "ticker": "ETH/USD", "price": 10000, "volume": 10000 } ] } // Snapshot of user portfolio { "topic": "portfolio", "event":...]]></description>
            <content:encoded><![CDATA[<p>Often among streaming API calls we encounter structures like the one shown below. There may be many message variants, defining scopes across multiple dimensions such as aspect of the system (<code>topic</code>) and purpose of the changes (<code>event</code>):</p><pre data-type="codeBlock" text="// Snapshot of all markets
{
  &quot;topic&quot;: &quot;market&quot;,
  &quot;event&quot;: &quot;snapshot&quot;,
  &quot;markets&quot;: [
    {
      &quot;ticker&quot;: &quot;BTC/USD&quot;,
      &quot;price&quot;: 1000000,
      &quot;volume&quot;: 1000
    },
    {
      &quot;ticker&quot;: &quot;ETH/USD&quot;,
      &quot;price&quot;: 10000,
      &quot;volume&quot;: 10000
    }
  ]
}

// Snapshot of user portfolio
{
  &quot;topic&quot;: &quot;portfolio&quot;,
  &quot;event&quot;: &quot;snapshot&quot;,
  &quot;open_orders&quot;: [
    {
      &quot;id&quot;: 0,
      &quot;side&quot;: &quot;sell&quot;,
      &quot;ticker&quot;: &quot;BTC/USD&quot;,
      &quot;limit_price&quot;: 1100000
    }
  ]
}

// Individual updates to markets
{
  &quot;topic&quot;: &quot;market&quot;,
  &quot;event&quot;: &quot;update&quot;,
  &quot;markets&quot;: [
    {
      &quot;ticker&quot;: &quot;BTC/USD&quot;,
      &quot;price&quot;: 1000001,
      &quot;volume&quot;: 1001
    }
  ]
}

// Individual updates to user portfolio
{
  &quot;topic&quot;: &quot;portfolio&quot;,
  &quot;event&quot;: &quot;update&quot;,
  &quot;open_orders&quot;: [
    {
      &quot;id&quot;: 1,
      &quot;side&quot;: &quot;buy&quot;,
      &quot;ticker&quot;: &quot;ETH/USD&quot;,
      &quot;limit_price&quot;: 9000
    }
  ]
}
"><code><span class="hljs-comment">// Snapshot of all markets</span>
<span class="hljs-punctuation">{</span>
  <span class="hljs-attr">"topic"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"market"</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"event"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"snapshot"</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"markets"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">"ticker"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"BTC/USD"</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"price"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1000000</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"volume"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1000</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">"ticker"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"ETH/USD"</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"price"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">10000</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"volume"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">10000</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>

<span class="hljs-comment">// Snapshot of user portfolio</span>
<span class="hljs-punctuation">{</span>
  <span class="hljs-attr">"topic"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"portfolio"</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"event"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"snapshot"</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"open_orders"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">"id"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">0</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"side"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"sell"</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"ticker"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"BTC/USD"</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"limit_price"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1100000</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>

<span class="hljs-comment">// Individual updates to markets</span>
<span class="hljs-punctuation">{</span>
  <span class="hljs-attr">"topic"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"market"</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"event"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"update"</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"markets"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">"ticker"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"BTC/USD"</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"price"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1000001</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"volume"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1001</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>

<span class="hljs-comment">// Individual updates to user portfolio</span>
<span class="hljs-punctuation">{</span>
  <span class="hljs-attr">"topic"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"portfolio"</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"event"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"update"</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">"open_orders"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">"id"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"side"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"buy"</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"ticker"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"ETH/USD"</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">"limit_price"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">9000</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>
</code></pre><p>In message processor we want better readability and maintenance. Strong-typing allow us to catch potential bugs early at the build/compile stage, and codes written that way are usually more self-descriptive. The question is, how do we model the messages above to achieve such goals?</p><p>As the message processor, I want each type of the messages to have a clear and easy-to-follow code path. Something like this would be nice at the first glance:</p><pre data-type="codeBlock" text="while let Some(message) = message_stream.next() {
    match message {
        Ok(MarketSnapshot(data)) =&gt; // Handle market snapshot
        Ok(MarketUpdate(data)) =&gt; // Handle market update
        Ok(PortfolioSnapshot(data)) =&gt; // Handle portfolio snapshot
        Ok(PortfolioUpdate(data)) =&gt; // Handle portfolio update
        Err(reason) =&gt; // Handle error
        _ =&gt; {} // Ignore other messages (if any)
    }
}
"><code><span class="hljs-keyword">while</span> let Some(message) <span class="hljs-operator">=</span> message_stream.next() {
    match message {
        Ok(MarketSnapshot(data)) <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-comment">// Handle market snapshot</span>
        Ok(MarketUpdate(data)) <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-comment">// Handle market update</span>
        Ok(PortfolioSnapshot(data)) <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-comment">// Handle portfolio snapshot</span>
        Ok(PortfolioUpdate(data)) <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-comment">// Handle portfolio update</span>
        Err(reason) <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-comment">// Handle error</span>
        <span class="hljs-keyword">_</span> <span class="hljs-operator">=</span><span class="hljs-operator">></span> {} <span class="hljs-comment">// Ignore other messages (if any)</span>
    }
}
</code></pre><p><br>Using pattern matching allow us to write concise codes and avoid layers of messy <code>if…else</code> syntax (especially when the message has complex structures). Moreover, <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://doc.rust-lang.org/book/ch06-02-match.html#matches-are-exhaustive">pattern matching is exhaustive-enforced by the compiler</a> so it eliminates unattended edge cases.</p><p>Going along with the initial idea, we define an <code>enum</code> with four variants, each representing a message:</p><pre data-type="codeBlock" text="struct MarketData {
  // ...
}

struct PortfolioData {
  // ...
}

enum Message {
    MarketSnapshot(MarketData),
    MarketUpdate(MarketData),
    PortfolioSnapshot(PortfolioData),
    PortfolioUpdate(PortfolioData),
}
"><code>struct MarketData {
  <span class="hljs-comment">// ...</span>
}

struct PortfolioData {
  <span class="hljs-comment">// ...</span>
}

enum Message {
    <span class="hljs-built_in">MarketSnapshot</span>(MarketData),
    <span class="hljs-built_in">MarketUpdate</span>(MarketData),
    <span class="hljs-built_in">PortfolioSnapshot</span>(PortfolioData),
    <span class="hljs-built_in">PortfolioUpdate</span>(PortfolioData),
}
</code></pre><p>The problem is that it wouldn’t map easily to the JSON schema above, because the message variants are determined by two fields: <code>topic</code> and <code>event</code>, which are parts of the JSON messages and we need extra logic for translation.</p><p>We can use Serde, the popular framework for data serialization on Rust, to do such translation by “tagging” enum representation internally, so that:</p><pre data-type="codeBlock" text="#[derive(Serialize, Deserialize)]
#[serde(tag = &quot;topic&quot;, rename_all = &quot;camelCase&quot;)]
enum Message {
    Market(MarketData),
    ...
}
"><code>#[derive(Serialize, Deserialize)]
#[serde(tag <span class="hljs-operator">=</span> <span class="hljs-string">"topic"</span>, rename_all <span class="hljs-operator">=</span> <span class="hljs-string">"camelCase"</span>)]
<span class="hljs-keyword">enum</span> <span class="hljs-title">Message</span> {
    Market(MarketData),
    ...
}
</code></pre><p>would serialize to (and vise versa):</p><pre data-type="codeBlock" text="{
  &quot;topic&quot;: &quot;market&quot;, // i.e. camelCase of the enum variant &quot;Market&quot; thanks to the `rename_all` attribute
  ... // market data
}
"><code>{
  <span class="hljs-string">"topic"</span>: <span class="hljs-string">"market"</span>, <span class="hljs-comment">// i.e. camelCase of the enum variant "Market" thanks to the `rename_all` attribute</span>
  ... <span class="hljs-comment">// market data</span>
}
</code></pre><p>But that’s not all! We have two fields to tag: <code>topic</code> and <code>event</code>, and both of them are at the root level of the JSON message. Unfortunately, Serde does not support multiple tags on one <code>enum</code>, so we have to do it by nesting data structures:</p><pre data-type="codeBlock" text="#[derive(Serialize, Deserialize)]
#[serde(tag = &quot;topic&quot;, rename_all = &quot;camelCase&quot;)]
enum MessageTopic {
    Market(MessageEvent&lt;MarketData&gt;),
    Portfolio(MessageEvent&lt;PortfolioData&gt;),
}

#[derive(Serialize, Deserialize)]
#[serde(tag = &quot;event&quot;, rename_all = &quot;camelCase&quot;)]
enum MessageEvent&lt;T&gt; {
    Snapshot(T),
    Update(T)
}
"><code>#<span class="hljs-selector-attr">[derive(Serialize, Deserialize)]</span>
#<span class="hljs-selector-attr">[serde(tag = <span class="hljs-string">"topic"</span>, rename_all = <span class="hljs-string">"camelCase"</span>)]</span>
enum MessageTopic {
    <span class="hljs-built_in">Market</span>(MessageEvent&#x3C;MarketData>),
    <span class="hljs-built_in">Portfolio</span>(MessageEvent&#x3C;PortfolioData>),
}

#<span class="hljs-selector-attr">[derive(Serialize, Deserialize)]</span>
#<span class="hljs-selector-attr">[serde(tag = <span class="hljs-string">"event"</span>, rename_all = <span class="hljs-string">"camelCase"</span>)]</span>
enum MessageEvent&#x3C;T> {
    <span class="hljs-built_in">Snapshot</span>(T),
    <span class="hljs-built_in">Update</span>(T)
}
</code></pre><p>By having an <code>enum</code> inside another <code>enum</code>, we are able to tag each of them separately and still translate both at the root level of the JSON message. It would serialize to:</p><pre data-type="codeBlock" text="{
  &quot;topic&quot;: &quot;market&quot;,
  &quot;event&quot;: &quot;snapshot&quot;,
  ... // market data
}
"><code>{
  <span class="hljs-string">"topic"</span>: <span class="hljs-string">"market"</span>,
  <span class="hljs-string">"event"</span>: <span class="hljs-string">"snapshot"</span>,
  ... <span class="hljs-comment">// market data</span>
}
</code></pre><p>With that, we can now write the processor like this:</p><pre data-type="codeBlock" text="while let Some(message) = message_stream.next() {
    match message {
        Ok(MessageTopic::Market(MessageEvent::Snapshot(market_data))) =&gt; // Handle market snapshot
        Ok(MessageTopic::Market(MessageEvent::Update(market_data))) =&gt; // Handle market update
        Ok(MessageTopic::Portfolio(MessageEvent::Snapshot(portfolio_data))) =&gt; // Handle portfolio snapshot
        Ok(MessageTopic::Portfolio(MessageEvent::Update(portfolio_data))) =&gt; // Handle portfolio update
        Err(reason) =&gt; // Handle error
        _ =&gt; {} // Ignore other messages (if any)
    }
}
"><code><span class="hljs-keyword">while</span> let Some(message) <span class="hljs-operator">=</span> message_stream.next() {
    match message {
        Ok(MessageTopic::Market(MessageEvent::Snapshot(market_data))) <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-comment">// Handle market snapshot</span>
        Ok(MessageTopic::Market(MessageEvent::Update(market_data))) <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-comment">// Handle market update</span>
        Ok(MessageTopic::Portfolio(MessageEvent::Snapshot(portfolio_data))) <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-comment">// Handle portfolio snapshot</span>
        Ok(MessageTopic::Portfolio(MessageEvent::Update(portfolio_data))) <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-comment">// Handle portfolio update</span>
        Err(reason) <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-comment">// Handle error</span>
        <span class="hljs-keyword">_</span> <span class="hljs-operator">=</span><span class="hljs-operator">></span> {} <span class="hljs-comment">// Ignore other messages (if any)</span>
    }
}
</code></pre><p>Check <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/Detoo/serde-multi-dim-tags/blob/master/src/main.rs">here</a> for a runnable example.</p>]]></content:encoded>
            <author>detoo-2@newsletter.paragraph.com (detoo)</author>
        </item>
        <item>
            <title><![CDATA[Fixing 403 Forbidden on Node.js v18+]]></title>
            <link>https://paragraph.com/@detoo-2/fixing-403-forbidden-on-node-js-v18</link>
            <guid>WqU1xDC7dRNF4CM3PeoB</guid>
            <pubDate>Wed, 14 Aug 2024 20:44:25 GMT</pubDate>
            <description><![CDATA[It was quite annoying that, after upgrading node.js from v16 to v20, one of my old scripts stopped working and was throwing AxiosError: Request failed with status code 403. I knew it was the node version because, as soon as I switch back to node v16, the problem disappears. After hours of debugging, it turns out to be the term !CAMELLIA (i.e. don’t use Camellia-based algorithm) in node’s default cipher list, probably introduced after v18. It wasn’t clear to me why the remote endpoint requires...]]></description>
            <content:encoded><![CDATA[<p>It was quite annoying that, after upgrading node.js from v16 to v20, one of my old scripts stopped working and was throwing <code>AxiosError: Request failed with status code 403</code>. I knew it was the node version because, as soon as I switch back to node v16, the problem disappears.</p><p>After hours of debugging, it turns out to be the term <code>!CAMELLIA</code> (i.e. don’t use Camellia-based algorithm) in node’s default cipher list, probably introduced after v18. It wasn’t clear to me why the remote endpoint requires such algorithm, but as soon as I remove the term everything was working again.</p><pre data-type="codeBlock" text="const agent = new https.Agent({
    // Note it is almost the same as node v20&apos;s default cipher list except without `!CAMELLIA`
    ciphers: &apos;TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP&apos;
})

// Override the http.agent with ours
axios.post(&quot;https://the-picky-endpoint.com&quot;, {}, { httpsAgent: agent })
"><code>const agent = new https.<span class="hljs-title class_">Agent</span>({
    <span class="hljs-regexp">//</span> <span class="hljs-title class_">Note</span> it is almost the same as node v20<span class="hljs-string">'s default cipher list except without `!CAMELLIA`
    ciphers: '</span><span class="hljs-variable constant_">TLS_AES_256_GCM_SHA384</span><span class="hljs-symbol">:TLS_CHACHA20_POLY1305_SHA256</span><span class="hljs-symbol">:TLS_AES_128_GCM_SHA256</span><span class="hljs-symbol">:ECDHE-RSA-AES128-GCM-SHA256</span><span class="hljs-symbol">:ECDHE-ECDSA-AES128-GCM-SHA256</span><span class="hljs-symbol">:ECDHE-RSA-AES256-GCM-SHA384</span><span class="hljs-symbol">:ECDHE-ECDSA-AES256-GCM-SHA384</span><span class="hljs-symbol">:DHE-RSA-AES128-GCM-SHA256</span><span class="hljs-symbol">:ECDHE-RSA-AES128-SHA256</span><span class="hljs-symbol">:DHE-RSA-AES128-SHA256</span><span class="hljs-symbol">:ECDHE-RSA-AES256-SHA384</span><span class="hljs-symbol">:DHE-RSA-AES256-SHA384</span><span class="hljs-symbol">:ECDHE-RSA-AES256-SHA256</span><span class="hljs-symbol">:DHE-RSA-AES256-SHA256</span><span class="hljs-symbol">:HIGH</span><span class="hljs-symbol">:</span>!<span class="hljs-symbol">aNULL:</span>!<span class="hljs-symbol">eNULL:</span>!<span class="hljs-variable constant_">EXPORT</span><span class="hljs-symbol">:</span>!<span class="hljs-variable constant_">DES</span><span class="hljs-symbol">:</span>!<span class="hljs-variable constant_">RC4</span><span class="hljs-symbol">:</span>!<span class="hljs-variable constant_">MD5</span><span class="hljs-symbol">:</span>!<span class="hljs-variable constant_">PSK</span><span class="hljs-symbol">:</span>!<span class="hljs-variable constant_">SRP</span><span class="hljs-string">'
})

// Override the http.agent with ours
axios.post("https://the-picky-endpoint.com", {}, { httpsAgent: agent })
</span></code></pre><p><br>Finally! Onwards!</p>]]></content:encoded>
            <author>detoo-2@newsletter.paragraph.com (detoo)</author>
        </item>
        <item>
            <title><![CDATA[Retrofit Essential geth API on Other Full Nodes with Reverse Proxy]]></title>
            <link>https://paragraph.com/@detoo-2/retrofit-essential-geth-api-on-other-full-nodes-with-reverse-proxy</link>
            <guid>UMkZO6GEOWIL4wc0MlGw</guid>
            <pubDate>Thu, 04 Jul 2024 22:15:59 GMT</pubDate>
            <description><![CDATA[TIL Optimism core utilities currently depend on geth-specific RPC and, if your backend / full node is not geth or does not support such RPC, your app will fail at common operations such as sending a tx:2024-06-24 19:51:37.478+00:00 | vert.x-worker-thread-18 | TRACE | AbstractJsonRpcExecutor | {"jsonrpc":"2.0","id":22,"method":"eth_maxPriorityFeePerGas"} 2024-06-24 19:51:37.478+00:00 | vert.x-worker-thread-18 | DEBUG | JsonRpcExecutor | JSON-RPC request -> eth_maxPriorityFeePerGas null 2024-06...]]></description>
            <content:encoded><![CDATA[<p>TIL <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum-optimism/optimism/tree/f99eca3971b2875b9c09b747561a0518e27f051e/op-service">Optimism core utilities</a> currently <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum-optimism/optimism/blob/f99eca3971b2875b9c09b747561a0518e27f051e/op-service/txmgr/txmgr.go#L101">depend on</a> <code>geth</code>-specific RPC and, if your backend / full node is not <code>geth</code> or does not support such RPC, your app will fail at common operations such as <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum-optimism/optimism/blob/f99eca3971b2875b9c09b747561a0518e27f051e/op-service/txmgr/txmgr.go#L726">sending a tx</a>:</p><pre data-type="codeBlock" text="2024-06-24 19:51:37.478+00:00 | vert.x-worker-thread-18 | TRACE | AbstractJsonRpcExecutor | {&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:22,&quot;method&quot;:&quot;eth_maxPriorityFeePerGas&quot;}

2024-06-24 19:51:37.478+00:00 | vert.x-worker-thread-18 | DEBUG | JsonRpcExecutor | JSON-RPC request -&gt; eth_maxPriorityFeePerGas null

2024-06-24 19:51:37.479+00:00 | vert.x-worker-thread-18 | TRACE | AbstractJsonRpcExecutor | {&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:22,&quot;error&quot;:{&quot;code&quot;:-32601,&quot;message&quot;:&quot;Method not found&quot;}}
"><code><span class="hljs-number">2024</span><span class="hljs-operator">-</span>06<span class="hljs-number">-24</span> <span class="hljs-number">19</span>:<span class="hljs-number">51</span>:<span class="hljs-number">37.478</span><span class="hljs-operator">+</span>00:00 <span class="hljs-operator">|</span> vert.x-worker<span class="hljs-operator">-</span>thread<span class="hljs-number">-18</span> <span class="hljs-operator">|</span> TRACE <span class="hljs-operator">|</span> AbstractJsonRpcExecutor <span class="hljs-operator">|</span> {<span class="hljs-string">"jsonrpc"</span>:<span class="hljs-string">"2.0"</span>,<span class="hljs-string">"id"</span>:<span class="hljs-number">22</span>,<span class="hljs-string">"method"</span>:<span class="hljs-string">"eth_maxPriorityFeePerGas"</span>}

<span class="hljs-number">2024</span><span class="hljs-operator">-</span>06<span class="hljs-number">-24</span> <span class="hljs-number">19</span>:<span class="hljs-number">51</span>:<span class="hljs-number">37.478</span><span class="hljs-operator">+</span>00:00 <span class="hljs-operator">|</span> vert.x-worker<span class="hljs-operator">-</span>thread<span class="hljs-number">-18</span> <span class="hljs-operator">|</span> DEBUG <span class="hljs-operator">|</span> JsonRpcExecutor <span class="hljs-operator">|</span> JSON<span class="hljs-operator">-</span>RPC request <span class="hljs-operator">-</span><span class="hljs-operator">></span> eth_maxPriorityFeePerGas null

<span class="hljs-number">2024</span><span class="hljs-operator">-</span>06<span class="hljs-number">-24</span> <span class="hljs-number">19</span>:<span class="hljs-number">51</span>:<span class="hljs-number">37.479</span><span class="hljs-operator">+</span>00:00 <span class="hljs-operator">|</span> vert.x-worker<span class="hljs-operator">-</span>thread<span class="hljs-number">-18</span> <span class="hljs-operator">|</span> TRACE <span class="hljs-operator">|</span> AbstractJsonRpcExecutor <span class="hljs-operator">|</span> {<span class="hljs-string">"jsonrpc"</span>:<span class="hljs-string">"2.0"</span>,<span class="hljs-string">"id"</span>:<span class="hljs-number">22</span>,<span class="hljs-string">"error"</span>:{<span class="hljs-string">"code"</span>:<span class="hljs-number">-32601</span>,<span class="hljs-string">"message"</span>:<span class="hljs-string">"Method not found"</span>}}
</code></pre><p>Given I don’t want to switch back to <code>geth</code>, and it costs too much to switch to a cloud infra provider, also I don’t want to hack the app’s Optimism dependency. What else can I do?</p><p>Due to the <code>geth</code>-specific RPC <code>eth_maxPriorityFeePerGas</code> is sparsely called. I could potentially redirect only those particular calls to a cloud infra provider and therefore keep the usage low under free tier, then send everything else to my own full node. For that I need a reverse proxy with conditional routing, which can be done with <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://caddyserver.com/">Caddy</a>.</p><h1 id="h-prerequisites" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Prerequisites</h1><p>Assume our system already have Go installed.</p><p>To install Caddy:</p><pre data-type="codeBlock" text="sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf &apos;https://dl.cloudsmith.io/public/caddy/stable/gpg.key&apos; | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf &apos;https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt&apos; | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
"><code>sudo apt install <span class="hljs-operator">-</span>y debian<span class="hljs-operator">-</span>keyring debian<span class="hljs-operator">-</span>archive<span class="hljs-operator">-</span>keyring apt<span class="hljs-operator">-</span>transport<span class="hljs-operator">-</span>https curl
curl <span class="hljs-operator">-</span>1sLf <span class="hljs-string">'https://dl.cloudsmith.io/public/caddy/stable/gpg.key'</span> <span class="hljs-operator">|</span> sudo gpg <span class="hljs-operator">-</span><span class="hljs-operator">-</span>dearmor <span class="hljs-operator">-</span>o <span class="hljs-operator">/</span>usr<span class="hljs-operator">/</span>share<span class="hljs-operator">/</span>keyrings<span class="hljs-operator">/</span>caddy<span class="hljs-operator">-</span>stable<span class="hljs-operator">-</span>archive<span class="hljs-operator">-</span>keyring.gpg
curl <span class="hljs-operator">-</span>1sLf <span class="hljs-string">'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt'</span> <span class="hljs-operator">|</span> sudo tee <span class="hljs-operator">/</span>etc<span class="hljs-operator">/</span>apt<span class="hljs-operator">/</span>sources.list.d/caddy<span class="hljs-operator">-</span>stable.list
sudo apt update
sudo apt install caddy
</code></pre><p>To install xcaddy (development tools):</p><pre data-type="codeBlock" text="sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf &apos;https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key&apos; | sudo gpg --dearmor -o /usr/share/keyrings/caddy-xcaddy-archive-keyring.gpg
curl -1sLf &apos;https://dl.cloudsmith.io/public/caddy/xcaddy/debian.deb.txt&apos; | sudo tee /etc/apt/sources.list.d/caddy-xcaddy.list
sudo apt update
sudo apt install xcaddy
"><code>sudo apt install <span class="hljs-operator">-</span>y debian<span class="hljs-operator">-</span>keyring debian<span class="hljs-operator">-</span>archive<span class="hljs-operator">-</span>keyring apt<span class="hljs-operator">-</span>transport<span class="hljs-operator">-</span>https
curl <span class="hljs-operator">-</span>1sLf <span class="hljs-string">'https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key'</span> <span class="hljs-operator">|</span> sudo gpg <span class="hljs-operator">-</span><span class="hljs-operator">-</span>dearmor <span class="hljs-operator">-</span>o <span class="hljs-operator">/</span>usr<span class="hljs-operator">/</span>share<span class="hljs-operator">/</span>keyrings<span class="hljs-operator">/</span>caddy<span class="hljs-operator">-</span>xcaddy<span class="hljs-operator">-</span>archive<span class="hljs-operator">-</span>keyring.gpg
curl <span class="hljs-operator">-</span>1sLf <span class="hljs-string">'https://dl.cloudsmith.io/public/caddy/xcaddy/debian.deb.txt'</span> <span class="hljs-operator">|</span> sudo tee <span class="hljs-operator">/</span>etc<span class="hljs-operator">/</span>apt<span class="hljs-operator">/</span>sources.list.d/caddy<span class="hljs-operator">-</span>xcaddy.list
sudo apt update
sudo apt install xcaddy
</code></pre><h1 id="h-implementation" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Implementation</h1><p>Our goal is to run a reverse proxy at <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://localhost:8545">localhost:8545</a> that redirects ETH RPC calls to either of the two upstreams: (1) <code>eth_maxPriorityFeePerGas</code> to a cloud infra provider (assume Alchemy Ethereum mainnet), and (2) all other calls to our full node (assume running at local network 192.168.1.123:8545).</p><p>Unlike regular reverse proxies, we need to read the request body in order to determine the route. Such feature does not come built-in so we will build our own <code>caddy</code> with a custom plugin to match request body payloads.</p><p>In addition, reading request body may come with performance penalty given payload size is unbounded. Luckily, since we know the exact payload to look for, we can hard-code a size cap to avoid unnecessary loads.</p><p>Create and initialize the project folder:</p><pre data-type="codeBlock" text="mkdir -p /path/to/my/caddy-reqbody-matcher
cd /path/to/my/caddy-reqbody-matcher
go mod init github.com/Detoo/caddy-reqbody-matcher
"><code>mkdir <span class="hljs-operator">-</span>p <span class="hljs-operator">/</span>path<span class="hljs-operator">/</span>to<span class="hljs-operator">/</span>my<span class="hljs-operator">/</span>caddy<span class="hljs-operator">-</span>reqbody<span class="hljs-operator">-</span>matcher
cd <span class="hljs-operator">/</span>path<span class="hljs-operator">/</span>to<span class="hljs-operator">/</span>my<span class="hljs-operator">/</span>caddy<span class="hljs-operator">-</span>reqbody<span class="hljs-operator">-</span>matcher
go mod init github.com/Detoo<span class="hljs-operator">/</span>caddy<span class="hljs-operator">-</span>reqbody<span class="hljs-operator">-</span>matcher
</code></pre><p>Create the request body matcher plugin: <code>reqbody-matcher.go</code> with the following codes:</p><pre data-type="codeBlock" text="package keywordmatcher

import (
    &quot;bytes&quot;
    &quot;io/ioutil&quot;
    &quot;net/http&quot;
    &quot;unicode/utf8&quot;
    
    &quot;github.com/caddyserver/caddy/v2&quot;
    &quot;github.com/caddyserver/caddy/v2/caddyconfig/caddyfile&quot;
)

// KeywordMatcher implements caddyhttp.MatchInterface
type KeywordMatcher struct {
    Keyword string `json:&quot;keyword,omitempty&quot;`
}

// Match implements caddyhttp.MatchInterface
func (m KeywordMatcher) Match(r *http.Request) bool {
    // Read the request body
    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        return false
    }
    
    // Restore the io.ReadCloser to its original state
    r.Body = ioutil.NopCloser(bytes.NewReader(body))
    
    // Check if the body is a string and within the character limit
    if utf8.Valid(body) &amp;&amp; len(body) &lt; 1000 {
        // Check if the keyword exists in the request body
        return bytes.Contains(body, []byte(m.Keyword))
    }
    
    return false
}

// CaddyModule returns the Caddy module information.
func (KeywordMatcher) CaddyModule() caddy.ModuleInfo {
    return caddy.ModuleInfo{
        ID:  &quot;http.matchers.reqbody_keyword&quot;,
        New: func() caddy.Module { return new(KeywordMatcher) },
    }
}

// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (m *KeywordMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
    d.Next() // consume directive name
    
    // require an argument
    if !d.NextArg() {
        return d.ArgErr()
    }
    
    // store the argument
    m.Keyword = d.Val()
    return nil
}

func init() {
    // Register your matcher with Caddy
    caddy.RegisterModule(KeywordMatcher{})
}
"><code><span class="hljs-keyword">package</span> keywordmatcher

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"bytes"</span>
    <span class="hljs-string">"io/ioutil"</span>
    <span class="hljs-string">"net/http"</span>
    <span class="hljs-string">"unicode/utf8"</span>
    
    <span class="hljs-string">"github.com/caddyserver/caddy/v2"</span>
    <span class="hljs-string">"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"</span>
)

<span class="hljs-comment">// KeywordMatcher implements caddyhttp.MatchInterface</span>
<span class="hljs-keyword">type</span> KeywordMatcher <span class="hljs-keyword">struct</span> {
    Keyword <span class="hljs-type">string</span> <span class="hljs-string">`json:"keyword,omitempty"`</span>
}

<span class="hljs-comment">// Match implements caddyhttp.MatchInterface</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(m KeywordMatcher)</span></span> Match(r *http.Request) <span class="hljs-type">bool</span> {
    <span class="hljs-comment">// Read the request body</span>
    body, err := ioutil.ReadAll(r.Body)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
    }
    
    <span class="hljs-comment">// Restore the io.ReadCloser to its original state</span>
    r.Body = ioutil.NopCloser(bytes.NewReader(body))
    
    <span class="hljs-comment">// Check if the body is a string and within the character limit</span>
    <span class="hljs-keyword">if</span> utf8.Valid(body) &#x26;&#x26; <span class="hljs-built_in">len</span>(body) &#x3C; <span class="hljs-number">1000</span> {
        <span class="hljs-comment">// Check if the keyword exists in the request body</span>
        <span class="hljs-keyword">return</span> bytes.Contains(body, []<span class="hljs-type">byte</span>(m.Keyword))
    }
    
    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
}

<span class="hljs-comment">// CaddyModule returns the Caddy module information.</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(KeywordMatcher)</span></span> CaddyModule() caddy.ModuleInfo {
    <span class="hljs-keyword">return</span> caddy.ModuleInfo{
        ID:  <span class="hljs-string">"http.matchers.reqbody_keyword"</span>,
        New: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> caddy.Module { <span class="hljs-keyword">return</span> <span class="hljs-built_in">new</span>(KeywordMatcher) },
    }
}

<span class="hljs-comment">// UnmarshalCaddyfile implements caddyfile.Unmarshaler.</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(m *KeywordMatcher)</span></span> UnmarshalCaddyfile(d *caddyfile.Dispenser) <span class="hljs-type">error</span> {
    d.Next() <span class="hljs-comment">// consume directive name</span>
    
    <span class="hljs-comment">// require an argument</span>
    <span class="hljs-keyword">if</span> !d.NextArg() {
        <span class="hljs-keyword">return</span> d.ArgErr()
    }
    
    <span class="hljs-comment">// store the argument</span>
    m.Keyword = d.Val()
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">init</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// Register your matcher with Caddy</span>
    caddy.RegisterModule(KeywordMatcher{})
}
</code></pre><p>Build <code>caddy</code> executable with the custom plugin. If everything compiles, we will find the artifact under the same folder.</p><pre data-type="codeBlock" text="xcaddy build --with github.com/Detoo/caddy-reqbody-matcher=/path/to/my/caddy-reqbody-matcher
"><code>xcaddy build <span class="hljs-operator">-</span><span class="hljs-operator">-</span>with github.com/Detoo<span class="hljs-operator">/</span>caddy<span class="hljs-operator">-</span>reqbody<span class="hljs-operator">-</span>matcher<span class="hljs-operator">=</span><span class="hljs-operator">/</span>path<span class="hljs-operator">/</span>to<span class="hljs-operator">/</span>my<span class="hljs-operator">/</span>caddy<span class="hljs-operator">-</span>reqbody<span class="hljs-operator">-</span>matcher
</code></pre><p>Configure the reverse proxy and our custom plugin by creating the config file <code>Caddyfile</code>:</p><pre data-type="codeBlock" text="# Reverse proxy for ETH JSON RPC with geth backup
http://localhost:8545 {
    # Match geth-specific requests
    @reqbodyMatcher {
        reqbody_keyword &quot;eth_maxPriorityFeePerGas&quot;
    }

    # Default to our own node
    reverse_proxy 192.168.1.123:8545

    # Redirect geth-specific requests
    reverse_proxy @reqbodyMatcher {
        rewrite /v2/&lt;your-api-key&gt;{uri}
        to &quot;https://eth-mainnet.g.alchemy.com&quot;
        header_up Host eth-mainnet.g.alchemy.com
    }
}
"><code># Reverse proxy <span class="hljs-keyword">for</span> ETH JSON RPC with geth backup
http:<span class="hljs-comment">//localhost:8545 {</span>
    # Match geth<span class="hljs-operator">-</span>specific requests
    @reqbodyMatcher {
        reqbody_keyword <span class="hljs-string">"eth_maxPriorityFeePerGas"</span>
    }

    # Default to our own node
    reverse_proxy <span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.123</span>:<span class="hljs-number">8545</span>

    # Redirect geth<span class="hljs-operator">-</span>specific requests
    reverse_proxy @reqbodyMatcher {
        rewrite <span class="hljs-operator">/</span>v2<span class="hljs-operator">/</span><span class="hljs-operator">&#x3C;</span>your<span class="hljs-operator">-</span>api<span class="hljs-operator">-</span>key<span class="hljs-operator">></span>{uri}
        to <span class="hljs-string">"https://eth-mainnet.g.alchemy.com"</span>
        header_up Host eth<span class="hljs-operator">-</span>mainnet.g.alchemy.com
    }
}
</code></pre><p>Install it as a <code>systemctl</code> service so it runs 24/7. Create a new file <code>caddy-reqbody-matcher.service</code> with the following contents:</p><pre data-type="codeBlock" text="# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy&apos;s API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.

[Unit]
Description=Caddy-reqbody-matcher
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=detoo
Group=detoo
WorkingDirectory=/path/to/my/caddy-reqbody-matcher
ExecStart=/path/to/my/caddy-reqbody-matcher/caddy run --environ
ExecReload=/path/to/my/caddy-reqbody-matcher/caddy reload --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
"><code><span class="hljs-comment"># WARNING: This service does not use the --resume flag, so if you</span>
<span class="hljs-comment"># use the API to make changes, they will be overwritten by the</span>
<span class="hljs-comment"># Caddyfile next time the service is restarted. If you intend to</span>
<span class="hljs-comment"># use Caddy's API to configure it, add the --resume flag to the</span>
<span class="hljs-comment"># `caddy run` command or use the caddy-api.service file instead.</span>

<span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=Caddy-reqbody-matcher
<span class="hljs-attr">After</span>=network.target network-<span class="hljs-literal">on</span>line.target
<span class="hljs-attr">Requires</span>=network-<span class="hljs-literal">on</span>line.target

<span class="hljs-section">[Service]</span>
<span class="hljs-attr">Type</span>=notify
<span class="hljs-attr">User</span>=detoo
<span class="hljs-attr">Group</span>=detoo
<span class="hljs-attr">WorkingDirectory</span>=/path/to/my/caddy-reqbody-matcher
<span class="hljs-attr">ExecStart</span>=/path/to/my/caddy-reqbody-matcher/caddy run --environ
<span class="hljs-attr">ExecReload</span>=/path/to/my/caddy-reqbody-matcher/caddy reload --force
<span class="hljs-attr">TimeoutStopSec</span>=<span class="hljs-number">5</span>s
<span class="hljs-attr">LimitNOFILE</span>=<span class="hljs-number">1048576</span>
<span class="hljs-attr">PrivateTmp</span>=<span class="hljs-literal">true</span>
<span class="hljs-attr">ProtectSystem</span>=full
<span class="hljs-attr">AmbientCapabilities</span>=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

<span class="hljs-section">[Install]</span>
<span class="hljs-attr">WantedBy</span>=multi-user.target
</code></pre><p>Install and start the service:</p><pre data-type="codeBlock" text="sudo systemctl enable /path/to/my/caddy-reqbody-matcher/caddy-reqbody-matcher.service
sudo systemctl restart caddy-reqbody-matcher
"><code>sudo systemctl enable <span class="hljs-operator">/</span>path<span class="hljs-operator">/</span>to<span class="hljs-operator">/</span>my<span class="hljs-operator">/</span>caddy<span class="hljs-operator">-</span>reqbody<span class="hljs-operator">-</span>matcher<span class="hljs-operator">/</span>caddy<span class="hljs-operator">-</span>reqbody<span class="hljs-operator">-</span>matcher.service
sudo systemctl restart caddy<span class="hljs-operator">-</span>reqbody<span class="hljs-operator">-</span>matcher
</code></pre><p>Now the reverse proxy should be running at <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://localhost:8545">localhost:8545</a>, to verify:</p><pre data-type="codeBlock" text="# This should go to our own full node
curl --header &apos;Content-Type: application/json&apos; --data-raw &apos;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:22,&quot;method&quot;:&quot;eth_blockNumber&quot;}&apos; http://localhost:8545

# This should go to cloud infra provider
curl --header &apos;Content-Type: application/json&apos; --data-raw &apos;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;id&quot;:22,&quot;method&quot;:&quot;eth_maxPriorityFeePerGas&quot;}&apos; http://localhost:8545
"><code># This should go to our own full node
curl <span class="hljs-operator">-</span><span class="hljs-operator">-</span>header <span class="hljs-string">'Content-Type: application/json'</span> <span class="hljs-operator">-</span><span class="hljs-operator">-</span>data<span class="hljs-operator">-</span>raw <span class="hljs-string">'{"jsonrpc":"2.0","id":22,"method":"eth_blockNumber"}'</span> http:<span class="hljs-comment">//localhost:8545</span>

# This should go to cloud infra provider
curl <span class="hljs-operator">-</span><span class="hljs-operator">-</span>header <span class="hljs-string">'Content-Type: application/json'</span> <span class="hljs-operator">-</span><span class="hljs-operator">-</span>data<span class="hljs-operator">-</span>raw <span class="hljs-string">'{"jsonrpc":"2.0","id":22,"method":"eth_maxPriorityFeePerGas"}'</span> http:<span class="hljs-comment">//localhost:8545</span>
</code></pre><p>Now we have a proper backend to handle all RPCs!</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b3ba0ee15cf7cd008588784354d5686a982144c79aa8597a83c09f6437bdc2e2.png" alt="gotta handle them all" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">gotta handle them all</figcaption></figure>]]></content:encoded>
            <author>detoo-2@newsletter.paragraph.com (detoo)</author>
        </item>
        <item>
            <title><![CDATA[Homemade Ethereum Validator Internet Access Failover]]></title>
            <link>https://paragraph.com/@detoo-2/homemade-ethereum-validator-internet-access-failover</link>
            <guid>sS0bM8LwQtLfJbIdo8Mk</guid>
            <pubDate>Sun, 11 Sep 2022 18:39:25 GMT</pubDate>
            <description><![CDATA[uh!As an Ethereum PoS Validator, we want to stay online as long as we can. We could do as much to build a reliable node that keeps itself available, but one thing we don’t have much control of is the internet providers. Unfortunately, where I setup the node I have only Comcast, which is the nation’s most beloved provider and is famous for its highly reliable uptime as well as its hassle-free cancellation process (kek). But no High Availability is too high, that’s why I have also subscribed to...]]></description>
            <content:encoded><![CDATA[<figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ac237216322f1914f0051a1064402e46de600801de7229840e77a9fb4b6eb263.png" alt="uh!" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">uh!</figcaption></figure><p>As an Ethereum PoS Validator, we want to stay online as long as we can. We could do as much to build a reliable node that keeps itself available, but one thing we don’t have much control of is the internet providers.</p><p>Unfortunately, where I setup the node I have only Comcast, which is the nation’s most beloved provider and is famous for its highly reliable uptime as well as its hassle-free cancellation process (kek).</p><p>But no High Availability is too high, that’s why I have also subscribed to Starlink. Because hey, sending blocks to the space! How cool is that?</p><h1 id="h-challenges" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Challenges</h1><p>The idea is to hot switch providers whenever it fails to reach the internet. Sounds simple, but it turns out to be not as straightforward as I thought.</p><p>For one, simply having both interfaces online is not enough. As when one fails, it usually fails at WAN level instead of LAN. From the node OS (ex. Ubuntu)’s point of view the connection is still on, so it will not switch to the other interface and will keep sending the packets through the failing one.</p><p>It seems failover on internet/DNS accessibility requires higher layer logic than a network manager would normally support. I also tried <code>netplan</code> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://www.hughesportal.com/blog.php?article=setup-network-bonding-using-netplan">bonding</a> but it seemed to get stuck for the same reason.</p><h1 id="h-homemade-solutions" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Homemade Solutions</h1><p>Although the network manager alone might not be suitable for our need, it is good at setting interface priority. We could use it in combination of writing ourselves a simple cronjob to check the internet connections periodically, and then tell the network manager to switch interface accordingly.</p><p>The following example assumes two internet-facing interfaces: <code>eno1</code>, which is ethernet-based; and <code>wlp3s0</code>, which is WiFi-based. Both of them are connected to regular consumer routers respectively with DHCP support.</p><p>Again we use <code>netplan</code> as the network manager. First create the following config files (they are separated into multiple files for easier management):</p><ul><li><p><code>/etc/netplan/00-installer-config-eno1.yaml</code>: basic configs for <code>eno1</code></p><pre data-type="codeBlock" text="network:
  version: 2
  ethernets:
    eno1:
      dhcp4: true
"><code><span class="hljs-attr">network:</span>
  <span class="hljs-attr">version:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">ethernets:</span>
    <span class="hljs-attr">eno1:</span>
      <span class="hljs-attr">dhcp4:</span> <span class="hljs-literal">true</span>
</code></pre></li><li><p><code>/etc/netplan/01-installer-config-wlp3s0.yaml</code>: basic configs for <code>wlp3s0</code> (remember to update the access-point name and password)</p><pre data-type="codeBlock" text="network:
  version: 2
  wifis:
    wlp3s0:
      access-points:
        YourWiFiName:
          password: YourWiFiPassword
      dhcp4: true
"><code><span class="hljs-attr">network:</span>
  <span class="hljs-attr">version:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">wifis:</span>
    <span class="hljs-attr">wlp3s0:</span>
      <span class="hljs-attr">access-points:</span>
        <span class="hljs-attr">YourWiFiName:</span>
          <span class="hljs-attr">password:</span> <span class="hljs-string">YourWiFiPassword</span>
      <span class="hljs-attr">dhcp4:</span> <span class="hljs-literal">true</span>
</code></pre></li><li><p><code>/etc/netplan/02-prioritize-eno1.yaml</code>: configs for prioritizing <code>eno1</code></p><pre data-type="codeBlock" text="network:
  version: 2
  ethernets:
    eno1:
      dhcp4-overrides:
        route-metric: 100 # lower number = higher priority
  wifis:
    wlp3s0:
      dhcp4-overrides:
        route-metric: 200
"><code><span class="hljs-attr">network:</span>
  <span class="hljs-attr">version:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">ethernets:</span>
    <span class="hljs-attr">eno1:</span>
      <span class="hljs-attr">dhcp4-overrides:</span>
        <span class="hljs-attr">route-metric:</span> <span class="hljs-number">100</span> <span class="hljs-comment"># lower number = higher priority</span>
  <span class="hljs-attr">wifis:</span>
    <span class="hljs-attr">wlp3s0:</span>
      <span class="hljs-attr">dhcp4-overrides:</span>
        <span class="hljs-attr">route-metric:</span> <span class="hljs-number">200</span>
</code></pre></li><li><p><code>/etc/netplan/02-prioritize-wlp3s0.yaml.disabled</code>: configs for prioritizing <code>wlp3s0</code>. Note the postfix “disabled” because we use <code>eno1</code> by default</p><pre data-type="codeBlock" text="network:
  version: 2
  ethernets:
    eno1:
      dhcp4-overrides:
        route-metric: 200
  wifis:
    wlp3s0:
      dhcp4-overrides:
        route-metric: 100 # lower number = higher priority
"><code><span class="hljs-attr">network:</span>
  <span class="hljs-attr">version:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">ethernets:</span>
    <span class="hljs-attr">eno1:</span>
      <span class="hljs-attr">dhcp4-overrides:</span>
        <span class="hljs-attr">route-metric:</span> <span class="hljs-number">200</span>
  <span class="hljs-attr">wifis:</span>
    <span class="hljs-attr">wlp3s0:</span>
      <span class="hljs-attr">dhcp4-overrides:</span>
        <span class="hljs-attr">route-metric:</span> <span class="hljs-number">100</span> <span class="hljs-comment"># lower number = higher priority</span>
</code></pre></li></ul><p>The first two files are basic configs that apply all the time. The last two files are mutually-exclusive and there should be only one enabled at any given time. This way we can tell the network manager to use either <code>eno1</code> or <code>wlp3s0</code>, whichever is available. And then we run a cronjob to perform the check periodically.</p><p>Create the script for cronjob at <code>~/bin/internet-failover/internet-failover.sh</code> or other places of your choice:</p><pre data-type="codeBlock" text="#!/bin/bash

# choose a realiable URL or IP to check against.
# we use URL here to also check against DNS failures
CHECK_URL=&quot;www.yahoo.com&quot;

# remember to update the interface IDs if necessary
IF0=&quot;eno1&quot;
IF1=&quot;wlp3s0&quot;

PRI_PLAN_PATH_PREFIX=&quot;/etc/netplan/02-prioritize-&quot;
PRI_PLAN_PATH_POSTFIX=&quot;.yaml&quot;

# determine the current primary interface by finding the plan files
if [ -f &quot;$PRI_PLAN_PATH_PREFIX$IF0$PRI_PLAN_PATH_POSTFIX&quot; ]; then
    echo &quot;current primary plan: \$IF0&quot;
    CUR_IF=&quot;$IF0&quot;
    NEXT_IF=&quot;$IF1&quot;
else
    echo &quot;current primary plan: \$IF1&quot;
    CUR_IF=&quot;$IF1&quot;
    NEXT_IF=&quot;$IF0&quot;
fi

echo &quot;checking if internet is available...&quot;
ping -I $CUR_IF -q -c 2 $CHECK_URL &gt; /dev/null 2&gt;&amp;1

if [ $? -ne 0]; then
    echo &quot;[WARN] internet not available&quot;
    echo &quot;switching primary plan to: \$NEXT_IF ...&quot;

    mv &quot;$PRI_PLAN_PATH_PREFIX$CUR_IF$PRI_PLAN_PATH_POSTFIX&quot; &quot;$PRI_PLAN_PATH_PREFIX$CUR_IF$PRI_PLAN_PATH_POSTFIX.disabled&quot;
    mv &quot;$PRI_PLAN_PATH_PREFIX$NEXT_IF$PRI_PLAN_PATH_POSTFIX&quot;.disabled &quot;$PRI_PLAN_PATH_PREFIX$NEXT_IF$PRI_PLAN_PATH_POSTFIX&quot;
    /usr/sbin/netplan apply
fi
"><code><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-comment"># choose a realiable URL or IP to check against.</span>
<span class="hljs-comment"># we use URL here to also check against DNS failures</span>
CHECK_URL=<span class="hljs-string">"www.yahoo.com"</span>

<span class="hljs-comment"># remember to update the interface IDs if necessary</span>
IF0=<span class="hljs-string">"eno1"</span>
IF1=<span class="hljs-string">"wlp3s0"</span>

PRI_PLAN_PATH_PREFIX=<span class="hljs-string">"/etc/netplan/02-prioritize-"</span>
PRI_PLAN_PATH_POSTFIX=<span class="hljs-string">".yaml"</span>

<span class="hljs-comment"># determine the current primary interface by finding the plan files</span>
<span class="hljs-keyword">if</span> [ -f <span class="hljs-string">"$PRI_PLAN_PATH_PREFIX$IF0<span class="hljs-variable">$PRI_PLAN_PATH_POSTFIX</span>"</span> ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"current primary plan: \$IF0"</span>
    CUR_IF=<span class="hljs-string">"<span class="hljs-variable">$IF0</span>"</span>
    NEXT_IF=<span class="hljs-string">"<span class="hljs-variable">$IF1</span>"</span>
<span class="hljs-keyword">else</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"current primary plan: \$IF1"</span>
    CUR_IF=<span class="hljs-string">"<span class="hljs-variable">$IF1</span>"</span>
    NEXT_IF=<span class="hljs-string">"<span class="hljs-variable">$IF0</span>"</span>
<span class="hljs-keyword">fi</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"checking if internet is available..."</span>
ping -I <span class="hljs-variable">$CUR_IF</span> -q -c 2 <span class="hljs-variable">$CHECK_URL</span> > /dev/null 2>&#x26;1

<span class="hljs-keyword">if</span> [ $? -ne 0]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"[WARN] internet not available"</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"switching primary plan to: \$NEXT_IF ..."</span>

    <span class="hljs-built_in">mv</span> <span class="hljs-string">"$PRI_PLAN_PATH_PREFIX$CUR_IF<span class="hljs-variable">$PRI_PLAN_PATH_POSTFIX</span>"</span> <span class="hljs-string">"$PRI_PLAN_PATH_PREFIX$CUR_IF<span class="hljs-variable">$PRI_PLAN_PATH_POSTFIX</span>.disabled"</span>
    <span class="hljs-built_in">mv</span> <span class="hljs-string">"$PRI_PLAN_PATH_PREFIX$NEXT_IF<span class="hljs-variable">$PRI_PLAN_PATH_POSTFIX</span>"</span>.disabled <span class="hljs-string">"$PRI_PLAN_PATH_PREFIX$NEXT_IF<span class="hljs-variable">$PRI_PLAN_PATH_POSTFIX</span>"</span>
    /usr/sbin/netplan apply
<span class="hljs-keyword">fi</span>
</code></pre><p>Create the cronjob and set it to run every minute:</p><pre data-type="codeBlock" text="sudo crontab -e

# add the following line (remember to update the path to the script if necessary):
*/1 * * * * /home/username/bin/internet-failover/internet-failover.sh 2&gt;&amp;1 | logger -t internet-failover

# save it

# check execution
tail -f /var/log/syslog | grep internet-failover

# after a minute or so we should see the logs
CRON: (root) CMD (/home/username/bin/internet-failover/internet-failover.sh 2&gt;&amp;1 | logger -t internet-failover)
internet-failover: current primary plan: eno1
internet-failover: checking if internet is available...
"><code>sudo crontab <span class="hljs-operator">-</span>e

# add the following line (remember to update the path to the script <span class="hljs-keyword">if</span> necessary):
<span class="hljs-operator">*</span><span class="hljs-operator">/</span><span class="hljs-number">1</span> <span class="hljs-operator">*</span> <span class="hljs-operator">*</span> <span class="hljs-operator">*</span> <span class="hljs-operator">*</span> <span class="hljs-operator">/</span>home<span class="hljs-operator">/</span>username<span class="hljs-operator">/</span>bin<span class="hljs-operator">/</span>internet<span class="hljs-operator">-</span>failover<span class="hljs-operator">/</span>internet<span class="hljs-operator">-</span>failover.sh <span class="hljs-number">2</span><span class="hljs-operator">></span><span class="hljs-operator">&#x26;</span><span class="hljs-number">1</span> <span class="hljs-operator">|</span> logger <span class="hljs-operator">-</span>t internet<span class="hljs-operator">-</span>failover

# save it

# check execution
tail <span class="hljs-operator">-</span>f <span class="hljs-operator">/</span><span class="hljs-keyword">var</span><span class="hljs-operator">/</span>log<span class="hljs-operator">/</span>syslog <span class="hljs-operator">|</span> grep internet<span class="hljs-operator">-</span>failover

# after a minute or so we should see the logs
CRON: (root) CMD (<span class="hljs-operator">/</span>home<span class="hljs-operator">/</span>username<span class="hljs-operator">/</span>bin<span class="hljs-operator">/</span>internet<span class="hljs-operator">-</span>failover<span class="hljs-operator">/</span>internet<span class="hljs-operator">-</span>failover.sh <span class="hljs-number">2</span><span class="hljs-operator">></span><span class="hljs-operator">&#x26;</span><span class="hljs-number">1</span> <span class="hljs-operator">|</span> logger <span class="hljs-operator">-</span>t internet<span class="hljs-operator">-</span>failover)
internet<span class="hljs-operator">-</span>failover: current primary plan: eno1
internet<span class="hljs-operator">-</span>failover: checking <span class="hljs-keyword">if</span> internet <span class="hljs-keyword">is</span> available...
</code></pre><p>If we are feeling lucky, try to disable the internet for <code>eno1</code> and we will see the cronjob detects it and switches the provider:</p><pre data-type="codeBlock" text="CRON: (root) CMD (/home/username/bin/internet-failover/internet-failover.sh 2&gt;&amp;1 | logger -t internet-failover)
internet-failover: current primary plan: eno1
internet-failover: checking if internet is available...
internet-failover: [WARN] internet not available
internet-failover: switching primary plan to: wlp3s0 ...
"><code>CRON: (root) CMD (<span class="hljs-operator">/</span>home<span class="hljs-operator">/</span>username<span class="hljs-operator">/</span>bin<span class="hljs-operator">/</span>internet<span class="hljs-operator">-</span>failover<span class="hljs-operator">/</span>internet<span class="hljs-operator">-</span>failover.sh <span class="hljs-number">2</span><span class="hljs-operator">></span><span class="hljs-operator">&#x26;</span><span class="hljs-number">1</span> <span class="hljs-operator">|</span> logger <span class="hljs-operator">-</span>t internet<span class="hljs-operator">-</span>failover)
internet<span class="hljs-operator">-</span>failover: current primary plan: eno1
internet<span class="hljs-operator">-</span>failover: checking <span class="hljs-keyword">if</span> internet <span class="hljs-keyword">is</span> available...
internet<span class="hljs-operator">-</span>failover: [WARN] internet not available
internet<span class="hljs-operator">-</span>failover: switching primary plan to: wlp3s0 ...
</code></pre><p>Stake long and prosper!</p>]]></content:encoded>
            <author>detoo-2@newsletter.paragraph.com (detoo)</author>
        </item>
        <item>
            <title><![CDATA[Start Your Engine API for Merge™]]></title>
            <link>https://paragraph.com/@detoo-2/start-your-engine-api-for-merge</link>
            <guid>SH9spvR5lKGpRFYaxGfP</guid>
            <pubDate>Sun, 04 Sep 2022 04:19:55 GMT</pubDate>
            <description><![CDATA[--authrpc.port=8551Merge™ is coming. Most of us have probably already heard of the renaming and the introduction of Execution Client (ex. Geth) and the Consensus Client (ex. Lighthouse Beacon Node). It also introduced a new interface called Engine API. As a pre-merge validator, what exactly has changed? How do I upgrade it and verify if it is ready for merge?Below assumes we have existing (pre-merge) nodes running and will focus only on the changes.Execution ClientThere are multiple options f...]]></description>
            <content:encoded><![CDATA[<figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/52aa12e67129b2fb733e14c4b6c5a135bf682e97bcfca8ecc6a593d71f3dd48d.png" alt="--authrpc.port=8551" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">--authrpc.port=8551</figcaption></figure><p>Merge™ is coming. Most of us have probably already heard of the renaming and the introduction of Execution Client (ex. Geth) and the Consensus Client (ex. Lighthouse Beacon Node). It also introduced a new interface called Engine API. As a pre-merge validator, what exactly has changed? How do I upgrade it and verify if it is ready for merge?</p><blockquote><p>Below assumes we have existing (pre-merge) nodes running and will focus only on the changes.</p></blockquote><h1 id="h-execution-client" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Execution Client</h1><p>There are multiple options for running an Execution Client. I’ll focus on <code>geth</code> here.</p><p>The current merge-ready <code>geth</code> version is <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/ethereum/go-ethereum/releases/tag/v1.10.23">v1.10.23</a> as of writing. Upgrading from a pre-merge <code>geth</code> version you need to add the following arguments:</p><pre data-type="codeBlock" text="--authrpc.addr=localhost
--authrpc.port=8551
"><code><span class="hljs-operator">-</span><span class="hljs-operator">-</span>authrpc.addr=localhost
<span class="hljs-operator">-</span><span class="hljs-operator">-</span>authrpc.port=<span class="hljs-number">8551</span>
</code></pre><p>Note if you are running Beacon nodes or Validator nodes in a container, you might want to add the extra arguments so that <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://geth.ethereum.org/docs/interface/consensus-clients">incoming requests from virtual hosts are accepted</a>:</p><pre data-type="codeBlock" text="--authrpc.vhosts=&quot;*&quot;
"><code><span class="hljs-operator">-</span><span class="hljs-operator">-</span>authrpc.vhosts=<span class="hljs-string">"*"</span>
</code></pre><p>Restart <code>geth</code>, if everything runs properly you will see:</p><pre data-type="codeBlock" text="WARN Chain pre-merge, sync via PoW (ensure beacon client is ready)
...
WARN Engine API enabled                       protocol=eth
WARN Engine API started but chain not configured for merge yet
...
INFO Generated JWT secret                     path=/home/you/.ethereum/geth/jwtsecret
...
INFO WebSocket enabled                        url=ws://127.0.0.1:8551
INFO HTTP server started                      endpoint=127.0.0.1:8551 auth=true  prefix= cors=localhost vhosts=*
"><code>WARN Chain pre<span class="hljs-operator">-</span>merge, sync via PoW (ensure beacon client <span class="hljs-keyword">is</span> ready)
...
WARN Engine API enabled                       protocol<span class="hljs-operator">=</span>eth
WARN Engine API started but chain not configured <span class="hljs-keyword">for</span> merge yet
...
INFO Generated JWT secret                     path<span class="hljs-operator">=</span><span class="hljs-operator">/</span>home<span class="hljs-operator">/</span>you<span class="hljs-operator">/</span>.ethereum/geth<span class="hljs-operator">/</span>jwtsecret
...
INFO WebSocket enabled                        url<span class="hljs-operator">=</span>ws:<span class="hljs-comment">//127.0.0.1:8551</span>
INFO HTTP server started                      endpoint<span class="hljs-operator">=</span><span class="hljs-number">127.0</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span>:<span class="hljs-number">8551</span> auth<span class="hljs-operator">=</span><span class="hljs-literal">true</span>  prefix<span class="hljs-operator">=</span> cors<span class="hljs-operator">=</span>localhost vhosts<span class="hljs-operator">=</span><span class="hljs-operator">*</span>
</code></pre><p>Note the <code>pre-merge</code> status and the generated JWT secret location in the logs.</p><h1 id="h-consensus-client-beacon-node" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Consensus Client (Beacon Node)</h1><p>Again there are multiple options for running a Beacon Node, here I will use Lighthouse.</p><p>As of writing, the current merge-ready Lighthouse version is <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/sigp/lighthouse/releases/tag/v3.1.0">v3.1.0</a>.</p><p>Add the following merge-related arguments:</p><pre data-type="codeBlock" text="--execution-endpoints http://localhost:8551
--execution-jwt=&quot;/home/you/.ethereum/geth/jwtsecret&quot;
"><code><span class="hljs-operator">-</span><span class="hljs-operator">-</span>execution<span class="hljs-operator">-</span>endpoints http:<span class="hljs-comment">//localhost:8551</span>
<span class="hljs-operator">-</span><span class="hljs-operator">-</span>execution<span class="hljs-operator">-</span>jwt<span class="hljs-operator">=</span><span class="hljs-string">"/home/you/.ethereum/geth/jwtsecret"</span>
</code></pre><p>Note the port <code>8551</code> is different from the legacy RPC port (<code>8545</code>) because it is an exclusive interface for the Beacon Node only. Also note the <code>--execution-jwt</code> location should match the location shown in <code>geth</code> logs above.</p><p>Restart the Beacon Node. It will take a few minutes to migrate the database from the previous version. Once it is connected and synced with the Execution Client we’ll see:</p><pre data-type="codeBlock" text="INFO Ready for the merge current_difficulty: 57833190587477479896205, terminal_total_difficulty: 58750000000000000000000, service: slot_notifier
"><code><span class="hljs-attr">INFO Ready for the merge current_difficulty:</span> <span class="hljs-number">57833190587477479896205</span><span class="hljs-string">,</span> <span class="hljs-attr">terminal_total_difficulty:</span> <span class="hljs-number">58750000000000000000000</span><span class="hljs-string">,</span> <span class="hljs-attr">service:</span> <span class="hljs-string">slot_notifier</span>
</code></pre><p>That means both clients are ready! The Engine API has started, the TTD (Terminal Total Difficulty) is nominal, we are ready for Merge™</p><h1 id="h-one-more-thing" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">One More Thing</h1><p>Go to <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://bordel.wtf/">bordel.wtf</a> and check out the estimated Merge time if you want to see it happen live!</p>]]></content:encoded>
            <author>detoo-2@newsletter.paragraph.com (detoo)</author>
        </item>
        <item>
            <title><![CDATA[Merge or Macro?]]></title>
            <link>https://paragraph.com/@detoo-2/merge-or-macro</link>
            <guid>Gb34KqehtzuHONygwwtl</guid>
            <pubDate>Fri, 02 Sep 2022 21:50:21 GMT</pubDate>
            <description><![CDATA[Just couldn’t resist and drew this when listening to Bankless Friday Rollup 🤣]]></description>
            <content:encoded><![CDATA[<p>Just couldn’t resist and drew this when listening to <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.youtube.com/watch?v=1t3SyjtHhcI">Bankless Friday Rollup</a> 🤣</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/3bd1a4c196b9c5d466850f50bd1befccff55c97ab9a25cb564639ff40cfc194b.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure>]]></content:encoded>
            <author>detoo-2@newsletter.paragraph.com (detoo)</author>
        </item>
    </channel>
</rss>