<?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: Yash Kumar Saini</title>
    <description>The latest articles on DEV Community by Yash Kumar Saini (@yashksaini).</description>
    <link>https://dev.to/yashksaini</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1242333%2F27bdd497-8c93-4290-a901-94eec3759056.jpeg</url>
      <title>DEV Community: Yash Kumar Saini</title>
      <link>https://dev.to/yashksaini</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yashksaini"/>
    <language>en</language>
    <item>
      <title>I Built My Friend a Mock Interviewer That Read His Rust Code</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Mon, 05 Oct 2026 13:53:30 +0000</pubDate>
      <link>https://dev.to/yashksaini/i-built-my-friend-a-mock-interviewer-that-read-his-rust-code-1gif</link>
      <guid>https://dev.to/yashksaini/i-built-my-friend-a-mock-interviewer-that-read-his-rust-code-1gif</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;grill&lt;/code&gt; is a mock technical interviewer that has read your code.&lt;/strong&gt; It runs in a terminal, on an open model, on your own laptop.&lt;/p&gt;

&lt;p&gt;I built it for my friend &lt;a href="https://github.com/AkashJana18" rel="noopener noreferrer"&gt;Akash&lt;/a&gt;. He has spent the last year writing Rust, ships TypeScript on the side, and is looking for a full-time role. The rounds he dreads are the Rust and JavaScript deep dives.&lt;/p&gt;

&lt;p&gt;Most interview prep tools ask the same textbook questions: what is a closure, explain ownership. Real senior interviews do not go that way. They go "walk me through your project", and then the interviewer points at one line you hoped nobody would notice and asks why.&lt;/p&gt;

&lt;p&gt;You cannot practise that with a question bank, because the questions depend on your code. So &lt;code&gt;grill&lt;/code&gt; does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reads his repositories&lt;/strong&gt; and finds the lines worth a question: &lt;code&gt;unsafe&lt;/code&gt; blocks, locks, lifetimes, spawned tasks, &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;forEach(async ...)&lt;/code&gt;, &lt;code&gt;any&lt;/code&gt;, empty &lt;code&gt;catch&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interviews him on them.&lt;/strong&gt; One question about that exact code, one follow-up that presses on the weakest part of his answer, then a grade from 0 to 4 with what he missed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remembers what he got wrong.&lt;/strong&gt; The next session draws more from his worst topics.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the scanner on four of Akash's public repositories. This is real output:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fup511l6jxyc21okamnb9.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fup511l6jxyc21okamnb9.png" alt="Terminal output of grill ingest finding 116 interview-worthy spots across four repositories" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;116 spots, and the top one is in &lt;code&gt;miccli&lt;/code&gt;, his voice dictation CLI: an &lt;code&gt;Arc&amp;lt;Mutex&amp;lt;...&amp;gt;&amp;gt;&lt;/code&gt; shared with a real-time audio callback. That is exactly the line a Rust interviewer would circle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

 

&lt;p&gt;A session looks like this. The code and location are real scanner output; paste your own question, answer and grade from a real run in place of the placeholder lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="err"&gt;──&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="err"&gt;──&lt;/span&gt;
&lt;span class="n"&gt;AkashJana18&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;miccli&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="py"&gt;.rs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;62&lt;/span&gt;  &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="mi"&gt;62&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;     &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;start_capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AudioStream&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="mi"&gt;63&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
   &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="mi"&gt;72&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Mutex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;PendingResampler&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;
        yashksaini-coder
      &lt;/a&gt; / &lt;a href="https://github.com/yashksaini-coder/grill" rel="noopener noreferrer"&gt;
        grill
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/yashksaini-coder/grill/docs/cover.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fyashksaini-coder%2Fgrill%2FHEAD%2Fdocs%2Fcover.png" alt="grill: a mock interviewer that has read your code" width="800"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;grill&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/sponsors/yashksaini-coder" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/905c0628741c74fd2dc51df9409ed09e4624cfed6a16787d8c2102a17f4e8506/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53706f6e736f722d2545322539442541342d6561346161613f6c6f676f3d67697468756273706f6e736f7273266c6f676f436f6c6f723d7768697465" alt="Sponsor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A mock interviewer that has read your code. It runs on your laptop, on an open model, with no network.&lt;/p&gt;

&lt;p&gt;Most interview prep asks textbook questions. Real senior interviews ask "walk me through your project", then press on the one line you hoped nobody would notice. &lt;code&gt;grill&lt;/code&gt; clones your repositories, finds those lines, and asks about them.&lt;/p&gt;

&lt;p&gt;Built for &lt;a href="https://github.com/AkashJana18" rel="noopener noreferrer"&gt;Akash&lt;/a&gt;, who writes Rust and TypeScript and is interviewing for full-time roles.&lt;/p&gt;

&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/yashksaini-coder/grill/docs/desk.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fyashksaini-coder%2Fgrill%2FHEAD%2Fdocs%2Fdesk.jpg" alt="I built my friend an interviewer that read his code" width="800"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;── 1 of 5 ──
AkashJana18/miccli src/audio.rs:62  shared-state
62 │     pub fn start_capture(&amp;amp;self) -&amp;gt; Result&amp;lt;AudioStream&amp;gt; {
63 │         let (tx, rx) = mpsc::channel();
   │         ...
72 │         let sink = Arc::new(Mutex::new(PendingResampler::new(
   │         ...

Why is `sink` behind a Mutex, and what does the audio callback do if that lock is contended?
  &amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The code and location above are real scanner output. The question is an example of the kind a model asks; yours will differ.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Demo&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/yashksaini-coder/grill/docs/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fyashksaini-coder%2Fgrill%2FHEAD%2Fdocs%2Fdemo.gif" alt="grill interviewing a real repository: ingest, spots, a two-round interview, the weak-spot map"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Recorded…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/yashksaini-coder/grill" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;About 1,300 lines of TypeScript including tests, three runtime dependencies, MIT licensed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:4b

git clone https://github.com/yashksaini-coder/grill &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;grill
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;link

&lt;/span&gt;grill ingest AkashJana18/miccli AkashJana18/solana-consensus-lab
grill start                    &lt;span class="c"&gt;# 5 questions&lt;/span&gt;
grill start &lt;span class="nt"&gt;-n&lt;/span&gt; 3 &lt;span class="nt"&gt;--lang&lt;/span&gt; rust   &lt;span class="c"&gt;# Rust only&lt;/span&gt;
grill report                   &lt;span class="c"&gt;# the weak-spot map&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo7mmx0ocyxryp72z98la.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo7mmx0ocyxryp72z98la.png" alt="Architecture of grill: ingest, interview and remember stages, all running locally on Ollama, Gemma and Mastra" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The open pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma&lt;/a&gt;&lt;/strong&gt;, Google's open-weight model, served by &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; on &lt;code&gt;localhost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://mastra.ai/docs" rel="noopener noreferrer"&gt;Mastra&lt;/a&gt;&lt;/strong&gt;, the open-source TypeScript agent framework, for the two agents and the tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Design rule: the model gets one small job at a time
&lt;/h3&gt;

&lt;p&gt;A 4B model running on a laptop is not going to plan an interview, hold a rubric in its head, and remember what it asked five turns ago. So I did not ask it to. The order of steps lives in ordinary TypeScript. The model is only ever asked to do one of three things: write a question, write a follow-up, or grade an exchange.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fobzwfj1p08ywsxmkl296.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fobzwfj1p08ywsxmkl296.png" alt="One interview round: scanner shows a snippet, interviewer asks, candidate answers, interviewer follows up, grader scores, store saves" width="799" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every one of those calls is handed the real snippet again. The model never has to recall code, so it has less room to invent it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: finding the lines worth asking about
&lt;/h3&gt;

&lt;p&gt;The scanner is a table of 38 patterns, 17 for Rust and 21 for JavaScript and TypeScript. Each one carries a topic, a weight, and the angle a good interviewer would take:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;rust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;concurrency&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;lock|read|write&lt;/span&gt;&lt;span class="se"&gt;)\(\)\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(\.&lt;/span&gt;&lt;span class="sr"&gt;await|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;unwrap&lt;/span&gt;&lt;span class="se"&gt;\(\)&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\?)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;how long the guard lives, whether it is held across an await, and what a poisoned or contended lock does&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="nf"&gt;js&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;async-in-loop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;async&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;forEach|map|filter|reduce&lt;/span&gt;&lt;span class="se"&gt;)\(\s&lt;/span&gt;&lt;span class="sr"&gt;*async&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;what actually awaits here, the order of completion, and how errors surface&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A regex hit on one line is not a question. So for every hit, the scanner walks up to the enclosing function, matches braces to find its end, and frames the whole thing. Weights are summed per function, overlapping frames are dropped in favour of the strongest, and each repository contributes its best 40 spots, round-robin across topics so a codebase full of &lt;code&gt;.unwrap()&lt;/code&gt; does not turn the whole interview into error handling.&lt;/p&gt;

&lt;p&gt;This is regex and brace counting, not a parser. It finds interesting code. It does not understand it. Understanding is the model's job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: two Mastra agents on Gemma
&lt;/h3&gt;

&lt;p&gt;Mastra talks to Ollama through its OpenAI-compatible endpoint, so there is no provider SDK and no API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;providerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interviewer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interviewer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interviewer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;INTERVIEWER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GRADER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;interviewer&lt;/strong&gt; is told to ask exactly one question, to name an identifier from the snippet so the candidate knows it read the code, and never to answer or hint.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;grader&lt;/strong&gt; is a separate agent with a separate prompt. It did not write the question, so it has no stake in the answer being good. It returns JSON:&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="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"one sentence"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"missed"&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="s2"&gt;"specific point"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"better"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"the answer you wanted"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small models wrap JSON in prose and code fences. Instead of trusting the reply, &lt;code&gt;grill&lt;/code&gt; pulls out the first balanced &lt;code&gt;{...}&lt;/code&gt;, validates it with zod, and if that fails, shows the model its own bad reply and asks once more.&lt;/p&gt;

&lt;p&gt;There is also an opt-in &lt;code&gt;read_source&lt;/code&gt; tool (&lt;code&gt;grill start --tools&lt;/code&gt;). It lets the interviewer read beyond the snippet, for example to see a caller or a type definition. It is confined to the repository: a path that resolves outside the checkout is refused. It needs a model with tool calling, so it is off by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: the weak-spot map
&lt;/h3&gt;

&lt;p&gt;Every round is saved as JSON under &lt;code&gt;~/.grill/sessions&lt;/code&gt;. The next session picks spots with a weighted draw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Hotspot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TopicStat&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;spot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weakness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stat&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repeat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;spot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.15&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="c1"&gt;// Squared, so the first session opens on the richest code, not on filler.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;spot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;weakness&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Score a 1 on lifetimes and lifetimes come back. Score a 4 on &lt;code&gt;unsafe&lt;/code&gt; and it fades. Spots already asked are rare but not banned, because being asked the same thing twice is how you find out whether you learned it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing an LLM app without an LLM
&lt;/h3&gt;

&lt;p&gt;I did not want the test suite to need a GPU. The tests start a fake OpenAI-compatible server on a random local port and point the real Mastra agents at it. That covers a full round, the JSON retry, &lt;code&gt;:skip&lt;/code&gt; and &lt;code&gt;:quit&lt;/code&gt;, and the tool call including the path-escape refusal. 18 tests, a few seconds, no model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For this project it is not a preference. A closed API would have made it a worse tool for Akash in four concrete ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;His fumbled answers stay his.&lt;/strong&gt; Interview practice only works if you are willing to be wrong. &lt;code&gt;grill&lt;/code&gt; stores every weak answer he gives, by design. That file sits in &lt;code&gt;~/.grill&lt;/code&gt; on his disk. With a hosted API, the same record of everything he does not know would be on someone else's server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private code works.&lt;/strong&gt; &lt;code&gt;grill ingest&lt;/code&gt; takes a local path. Work code under NDA, the repository he cannot push anywhere, the side project he is not ready to show: all of it can be interviewed on without leaving the machine. Once the model is pulled, a local-path session needs no network at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practice is free, so he can do a lot of it.&lt;/strong&gt; Three model calls per question, five questions per session, as many sessions as it takes. On a metered API that is a bill that grows with exactly the behaviour I want to encourage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;He can change the interviewer.&lt;/strong&gt; The rubric is a string in &lt;code&gt;src/interview/mastra-brain.ts&lt;/code&gt;. If he is preparing for a company that cares about system design more than language trivia, he edits the prompt. If he gets a better laptop, &lt;code&gt;GRILL_MODEL&lt;/code&gt; swaps in a bigger model in one environment variable. If Ollama is not his thing, &lt;code&gt;GRILL_URL&lt;/code&gt; points at llama.cpp or LM Studio. Nothing in the tool is tied to one vendor.&lt;/p&gt;

&lt;p&gt;Open weights made it private and free. An open framework made it changeable. He needs all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The scanner cannot tell a deliberate &lt;code&gt;unwrap()&lt;/code&gt; from a careless one. It surfaces candidates; some are dull.&lt;/li&gt;
&lt;li&gt;Question quality is the model's. A larger local model asks sharper questions.&lt;/li&gt;
&lt;li&gt;The grade is a study aid, not a verdict. Read the "missed" list, not the number.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma runs locally through Ollama and does all three jobs: asking, pressing, and grading. The whole design, with fixed orchestration and one small task per call, exists to make a small open-weight model enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Mastra.&lt;/strong&gt; Two Mastra agents over an open model, plus a sandboxed &lt;code&gt;read_source&lt;/code&gt; tool, wired to a local OpenAI-compatible endpoint with no provider SDK.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Dev log #24 Hardening the p2p stack: From flaky tests to WebSocket fixes</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Mon, 05 Oct 2026 09:12:47 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-24-hardening-the-p2p-stack-from-flaky-tests-to-websocket-fixes-55f</link>
      <guid>https://dev.to/yashksaini/dev-log-24-hardening-the-p2p-stack-from-flaky-tests-to-websocket-fixes-55f</guid>
      <description>&lt;p&gt;Hi, glad you found your way here. I'm Yash, a developer who contributes to open source, mostly p2p networking and AI tooling. Each week I write up what I shipped, what broke, and what fixing it taught me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you've ever had a CI run fail because a test expected a packet to arrive in exactly 200ms, you know my pain this week. I spent most of my time moving away from clock-based testing and toward progress-driven logic, alongside some critical bug fixes in the Python libp2p stack. Across 22 commits and 15 code reviews, it was a heavy week for p2p infrastructure and making sure our networking primitives actually behave when the network gets noisy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Most of my energy went into &lt;strong&gt;minip2p&lt;/strong&gt;, where I’m deep in the weeds of making the Rust implementation more resilient. I pushed 14 commits there, but the real story is in the refactoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  minip2p: Killing the Flaky Test
&lt;/h3&gt;

&lt;p&gt;There’s nothing that kills developer velocity quite like a flaky test suite. I opened and closed Issue #261 this week because our timing-budgeted tests were falling over whenever the CI runner got too busy. We’ve all been there: you put a &lt;code&gt;sleep&lt;/code&gt; in a test, it works on your machine, and then it fails in GitHub Actions because the runner is under-provisioned.&lt;/p&gt;

&lt;p&gt;I landed PR #284 to address this. Instead of waiting for the clock, I refactored the tests to be driven by progress. For the TCP transport tests, I stopped waiting for an arbitrary 200ms and started waiting for the socket to actually fill and drain. I also had to isolate the relay reservation and timer-bound tests in &lt;code&gt;nextest&lt;/code&gt; to ensure they aren't fighting for resources. It’s the kind of unglamorous work that makes the whole project feel more professional.&lt;/p&gt;

&lt;p&gt;Beyond the tests, I landed a pretty significant refactor in PR #260. I wanted to consolidate how we handle framed exchanges. Now, AutoNAT and Identify (two core p2p protocols) share a framed exchange and are properly bounded by their declared lengths. It’s a bit of "defensive networking"—ensuring a peer can't just stream infinite data at us during a handshake.&lt;/p&gt;

&lt;h3&gt;
  
  
  py-libp2p: Fixing the Leaks
&lt;/h3&gt;

&lt;p&gt;On the Python side of things, I spent some time in &lt;strong&gt;py-libp2p&lt;/strong&gt;. I noticed a gnarly issue where the WebSocket transport was being a bit too generic with its error reporting. Basically, every failure after the initial connection was being swallowed and reported as a "handshake timeout," which is incredibly frustrating when you're trying to debug a real network issue. Even worse, it was leaking the TCP socket in the process.&lt;/p&gt;

&lt;p&gt;I closed Issue #1554 by landing PR #1556, which ensures we report the &lt;em&gt;real&lt;/em&gt; dial failure and, crucially, close the socket so we don't bleed file descriptors. I also followed up with PR #1555 to make sure we’re actually verifying server certificates on &lt;code&gt;wss://&lt;/code&gt; (secure WebSocket) dials. Security isn't optional, even in p2p.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trak: Cleaning up the Indexer
&lt;/h3&gt;

&lt;p&gt;I also made a quick pit stop at &lt;strong&gt;Trak&lt;/strong&gt;, a Rust project I've been helping out with. I noticed the indexer was getting distracted by test fixtures. If you have a directory full of mock code for tests, you don't necessarily want your primary code indexer treating those as real declarations. I landed PR #1 by updating the logic to read declarations from source code only and explicitly exclude test fixture trees. It’s a small change (+532/-12), but it makes the tool much more accurate for its users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests
&lt;/h2&gt;

&lt;p&gt;It was a "clean sweep" week for PRs—5 opened, 5 merged.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;[minip2p #284]&lt;/strong&gt;: The big test overhaul. Moving from &lt;code&gt;sleep()&lt;/code&gt; to actual state-checking.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;[minip2p #260]&lt;/strong&gt;: The core refactor. Sharing framed exchanges between protocols.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;[py-libp2p #1556]&lt;/strong&gt;: Fixing the WebSocket socket leak and improving error transparency.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;[py-libp2p #1555]&lt;/strong&gt;: Enforcing WSS certificate verification.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;[Trak #1]&lt;/strong&gt;: Narrowing the indexer's scope to exclude noise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code Reviews
&lt;/h2&gt;

&lt;p&gt;While I only had 22 commits of my own, I spent a massive amount of time in the &lt;strong&gt;dotnet-libp2p&lt;/strong&gt; repository. I provided 15 reviews this week, and honestly, this is where a lot of the "architectural" thinking happens. Helping the C# implementation stay in sync with the wider libp2p spec is a challenge, but the team there is shipping some great stuff.&lt;/p&gt;

&lt;p&gt;Some of the highlights from the review pile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;QUIC on Windows&lt;/strong&gt;: Reviewed a PR to enable the QUIC transport for Windows users.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Gossipsub Hardening&lt;/strong&gt;: Looked at changes for handling partial messages and using &lt;code&gt;IDONTWANT&lt;/code&gt; for large gossip messages to save bandwidth.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Security&lt;/strong&gt;: Approved a move to P-256 ephemeral keys for TLS and better validation of gossip signatures before they hit application hooks.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Reliability&lt;/strong&gt;: Gave the green light on PRs for preserving Yamux replies and handling segmented writes more gracefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reviewing 15 PRs while only opening 5 of my own definitely made this a "mentorship and alignment" week. It's important to keep the ecosystem moving together, especially when you're working across three different languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;This week was a polyglot special:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Rust&lt;/strong&gt;: My main focus for the heavy lifting in &lt;code&gt;minip2p&lt;/code&gt; and &lt;code&gt;Trak&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Python&lt;/strong&gt;: Used for the &lt;code&gt;py-libp2p&lt;/code&gt; maintenance and bug fixing.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;C#&lt;/strong&gt;: Purely on the review side, ensuring the .NET implementation is solid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The numbers tell a story of growth: &lt;strong&gt;+2,503 / -357&lt;/strong&gt;. That’s a lot of new code, but a good chunk of that was the test infrastructure and the refactored protocol handlers. I'm okay with a net positive line count when it means the tests are finally stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Next week, I want to double down on the &lt;code&gt;minip2p&lt;/code&gt; transport layer. Now that the tests are stable, I can actually start benchmarking performance without worrying about the CI runner's mood. I'm also keeping an eye on the open PRs in the Python and .NET stacks—there's a lot of momentum right now in the p2p space, and I want to make sure we keep that energy going.&lt;/p&gt;

&lt;p&gt;If you're building p2p tools, stop using &lt;code&gt;sleep()&lt;/code&gt; in your tests. Your future self (and your CI) will thank you. See you next week!&lt;/p&gt;




&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>development</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Dev log #23 Polishing the stack: Diagrams, async shutdowns, and a whole lot of code reviews</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Sun, 27 Sep 2026 14:02:10 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-polishing-the-stack-diagrams-async-shutdowns-and-a-whole-lot-of-code-reviews-162b</link>
      <guid>https://dev.to/yashksaini/dev-log-polishing-the-stack-diagrams-async-shutdowns-and-a-whole-lot-of-code-reviews-162b</guid>
      <description>&lt;p&gt;Hi, glad you found your way here. I'm Yash, a developer who contributes to open source, mostly p2p networking and AI tooling. Each week I write up what I shipped, what broke, and what fixing it taught me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This week was all about clarity—both in the code and how we explain it. I spent a significant chunk of time in the docs and refactoring event flows, while also diving deep into code reviews for the C# libp2p ecosystem. 15 commits, 5 PRs, and 12 code reviews later, the p2p stack feels a lot more solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Most of my energy this week went into &lt;strong&gt;minip2p&lt;/strong&gt;. It’s one thing to build a p2p library in Rust; it’s another thing entirely to make sure people actually understand how to use it. I spent a lot of time in &lt;code&gt;docs/pages&lt;/code&gt; and &lt;code&gt;docs/components&lt;/code&gt;, trying to visualize the invisible.&lt;/p&gt;

&lt;p&gt;I tackled a major refactor in the core crate, specifically routing all capabilities through Endpoint events. This was a hefty PR—1,365 additions and 939 deletions—aimed at making the internal event system more predictable. When you're dealing with p2p networking, state management is everything. Moving toward a more event-driven architecture for capabilities simplifies the mental model significantly.&lt;/p&gt;

&lt;p&gt;On the documentation front, I realized the landing page needed more "meat." I built out diagrams to explain the connection attempt flow and the event loop. I used Tailwind to style these diagrams directly, ensuring they look great and stay responsive. I also spent time correcting some claims about the event loop and &lt;code&gt;unsafe&lt;/code&gt; usage—it's vital to be precise when you're talking about Rust's safety guarantees. I even made sure to neutralize animation delays for users with reduced motion settings because accessibility shouldn't be an afterthought in dev tools.&lt;/p&gt;

&lt;p&gt;I also jumped over to &lt;strong&gt;dotnet-libp2p&lt;/strong&gt; to handle some protocol-level stability. I implemented async shutdown for the pubsub router and discovery protocols. In a distributed system, how you leave the network is just as important as how you join it. Without proper async teardown, you end up with hanging resources or weird state issues on the peer side. While I was there, I bumped &lt;code&gt;Microsoft.SourceLink.GitHub&lt;/code&gt; to clear up a pesky NU1902 restore failure that was gunking up the CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests
&lt;/h2&gt;

&lt;p&gt;I had five PRs on the move this week, and I'm happy to say four of them are already merged.&lt;/p&gt;

&lt;p&gt;The big one was &lt;a href="https://github.com/deepso7/minip2p/pull/203" rel="noopener noreferrer"&gt;Route all capabilities through Endpoint events (#177)&lt;/a&gt;. It was a deep-tissue refactor of the &lt;code&gt;minip2p&lt;/code&gt; core. It’s always a bit nerve-wracking to move over 2,000 lines of code in a networking stack, but the result is a much cleaner interface for how we handle peer capabilities.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy2twpbgpztq8oe8s2hew.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy2twpbgpztq8oe8s2hew.png" alt="Minip2p Landing page" width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the Python world, I opened an enhancement for &lt;strong&gt;py-libp2p&lt;/strong&gt; to &lt;a href="https://github.com/libp2p/py-libp2p/pull/1544" rel="noopener noreferrer"&gt;randomise per-round lookup candidate selection&lt;/a&gt; in the Kademlia DHT. Randomization is a small change—only about 150 lines—but it’s a huge win for network resilience. It prevents the lookup process from getting stuck in a loop or being biased toward the same set of peers every time. That one is still open and undergoing review.&lt;/p&gt;

&lt;p&gt;The rest were mostly focused on the C# side, getting those async shutdowns merged and fixing the build pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issues &amp;amp; Discussions
&lt;/h2&gt;

&lt;p&gt;I spent some time clearing the deck on the issue tracker for &lt;code&gt;minip2p&lt;/code&gt;. I opened and subsequently closed &lt;a href="https://github.com/deepso7/minip2p/issues/209" rel="noopener noreferrer"&gt;Add UI sections to the landing page&lt;/a&gt;. This was the driver for all that Tailwind and diagram work I mentioned earlier. It’s satisfying to see a "docs" issue result in tangible, visual improvements to a project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Reviews
&lt;/h2&gt;

&lt;p&gt;If I wasn't writing code this week, I was reading it. I gave 12 code reviews, almost exclusively in the &lt;strong&gt;dotnet-libp2p&lt;/strong&gt; repo. &lt;/p&gt;

&lt;p&gt;The Nethermind team has been crushing it, and I spent a lot of time reviewing their Gossipsub implementation. We looked at everything from &lt;a href="https://github.com/NethermindEth/dotnet-libp2p/pull/213" rel="noopener noreferrer"&gt;bounding Gossipsub control-plane work&lt;/a&gt; to &lt;a href="https://github.com/NethermindEth/dotnet-libp2p/pull/210" rel="noopener noreferrer"&gt;supporting partial messages&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Reviewing 12 PRs while only opening 5 of my own definitely made this a "mentorship and maintenance" week. I approved work on TLS remote public key exposure, pubsub TTL cache eviction fixes, and hardening strict pubsub no-sign validation. It’s a lot of specialized p2p logic, but keeping the C# implementation at parity with the Go and Rust versions is a massive goal for the ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;It was a polyglot week, leaning heavily into the typed world.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Rust&lt;/strong&gt;: My primary focus for &lt;code&gt;minip2p&lt;/code&gt;. I spent a lot of time balancing core logic with documentation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;C#&lt;/strong&gt;: Used for the protocol-level work in &lt;code&gt;dotnet-libp2p&lt;/code&gt;. The async/await pattern in C# is a different beast compared to Rust's futures, but it's equally powerful for networking.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Python&lt;/strong&gt;: Just a touch of it for the Kademlia DHT work in &lt;code&gt;py-libp2p&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With 2,661 additions and 1,439 deletions, it was a net-positive week in terms of LOC, but a lot of that "weight" is in the new documentation and diagrams. The refactors actually helped trim some of the fat in the core logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Next week, I want to see that Kademlia randomization PR in &lt;code&gt;py-libp2p&lt;/code&gt; get across the finish line. I’m also planning to dive back into the &lt;code&gt;minip2p&lt;/code&gt; Rust crates to see if there are more areas where I can simplify the event logic now that the capability routing is merged. &lt;/p&gt;

&lt;p&gt;There's always more to document, but for now, I'm just happy the landing page finally explains the event loop without me having to jump into a Discord call to draw it on a virtual whiteboard!&lt;/p&gt;




</description>
      <category>networking</category>
      <category>opensource</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Dev log #22 Tearing out the old: Deleting 3,800 lines of legacy p2p code</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Mon, 21 Sep 2026 04:00:59 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-22-tearing-out-the-old-deleting-3800-lines-of-legacy-p2p-code-3n0i</link>
      <guid>https://dev.to/yashksaini/dev-log-22-tearing-out-the-old-deleting-3800-lines-of-legacy-p2p-code-3n0i</guid>
      <description>&lt;p&gt;Hi, glad you found your way here. I'm Yash, a developer who contributes to open source, mostly p2p networking and AI tooling. Each week I write up what I shipped, what broke, and what fixing it taught me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;There is no feeling quite like watching a PR diff show thousands of red lines and only a handful of green ones. This week was all about that catharsis. I spent my time simplifying the p2p stack, specifically by ripping out legacy protocols in Rust and hardening storage layers in Python. Across 5 commits and 3 PRs, I managed to delete nearly 4,000 lines of code while only adding about 160. It was a week for codebase health and architectural clarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Most of my energy this week went into &lt;strong&gt;minip2p&lt;/strong&gt;, a Rust-based peer-to-peer networking stack I've been contributing to. If you’ve spent any time in the libp2p ecosystem, you know that Floodsub is the "old way" of doing pubsub—it’s simple, but it’s incredibly noisy and inefficient because it just broadcasts everything to everyone. Gossipsub has been the standard for a long time now, and maintaining both in a "mini" implementation started to feel like unnecessary baggage.&lt;/p&gt;

&lt;p&gt;I pushed 3 commits to &lt;code&gt;minip2p&lt;/code&gt; that effectively scrubbed Floodsub from the entire project. This wasn't just a "delete the file" kind of task; it was a breaking change that rippled through the public API and the documentation. I had to touch 53 files to make it happen. The commit messages tell the story: I started by dropping it from the README and the docs site, then moved into the core pubsub logic to make Gossipsub the only available router, and finally cleaned up the bindings so users don't even have to (or get to) select a router anymore. It’s a breaking change—marked with the &lt;code&gt;!&lt;/code&gt; in my commit messages—but the codebase is so much leaner for it. We're talking 141 additions against 3,876 deletions. That is a lot of technical debt gone in one go.&lt;/p&gt;

&lt;p&gt;On the Python side, I spent some time with &lt;strong&gt;py-libp2p&lt;/strong&gt;. This was less about architectural shifts and more about the "un-glamorous" work that keeps a project viable: fixing flaky tests. I’ve been annoyed by &lt;code&gt;test_echo_thin_waist&lt;/code&gt; for a bit because of how it handled subprocesses. If a test doesn't reap its children properly, you end up with zombie processes and weird CI failures that have nothing to do with your actual code. I pushed 2 commits to ensure we're reaping those echo subprocesses cleanly. It’s a small change—18 lines added—but it makes the local dev experience and the CI pipeline significantly more stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests
&lt;/h2&gt;

&lt;p&gt;I had three major PRs in flight this week, two of which are already merged.&lt;/p&gt;

&lt;p&gt;The big one was &lt;a href="https://github.com/deepso7/minip2p/pull/199" rel="noopener noreferrer"&gt;Remove Floodsub from every public layer (#199)&lt;/a&gt; in &lt;code&gt;minip2p&lt;/code&gt;. This was the culmination of the work I mentioned above. Landing a PR that deletes 4,258 lines (including some generated code and docs) feels like a massive win. It simplifies the mental model for anyone new coming into the repo. You don't have to ask "which pubsub should I use?" anymore. There's only one, and it's the one that works at scale.&lt;/p&gt;

&lt;p&gt;I also landed a fix in &lt;code&gt;py-libp2p&lt;/code&gt;: &lt;a href="https://github.com/libp2p/py-libp2p/pull/1542" rel="noopener noreferrer"&gt;test(examples): reap echo subprocess cleanly in test_echo_thin_waist (fixes #1541)&lt;/a&gt;. This addressed a specific issue where the echo example tests were leaving processes hanging. It’s a small fix but a necessary one for keeping the "thin waist" of the protocol stack actually thin and manageable.&lt;/p&gt;

&lt;p&gt;Finally, I have an open PR over at &lt;strong&gt;sixb&lt;/strong&gt; titled &lt;a href="https://github.com/sixb-ai/sixb/pull/612" rel="noopener noreferrer"&gt;storage: durable, swept file upload sessions for pg and sqlite&lt;/a&gt;. This is a much heavier lift on the "new feature" side of things. I’m building out a more resilient way to handle file uploads, ensuring that sessions are durable across restarts and properly "swept" (cleaned up) when they expire. It’s currently at 1,493 additions, which is where most of my "new" code lived this week. I'm supporting both PostgreSQL and SQLite, which always adds a layer of complexity when you're trying to keep the abstraction clean. It’s still in review, but I’m looking forward to getting this landed to make the storage layer more robust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Reviews
&lt;/h2&gt;

&lt;p&gt;I managed to get into the reviewer's seat twice this week, which I always try to do to stay connected with what else is happening in the ecosystem.&lt;/p&gt;

&lt;p&gt;First, I looked at a PR in &lt;strong&gt;agent-orchestrator&lt;/strong&gt;: &lt;a href="https://github.com/Untrivial-ai/agent-orchestrator/pull/5137" rel="noopener noreferrer"&gt;feat(packaging): support Arch Linux and stop self-updates on system installs&lt;/a&gt;. Packaging is one of those things that is easy to get wrong, especially when it comes to "self-updating" logic. You really don't want a binary trying to update itself if it was installed via &lt;code&gt;pacman&lt;/code&gt;. I approved this one as it makes the tool a much better citizen on Linux systems.&lt;/p&gt;

&lt;p&gt;I also reviewed a PR in &lt;strong&gt;py-libp2p&lt;/strong&gt;: &lt;a href="https://github.com/libp2p/py-libp2p/pull/1139" rel="noopener noreferrer"&gt;eg/1130-add-gossipsub-comparison-and-standalone-examples&lt;/a&gt;. This was a nice coincidence given my work on &lt;code&gt;minip2p&lt;/code&gt;. It adds better examples and comparisons for Gossipsub, which is exactly what the project needs as we steer people away from older protocols. Good docs and examples are the best way to prevent people from reaching for the wrong tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;This was a classic polyglot week for me, split between &lt;strong&gt;Rust&lt;/strong&gt; and &lt;strong&gt;Python&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Rust work was deep and structural. When you're working with p2p protocols, Rust’s type system is a godsend for making sure you don't leave any edge cases unhandled when ripping out a whole protocol layer. The net-negative line count this week is a badge of honor. Refactoring isn't always glamorous, but it’s how you keep a project from collapsing under its own weight.&lt;/p&gt;

&lt;p&gt;The Python work was more about the glue and the storage logic. Python is great for iterating on things like storage sessions and test harnesses where you want to move fast. Even though I was adding code in my &lt;code&gt;sixb&lt;/code&gt; PR, the overall week was still dominated by the massive deletions in &lt;code&gt;minip2p&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Next week, I’m hoping to get that &lt;code&gt;sixb&lt;/code&gt; storage PR merged. It’s a big chunk of code, and I want to make sure the "sweeping" logic for old sessions is rock solid before it hits production. I'll also be keeping an eye on &lt;code&gt;minip2p&lt;/code&gt; to see if my Floodsub removal caused any unexpected friction for downstream users—though, honestly, I think everyone will be happy to see it gone.&lt;/p&gt;

&lt;p&gt;There's also some more work to do on the &lt;code&gt;py-libp2p&lt;/code&gt; test suite. Now that the echo tests are stable, I want to look at some of the other long-running examples that occasionally time out in CI. It’s a never-ending battle, but someone’s gotta do it!&lt;/p&gt;




</description>
      <category>productivity</category>
      <category>discuss</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Dev log #21 Hardening WebRTC-Direct and solving SCM leaks</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Sat, 19 Sep 2026 05:49:17 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-hardening-webrtc-direct-and-solving-scm-leaks-i5k</link>
      <guid>https://dev.to/yashksaini/dev-log-hardening-webrtc-direct-and-solving-scm-leaks-i5k</guid>
      <description>&lt;p&gt;Hi, glad you found your way here. I'm Yash, a developer who contributes to open source, mostly p2p networking and AI tooling. Each week I write up what I shipped, what broke, and what fixing it taught me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This week was all about the "glue" that holds distributed systems together. I spent my time jumping between the Python implementation of libp2p and a Go-based agent orchestrator. It was a week of deep-dives into networking internals—specifically getting WebRTC-Direct to behave across different language implementations—and fixing some frustrating SCM polling issues. Across 8 commits and 11 PRs, I focused on making sure connections are actually closed when they're supposed to be and that our agents aren't hallucinating CI failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Most of my energy went into &lt;a href="https://github.com/libp2p/py-libp2p" rel="noopener noreferrer"&gt;py-libp2p&lt;/a&gt;, where I’m currently neck-deep in WebRTC-Direct work. Networking is one of those things where it works 99% of the time, and then that last 1% takes up 90% of your brainpower.&lt;/p&gt;

&lt;h3&gt;
  
  
  Networking Internals (py-libp2p)
&lt;/h3&gt;

&lt;p&gt;I pushed 6 commits to &lt;code&gt;py-libp2p&lt;/code&gt; this week, mostly centered on stabilizing the transport layer. One of the big wins was fixing a race condition in the WebRTC dialer where ICE credentials weren't being set correctly before the first-contact replay. This was specifically blocking interoperability between Go and Python nodes—one of those gnarly "it works in isolation but fails in the wild" bugs. I also spent some time on the housekeeping side, fixing socket leaks in both &lt;code&gt;mplex&lt;/code&gt; and the &lt;code&gt;swarm&lt;/code&gt;. If you've ever wondered why your long-running nodes eventually run out of file descriptors, it’s usually because of unclosed secured connections. I made sure that when we shut down, we actually release the underlying sockets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent Orchestration (agent-orchestrator)
&lt;/h3&gt;

&lt;p&gt;Over in &lt;a href="https://github.com/Untrivial-ai/agent-orchestrator" rel="noopener noreferrer"&gt;agent-orchestrator&lt;/a&gt;, the work was more about the developer experience and SCM integration. I tackled a weird edge case where workspace files with backslashes in their names were resolving incorrectly on Linux and macOS. It’s a classic cross-platform path handling headache, but getting it right means one less "it works on my machine" conversation. I also looked into how the orchestrator handles GitHub polling—specifically an issue where it was re-listing every open PR because the &lt;code&gt;since&lt;/code&gt; parameter wasn't being respected by the endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests
&lt;/h2&gt;

&lt;p&gt;It was a heavy PR week—11 in total. I had 4 merged, 3 still open, and 4 closed as I refined my approach.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;py-libp2p&lt;/code&gt; world, &lt;a href="https://github.com/libp2p/py-libp2p/pull/1513" rel="noopener noreferrer"&gt;PR #1513&lt;/a&gt; was a big documentation push. I documented the STUN listener versus the SDP harness and provided guidance on v1 vs v2 dialing. Clear docs are just as important as clean code when you’re building p2p primitives. I also have &lt;a href="https://github.com/libp2p/py-libp2p/pull/1532" rel="noopener noreferrer"&gt;PR #1532&lt;/a&gt; open, which introduces a true ICE-Lite listener for WebRTC-Direct. This is a significant addition (+166 lines) that will make the listener much more robust.&lt;/p&gt;

&lt;p&gt;On the bug-squashing front, &lt;a href="https://github.com/libp2p/py-libp2p/pull/1497" rel="noopener noreferrer"&gt;PR #1497&lt;/a&gt; and &lt;a href="https://github.com/libp2p/py-libp2p/pull/1495" rel="noopener noreferrer"&gt;PR #1495&lt;/a&gt; landed to fix the connection shutdown and WebRTC dial issues I mentioned earlier. &lt;/p&gt;

&lt;p&gt;For &lt;code&gt;agent-orchestrator&lt;/code&gt;, I had a few PRs that I ended up closing (&lt;a href="https://github.com/Untrivial-ai/agent-orchestrator/pull/5089" rel="noopener noreferrer"&gt;#5089&lt;/a&gt;, &lt;a href="https://github.com/Untrivial-ai/agent-orchestrator/pull/5088" rel="noopener noreferrer"&gt;#5088&lt;/a&gt;, &lt;a href="https://github.com/Untrivial-ai/agent-orchestrator/pull/5057" rel="noopener noreferrer"&gt;#5057&lt;/a&gt;, and &lt;a href="https://github.com/Untrivial-ai/agent-orchestrator/pull/5056" rel="noopener noreferrer"&gt;#5056&lt;/a&gt;). Sometimes you start a fix and realize there's a better architectural way to handle it, or you're iterating fast and a better solution emerges. I’d rather close a PR and open a better one than merge something that isn't quite right. However, &lt;a href="https://github.com/Untrivial-ai/agent-orchestrator/pull/5055" rel="noopener noreferrer"&gt;PR #5055&lt;/a&gt; did land, which fixed that backslash filename bug on non-Windows systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issues &amp;amp; Discussions
&lt;/h2&gt;

&lt;p&gt;I opened and closed 6 issues this week, which really tells the story of the week's detective work. &lt;/p&gt;

&lt;p&gt;The most interesting one is &lt;a href="https://github.com/libp2p/py-libp2p/issues/1498" rel="noopener noreferrer"&gt;Issue #1498&lt;/a&gt; regarding residual unclosed-socket leaks. This is currently gating our &lt;code&gt;filterwarnings&lt;/code&gt; guard in the test suite. Until we can guarantee the tests aren't leaking resources, we can't turn on the stricter CI checks. It's a bit of a slog, but it's the kind of maintenance that prevents technical debt from spiraling.&lt;/p&gt;

&lt;p&gt;I also opened &lt;a href="https://github.com/libp2p/py-libp2p/issues/1512" rel="noopener noreferrer"&gt;Issue #1512&lt;/a&gt; for the ICE-Lite listener work. It's one of those features that started as a "nice to have" and quickly became a "need to have" for proper WebRTC-Direct support.&lt;/p&gt;

&lt;p&gt;On the &lt;code&gt;agent-orchestrator&lt;/code&gt; side, &lt;a href="https://github.com/Untrivial-ai/agent-orchestrator/issues/5046" rel="noopener noreferrer"&gt;Issue #5046&lt;/a&gt; was a fun find: the Copilot adapter was adding a permanent line to &lt;code&gt;.git/info/exclude&lt;/code&gt; for every single session. That’s a great way to end up with a massive, messy exclude file if you’re not careful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Reviews
&lt;/h2&gt;

&lt;p&gt;Reviewing is where I get to see how others are thinking about the same problems. I did 2 reviews this week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;py-libp2p&lt;/strong&gt;: I requested changes on &lt;a href="https://github.com/libp2p/py-libp2p/pull/1139" rel="noopener noreferrer"&gt;PR #1139&lt;/a&gt;, which adds Gossipsub comparison and standalone examples. Gossipsub is complex, and while examples are great, they need to be rock-solid so they don't mislead new users.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;agent-orchestrator&lt;/strong&gt;: I approved &lt;a href="https://github.com/Untrivial-ai/agent-orchestrator/pull/5137" rel="noopener noreferrer"&gt;PR #5137&lt;/a&gt;, which adds support for Arch Linux and—thankfully—stops self-updates on system installs. System-managed packages should be handled by the package manager, not the app itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;It was a Python-heavy week (6 commits/PRs) with a solid side of Go (2 commits/PRs). &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt;: My focus here was almost entirely on the networking stack—handling sockets, bytes, and protocols.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt;: Used for the orchestrator work, dealing more with SCM APIs and file system abstractions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With 508 additions and only 41 deletions, this was definitely a "building and fixing" week rather than a "cleaning and refactoring" week. I’m adding new capabilities to the WebRTC stack, and that naturally comes with a higher line count.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The goal for next week is to get the ICE-Lite listener PR (#1532) in &lt;code&gt;py-libp2p&lt;/code&gt; across the finish line. I also want to circle back to those socket leaks in the test suite. It's not the most glamorous work, but seeing a clean, green CI with no resource warnings is a special kind of satisfying. On the orchestrator side, I'll be keeping an eye on the SCM polling fixes to make sure our agent's view of the world stays in sync with GitHub.&lt;/p&gt;

&lt;p&gt;Catch you next week!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>devrel</category>
      <category>discuss</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>The receipt should come from the person who received it</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Sun, 06 Sep 2026 17:33:22 +0000</pubDate>
      <link>https://dev.to/yashksaini/the-receipt-should-come-from-the-person-who-received-it-4kog</link>
      <guid>https://dev.to/yashksaini/the-receipt-should-come-from-the-person-who-received-it-4kog</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-09-03"&gt;Weekend Challenge: Generosity Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Every donation platform proves the same single fact: money left your account. The fact you actually care about is the second one, that it reached a person, and the receipt for the first is issued by the organisation holding the money. The thing you're trusting and the thing producing the evidence are the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cairn&lt;/strong&gt; changes who issues the receipt.&lt;/p&gt;

&lt;p&gt;You lock SOL in an escrow no intermediary can open. The only thing that moves it is a signature from the recipient's own wallet, and that transaction carries the hash of a recording where they say, in their own words, what they received. The receipt comes from the person at the end of the chain, and the chain welds it to the transfer.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvypf89ux881gr37ibyc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvypf89ux881gr37ibyc.png" alt="Cairn — proof of arrival for charitable giving" width="799" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three commands, and the third one is the whole project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cairn give     &lt;span class="nt"&gt;--to&lt;/span&gt; &amp;lt;them&amp;gt; &lt;span class="nt"&gt;--amount&lt;/span&gt; 0.05 &lt;span class="nt"&gt;--for&lt;/span&gt; &lt;span class="s2"&gt;"one term of school fees"&lt;/span&gt;
cairn receive  &lt;span class="nt"&gt;--escrow&lt;/span&gt; &amp;lt;pubkey&amp;gt; &lt;span class="nt"&gt;--audio&lt;/span&gt; receipt.wav   &lt;span class="c"&gt;# they run this&lt;/span&gt;
cairn verify   &amp;lt;signature&amp;gt;                             &lt;span class="c"&gt;# anyone runs this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;cairn verify&lt;/code&gt; re-downloads the recording, re-hashes the bytes it gets back, rebuilds the receipt from scratch and compares the result against what's on chain. It doesn't ask my server to be honest. With &lt;code&gt;cairn hash&lt;/code&gt; it doesn't need my server to exist.&lt;/p&gt;

&lt;p&gt;A cairn is a stack of stones travellers raise along a route. Each person who passes adds one. It proves somebody came through, it guides whoever comes next, and it works with nobody owning it and nobody supervising it. Every clause maps onto something the software does, which is the only reason the name stuck.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/yashksaini-coder/Cairn/blob/main/docs/COMMANDS.md" rel="noopener noreferrer"&gt;docs/COMMANDS.md&lt;/a&gt;&lt;/strong&gt; walks the whole loop with real captured terminal output at every step — funding an escrow, recording a receipt, releasing, and verifying.&lt;/p&gt;

&lt;p&gt;The command that matters:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiundtjm6idmqf6pcxb4d.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiundtjm6idmqf6pcxb4d.png" alt="cairn verify, passing" width="799" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Those two hashes were produced by different code paths, minutes apart. The same command against a recording whose bytes have been altered:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz79iebooscqoaah2msdq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz79iebooscqoaah2msdq.png" alt="cairn verify, mismatch" width="799" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two visibly different hashes and a non-zero exit code, so it can gate a script.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;
        yashksaini-coder
      &lt;/a&gt; / &lt;a href="https://github.com/yashksaini-coder/Cairn" rel="noopener noreferrer"&gt;
        Cairn
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/yashksaini-coder/Cairn/assets/Banner.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fyashksaini-coder%2FCairn%2FHEAD%2Fassets%2FBanner.png" alt="Cairn — proof of arrival for charitable giving" width="100%"&gt;&lt;/a&gt;
&lt;/div&gt;



&lt;p&gt;A weekend build. Lock some SOL for someone; they only get it by signing a
transaction that carries the hash of a voice recording saying what they
received.&lt;/p&gt;
&lt;p&gt;Devnet. Not audited. About 4,500 lines of Rust, of which 400 are the test
suite, and no frontend at all.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;cairn give     --to &lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;them&lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt; --amount 0.05 --for &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;one term of school fees&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
cairn receive  --escrow &lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;pubkey&lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt; --audio receipt.wav     &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; they run this&lt;/span&gt;
cairn verify   &lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;signature&lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt;                               &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; anyone runs this&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/yashksaini-coder/Cairn/assets/demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fyashksaini-coder%2FCairn%2FHEAD%2Fassets%2Fdemo.gif" alt="A terminal session: funds are locked against a description, the recipient records a receipt and releases them, verification passes, the stored recording is altered, and verification then fails." width="100%"&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;p&gt;A real session against a local validator — the escrow is created on chain, the
receipt is signed by the recipient's key, and the failure at the end is a
genuine hash mismatch after the stored recording is altered. Also available as
&lt;a href="https://github.com/yashksaini-coder/Cairn/assets/demo.mp4" rel="noopener noreferrer"&gt;mp4&lt;/a&gt;, or replay it yourself with
&lt;code&gt;asciinema play assets/demo.cast&lt;/code&gt;.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The itch&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Donation platforms verify that money left you. They don't verify…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/yashksaini-coder/Cairn" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;A weekend build: roughly 4,500 lines of Rust, 400 of them the test suite. MIT.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cairn/
├── programs/cairn/      custody, authorization, state machine
├── crates/
│   ├── cairn-core/      canonical receipt, hashing, seeds, account layout
│   ├── cairn-api/       blob store, index, verifier. Holds no keys.
│   └── cairn-cli/       the client. All of it.
└── docs/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The axis everything turns on
&lt;/h3&gt;

&lt;p&gt;Two things here look similar and are not, and the entire design is the consequence of separating them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorization&lt;/strong&gt; is an ed25519 signature from the recipient key the donor names at creation. The program enforces it. No server key, operator key or admin override substitutes for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testimony&lt;/strong&gt; is the recording. Human evidence, hashed and committed on chain so it's tamper-evident and bound to exactly one transaction signature.&lt;/p&gt;

&lt;p&gt;The recording authorizes nothing. Audio used as an authentication factor is walked through by replay or a decent TTS model, so Cairn treats it as attached evidence with guaranteed integrity instead. That's a much smaller claim, and it's one the software can actually keep.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3g5kb4k2njlt0tssa3n6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3g5kb4k2njlt0tssa3n6.png" alt="Cairn architecture" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note the asymmetry. The recipient's way out is a signature; the donor's way back is a deadline. There's no third arrow and nobody standing in the middle.&lt;/p&gt;

&lt;h3&gt;
  
  
  One hash, and everything checks against it
&lt;/h3&gt;

&lt;p&gt;Strip Cairn down and a single sentence is left: two independently computed hashes agree. Everything below exists to make that sentence hard to fake and easy to check.&lt;/p&gt;

&lt;p&gt;It starts with the encoding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;receipt_hash = sha256(
    b"cairn:receipt:v1" ‖ escrow(32) ‖ audio_sha256(32) ‖ transcript ‖ locale(8) ‖ recorded_at(8)
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;locale&lt;/code&gt; is a fixed 8 bytes rather than a string, and that's the load-bearing choice. Every field except the transcript is fixed width, so for a preimage of length &lt;code&gt;L&lt;/code&gt; the transcript is always exactly &lt;code&gt;L - 96&lt;/code&gt; bytes. The encoding is injective with no length prefixes anywhere: no two distinct receipts concatenate to identical bytes. Add a second variable-length field and that evaporates — &lt;code&gt;("ab", "c")&lt;/code&gt; and &lt;code&gt;("a", "bc")&lt;/code&gt; land on the same preimage, and two different receipts share one hash. A test named &lt;code&gt;transcript_and_locale_do_not_smear&lt;/code&gt; exists purely to fail if someone later decides &lt;code&gt;locale&lt;/code&gt; should be a &lt;code&gt;String&lt;/code&gt;. Transcripts are NFC-normalised, trimmed and whitespace-collapsed first, so the same recording hashes the same regardless of which machine composed the receipt.&lt;/p&gt;

&lt;p&gt;Then there's the question of how many implementations of that function exist, and the answer is one. &lt;code&gt;cairn-core&lt;/code&gt; compiles to both &lt;code&gt;sbf-solana-solana&lt;/code&gt; and the host, so the on-chain program, the CLI and the API server all call the same &lt;code&gt;receipt_hash()&lt;/code&gt;. Not kept in sync, not mirrored, not generated from a shared schema — the same function, compiled twice. The account layout works the same way: &lt;code&gt;Escrow&lt;/code&gt; is defined once in &lt;code&gt;cairn-core&lt;/code&gt; and the program wraps it in a newtype that Anchor's traits hang off, so the bytes the program writes are the bytes the indexer reads, with a test asserting that Anchor's generated discriminator matches the constant the decoder uses.&lt;/p&gt;

&lt;p&gt;That count is also why the client is a CLI. A browser would need its own &lt;code&gt;receipt_hash()&lt;/code&gt;, which means hand-porting the normalisation into TypeScript, and JavaScript's &lt;code&gt;\s&lt;/code&gt; and Rust's &lt;code&gt;char::is_whitespace&lt;/code&gt; disagree about U+FEFF — a byte-order mark in a transcript gives you a browser reporting a valid receipt and a chain reporting a fake one. That character is patchable; the shape of it isn't. Any second implementation is a second answer, and an interface whose job is to display "these match" while running different code from the thing doing the matching argues against itself.&lt;/p&gt;

&lt;p&gt;With one function to check against, three commands do the checking rather than taking anything on faith. &lt;code&gt;cairn show&lt;/code&gt; re-hashes the description client-side against the on-chain &lt;code&gt;need_hash&lt;/code&gt;, so the API can serve whatever text it likes and it doesn't matter. &lt;code&gt;cairn receive&lt;/code&gt; rebuilds the receipt from the audio on the recipient's own disk and refuses to sign if the server's hash doesn't cover it. &lt;code&gt;cairn hash&lt;/code&gt; touches no network, needs no key and reads no database — compute a hash with the wifi off and compare it against a block explorer.&lt;/p&gt;

&lt;p&gt;Which leaves the server holding nothing worth stealing: no keypair, no signing path, no instruction it could call to move a lamport.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speech, and why the transcript is a convenience
&lt;/h3&gt;

&lt;p&gt;Transcription runs through &lt;a href="https://elevenlabs.io/" rel="noopener noreferrer"&gt;ElevenLabs Scribe&lt;/a&gt;. The recipient speaks whatever language they actually speak, and the transcript joins the canonical receipt alongside the locale tag.&lt;/p&gt;

&lt;p&gt;It's wired as best-effort enrichment rather than a dependency. The hash covers the audio bytes either way, so a receipt is valid with or without a transcript — the audio is the evidence, and the transcript is a convenience for whoever reads it later. Depending on a third party that lightly felt like the right call inside something whose whole argument is about not needing to trust third parties.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Cairn proves
&lt;/h3&gt;

&lt;p&gt;One sentence, precisely: that a specific recording, unaltered, was committed by the holder of a specific key in the same instruction that moved specific funds. There's no window in which the money has moved without a receipt, or a receipt exists without the money.&lt;/p&gt;

&lt;p&gt;Every design decision above exists to make that sentence checkable by a stranger who has no access to me, my server, or my database. Every state transition has a happy-path test in LiteSVM, and every guard has a negative test asserting its specific error code. The one that carries the trust model is &lt;code&gt;only_the_named_recipient_can_release&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Solana.&lt;/strong&gt; The escrow program isn't a payment rail bolted onto the side of the idea — it's the only reason the idea works. A receipt that anyone with database access can remove is a receipt back under the control of the party you were trying not to have to trust, which is the exact problem the project starts from. Putting it on Solana moves custody into a program-owned vault PDA with deterministic seeds and no operator key, makes the recipient's ed25519 signature the sole authorization primitive, and makes the receipt append-only by construction rather than by policy. The program is the trust model. Everything off-chain is a convenience wrapped around it, and it says so by holding no keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of ElevenLabs.&lt;/strong&gt; &lt;a href="https://elevenlabs.io/" rel="noopener noreferrer"&gt;Scribe&lt;/a&gt; closes the language gap that sits between a donor and a recipient by default. The audio is the evidence, but audio in a language you don't read is evidence you can't check, and a transcript makes it legible to someone on the other side of that gap. The locale tag travels inside the hash preimage alongside the transcript, so the language a receipt was given in is part of what gets committed — not metadata bolted on afterwards, but a field the signature covers.&lt;/p&gt;

&lt;p&gt;It was a good challenge that stretched me to make use of solana and AI together in a interesting manner. Interesting part was the idea of getting the scribe using eleven labs. Overall a good exercise to learn something new. &lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>ai</category>
      <category>rust</category>
    </item>
    <item>
      <title>Dev log #20 Deleting 180k lines and chasing socket leaks: A week in the OSS trenches</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Sun, 06 Sep 2026 13:46:16 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-deleting-180k-lines-and-chasing-socket-leaks-a-week-in-the-oss-trenches-4f9b</link>
      <guid>https://dev.to/yashksaini/dev-log-deleting-180k-lines-and-chasing-socket-leaks-a-week-in-the-oss-trenches-4f9b</guid>
      <description>&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A massive week of cleanup and deep technical dives. From deleting 180k lines in my vault to fixing socket leaks in libp2p and contributing to the Rust compiler, I hit 102 commits and a perfect 7-day streak.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;There is something incredibly therapeutic about a net-negative week. I pushed 102 commits across a dozen repos, but the headline stat isn't the additions—it’s the 188,000 lines of code I deleted. Between chasing down residual socket leaks in &lt;code&gt;py-libp2p&lt;/code&gt;, building out a resumable progress log for &lt;code&gt;Reachable&lt;/code&gt;, and even opening a PR on the Rust compiler, it’s been a high-velocity seven days.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Great Purge and Portfolio Polish
&lt;/h2&gt;

&lt;p&gt;I started the week with a "scorched earth" approach to my &lt;code&gt;obsidian-vault&lt;/code&gt;. I realized I was tracking way too much of Obsidian's internal UI state and plugin noise, which made the git history a mess. I ripped out about 179,000 lines by stopping the tracking of that ephemeral state. Now, my syncs are clean, and the vault actually represents my thoughts rather than my editor's window positions. It’s a good reminder that just because you &lt;em&gt;can&lt;/em&gt; version control something doesn't mean you &lt;em&gt;should&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;On the &lt;a href="https://github.com/yashksaini-coder/portfolio" rel="noopener noreferrer"&gt;portfolio&lt;/a&gt; front, I spent a good chunk of time in &lt;code&gt;app/projects&lt;/code&gt; and &lt;code&gt;components/common&lt;/code&gt;. I finally stripped out some old "catalogue-numbering" chrome that was cluttering the UI and gave each project row its own distinct card surface. I also fixed a nagging SEO issue by moving the canonical origin to &lt;code&gt;yashksaini.vercel.app&lt;/code&gt;. The big feature update here was the new "spotlight" section, where I’m now featuring &lt;a href="https://github.com/yashksaini-coder/GitBanner" rel="noopener noreferrer"&gt;GitBanner&lt;/a&gt; and &lt;a href="https://github.com/yashksaini-coder/Reachable" rel="noopener noreferrer"&gt;Reachable&lt;/a&gt; right at the top. If you’re going to build things, you might as well show them off properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep in the Networking Guts
&lt;/h2&gt;

&lt;p&gt;A lot of my mental energy this week went into &lt;a href="https://github.com/libp2p/py-libp2p" rel="noopener noreferrer"&gt;py-libp2p&lt;/a&gt;. Networking bugs are the ultimate test of patience. I opened two pretty critical issues regarding socket leaks. In &lt;a href="https://github.com/libp2p/py-libp2p/issues/1485" rel="noopener noreferrer"&gt;Issue #1485&lt;/a&gt;, I found that the Swarm was leaking dialed connection sockets on shutdown because the service-manager stop path wasn't cleaning up properly. I followed that up with &lt;a href="https://github.com/libp2p/py-libp2p/issues/1487" rel="noopener noreferrer"&gt;Issue #1487&lt;/a&gt;, which identified residual leaks in the muxer connection test fixtures.&lt;/p&gt;

&lt;p&gt;I didn't just report them, though. I’ve been working on &lt;a href="https://github.com/libp2p/py-libp2p/pull/1486" rel="noopener noreferrer"&gt;PR #1486&lt;/a&gt; to ensure connections are explicitly closed on shutdown so their sockets are released back to the OS. I also pushed forward on &lt;a href="https://github.com/libp2p/py-libp2p/pull/1471" rel="noopener noreferrer"&gt;WebRTC-Direct interop tests&lt;/a&gt;. Getting Python and Go to agree on WebRTC-Direct is... a journey. We had a case where the ICE handshake succeeded, but then the connection was immediately torn down. It’s these kinds of edge cases that make p2p work both frustrating and deeply rewarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building and Fixing at Scale
&lt;/h2&gt;

&lt;p&gt;I also spent some time on &lt;a href="https://github.com/xevrion/breakscale" rel="noopener noreferrer"&gt;breakscale&lt;/a&gt;, which is a project that really challenges how I think about failure states. I found a bug (Issue #51) where a node losing 25% of its requests to timeouts was still reporting a 0% failure rate. That’s a dangerous blind spot. I merged &lt;a href="https://github.com/xevrion/breakscale/pull/57" rel="noopener noreferrer"&gt;PR #57&lt;/a&gt; to ensure timeouts are counted correctly so the failure rate actually reflects reality. I also fixed a nasty data loss bug in &lt;a href="https://github.com/xevrion/breakscale/pull/54" rel="noopener noreferrer"&gt;PR #54&lt;/a&gt;—previously, if the validator rejected a saved design, the next save or delete would just erase it from storage. Now, we keep the design even if the current build can't "open" it.&lt;/p&gt;

&lt;p&gt;Over at &lt;a href="https://github.com/yashksaini-coder/Reachable" rel="noopener noreferrer"&gt;Reachable&lt;/a&gt;, the focus was on the demo experience. I built out a resumable progress log, a sound score mixer, and a frame renderer. This project is becoming a really cool playground for TypeScript-based media tools. I’m particularly happy with the "paste-ready" voiceover script block—it’s one of those small DX (developer experience) features that makes the tool feel much more polished.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tooling Life: Neovim and DevNotion
&lt;/h2&gt;

&lt;p&gt;You can’t be a dev without constantly tweaking your tools. I did some work on my &lt;a href="https://github.com/yashksaini-coder/nvim" rel="noopener noreferrer"&gt;nvim&lt;/a&gt; config, specifically rebuilding the keymap reference around a prefix tree. I also fixed a frustrating bug where &lt;code&gt;&amp;lt;leader&amp;gt;bd&lt;/code&gt; would leave a blank window open. Now it closes the window properly, and I can open a terminal from any window type, not just "normal" ones. It’s the little things that keep the flow state alive.&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;, I focused on the documentation and the "blog log." I reworked the masthead and made the ledger header sticky while scrolling. I also made a conscious decision to drop the sequential numbering from posts. Now they’re just titled "Dev log: ." It feels less like a chore and more like a journal. I also had to escape some pipes in the headlines to prevent the markdown tables in the logs from breaking—classic string manipulation fun.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests of Note
&lt;/h2&gt;

&lt;p&gt;Beyond the ones I've mentioned, a few others stood out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Rust Compiler&lt;/strong&gt;: I opened &lt;a href="https://github.com/rust-lang/rust/pull/162123" rel="noopener noreferrer"&gt;PR #162123&lt;/a&gt; to report unsatisfied opaque item bounds instead of a generic type mismatch under the &lt;code&gt;-Znext-solver&lt;/code&gt;. Contributing to the Rust compiler is always a "hold your breath" moment, but it's the best way to learn how the magic happens.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Agent Orchestrator&lt;/strong&gt;: Merged a fix in &lt;a href="https://github.com/Untrivial-ai/agent-orchestrator/pull/4717" rel="noopener noreferrer"&gt;PR #4717&lt;/a&gt; to carry the resolved model through metadata merging. This fixed a bug where &lt;code&gt;ao spawn&lt;/code&gt; would record an empty model.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Sixb&lt;/strong&gt;: Merged &lt;a href="https://github.com/sixb-ai/sixb/pull/478" rel="noopener noreferrer"&gt;PR #478&lt;/a&gt; which adds the project model catalog to both the core and server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack &amp;amp; Vibe
&lt;/h2&gt;

&lt;p&gt;This was a true polyglot week. TypeScript dominated the frontend and CLI work, Python was the heavy hitter for the libp2p networking logic, and I dipped into Rust for the compiler work. I even wrote a bit of C++ for some basic alphabet-printing programs in my &lt;a href="https://github.com/yashksaini-coder/Learning-2026" rel="noopener noreferrer"&gt;Learning-2026&lt;/a&gt; repo—sometimes you just need to get back to the basics to clear your head.&lt;/p&gt;

&lt;p&gt;The 7-day streak feels good. It wasn't a forced grind; it was just one of those weeks where every time I sat down, there was a clear problem to solve. Whether it was a socket leak in Python or a CSS alignment issue in my portfolio, the momentum stayed high.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Next week is going to be about closing the loop on those &lt;code&gt;py-libp2p&lt;/code&gt; leaks. I want to get the connection-closing logic merged and see if I can figure out why the Go and Python WebRTC-Direct implementations are talking past each other. I’ll also be spending more time on the &lt;code&gt;Reachable&lt;/code&gt; frame renderer—there’s some performance optimization I want to try out.&lt;/p&gt;

&lt;p&gt;Catch you in the next push.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>rust</category>
      <category>networking</category>
    </item>
    <item>
      <title>Dev log #19 WebRTC v2 flows, agentic orchestration, and a perfectly synced vault</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Mon, 31 Aug 2026 06:52:21 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-19-webrtc-v2-flows-agentic-orchestration-and-a-perfectly-synced-vault-5egd</link>
      <guid>https://dev.to/yashksaini/dev-log-19-webrtc-v2-flows-agentic-orchestration-and-a-perfectly-synced-vault-5egd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This week was a high-output sprint across the stack—from low-level p2p networking in Python to refining agentic workflows in TypeScript. I pushed 53 commits and opened 13 PRs, maintaining a perfect 7-day streak while balancing deep protocol work with personal knowledge management.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I didn't really intend for this to be a "build everything" week, but that’s exactly where the momentum took me. Between hardening WebRTC implementations in &lt;code&gt;py-libp2p&lt;/code&gt; and chasing down edge cases in agent orchestration, I managed to ship 53 commits and keep my daily streak alive for the full seven days. The stats show a heavy tilt toward new code—over 12,000 additions—as I laid the groundwork for better telemetry and more robust p2p networking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Deep in the Networking weeds: py-libp2p
&lt;/h3&gt;

&lt;p&gt;Most of my "deep work" hours went into &lt;code&gt;py-libp2p&lt;/code&gt;. Networking code is unforgiving, but incredibly satisfying when it clicks. I spent a significant chunk of time in &lt;code&gt;libp2p/kad_dht&lt;/code&gt; implementing configurable subnet-diversity limits and table-wide IP-group caps. If you've ever dealt with Sybil attacks or just messy peer distributions, you know why this matters—it’s about making the DHT resilient, not just functional.&lt;/p&gt;

&lt;p&gt;On the transport side, I was neck-deep in &lt;code&gt;libp2p/transport&lt;/code&gt; fixing WebRTC issues. I had to guard private-slot writes and ensure we’re using &lt;code&gt;close_peer_connection&lt;/code&gt; at every production site to avoid hanging resources. I also spent time on a tricky Windows-specific bug where we needed to listen on a concrete non-loopback interface for tests to actually pass. &lt;/p&gt;

&lt;h3&gt;
  
  
  Hardening Reachable &amp;amp; Breakscale
&lt;/h3&gt;

&lt;p&gt;I’ve been refining &lt;strong&gt;Reachable&lt;/strong&gt;, specifically making the "Ask" feature behave more like a natural conversation. I had to harden the store behind it to ensure state doesn't drift when the UI gets complex. &lt;/p&gt;

&lt;p&gt;Over at &lt;strong&gt;breakscale&lt;/strong&gt;, I hit a weird one: Vitest failing on Node 26 because of &lt;code&gt;jsdom&lt;/code&gt;'s &lt;code&gt;localStorage&lt;/code&gt; implementation. I opened an issue, tracked it down, and pushed a fix to put &lt;code&gt;localStorage&lt;/code&gt; back where it belongs. I also shipped a feature to improve design descriptions—focusing on what a design is &lt;em&gt;made of&lt;/em&gt; rather than just its dimensions. It’s a small UX shift, but it makes the tool feel much more intuitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Digital Brain: obsidian-vault
&lt;/h3&gt;

&lt;p&gt;I treat my &lt;code&gt;obsidian-vault&lt;/code&gt; like a production repo, and this week it saw 35 commits. I was mostly syncing UI states and fixing templates. I noticed a bug in my "Concept" template where the parent pointer was recursively pointing at the template itself (classic meta-mess). I also logged three separate stream sessions, which has been a great way to document my thought process while I code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests &amp;amp; Orchestration
&lt;/h2&gt;

&lt;p&gt;I opened 13 PRs this week, and while some are still in flight, they represent a lot of the "glue" work I do across the ecosystem. &lt;/p&gt;

&lt;p&gt;A lot of my focus was on &lt;code&gt;agent-orchestrator&lt;/code&gt;. I’m working on carrying request IDs into daemon-emitted events to make telemetry actually useful. There’s nothing worse than seeing an error in a log and having no way to trace it back to the original trigger. I also fixed a dev-server proxy issue where it was pointing at the wrong daemon—one of those "how did this ever work?" bugs that felt great to squash.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;corsair&lt;/code&gt; repo, I’ve been on a mission to improve reliability and performance. I opened PRs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Cover &lt;code&gt;verifyHmacSignature&lt;/code&gt; with proper unit tests (security code without tests is just a disaster waiting to happen).&lt;/li&gt;
&lt;li&gt;  Add database indexes on &lt;code&gt;corsair_entities&lt;/code&gt; and &lt;code&gt;corsair_accounts&lt;/code&gt; query paths. As the data grows, those sequential scans were starting to hurt.&lt;/li&gt;
&lt;li&gt;  Clean up CLI error handling so async command failures don't just dump unhandled promise rejections into the terminal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Issues &amp;amp; Community
&lt;/h2&gt;

&lt;p&gt;I opened 7 issues this week, mostly as a way to "work in the open." When I find a bug in &lt;code&gt;agent-orchestrator&lt;/code&gt; (like the Connect Mobile tailnet proxy stranding processes on ungraceful exits) or a performance bottleneck in &lt;code&gt;corsair&lt;/code&gt;, I document it before I fix it. It helps me stay organized and gives the maintainers a heads-up on what I’m poking at.&lt;/p&gt;

&lt;p&gt;One specific bug I’m still tracking is in &lt;code&gt;omarchy&lt;/code&gt;, where selecting the default agent somehow triggers a version downgrade because of a &lt;code&gt;mise&lt;/code&gt; release cooldown. It’s a weird interaction between the version manager and the agent logic that I’m hoping to resolve next week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack &amp;amp; Vibe
&lt;/h2&gt;

&lt;p&gt;This was a polyglot week, but Python and TypeScript were the clear winners.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Python:&lt;/strong&gt; 84MB of code touched (mostly libp2p internals).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;TypeScript:&lt;/strong&gt; 58MB of code across frontend tools and CLI utilities.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The Mix:&lt;/strong&gt; A bit of Lua for my &lt;code&gt;nvim&lt;/code&gt; config updates and some Shell/Makefile glue to keep the CI pipelines happy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With 12,205 additions against 1,500 deletions, this was definitely a "growth" week. I wasn't just cleaning up; I was shipping new features and expanding test suites. The 7-day streak wasn't forced—when you're bouncing between p2p protocols and AI agents, there's always one more thing you want to tweak before calling it a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Next week, I’m planning to close out those WebRTC v2 PRs in &lt;code&gt;py-libp2p&lt;/code&gt;. The specs are solid, but the implementation details around data-channel framing as a byte stream are still a bit finicky. I also want to get those &lt;code&gt;corsair&lt;/code&gt; database indexes merged and see the real-world performance delta.&lt;/p&gt;

&lt;p&gt;And of course, more streaming. I’ve found that logging my sessions in Obsidian has made me much more intentional about the code I write. If I have to explain it to a "future me" in a log, I'm less likely to take the lazy shortcut.&lt;/p&gt;

&lt;p&gt;Catch you in the next push. Keep hacking.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>Dev log #18 From Redis Brokers to Knowledge Graphs: A 177-Commit Sprint</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Sun, 23 Aug 2026 16:26:50 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-18-from-redis-brokers-to-knowledge-graphs-a-177-commit-sprint-1mni</link>
      <guid>https://dev.to/yashksaini/dev-log-18-from-redis-brokers-to-knowledge-graphs-a-177-commit-sprint-1mni</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Hit a perfect 7-day streak this week, shipping 177 commits across systems architecture and personal tooling. Between hardening Redis brokers and fixing terminal hijacking in tmux, I managed to push nearly 7k lines of code across eight different projects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;There’s a specific kind of satisfaction that comes from a perfect 7-day streak where the flow state just doesn't quit. This week was a massive 177-commit sprint, largely split between hardening the infrastructure for Reachable and refining the data pipelines in my opportunity-radar. I spent a lot of time in the "maintenance and hardening" phase—fixing memory leaks, bounding caches, and making sure my CLI tools don't hijack my terminal. With nearly 7,000 lines added and 2,700 removed, the codebase feels significantly more robust than it did last Monday.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reachable: Security and Performance
&lt;/h3&gt;

&lt;p&gt;I spent the lion's share of my time (84 commits!) on &lt;strong&gt;Reachable&lt;/strong&gt;. Most of the work was centered around the &lt;code&gt;worker/reachable&lt;/code&gt; and &lt;code&gt;web/components&lt;/code&gt; areas. One of the more critical changes was in the ingest layer: I bounded the packument cache by size. If you've ever dealt with massive metadata ingestion, you know how quickly an unbounded cache can eat your RAM, so getting that logic in place was a huge win for stability.&lt;/p&gt;

&lt;p&gt;On the security side, I reworked how we handle keys. I updated the system to show minted keys as masked values instead of printing them whole (basic security hygiene, but easy to overlook early on) and added the ability for testers to revoke keys immediately. Waiting out a TTL just to test a revocation flow is a special kind of developer hell, so I’m glad that’s behind me. I also did some housekeeping by stripping out internal working notes from the docs and pointing everything to the proper references.&lt;/p&gt;

&lt;h3&gt;
  
  
  Opportunity-Radar: Scraping and UI
&lt;/h3&gt;

&lt;p&gt;My Python-based &lt;strong&gt;opportunity-radar&lt;/strong&gt; saw 40 commits this week. Scraping is always a game of whack-a-mole with edge cases. I spent a good chunk of Tuesday fixing a bug where neighboring listings were getting "glued" onto titles—one of those classic regex-adjacent headaches. &lt;/p&gt;

&lt;p&gt;To make the data actually usable, I implemented paging for the opportunities table and derived category filters directly from the data instead of hardcoding them. I also hooked up the dashboard to supply run tokens directly, which makes the manual trigger flow much smoother. It’s starting to feel like a real product rather than just a collection of scripts.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Personal Stack: Obsidian and Portfolio
&lt;/h3&gt;

&lt;p&gt;I’m a big believer in "sharpening the saw," so I put 27 commits into my &lt;strong&gt;obsidian-vault&lt;/strong&gt;. I’ve been working on syncing the graph zoom and UI state across devices. There’s something deeply nerdy but satisfying about getting your &lt;code&gt;#stream&lt;/code&gt; and &lt;code&gt;#reference&lt;/code&gt; tags to color-coordinate perfectly across your knowledge base.&lt;/p&gt;

&lt;p&gt;I also gave my &lt;strong&gt;portfolio&lt;/strong&gt; some love. I’m a bit of a stickler for UX, so I spent time enlarging the ⌘K hint chips in the header so they actually look like controls. I also fixed a gnarly bug in the hero section where scrub audio would stay locked after a page reload. If you’re going to have audio on a dev portfolio, it has to work perfectly, or it’s just noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests
&lt;/h2&gt;

&lt;p&gt;I opened 6 PRs this week, and the results were a mix of "shipped it" and "back to the drawing board."&lt;/p&gt;

&lt;p&gt;Over at &lt;strong&gt;sixb&lt;/strong&gt;, I had two successful merges. One was a critical fix for the broker: I bounded a Redis command so a single stuck reply couldn't hang the entire publication pipeline. The other involved releasing event subscriptions for action runs that settled late—essentially plugging a memory leak I’d been hunting for a while. I did close one massive PR (+2,030 lines) regarding connector stores and sealed credentials without merging it. It was a big architectural pivot, and after looking at the diff, I realized I wanted to approach the credential sealing differently. Sometimes the best thing you can do for a codebase is realize a 2,000-line change isn't the right one.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;agent-orchestrator&lt;/strong&gt; repo, I’ve got two PRs currently open. They both deal with tmux integration. One fixes a bug where destroying a session would accidentally hand the orchestrator's terminal over to another session, and the other handles resuming agents when using a non-default tmux base-index. If you’ve ever had your terminal focus stolen by a background process, you know why these are high priority for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issues &amp;amp; Discussions
&lt;/h2&gt;

&lt;p&gt;I was pretty active on the issue tracker this week, opening 5 issues across various projects. &lt;/p&gt;

&lt;p&gt;The most interesting one was in &lt;strong&gt;hydradb&lt;/strong&gt;, where I noticed an inconsistency in how HTTP and Bolt map &lt;code&gt;CLASS_FRESHNESS&lt;/code&gt; errors. One treats them as retryable, the other doesn't. That’s the kind of subtle bug that causes "it works on my machine" tickets, so I’m hoping to dive into the fix for that soon.&lt;/p&gt;

&lt;p&gt;I also tracked down a CPU spike in &lt;strong&gt;nezuko&lt;/strong&gt; where the runtime was spinning at 100% while idle and leaking file descriptors. There’s no faster way to lose my trust in a tool than having it cook my CPU while it’s doing literally nothing, so getting that issue documented (and subsequently closed) was a relief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;This was a true polyglot week. &lt;strong&gt;TypeScript&lt;/strong&gt; was my primary driver for the web and worker logic in Reachable and sixb, while &lt;strong&gt;Python&lt;/strong&gt; handled the heavy lifting for the data scraping in opportunity-radar. I also dipped into &lt;strong&gt;Go&lt;/strong&gt; to add a &lt;code&gt;AO_DISABLE_GPU&lt;/code&gt; escape hatch for the agent-orchestrator—sometimes you just need a way to bypass broken drivers when you're working on desktop tools.&lt;/p&gt;

&lt;p&gt;The stats tell the story: 177 commits over 7 days. My add/delete ratio was about 2.5:1. While I love a good refactoring week where I delete more than I add, this week was clearly about expansion. Building out the paging systems, the credential stores (even the ones I pivoted on), and the dashboard features meant a lot of new logic hitting the disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Next week, I’m focusing on the tmux fixes in agent-orchestrator. I want to get those PRs merged so the terminal handling is rock solid. I’m also planning to revisit the credential sealing logic I backed out of in sixb—I have a better idea now of how to handle the "sealed" part without making the storage layer too brittle.&lt;/p&gt;

&lt;p&gt;Consistency is key, and hitting a 7-day streak feels like I've got the momentum I need heading into September. See you on the next push!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>contentwriting</category>
      <category>productivity</category>
      <category>cli</category>
    </item>
    <item>
      <title>One rented /24 could eclipse a Kademlia node. Now it takes ten.</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Thu, 20 Aug 2026 18:56:59 +0000</pubDate>
      <link>https://dev.to/yashksaini/one-rented-24-could-eclipse-a-kademlia-node-now-it-takes-ten-121p</link>
      <guid>https://dev.to/yashksaini/one-rented-24-could-eclipse-a-kademlia-node-now-it-takes-ten-121p</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setting
&lt;/h2&gt;

&lt;p&gt;py-libp2p speaks Kademlia — a distributed hash table where every node keeps a routing table of &lt;em&gt;other&lt;/em&gt; nodes, bucketed by how far their IDs sit from its own. You find a peer or a record by asking the nodes closest to the target, who point you closer, and closer, until you arrive. It's elegant, and it works because of one quiet assumption: &lt;strong&gt;the peers in your routing table are a fair sample of the network.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An eclipse attack breaks exactly that assumption. If an attacker can get &lt;em&gt;their&lt;/em&gt; nodes into enough of your routing buckets — specifically the closest-K slots for a target key — they don't need to break any crypto. They just surround you. Every lookup you make gets answered by them. They can hide records, feed you stale routing, or silently partition you from the real DHT. You're still online. You're still "connected." You're just connected to a lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing authenticates the record, not the routing table
&lt;/h2&gt;

&lt;p&gt;The reason this one nags at me is that the DHT already has an integrity story: records are &lt;strong&gt;signed&lt;/strong&gt;. So my instinct — and I don't think I'm alone — was that the surface was mostly covered. If a malicious peer can't forge a record, how much damage can it do?&lt;/p&gt;

&lt;p&gt;A lot, it turns out, because signing and eclipse answer different questions.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wg3k1oowvnql6fwstpj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wg3k1oowvnql6fwstpj.png" alt="Two different questions — signing only answers one" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Record signing&lt;/strong&gt; authenticates &lt;em&gt;content&lt;/em&gt;. Hand me a DHT record and I can verify the owning key produced it. I can't be fed forged values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eclipse&lt;/strong&gt; attacks &lt;em&gt;membership&lt;/em&gt;. The attacker never forges anything. They make sure the only peers you ever &lt;em&gt;ask&lt;/em&gt; are theirs, then answer with perfectly valid, perfectly signed records — just never the &lt;em&gt;whole&lt;/em&gt; set. Withholding has no signature. Silence has no signature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the real surface isn't the record. It's &lt;strong&gt;how a peer earns a slot in a k-bucket.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the attack to measure it
&lt;/h2&gt;

&lt;p&gt;I didn't want to argue this from a whiteboard, so I built an eclipse-attack simulation inside py-libp2p — it lives at &lt;code&gt;tests/examples/attack_simulation/eclipse_attack/&lt;/code&gt;. It spins up a network of honest &lt;code&gt;KadDHT&lt;/code&gt; nodes, introduces malicious peers that &lt;strong&gt;flood the honest nodes' routing tables and poison DHT entries&lt;/strong&gt;, and then measures what actually degrades: a &lt;code&gt;RealAttackMetrics&lt;/code&gt; collector runs real lookups against the network and records the success rate as the attack takes hold.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/examples/attack_simulation/eclipse_attack/attack_scenarios.py
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_nursery&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;nursery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;malicious_peers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;honest_peers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;nursery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_soon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;poison_dht_entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;nursery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_soon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flood_peer_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                   &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;honest_peer_tables&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point of the harness is to turn "eclipse is possible" into a number: &lt;strong&gt;what fraction of lookups can an attacker capture, and how does that change once the routing table stops admitting them so cheaply?&lt;/strong&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcrbt1e8pk54a0x6fvc16.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcrbt1e8pk54a0x6fvc16.png" alt="How an eclipse fills a k-bucket — diverse subnets vs one attacker subnet" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The point of the harness is to turn "eclipse is possible" into a number. I drove the fork's real &lt;code&gt;KBucket.add_peer&lt;/code&gt; (&lt;code&gt;k = 20&lt;/code&gt;) with Sybil peers from attacker-controlled &lt;code&gt;/24&lt;/code&gt;s, running it with &lt;code&gt;MAX_PEERS_PER_SUBNET&lt;/code&gt; set to &lt;code&gt;0&lt;/code&gt; (the pre-#1399 behaviour) versus its default of &lt;code&gt;2&lt;/code&gt;. This is a component-level measurement — it isolates exactly what the admission rule changes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attacker capability&lt;/th&gt;
&lt;th&gt;Pre-#1399 (no filter)&lt;/th&gt;
&lt;th&gt;With #1399 (max 2 / subnet)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bucket slots captured — flood from a single &lt;code&gt;/24&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20 / 20 (100%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 / 20 (10%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distinct &lt;code&gt;/24&lt;/code&gt;s needed to fully own a 20-slot bucket&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share of slots taken when the bucket already holds 10 honest, subnet-diverse peers&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before the fix, one rented &lt;code&gt;/24&lt;/code&gt; owns the whole bucket. After it, an attacker needs &lt;strong&gt;ten&lt;/strong&gt; genuinely distinct networks to do the same — the cost moved from "grind cheap IDs" to "rent diverse address space," which is the entire point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause
&lt;/h2&gt;

&lt;p&gt;In vanilla Kademlia, a peer earns a bucket slot mostly by being live and having an ID that lands in range. Node IDs are cheap — you can grind as many as you want. So an attacker spins up a Sybil fleet and, crucially, parks them at &lt;strong&gt;IP addresses in a subnet they control&lt;/strong&gt;. Their cheap resource is node IDs; the resource they'd actually have to &lt;em&gt;spend&lt;/em&gt; is distinct network positions — and nothing in the base admission rule was pricing that.&lt;/p&gt;

&lt;p&gt;A rented cloud block is typically a &lt;strong&gt;/24&lt;/strong&gt;, not a scattering of unrelated addresses. That's the lever: if admission is priced in &lt;em&gt;distinct subnets&lt;/em&gt; rather than &lt;em&gt;distinct IDs&lt;/em&gt;, surrounding a node suddenly costs real, diverse address space the attacker can't cheaply conjure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix — #1399: enforce IP subnet diversity in k-buckets
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/libp2p/py-libp2p/pull/1399" rel="noopener noreferrer"&gt;libp2p/py-libp2p#1399&lt;/a&gt; (closes &lt;a href="https://github.com/libp2p/py-libp2p/issues/1383" rel="noopener noreferrer"&gt;#1383&lt;/a&gt;) makes &lt;code&gt;KBucket.add_peer&lt;/code&gt; reject a new peer when its globally-routable &lt;strong&gt;/24 (IPv4)&lt;/strong&gt; or &lt;strong&gt;/48 (IPv6)&lt;/strong&gt; subnet already holds &lt;code&gt;MAX_PEERS_PER_SUBNET&lt;/code&gt; (default &lt;strong&gt;2&lt;/strong&gt;) peers in that bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# IP/subnet diversity limits for k-buckets (issue #1383). Within one bucket,
# at most MAX_PEERS_PER_SUBNET peers may share the same globally-routable subnet.
# /24 matches realistic attacker economics — a rented cloud block is typically
# a /24, not a /16 — and avoids bundling an ASN dataset.
&lt;/span&gt;&lt;span class="n"&gt;MAX_PEERS_PER_SUBNET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_subnet_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PeerInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;peer_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p2p-circuit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;      &lt;span class="c1"&gt;# relayed addr = relay's IP, not the peer's
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix_len&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SUBNET_PREFIX_LEN_V4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SUBNET_PREFIX_LEN_V6&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="bp"&gt;...&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="c1"&gt;# loopback/private/CGNAT/link-local → exempt
&lt;/span&gt;                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix_len&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The details are where the real engineering is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only globally-routable addresses are grouped&lt;/strong&gt; (&lt;code&gt;ip.is_global&lt;/code&gt;) — loopback, RFC1918/ULA private, CGNAT (&lt;code&gt;100.64.0.0/10&lt;/code&gt;), link-local, and documentation ranges are exempt, so CI and local testnets are unaffected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS-named and relayed (&lt;code&gt;p2p-circuit&lt;/code&gt;) peers are exempt&lt;/strong&gt; — a circuit address exposes the &lt;em&gt;relay's&lt;/em&gt; IP, not the peer's, so grouping on it would wrongly bundle distinct peers behind a shared relay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-homed peers&lt;/strong&gt; are grouped by their first globally-routable address (a deliberate divergence from go-libp2p, which checks &lt;em&gt;every&lt;/em&gt; address — simpler, and it avoids false rejections of legitimately multi-homed peers).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject-only, no eviction&lt;/strong&gt; — matching go-libp2p's &lt;code&gt;TryAddPeer&lt;/code&gt;, which avoids opening a churn/DoS vector where an attacker forces evictions.&lt;/li&gt;
&lt;li&gt;Opt-out via &lt;code&gt;MAX_PEERS_PER_SUBNET &amp;lt;= 0&lt;/code&gt;, and a guard so a subnet rejection on a &lt;em&gt;non-full&lt;/em&gt; bucket can't trigger a spurious bucket split.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No protocol or wire change — just a stricter admission rule at exactly the layer the eclipse attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The trap wasn't a bug in the code — it was a bug in the &lt;em&gt;question&lt;/em&gt;. "Records are signed, so the DHT is safe" quietly swaps the question you need answered ("is this set of peers a fair sample?") for one you've already answered ("is this record authentic?"). Authentication and membership-diversity are orthogonal: signing is a real defense against forgery and &lt;strong&gt;no defense at all&lt;/strong&gt; against being surrounded by valid liars.&lt;/p&gt;

&lt;p&gt;The broader habit I'm keeping: when someone says "that's already handled," ask &lt;em&gt;which property&lt;/em&gt; is handled. The most dangerous vulnerabilities live in the gap between two defenses that each look complete on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fix: &lt;a href="https://github.com/libp2p/py-libp2p/pull/1399" rel="noopener noreferrer"&gt;libp2p/py-libp2p#1399 — feat(kad_dht): enforce IP subnet diversity in k-buckets&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issue: &lt;a href="https://github.com/libp2p/py-libp2p/issues/1383" rel="noopener noreferrer"&gt;#1383&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Eclipse-attack simulation: &lt;a href="https://github.com/libp2p/py-libp2p/tree/main/tests/examples/attack_simulation/eclipse_attack/" rel="noopener noreferrer"&gt;&lt;code&gt;tests/examples/attack_simulation/eclipse_attack/&lt;/code&gt;&lt;/a&gt; in py-libp2p&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>The /sdp endpoint that trusted its callers: fixing a memory-amplification DoS in py-libp2p</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Thu, 20 Aug 2026 18:56:05 +0000</pubDate>
      <link>https://dev.to/yashksaini/the-sdp-endpoint-that-trusted-its-callers-fixing-a-memory-amplification-dos-in-py-libp2p-3g2e</link>
      <guid>https://dev.to/yashksaini/the-sdp-endpoint-that-trusted-its-callers-fixing-a-memory-amplification-dos-in-py-libp2p-3g2e</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/libp2p/py-libp2p" rel="noopener noreferrer"&gt;py-libp2p&lt;/a&gt; is the Python implementation of libp2p — the peer-to-peer networking stack that underpins IPFS, Filecoin, and Ethereum-class nodes. I've been working on its &lt;strong&gt;WebRTC-Direct&lt;/strong&gt; transport, which lets two peers connect without a certificate authority: the peer's multiaddr carries a hash of its TLS cert, and the DTLS handshake is verified against it.&lt;/p&gt;

&lt;p&gt;Before that encrypted transport exists, the two sides have to swap SDP offer/answer blobs. Until the STUN-based listener lands (&lt;a href="https://github.com/libp2p/py-libp2p/issues/1352" rel="noopener noreferrer"&gt;#1352&lt;/a&gt;), py-libp2p ships a minimal dev harness for this: a tiny hand-rolled HTTP server (no aiohttp dependency) that accepts an SDP offer over &lt;code&gt;POST /sdp&lt;/code&gt; and hands the body to an offer handler. This post is about a memory-amplification DoS I found and fixed in that harness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;POST /sdp&lt;/code&gt; handler read the caller's &lt;code&gt;Content-Length&lt;/code&gt; and buffered exactly that many bytes — &lt;strong&gt;with no upper bound&lt;/strong&gt; — before the handshake ever happened. Here's the pre-fix read path in &lt;code&gt;_aiortc_helpers.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Read HTTP request line (consumed but not used) + headers
&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SDP_HTTP_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&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;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SDP_HTTP_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;content_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content_length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readexactly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content_length&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SDP_HTTP_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are attacker-controlled and unbounded:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The body.&lt;/strong&gt; &lt;code&gt;content_length&lt;/code&gt; comes straight from the request. &lt;code&gt;reader.readexactly(content_length)&lt;/code&gt; accumulates that many bytes into memory — and &lt;code&gt;readexactly&lt;/code&gt; bypasses the &lt;code&gt;StreamReader&lt;/code&gt;'s default 64 KiB flow-control limit, so nothing throttles it. &lt;code&gt;body.decode()&lt;/code&gt; makes a second copy, the handler string a third. Net amplification ≈ &lt;strong&gt;2× the bytes on the wire, with no ceiling.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The headers.&lt;/strong&gt; &lt;code&gt;while True&lt;/code&gt; with only a &lt;em&gt;per-line&lt;/em&gt; timeout: an attacker can keep sending header lines forever — the loop never counts them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is pre-handshake, unauthenticated input. Anyone who can reach the port can trigger it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3ktz26wodvajo5cwxov.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3ktz26wodvajo5cwxov.png" alt="Anatomy of the /sdp memory-amplification DoS — before vs after" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where I found it:&lt;/strong&gt; a reviewer flagged the &lt;em&gt;shape&lt;/em&gt; of this inside my own WebRTC PR (&lt;a href="https://github.com/libp2p/py-libp2p/pull/1309" rel="noopener noreferrer"&gt;#1309&lt;/a&gt;) — a one-line "reads the whole body into memory with no cap, availability risk" note. Easy to read as a style nit; it wasn't. Pre-handshake input is attacker-controlled by definition, and "reads the whole body with no cap" is the entire exploit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The numbers.&lt;/strong&gt; I built a reproduction harness that imports the &lt;em&gt;real&lt;/em&gt; &lt;code&gt;run_signaling_server&lt;/code&gt; at the pre-fix parent (&lt;code&gt;9506041&lt;/code&gt;) and the merged fix (&lt;code&gt;759c75b&lt;/code&gt;), fires the malicious request, and samples RSS on a 20 ms timer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (pre-fix)&lt;/th&gt;
&lt;th&gt;After (#1396)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS, 512 MiB malicious body&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1,113 MB&lt;/strong&gt; (+1,052)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;61 MB&lt;/strong&gt; (+0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS, 1 GiB malicious body&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2,137 MB&lt;/strong&gt; (+2,076)&lt;/td&gt;
&lt;td&gt;rejected, idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS, 4 concurrent × 300 MiB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1,575 MB&lt;/strong&gt; (+1,514)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;88 MB&lt;/strong&gt; (+27)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to refuse oversized request&lt;/td&gt;
&lt;td&gt;never (buffers to OOM)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.3 ms&lt;/strong&gt; (&lt;code&gt;413&lt;/code&gt; on headers alone)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body size accepted&lt;/td&gt;
&lt;td&gt;unbounded&lt;/td&gt;
&lt;td&gt;32 KiB (&lt;code&gt;413&lt;/code&gt; above)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo5jcjech9v0gnnouoqs6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo5jcjech9v0gnnouoqs6.png" alt="Peak RSS under a malicious POST, before vs after #1396" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Measured in a Python 3.11 / aiortc 1.15 sandbox on a single asyncio loop — re-run the harness on your own hardware for local figures; the ~2× ratio holds.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PR: &lt;a href="https://github.com/libp2p/py-libp2p/pull/1396" rel="noopener noreferrer"&gt;libp2p/py-libp2p#1396 — fix(webrtc): harden /sdp HTTP server against memory-amplification DoS&lt;/a&gt;&lt;/strong&gt; · merged &lt;strong&gt;Aug 14, 2026&lt;/strong&gt; · closes &lt;strong&gt;&lt;a href="https://github.com/libp2p/py-libp2p/issues/1354" rel="noopener noreferrer"&gt;#1354&lt;/a&gt;&lt;/strong&gt; · commit &lt;a href="https://github.com/libp2p/py-libp2p/commit/759c75b" rel="noopener noreferrer"&gt;&lt;code&gt;759c75b&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The fix adds three bounded constants and rejects &lt;em&gt;before&lt;/em&gt; touching any body buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Bounds for the HTTP /sdp dev harness — defend against memory-amplification
# DoS while the harness exists (until the STUN-based listener lands, #1352).
&lt;/span&gt;&lt;span class="n"&gt;_MAX_SDP_BODY_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;  &lt;span class="c1"&gt;# 32 KiB; SDP offers are typically 1–4 KiB
&lt;/span&gt;&lt;span class="n"&gt;_MAX_HEADER_LINES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;
&lt;span class="n"&gt;_MAX_HEADER_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;    &lt;span class="c1"&gt;# 8 KiB total across all header lines
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Headers are bounded by line count and cumulative bytes (the &lt;code&gt;for/else&lt;/code&gt; fires a &lt;code&gt;400&lt;/code&gt; if the terminator never arrives):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;header_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_MAX_HEADER_LINES&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SDP_HTTP_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;header_bytes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;header_bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_MAX_HEADER_BYTES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1 400 Bad Request&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 0&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1 400 Bad Request&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 0&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;Content-Length&lt;/code&gt; is validated before any body buffer exists — malformed/negative → &lt;code&gt;400&lt;/code&gt;, oversized → &lt;code&gt;413&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;raw_cl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;content_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_cl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1 400 Bad Request&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 0&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content_length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1 400 Bad Request&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 0&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content_length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_MAX_SDP_BODY_SIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1 413 Payload Too Large&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 0&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PR also ships a 228-line regression test (&lt;a href="https://github.com/libp2p/py-libp2p/pull/1396/files" rel="noopener noreferrer"&gt;&lt;code&gt;test_aiortc_helpers.py&lt;/code&gt;&lt;/a&gt;) whose key assertion checks that RSS &lt;em&gt;didn't grow&lt;/em&gt; after a &lt;code&gt;413&lt;/code&gt; — not just the status code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_too_large&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rss_before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getrusage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUSAGE_SELF&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;ru_maxrss&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST /sdp HTTP/1.1&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 999999999&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;413&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;
    &lt;span class="n"&gt;rss_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getrusage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUSAGE_SELF&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;ru_maxrss&lt;/span&gt;
    &lt;span class="nf"&gt;assert &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rss_after&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rss_before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;one_mib&lt;/span&gt;   &lt;span class="c1"&gt;# body buffer was never allocated
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;I deliberately chose a &lt;strong&gt;hard cap&lt;/strong&gt; over a streaming-with-ceiling read. SDP offers are 1–4 KiB in practice; no legitimate caller needs to POST megabytes here, so a fixed 32 KiB ceiling with early rejection is simpler &lt;em&gt;and&lt;/em&gt; safer than draining a bounded stream. Rejecting on the &lt;code&gt;Content-Length&lt;/code&gt; header alone means the oversized request never allocates a buffer at all — which is why the "after" column of the table plateaus at the idle baseline and the refusal lands in 0.3 ms.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg3fgm3ow3kt4vt2u4355.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg3fgm3ow3kt4vt2u4355.png" alt="The fix: three gates before a byte of body is buffered" width="800" height="622"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact in context:&lt;/strong&gt; py-libp2p underpins Python IPFS/Filecoin-class nodes. A signaling endpoint that OOM-kills the whole process on unauthenticated, pre-handshake input is a cheap remote crash — no credentials, no handshake, one connection. Bounding it to 32 KiB turns "attacker picks my memory ceiling" into "attacker gets a 413."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; the reviewer's one-line "availability risk" aside was a working exploit, and I almost filed it under style. Pre-handshake input is attacker-controlled by definition — "trust the &lt;code&gt;Content-Length&lt;/code&gt;" is never a safe default. And the &lt;em&gt;test&lt;/em&gt; matters as much as the fix: asserting &lt;code&gt;413&lt;/code&gt; proves the status code; asserting RSS-didn't-grow proves the property — that no buffer was allocated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reproduce it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/libp2p/py-libp2p &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;py-libp2p
pip &lt;span class="nb"&gt;install &lt;/span&gt;aiortc
git checkout 9506041   &lt;span class="c"&gt;# PRE-FIX: RSS climbs to ~1.1 GB, 200 OK&lt;/span&gt;
python3 exploit.py PREFIX body 512
git checkout 759c75b   &lt;span class="c"&gt;# POST-FIX (#1396): 413 in 0.3 ms, RSS flat&lt;/span&gt;
python3 exploit.py POSTFIX bodyreject 512
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;There's no first-class trio integration for Sentry (its Python integrations are asyncio / Django / Flask / FastAPI / AIOHTTP / etc.), so I attached manually — and it fit cleanly because the pieces genuinely apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;/sdp&lt;/code&gt; server is plain asyncio&lt;/strong&gt;, so wrapping the handler in &lt;code&gt;sentry_sdk.start_transaction(op="sdp.post", name="/sdp handler")&lt;/code&gt; gave me a real trace waterfall — the span sitting on &lt;code&gt;reader.readexactly()&lt;/code&gt; is visibly the culprit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AsyncioIntegration&lt;/code&gt; actually applies&lt;/strong&gt; — the WebRTC bridge from #1309 runs a real asyncio event loop in a daemon thread, so the integration isn't decorative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SocketIntegration&lt;/code&gt;&lt;/strong&gt; adds DNS/connect spans for free, on-topic for a P2P lib.&lt;/li&gt;
&lt;li&gt;I emitted RSS as a &lt;strong&gt;custom measurement&lt;/strong&gt; so the before/after shows up inside Sentry:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk.integrations.asyncio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncioIntegration&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk.integrations.socket&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SocketIntegration&lt;/span&gt;

&lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;YOUR_DSN&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;traces_sample_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;profiles_sample_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;integrations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AsyncioIntegration&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;SocketIntegration&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rss_mb&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getrusage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUSAGE_SELF&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;ru_maxrss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;  &lt;span class="c1"&gt;# Linux: KB→MB
&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sdp.post&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/sdp handler&lt;/span&gt;&lt;span class="sh"&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;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rss_before_mb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rss_mb&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;megabyte&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... handle request ...
&lt;/span&gt;    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rss_after_mb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rss_mb&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;megabyte&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the captured trace — the &lt;code&gt;sdp.read_body&lt;/code&gt; span running &lt;code&gt;reader.readexactly(157286400)&lt;/code&gt; is the whole story, and the &lt;code&gt;rss_before_mb&lt;/code&gt; / &lt;code&gt;rss_after_mb&lt;/code&gt; measurements ride along on the transaction:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo0t4md6a081hwvzypz0j.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo0t4md6a081hwvzypz0j.png" alt="Sentry trace waterfall — the readexactly span is the culprit" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seer's root-cause analysis&lt;/strong&gt; — this was the standout. I ran Seer on the captured event and, with no knowledge of the fix I'd already shipped, it pinned the exact cause and proposed essentially the same patch:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Focj1p8hwyczj4x0yrs8n.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Focj1p8hwyczj4x0yrs8n.png" alt="Sentry Seer RCA — independently proposes a body cap plus header caps" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Quoting it: Seer flagged a &lt;strong&gt;"+196 MB in one request"&lt;/strong&gt; spike, found that &lt;code&gt;handle()&lt;/code&gt; "reads &lt;code&gt;Content-Length&lt;/code&gt; directly from the attacker-controlled HTTP header and passes it verbatim to &lt;code&gt;reader.readexactly(content_length)&lt;/code&gt; … with no upper-bound check," &lt;em&gt;and&lt;/em&gt; separately caught the header loop as an "unbounded while-loop." Its recommended fix — a &lt;code&gt;MAX_SDP_BODY_SIZE&lt;/code&gt; cap that returns &lt;code&gt;413 Payload Too Large&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; the read, plus per-line/total header caps — is the same shape as &lt;a href="https://github.com/libp2p/py-libp2p/pull/1396" rel="noopener noreferrer"&gt;#1396&lt;/a&gt;. The only real difference: Seer suggested a 64 KiB cap; I shipped 32 KiB. Honest verdict: &lt;strong&gt;right, and specific enough to act on&lt;/strong&gt; — the uncommon case where the AI root-cause analysis lands the actual fix rather than a vague "add validation."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On Session Replay:&lt;/strong&gt; not applicable — there's no browser in this path, so I didn't fake a screenshot. The honest "N/A" is more useful to a maintainer than a contrived one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I pasted the pre-fix read path into &lt;strong&gt;Google AI Studio (Gemini)&lt;/strong&gt; and asked for three concrete hypotheses for the unbounded memory growth, ranked by likelihood, with a one-line fix for each.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qj0d94puh6g469i9nqc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qj0d94puh6g469i9nqc.png" alt="Gemini ranks " width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Its &lt;strong&gt;#1, ranked "Most Likely," was exactly right&lt;/strong&gt;: &lt;em&gt;"the server trusts the client-provided &lt;code&gt;content-length&lt;/code&gt; header and attempts to buffer the full amount into RAM … an attacker can send a large value (e.g. &lt;code&gt;10^9&lt;/code&gt;) and slowly stream data, forcing the server to grow its internal buffer to match."&lt;/em&gt; It even independently named the ~2× amplification I'd measured — &lt;em&gt;"Python's object-copying behaviour is acting as a memory multiplier."&lt;/em&gt; The guard it proposed (&lt;code&gt;if content_length &amp;gt; 1_048_576: raise …&lt;/code&gt;) is the same shape as the fix I shipped in &lt;a href="https://github.com/libp2p/py-libp2p/pull/1396" rel="noopener noreferrer"&gt;#1396&lt;/a&gt; — the only difference is the cap size (Gemini picked 1 MiB; I used 32 KiB, since real SDP offers are 1–4 KiB). Honest read: for a tightly-scoped prompt, Gemini's top-ranked hypothesis landed the real bug on the first try.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>webrtc</category>
      <category>security</category>
    </item>
    <item>
      <title>The test was green. Every real connection would have failed.</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Thu, 20 Aug 2026 18:54:53 +0000</pubDate>
      <link>https://dev.to/yashksaini/the-test-was-green-every-real-connection-would-have-failed-3e37</link>
      <guid>https://dev.to/yashksaini/the-test-was-green-every-real-connection-would-have-failed-3e37</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setting
&lt;/h2&gt;

&lt;p&gt;WebRTC-Direct in libp2p has a neat trick for connecting without a certificate authority: the peer's multiaddr &lt;em&gt;contains&lt;/em&gt; a hash of its TLS certificate — &lt;code&gt;/webrtc-direct/certhash/&amp;lt;...&amp;gt;&lt;/code&gt;. When you dial, the DTLS handshake presents a cert, you hash it, and you check it against the &lt;code&gt;certhash&lt;/code&gt; in the address. No CA, no trust store — the address &lt;em&gt;is&lt;/em&gt; the pin.&lt;/p&gt;

&lt;p&gt;For that to work, py-libp2p has to make aiortc use &lt;em&gt;our&lt;/em&gt; libp2p-generated certificate, not the one aiortc auto-generates for itself. So the code pins it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;certificates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rtc_cert&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Obvious. And, as of aiortc ≥ 1.5, completely wrong — in two different ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first sign
&lt;/h2&gt;

&lt;p&gt;The first way was loud: aiortc ≥ 1.5 &lt;strong&gt;dropped &lt;code&gt;certificates=&lt;/code&gt; from &lt;code&gt;RTCConfiguration&lt;/code&gt;&lt;/strong&gt;. Passing it raises &lt;code&gt;TypeError&lt;/code&gt;. Easy to spot, easy to "fix" — just move the cert onto the object after construction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_certificates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rtc_cert&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# looks right. isn't.
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests went green. The loopback echo test — open a data channel, send a payload, get it back — passed. I could have shipped it right there.&lt;/p&gt;

&lt;p&gt;The quiet way is the one that would have burned every user.&lt;/p&gt;

&lt;h2&gt;
  
  
  The investigation
&lt;/h2&gt;

&lt;p&gt;Here's the thing that made this a &lt;em&gt;story&lt;/em&gt; and not a one-liner. &lt;code&gt;pc._certificates = [...]&lt;/code&gt; is a &lt;strong&gt;silent no-op.&lt;/strong&gt; aiortc never reads &lt;code&gt;_certificates&lt;/code&gt;. Inside the class, it stores and reads the cert as &lt;code&gt;self.__certificates&lt;/code&gt; — a &lt;em&gt;double-underscore&lt;/em&gt; attribute. And Python does something specific with double-underscore names inside a class body: it &lt;strong&gt;mangles&lt;/strong&gt; them. &lt;code&gt;self.__certificates&lt;/code&gt; inside &lt;code&gt;class RTCPeerConnection&lt;/code&gt; is compiled to &lt;code&gt;self._RTCPeerConnection__certificates&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So from the outside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pc._certificates&lt;/code&gt; — a brand-new attribute I invented. aiortc reads it never.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pc._RTCPeerConnection__certificates&lt;/code&gt; — the &lt;em&gt;actual&lt;/em&gt; slot aiortc reads at &lt;code&gt;createOffer&lt;/code&gt;/&lt;code&gt;createAnswer&lt;/code&gt; time to write the SDP &lt;code&gt;a=fingerprint&lt;/code&gt; line (see &lt;code&gt;aiortc/rtcpeerconnection.py:295&lt;/code&gt; and &lt;code&gt;:1129&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was setting the first one. aiortc was reading the second one — still holding its own auto-generated cert. The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;/webrtc-direct/certhash/&amp;lt;...&amp;gt;&lt;/code&gt; multiaddr advertised &lt;strong&gt;our&lt;/strong&gt; libp2p cert hash.&lt;/li&gt;
&lt;li&gt;The DTLS handshake used &lt;strong&gt;aiortc's&lt;/strong&gt; auto-generated cert.&lt;/li&gt;
&lt;li&gt;Every real dial would fail peer verification with &lt;code&gt;Remote DTLS fingerprint does not match certhash&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the loopback echo test &lt;em&gt;still passed&lt;/em&gt; — because loopback doesn't validate the DTLS fingerprint against a multiaddr. It just opens a channel and echoes bytes. The one property that was broken was the one property the happy-path test never checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9cstrtlc4fo392gw0f7p.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9cstrtlc4fo392gw0f7p.png" alt="Why pc._certificates is a silent no-op — Python name mangling" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a bug that lives in the seam between two systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python's language rule:&lt;/strong&gt; name mangling rewrites &lt;code&gt;__name&lt;/code&gt; references &lt;em&gt;inside a class body&lt;/em&gt; so subclasses can't accidentally clobber a base class's "private" attributes. It's a language feature doing exactly what it's designed to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;aiortc's library internals:&lt;/strong&gt; it stores the cert under &lt;code&gt;__certificates&lt;/code&gt;, i.e. it &lt;em&gt;relies&lt;/em&gt; on that mangling for its own encapsulation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither is wrong on its own. But when you reach into a library's private state from outside the class, the mangled name is the &lt;em&gt;only&lt;/em&gt; name that works — and the un-mangled one you'd naturally type is a no-op that fails silently. There's no &lt;code&gt;AttributeError&lt;/code&gt;, no &lt;code&gt;TypeError&lt;/code&gt;, no warning. You just create a junk attribute and move on, and everything downstream keeps working &lt;em&gt;except the security property.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Set the mangled attribute directly, after construction, before any SDP operation triggers the handshake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iceServers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ice_servers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ice_servers&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="n"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Replace aiortc's auto-generated cert. Must use the mangled name —
# aiortc reads only `self.__certificates`, which mangles to this.
&lt;/span&gt;&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_RTCPeerConnection__certificates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rtc_cert&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore[attr-defined]
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's an ugly line, and the docstring says so out loud — reaching into &lt;code&gt;_RTCPeerConnection__certificates&lt;/code&gt; is exactly the kind of private-state coupling that breaks on a library upgrade. But it's &lt;em&gt;correct&lt;/em&gt;, and it's &lt;em&gt;annotated&lt;/em&gt; so the next person knows why it can't just be &lt;code&gt;pc._certificates&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The canary that would have caught it
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8nlrat0yla811q1zayr6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8nlrat0yla811q1zayr6.png" alt="Two tests, one bug — only the invariant test sees it" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real fix wasn't the one-liner — it was the test that makes the silent failure loud. The happy-path echo test can't see this bug, so I added one that asserts the &lt;em&gt;invariant&lt;/em&gt;: the pinned cert's fingerprint must actually appear in the SDP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.trio&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_cert_pinning_lands_in_sdp_fingerprint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Guards against a class of cert-pin bug: if the pin is a no-op (e.g.
    written to a public attribute aiortc never reads because it actually
    stores the cert under a name-mangled private slot), the SDP a=fingerprint
    line reflects aiortc&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s auto-generated cert instead of ours.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebRTCCertificate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_aiortc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_coro&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;create_peer_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_rtc_certificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ice_servers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[]))&lt;/span&gt;

    &lt;span class="c1"&gt;# Need at least one data channel for the SCTP m-line, otherwise
&lt;/span&gt;    &lt;span class="c1"&gt;# createOffer omits the DTLS fingerprint entirely.
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_coro&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_create_dummy_channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;sdp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_coro&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_offer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_sdp_fingerprint_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SDP fingerprint does not match pinned cert.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the small landmine inside the fix for the &lt;em&gt;test&lt;/em&gt;: &lt;code&gt;createOffer&lt;/code&gt; omits the DTLS fingerprint entirely unless there's at least one SCTP m-line, so the canary has to create a dummy data channel first or it asserts against an empty offer. The bug had layers even in the reproduction.&lt;/p&gt;

&lt;p&gt;Revert the mangled-name fix and this test goes red immediately with a fingerprint mismatch. Keep the old &lt;code&gt;pc._certificates = [...]&lt;/code&gt; no-op and it's red. That's the whole point: it fails &lt;em&gt;closed&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Two lessons, and they're the same lesson from two directions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the invariant, not the happy path.&lt;/strong&gt; The echo test proved bytes moved. It said nothing about &lt;em&gt;which certificate&lt;/em&gt; moved them — and that was the only thing that mattered for security. A green test is only as good as the property it asserts, and "it worked end to end" quietly excludes every property your end-to-end path doesn't check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A silent no-op is worse than a crash.&lt;/strong&gt; If &lt;code&gt;pc._certificates = [...]&lt;/code&gt; had raised, I'd have fixed it in thirty seconds. Because it &lt;em&gt;succeeded&lt;/em&gt; — created a real attribute, threw no error — it sailed through review and tests and would have shipped a transport where every authenticated dial fails. When you reach across an encapsulation boundary into a library's private state, assume the language is doing something clever with the name, and write the test that proves your write actually landed where the library reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Commit: &lt;a href="https://github.com/libp2p/py-libp2p/commit/ca8331ff" rel="noopener noreferrer"&gt;&lt;code&gt;ca8331ff&lt;/code&gt; — fix(webrtc): pin DTLS certificate via aiortc's mangled private slot&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Canary test: &lt;a href="https://github.com/libp2p/py-libp2p/blob/main/tests/core/transport/webrtc/test_multiplexing_loopback.py" rel="noopener noreferrer"&gt;&lt;code&gt;test_cert_pinning_lands_in_sdp_fingerprint&lt;/code&gt;&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Context: &lt;a href="https://github.com/libp2p/py-libp2p/issues/546" rel="noopener noreferrer"&gt;#546&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;aiortc internals referenced: &lt;code&gt;aiortc/rtcpeerconnection.py:295, 1129&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
