<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Joey Innes</title>
    <link>https://rmrf.uk/</link>
    <description>A record of reflections and endeavours in computer broscience — systems programming in Zig, servers in Go, aerofoils, and the flying that keeps prompting them.</description>
    <generator>Hugo</generator>
    <language>en-GB</language>
    <copyright>&#169; 2026 Joey Innes</copyright>
    <lastBuildDate>Thu, 30 Jul 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://rmrf.uk/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Designing the pressure, not the shape</title>
      <link>https://rmrf.uk/cs/2026-07-30-designing-the-pressure-not-the-shape/</link>
      <pubDate>Thu, 30 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/cs/2026-07-30-designing-the-pressure-not-the-shape/</guid>
      <category>Computer Broscience</category>
      <description>The third post in the Foilworks series, and the one with no code in it. What a wing section is actually for, why there is no such thing as a good aerofoil, and the single budget that every trade in the field is paid out of.</description>
      <content:encoded>&lt;p&gt;The &lt;a href=&#34;https://rmrf.uk/cs/2026-04-28-two-stories-about-air/&#34;&gt;first post&lt;/a&gt; in this series was about getting lift out of a computer, and the &lt;a href=&#34;https://rmrf.uk/cs/2026-07-28-the-boundary-layer/&#34;&gt;second&lt;/a&gt; was about getting drag out of one. Between them they build an instrument: put a shape in, get numbers back.&lt;/p&gt;&#xA;&lt;p&gt;What I did not expect is how little that helps. I could compute a section&amp;rsquo;s lift and drag to three figures and still have no idea whether the section was any good — because &lt;em&gt;good&lt;/em&gt; turns out not to be a property a section has. It is a relationship between a section and a wing, and until you have pinned down the wing there is nothing to ask.&lt;/p&gt;&#xA;&lt;p&gt;So this post is about the question rather than the machinery. There is no code in it, and nothing in it comes from my own solver — that one is still failing tests I wrote for it, and is in no position to advise anybody. Everything plotted here is published reference data.&lt;/p&gt;&#xA;&lt;p&gt;This is also the last of the three posts that are about the field rather than about the thing I am building. If you only ever read one of them, it should probably be this one.&lt;/p&gt;&#xA;&lt;h2 id=&#34;there-is-no-such-thing-as-a-good-aerofoil&#34;&gt;There is no such thing as a good aerofoil&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#there-is-no-such-thing-as-a-good-aerofoil&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The first thing to pin down is not the shape. It is where you fly.&lt;/p&gt;&#xA;&lt;p&gt;A wing in steady flight makes exactly as much lift as the aircraft weighs — no more, or you would be climbing, and no less, or you would be falling. Weight does not change with speed. So the lift &lt;em&gt;coefficient&lt;/em&gt; is not something a designer picks: it is dictated, moment to moment, by how fast you are going.&lt;/p&gt;&#xA;&lt;p&gt;Double your speed and the dynamic pressure quadruples, so the coefficient you need drops to a quarter of what it was. Fly slowly and you need a large one.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/dz-envelope.svg&#34; alt=&#34;Lift coefficient against Reynolds number for four aircraft, each drawn as a curve falling to the right as speed increases: a model glider low and left, and a paraglider, a speedwing and a sailplane clustered near Reynolds one to three million&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Each line is one aircraft, flown from its slowest to its fastest. Nobody occupies a point.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;That single fact reshapes the problem. You are not designing for a condition, you are designing for a &lt;em&gt;track&lt;/em&gt; — a band of lift coefficient, at a band of Reynolds number, traced out as the aircraft speeds up and slows down. A paraglider on my numbers runs from about Cl 0.97 hands-up-slow to 0.27 on full speedbar. It has to be good across all of that, and it has to behave when it leaves it at the top.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The cluster in that figure is a coincidence worth noticing. A paraglider is a slow wing with a very long chord and a sailplane is a fast wing with a short one, and Reynolds number is speed times chord, so they arrive at the same place from opposite directions. The model glider is a genuinely different problem, and so is the tip of any tapered wing.&lt;/aside&gt;&#xA;&#xA;&lt;h2 id=&#34;the-deliverable-is-a-curve&#34;&gt;The deliverable is a curve&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-deliverable-is-a-curve&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Given a band rather than a point, the thing being designed is not a number. It is the whole relationship between lift and drag, plotted against itself: the &lt;strong&gt;polar&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/dz-polar.svg&#34; alt=&#34;Drag polar of a NACA 4412 at Reynolds one million, lift coefficient up and drag coefficient across, annotated with the least-drag point, a tangent line from the origin marking best lift-to-drag, the bucket, and the top of the curve&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;One curve, four readings, and no two of them at the same place on it.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;Four things get read off it, and the useful surprise is that they are all different points.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Least drag&lt;/strong&gt; is the leftmost point of the curve. For this section it is Cd 0.0059, at Cl 0.62.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Best glide&lt;/strong&gt; is not there. How far you travel per metre of height lost is lift divided by drag, and the largest ratio is where a line drawn from the origin just grazes the curve — which for this section is Cl 1.05, a long way above the least-drag point. Slowing down costs you drag and gains you more lift than it costs. That is why gliders have a speed to fly, and why it is not their minimum-drag speed.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The bucket&lt;/strong&gt; is the flat-bottomed region where drag barely responds to lift at all. Aiming that region is most of what a designer actually does.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The top&lt;/strong&gt; is where the curve turns over and the section stops working. Everything past it is stall, and it is the part of the polar that computational tools are worst at — mine has no useful opinion there whatsoever.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Minimum &lt;em&gt;sink&lt;/em&gt; is another point again: you lose height slowest where &lt;code&gt;Cl^1.5 / Cd&lt;/code&gt; is largest, because sink rate is drag times speed and the speed itself depends on Cl. It sits higher up the polar than best glide, which is to say at a slower airspeed. Thermalling and cruising want different speeds for this reason alone.&lt;/aside&gt;&#xA;&#xA;&lt;h2 id=&#34;the-budget&#34;&gt;The budget&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-budget&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Here is the piece of physics that everything else in this post is downstream of. It took me a while to see it, and once I did, most of the field stopped looking like a collection of tricks.&lt;/p&gt;&#xA;&lt;p&gt;Air flowing over the top of a wing speeds up, and where it speeds up the pressure falls. That low pressure is the lift — nearly all of it, as the first post&amp;rsquo;s hero image shows: the suction on top does most of the work.&lt;/p&gt;&#xA;&lt;p&gt;But the air has to leave. At the trailing edge the flow from the top and the flow from the bottom rejoin and depart together, so they must arrive at very nearly the same pressure — which is close to ambient, because the wing is not doing anything to the air a long way downstream. Whatever the pressure fell by at the front, it must climb back at the back.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;And that climb is what a boundary layer cannot do indefinitely.&lt;/strong&gt; The thin sheared film described in the last post is running on whatever momentum it has left near the wall. Falling pressure pushes it along; rising pressure holds it back. Ask for too much of a climb, and the air nearest the surface stops, reverses, and the flow lifts off.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/dz-budget.svg&#34; alt=&#34;Pressure over the upper surface of a NACA 4412 at two and eight degrees, plotted with negative Cp upwards, with the climb from the suction peak back to the trailing edge shaded in each case&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Same section, same trailing edge. The angle of attack decides how big the hill is.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;That is the budget, and it is the whole game:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Lift is how deep a hole you dig at the front. Drag and stall are the bill for climbing back out of it.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;At 2° this section digs to Cp −0.98 and has to recover 1.2, spread comfortably over the back five-sixths of the chord. At 8° it digs to −3.0, right on the nose, and has to recover 3.1 — nearly three times as much — starting immediately. Nothing about the tail changed. The wing has simply written a cheque the boundary layer has to cash.&lt;/p&gt;&#xA;&lt;p&gt;Every knob a designer turns is a decision about how to spend that, and there is no way to spend it twice.&lt;/p&gt;&#xA;&lt;h2 id=&#34;buying-a-laminar-run-and-what-it-costs&#34;&gt;Buying a laminar run, and what it costs&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#buying-a-laminar-run-and-what-it-costs&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The most valuable thing you can spend it on is keeping the boundary layer laminar.&lt;/p&gt;&#xA;&lt;p&gt;A laminar layer has roughly a fifth of the skin friction of a turbulent one at a chord Reynolds number of a million, and the gap widens as you go faster. It is also fragile, which is the point: it stays laminar only while the pressure is still falling. Falling pressure damps the little disturbances that would otherwise grow; rising pressure feeds them. Move the point where the pressure stops falling further aft, and transition follows it.&lt;/p&gt;&#xA;&lt;p&gt;So: hold the pressure falling to 60% of chord instead of 20%, and you have bought forty percent of the wing at a fifth the friction. But the same total climb now has to be done in 40% of the chord instead of 80%, at twice the steepness — and steeper means closer to separating.&lt;/p&gt;&#xA;&lt;p&gt;That is not a subtlety in aerofoil design. It &lt;em&gt;is&lt;/em&gt; aerofoil design. And it shows up in the polar as the thing that gives a bucket its shape.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/dz-transition.svg&#34; alt=&#34;Two stacked panels sharing a lift-coefficient axis. The upper shows the drag of a NACA 63-412 with a flat bottom between Cl 0 and 0.6; the lower shows the transition location, flat over the same range and then collapsing to the nose&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;The bottom panel is the cause and the top panel is the effect, and it is worth reading them in that order.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;Over the flat part, this section holds laminar flow to roughly two thirds of chord and drag hardly moves. Ask for more lift — dig a deeper hole at the nose — and transition runs forward from 0.59 to 0.05 of chord within half a point of Cl, putting a turbulent layer over the whole wing. The drag more than doubles. &lt;strong&gt;The wall of a drag bucket is not a separate phenomenon; it is that collapse, plotted upwards.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;This is the historically interesting bit. &lt;code&gt;NACA 63-412&lt;/code&gt; is not a shape description. The &lt;code&gt;4&lt;/code&gt; is a design lift coefficient of 0.4 and the &lt;code&gt;12&lt;/code&gt; is thickness, but the leading &lt;code&gt;6&lt;/code&gt; names the series and the &lt;code&gt;3&lt;/code&gt; after it says &lt;em&gt;the minimum pressure sits at 30% of chord&lt;/em&gt;. The name is a pressure distribution. That is the 1940s, and it is the moment the field stopped drawing shapes and started specifying pressures — which is where this post is going.&lt;/aside&gt;&#xA;&#xA;&lt;h2 id=&#34;two-sections-and-neither-of-them-is-better&#34;&gt;Two sections, and neither of them is better&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#two-sections-and-neither-of-them-is-better&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Which brings me to the pair in the hero image. NACA 4412 and NACA 63-412 are both twelve percent thick. Both are cambered. Both are being run at the same Reynolds number, in the same solver, at the same turbulence level.&lt;/p&gt;&#xA;&lt;p&gt;At Cl 0.35, the laminar-flow section has 19% less drag. At Cl 1.05, the older one has 37% less. They cross at about Cl 0.7.&lt;/p&gt;&#xA;&lt;p&gt;There is no reading of that data where one of them is the better section. There is only a question about which side of Cl 0.7 your wing spends its life on — and, since the answer is usually &amp;ldquo;both, at different times&amp;rdquo;, how much you care about each end.&lt;/p&gt;&#xA;&lt;p&gt;I would add one thing that took me longer to appreciate. The laminar section&amp;rsquo;s advantage is &lt;em&gt;contingent&lt;/em&gt; in a way the other&amp;rsquo;s is not. It depends on transition staying aft, which depends on the surface being smooth and the air being quiet. Squashed insects, rain, a repair, a gusty day: all of them move transition forward, and the bucket washes out. The 4412 was never relying on that, so it has less to lose.&lt;/p&gt;&#xA;&lt;p&gt;Preferring the design whose advantage survives being wrong is not a compromise. On anything you actually fly, it is the design decision.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-knobs&#34;&gt;The knobs&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-knobs&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;With the budget in mind, the classic parameters stop being arbitrary and turn into ways of spending it.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Camber&lt;/strong&gt; — how much the mean line arches, and where the arch is. Camber moves the whole low-drag region up in lift, which is nearly free performance if you fly at high lift coefficients. Real numbers, same family, same Reynolds number:&lt;/p&gt;&#xA;&lt;table&gt;&#xA;&#x9;&lt;thead&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;section&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;camber&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;least drag&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;at Cl&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;best L/D&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;th&gt;Cm there&lt;/th&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&lt;/thead&gt;&#xA;&#x9;&lt;tbody&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;NACA 0012&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;0%&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;0.0054&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;0.00&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;76&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;−0.007&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;NACA 2412&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;2%&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;0.0055&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;0.35&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;101&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;−0.055&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&#x9;&#x9;&lt;tr&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;NACA 4412&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;4%&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;0.0059&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;0.62&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;129&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&#x9;&#x9;&lt;td&gt;−0.100&lt;/td&gt;&#xA;&#x9;&#x9;&#x9;&lt;/tr&gt;&#xA;&#x9;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;The bucket walks up the lift axis almost in step with the camber, and the best lift-to-drag ratio nearly doubles. The price is in the last column: &lt;strong&gt;the pitching moment&lt;/strong&gt;. A cambered section wants to pitch nose-down, and something has to hold it. On a conventional aeroplane it is the tailplane, pushing down, which is lift that must itself be paid for in drag. On a soft wing there is no structure to react a moment at all — the lines and the internal pressure do it, and the wing changes shape while doing it. A Cm of −0.1 is a detail on a Cessna and a design constraint on a paraglider.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Thickness&lt;/strong&gt; — volume for spars, and on an inflated wing, the ability to hold its own shape. It also blunts the nose, which as we are about to see decides how the thing stalls. Past roughly 15% the drag starts climbing quickly, because the flow has to be accelerated harder over the shoulder and therefore decelerated harder afterwards. Same budget.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Where the thickness peaks&lt;/strong&gt; — move the maximum aft and the pressure keeps falling further back, which buys laminar run and costs recovery distance. This is the laminar-flow trade in geometric clothing.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The leading-edge radius&lt;/strong&gt; — small, and the suction peak at high angles becomes a spike; large, and it stays rounded. Almost nothing else about the section matters as much for what happens at the top of the polar.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The trailing edge&lt;/strong&gt; — a small amount of camber right at the back is worth a startling amount of lift. I found this out by accident, through a bug that built an unintended half-percent bulge at 95% of chord and moved the lift by several percent, because to the outer flow it reads as a small deflected flap. &lt;a href=&#34;https://rmrf.uk/cs/2026-07-28-the-boundary-layer/&#34;&gt;The last post&lt;/a&gt; makes the same point from the other direction: the boundary layer builds one of these on every wing, for free, whether you wanted it or not. Aft loading is also where the pitching moment is made or unmade, which is why reflexed sections — turned up at the tail — exist for tailless aircraft.&lt;/p&gt;&#xA;&lt;h2 id=&#34;stall-is-a-design-decision-not-an-accident&#34;&gt;Stall is a design decision, not an accident&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#stall-is-a-design-decision-not-an-accident&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The top of the polar deserves its own section, because it is the one place where being wrong is dangerous rather than merely inaccurate.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/dz-stall.svg&#34; alt=&#34;Three schematic lift curves against angle of attack: one rolling over gently near sixteen degrees, one peaking and dropping almost vertically at fifteen, and one breaking early and flattening out well below the others&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;All three peak in about the same place. Only one of them tells you first.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;There are three classical ways to run out of wing, and which one you get is largely decided by the nose.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Trailing-edge stall&lt;/strong&gt; is what thick, blunt-nosed sections do. Separation starts at the tail — where the boundary layer has been climbing longest and has least left — and creeps forward as the angle increases. Lift rolls over gradually, and the separated flow shakes the aircraft on the way, which is a warning.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Leading-edge stall&lt;/strong&gt; belongs to sharper, thinner sections. The suction peak at the nose gets severe enough that a small separation bubble sitting there bursts, and the entire upper surface lets go at once. The lift curve falls off a cliff, with nothing beforehand.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Thin-aerofoil stall&lt;/strong&gt; happens on very thin sections and at low Reynolds numbers. A bubble forms near the nose early and simply grows longer as the angle increases, so lift breaks well below what the section might otherwise have reached and then mushes along.&lt;/p&gt;&#xA;&lt;p&gt;The project instruction I wrote for myself before any of this existed is &lt;strong&gt;never optimise for maximum lift alone&lt;/strong&gt;, and this figure is why: the number at the top of the curve tells you nothing whatsoever about what happens immediately after it. And where there is a real trade — there usually is, because the sharp nose that buys peak lift is the same one that lets go abruptly — a section reaching Cl 1.7 and then dropping without warning is, for anything with a person hanging underneath it, a worse section than one reaching 1.5 and rolling over. For a soft wing it is worse still: the pressure distribution is not only making the lift, it is holding the wing&amp;rsquo;s shape, so losing it abruptly does not merely reduce lift — it can fold the wing.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Section choice is only half of stall design anyway. A wing is a spanwise arrangement of sections, and washout — twisting the tips to a lower angle — exists so that the root gives up first, while the tips and the ailerons on them are still flying. A gentle section on a badly twisted wing will still bite.&lt;/aside&gt;&#xA;&#xA;&lt;h2 id=&#34;the-same-shape-is-a-different-design-at-a-different-size&#34;&gt;The same shape is a different design at a different size&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-same-shape-is-a-different-design-at-a-different-size&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;One more thing that has to be part of the brief, because it changes the answer rather than refining it.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/dz-reynolds.svg&#34; alt=&#34;Drag polars of a NACA 4412 at Reynolds one million, five hundred thousand and two hundred thousand, the curves moving right and losing their bucket as Reynolds number falls&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Identical coordinates. The problem is not the same problem.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;Identical coordinates, three Reynolds numbers, and the best lift-to-drag ratio falls from 129 to 107 to 78. The drag at the bottom of the bucket nearly doubles.&lt;/p&gt;&#xA;&lt;p&gt;The mechanism is the one the last post ran into: as Reynolds number falls, the laminar boundary layer becomes less and less able to survive a pressure rise, until it starts separating &lt;em&gt;before&lt;/em&gt; it has transitioned. It then goes turbulent in mid-air and comes back down, leaving a laminar separation bubble sitting on the surface — which is expensive, and which is the single hardest thing in this whole subject to predict well.&lt;/p&gt;&#xA;&lt;p&gt;This is why model gliders use sections that would be poor choices full-size, and it is a live problem for real wings too. The tip of a tapered wing runs at maybe a half to a third of the root&amp;rsquo;s Reynolds number, and the tip is where stall behaviour gets decided. A section chosen on root numbers is not a section chosen for the wing.&lt;/p&gt;&#xA;&lt;h2 id=&#34;nobody-designs-the-shape&#34;&gt;Nobody designs the shape&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#nobody-designs-the-shape&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Which brings me to the thing I actually wanted to write down, and the reason for the title.&lt;/p&gt;&#xA;&lt;p&gt;Read back over everything above. Not one of the trades is naturally expressed in terms of coordinates. They are all statements about the pressure: how deep, how far back before it turns, how steep the climb, how sharp the peak at high angles. The shape is not the design. &lt;strong&gt;The shape is the thing that happens to produce the design.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;So serious aerofoil work inverts the problem. You specify the pressure distribution you want — a rooftop over the front to hold laminar flow, a recovery shaped to get back to the trailing edge without separating, whatever the brief needs — and you solve for the geometry that produces it. That is inverse design, and it is what Eppler&amp;rsquo;s code, Wortmann&amp;rsquo;s FX sections and Drela&amp;rsquo;s mixed-inverse mode in XFOIL all do.&lt;/p&gt;&#xA;&lt;p&gt;I do not have an inverse mode. Most people learning this do not. The amateur version of the same discipline still works and is worth adopting immediately: &lt;strong&gt;change one thing, and look at the pressure distribution rather than the outline.&lt;/strong&gt; Your eye cannot read an aerofoil — every section in the useful range looks like every other one — but it can read a Cp plot fluently after about a week. Where is the peak? How steep is the recovery? Did that change put the pressure rise somewhere the boundary layer can pay for?&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The theoretical end of this is Stratford&amp;rsquo;s recovery: the pressure rise that keeps the boundary layer exactly on the point of separating at &lt;em&gt;every&lt;/em&gt; station, so that none of the available climb is wasted anywhere. It is the optimum, and essentially nobody flies it, because a distribution that is everywhere on the edge of separating is everywhere one gust from separating. The optimum and the design are different objects.&lt;/aside&gt;&#xA;&#xA;&lt;h2 id=&#34;the-section-is-not-the-wing&#34;&gt;The section is not the wing&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-section-is-not-the-wing&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;A closing note on proportion, because it took me embarrassingly long to work out and it changes how much any of this is worth.&lt;/p&gt;&#xA;&lt;p&gt;A real wing is finite. It sheds vorticity off its tips, which tilts the whole force vector back, and that induced drag is set by span and lift distribution — not by section. At an aspect ratio of 5.5, which is paraglider territory, and Cl 0.6, the induced drag is about 0.0245. The section drag next to it is about 0.007. Add the lines and the pilot, and the section is a small minority of the total.&lt;/p&gt;&#xA;&lt;p&gt;So the honest framing is this. Choosing between a section with 0.0060 of drag and one with 0.0070 buys you a couple of percent of total drag, and choosing a better planform, or fewer lines, buys you more. That is not an argument for indifference — a couple of percent decides competitions, and section choice also decides the &lt;em&gt;stall&lt;/em&gt;, which decides whether the wing is any good to fly. But it is an argument against believing the answer lives here.&lt;/p&gt;&#xA;&lt;p&gt;And for the wings I care about there is a further discount. A soft wing is not the section you drew. It inflates into shape, deforms under load, has its nose cut open for the intakes, and is pulled into scallops between the ribs. Rigid-section data is the first term of the answer and not the answer, which is exactly why the 2D solver has to be both fast and trustworthy — it is going to be called a great many times inside something larger.&lt;/p&gt;&#xA;&lt;h2 id=&#34;next&#34;&gt;Next&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#next&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Which is the point I have been circling for three posts. What I want out of Foilworks is not a number. It is a polar you can believe across a band of lift coefficient and a decade of Reynolds number, that knows where it stops being right and says so.&lt;/p&gt;&#xA;&lt;p&gt;That is the brief, and the rest of this series is about trying to meet it. From here the posts stop being about aerodynamics and start being about the thing I built: how it is put together and why in Zig, how I tried to prove it was right and what that measurement caught me doing, and finally the rewrite that abandons the loop in the second post and solves the boundary layer and the outer flow as one system with a real wake attached.&lt;/p&gt;&#xA;&lt;p&gt;They are all, in the end, about the same worry. It is very easy to build something that produces plausible aerofoil numbers, and nothing in the output tells you which kind you have got.&lt;/p&gt;&#xA;&lt;p&gt;Every measured number here came from published XFOIL polars and the reference boundary layers committed to the project, checked while writing rather than remembered. The two schematic figures say so in their captions. If any of it is wrong, I would like to know.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>The boundary layer, where drag and stall live</title>
      <link>https://rmrf.uk/cs/2026-07-28-the-boundary-layer/</link>
      <pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/cs/2026-07-28-the-boundary-layer/</guid>
      <category>Computer Broscience</category>
      <description>The second post in the series. The other half of aerodynamics — a film about one percent of the chord thick in which all of a wing's drag and all of its stall behaviour are decided. Two numbers stand in for the whole velocity profile, one ratio decides everything, and the two solvers each need the other's answer before either can start.</description>
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://rmrf.uk/cs/2026-04-28-two-stories-about-air/&#34;&gt;Back in the spring&lt;/a&gt; I wrote about the half of aerodynamics where air has no friction. That half is fast, it is accurate, and it gives you lift and pressure. It also cannot tell you how much drag a wing has, or when it will stop flying — which are, unfortunately, the two things a person choosing a wing actually wants to know.&lt;/p&gt;&#xA;&lt;p&gt;This post is the other story. It is where drag lives and where stall lives, and it is where I have spent most of my time since. It is also much the harder of the two, for a reason that is worth stating at the outset: the outer flow and the film each need the other&amp;rsquo;s answer before either one can start.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-film&#34;&gt;The film&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-film&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Everywhere except very close to the skin, air behaves as if friction did not exist. Very close to the skin, friction is the only thing that matters, because air genuinely sticks to solid surfaces. The molecules touching the wing are, on average, going exactly the speed of the wing. A tiny distance away they are going full speed.&lt;/p&gt;&#xA;&lt;p&gt;Everything friction does to a wing happens in that gap.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/bl-film.svg&#34; alt=&#34;A wall with a velocity profile drawn at four stations along it. Each profile rises from zero at the wall to full speed at the top, and the height of the profile grows along the wall. A dashed red line marks the edge of the layer&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;The vertical scale here is a lie by a factor of about two hundred. Horizontally this is a slice of wing; vertically it is a film you could not see.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Reynolds number is the ratio of how hard the air is being pushed around to how hard it resists being sheared. At Re 10⁶ inertia wins by a million to one, which is exactly why the friction is confined to a film — and why a paraglider at Re 10⁵ is a genuinely different aerodynamic problem to an airliner at Re 10⁷, not the same one scaled down.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;The gap is thin. On the section in the hero image at Re 10⁶ it is about one percent of the chord over most of the wing. That thinness is the entire reason any of this is tractable, because it means the outer flow barely notices the layer exists, and the layer can be solved as if the outer flow were simply handed to it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;nobody-solves-the-profile&#34;&gt;Nobody solves the profile&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#nobody-solves-the-profile&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The obvious approach is to work out the actual shape of that velocity curve everywhere, which means solving a partial differential equation over a two-dimensional region for every case. People do this. It is called a Navier-Stokes solve, it takes minutes to hours, and it is not what I want when the point of the project is to sweep a hundred candidate sections.&lt;/p&gt;&#xA;&lt;p&gt;The trick, and it is a very old one, is to stop caring about the shape of the curve and keep only two numbers per station: how much air is missing, and how much push is missing.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/bl-integrals.svg&#34; alt=&#34;Two panels, each showing a velocity profile against a full-speed line. In the left panel the shortfall between the two is shaded blue and redrawn as a slab against the wall labelled delta star. In the right panel the shortfall weighted by speed is shaded red and redrawn as a shorter slab labelled theta&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Both areas are real quantities with units of length. Once you have them you can throw the profile away.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;δ*, the displacement thickness&lt;/strong&gt;, is the first. Add up the air that failed to arrive because of friction, and ask how far out you would have to move the wall to lose that much air from a flow with no friction at all. That distance is δ*. It is the number that matters most here, because it is the only thing about the boundary layer that the outer flow can see: as far as the frictionless solve is concerned, the wing is simply δ* fatter than it really is.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;θ, the momentum thickness&lt;/strong&gt;, is the second. Same idea, but weighting each bit of missing air by how fast it was going. This one is the drag, more or less directly — momentum that the air had at the front and does not have at the back went into the wing.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;&amp;ldquo;Integral method&amp;rdquo; means exactly this: you integrate the profile away in the direction across the layer, and what is left is a couple of ordinary differential equations along the surface. It is a marching problem rather than a field problem, which is why it costs a millisecond rather than a minute.&lt;/aside&gt;&#xA;&#xA;&lt;h2 id=&#34;one-number-decides-everything&#34;&gt;One number decides everything&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#one-number-decides-everything&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Their ratio, &lt;code&gt;H = δ*/θ&lt;/code&gt;, turns out to be the interesting one. It says nothing about how thick the layer is and everything about its &lt;em&gt;shape&lt;/em&gt; — specifically about how full the profile is near the wall, which is to say how much momentum the flow has left down there to keep pushing itself along.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/bl-shape.svg&#34; alt=&#34;Three velocity profiles side by side, all the same height. The first is nearly vertical with a steep rise at the wall and is labelled H 1.4 turbulent. The second is a smooth curve labelled H 2.6 laminar. The third has zero slope at the wall and an inflection, is drawn in red with a cross, and is labelled H 3.5 separated&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;The dashed line at each wall is the slope of the profile there, which is the skin friction. On the right it has gone to zero.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The laminar side of this is Thwaites&amp;rsquo; method, which is a quadrature rather than a marching solve and is therefore untroubled near the stagnation point, where everything else misbehaves. It has a flat spot: its shape-factor correlation pegs at 3.9155 and then stops responding to further deceleration, which on a strongly decelerating run can hold for fifteen stations in a row.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;A turbulent layer runs at H ≈ 1.4. It is constantly dragging fast air down from the outside and slinging slow air up, which keeps the wall region energetic — at the cost of far more skin friction. A laminar layer sits around 2.6: much slicker, much less able to cope with anything difficult.&lt;/p&gt;&#xA;&lt;p&gt;And when the flow is asked to climb into rising pressure — which is exactly what the whole rear half of an aerofoil asks of it — H climbs. Somewhere around 2.4 for a turbulent layer, and nearer 3.5 for a laminar one, the profile runs out of momentum at the wall, the flow stops, and it lifts off the surface. That is separation, and it is the mechanism behind stall.&lt;/p&gt;&#xA;&lt;p&gt;So the entire drama of a wing section is: how long can you stay laminar (cheap, but fragile), and having gone turbulent (expensive, but tough), can you get to the trailing edge before H runs away?&lt;/p&gt;&#xA;&lt;h2 id=&#34;waiting-for-a-wobble&#34;&gt;Waiting for a wobble&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#waiting-for-a-wobble&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The switch between the two is called transition, and it is the single most consequential thing in the whole model, because it decides both the drag and the toughness.&lt;/p&gt;&#xA;&lt;p&gt;It is not a threshold you cross. A laminar layer is unstable long before it is turbulent: a tiny disturbance at the right frequency grows, exponentially, as it travels downstream. The model I use — the e^N envelope, from Drela and Giles — integrates that growth along the surface and declares the flow turbulent once the most-amplified disturbance has grown by a factor of e^N.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/bl-amplification.svg&#34; alt=&#34;A curve of the amplification factor against distance along the chord, rising from zero and crossing a dashed line at 9, where a marker shows transition&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Real data from the reference solution on this exact section. Nothing happens for the first three percent of chord; then it grows for the next forty.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;N is a stand-in for how noisy the surrounding air is, since the disturbances have to come from somewhere. Ncrit 9 is a quiet wind tunnel — a growth factor of about eight thousand. Ncrit 4 or 5 is a gusty afternoon, and it moves transition, and therefore drag, a long way forward.&lt;/aside&gt;&#xA;&#xA;&lt;aside class=&#34;note&#34;&gt;The lower surface in the hero image never transitions at all at this angle, which is why it is drawn blue from nose to tail. The underside of a lifting section is being gently accelerated over most of its length, and a layer that is never asked to climb anything is never destabilised enough to trip.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;The obvious cheaper alternative is Michel&amp;rsquo;s criterion, which is a &lt;em&gt;local&lt;/em&gt; correlation: look up the state at this station, ask whether it has tripped yet, forget everything else. It is consistently wrong here, firing 0.1 to 0.2 of chord too early across Re 2×10⁵ to 10⁶ and costing about five points of mean drag error. The reason is the whole point of the previous paragraph. Amplification has memory. A lookup does not, and instability is a thing that accumulates.&lt;/p&gt;&#xA;&lt;h2 id=&#34;drag-at-last&#34;&gt;Drag, at last&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#drag-at-last&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;With a laminar run, a transition point and a turbulent run, you can finally compute a drag that means something. The route is Squire and Young&amp;rsquo;s, from 1937, and it is a lovely piece of accounting: take the momentum thickness at the back of the wing, and extrapolate what it would become infinitely far downstream, where the wake has finished mixing out and the pressure is ambient again. Momentum that never comes back is drag, so that far-downstream θ &lt;em&gt;is&lt;/em&gt; the drag, up to a factor of two.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Last post I made a fuss about never reporting the inviscid solver&amp;rsquo;s pressure drag as drag. That number was 0.000172 for this section at 6°, and the real answer is 0.0093 — a factor of fifty-four. It was never drag. It was the error left over from approximating a curve with flat panels.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;What that buys is the ability to ask where a wing&amp;rsquo;s drag actually comes from, and the answer changes completely across the range you fly in.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/bl-dragsplit.svg&#34; alt=&#34;Drag coefficient against angle of attack for a NACA 4412 at Reynolds one million, drawn as two stacked bands: a blue skin-friction band that stays almost flat, and a red pressure-drag band above it that is negligible at low angles and dominant by fourteen degrees&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Both bands are drag. Only one of them is friction, and it is the one that stops mattering.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;At small angles a wing&amp;rsquo;s drag is almost entirely &lt;strong&gt;skin friction&lt;/strong&gt; — the air rubbing along the surface, which is θ growing steadily along a well-behaved layer. That is the regime the whole integral method is comfortable in.&lt;/p&gt;&#xA;&lt;p&gt;Push the angle up and skin friction barely changes, while &lt;strong&gt;pressure drag&lt;/strong&gt; goes up by a factor of nineteen. Nothing is rubbing harder. What has happened is that the layer on top has thickened and begun to come away, so the outer flow can no longer close neatly around the back of the section, and the pressure on the rear no longer pushes forward as hard as the pressure on the front pushes back. That is the same d&amp;rsquo;Alembert cancellation from the last post, failing.&lt;/p&gt;&#xA;&lt;p&gt;Two consequences worth carrying forward. The first is that a cambered section&amp;rsquo;s least-drag angle is &lt;strong&gt;not&lt;/strong&gt; zero lift — it is somewhere near the lift it was drawn for, because that is where the pressure distribution is gentlest on both surfaces at once. The second is that Reynolds number moves all of this, and not always the way you would guess. Drop a NACA 0012 from Re 10⁶ to 5×10⁵ and its least drag rises from 0.0054 to 0.0062 — even though transition slides &lt;em&gt;aft&lt;/em&gt;, from 0.69 to 0.79 of chord, so it is getting a longer run of the cheap kind. The friction itself is rising faster than the extra laminar run can save.&lt;/p&gt;&#xA;&lt;p&gt;Go one step further down, to Re 2×10⁵, and something else appears entirely. Down there the laminar layer separates &lt;em&gt;before&lt;/em&gt; it ever transitions, sits off the surface for a few percent of chord, goes turbulent in mid-air and comes back down. That is a &lt;strong&gt;laminar separation bubble&lt;/strong&gt;, it is expensive, and it is the single hardest thing in this subject to predict. Everything in this post assumes the layer stays attached to the surface until it is good and ready to leave, and down there it simply does not.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-circular-argument&#34;&gt;The circular argument&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-circular-argument&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now the awkward part. The boundary layer needs to be told the speed at the edge of the layer, which comes from the frictionless solve. And the frictionless solve needs to be told the shape of the body, which — because of δ* — depends on the boundary layer.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/bl-coupling.svg&#34; alt=&#34;Two boxes joined by two arrows in a loop. The upper arrow, blue, is labelled here is the speed at the edge; the lower arrow, red, is labelled then the body is this much fatter, go again&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Neither solver can go first, so both of them go badly, several times, until they stop disagreeing.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;So you guess, and iterate. Solve the clean shape, march the layer, thicken the shape by δ* along the surface normals, re-panel it, solve again. Repeat until the two stop disagreeing. On the sections I have tried that takes between four and twenty-two passes, and the whole thing still runs in a few milliseconds, because the expensive part — factorising the influence matrix — is a couple of hundred rows and only has to happen once per shape.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;There are two textbook ways to hand the layer&amp;rsquo;s thickness back to the outer flow. The one described here moves the geometry. The other is &lt;em&gt;transpiration&lt;/em&gt;: leave the shape alone and let a little air blow out through the surface to mimic the displacement. Which one you can use is not a matter of taste — it depends on what your panels are made of, and choosing wrong produces an answer that looks plausible at zero lift and is wildly wrong everywhere else.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;This shape of problem — two models, each needing the other&amp;rsquo;s output as its input — is worth recognising because it is not special to aerodynamics. What &lt;em&gt;is&lt;/em&gt; specific here is that the loop has a hard edge to it, and the next two sections are about where that edge is.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-shape-the-air-actually-sees&#34;&gt;The shape the air actually sees&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-shape-the-air-actually-sees&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Here is what the coupling is doing, physically. At any real angle of attack the boundary layer on top of the section is much thicker than the one underneath, because the top is the surface being asked to climb the pressure hill. So the fattened body is not fattened symmetrically — it is fattened mostly on top, mostly at the back.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/bl-decamber.svg&#34; alt=&#34;The aft third of an aerofoil with the boundary layer drawn as a red band on both surfaces, much thicker above than below. Two dashed camber lines are drawn: the real one, and the one of the displaced body, which sits above it and is less curved&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Nobody put a flap on this wing. The boundary layer did it, and it does more of it the harder the wing is working.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;Which bends the effective camber line tail-up. Less camber means less lift, so the viscous answer sits below the frictionless one. That is &lt;strong&gt;viscous decambering&lt;/strong&gt;, and it is the entire lift correction — the whole reason a real wing makes less lift at a given angle than the frictionless theory promises.&lt;/p&gt;&#xA;&lt;p&gt;It also explains something that would otherwise look arbitrary. There is a fiddly detail in every scheme like this about what to do at the very back, where the displaced upper and lower surfaces have to meet at a single point again. It looks like bookkeeping. It is not: a bulge half a percent of chord deep over the last five percent of the wing reads to the outer flow as a small trailing-edge flap, and aft camber is disproportionately good at making lift. Get that closure wrong and you can move the lift by several percent in whichever direction your mistake happened to point.&lt;/p&gt;&#xA;&lt;h2 id=&#34;where-the-loop-breaks&#34;&gt;Where the loop breaks&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#where-the-loop-breaks&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;That iteration works right up until the boundary layer starts to separate, and then it fails in a way that is worth understanding, because it is why the state of the art looks the way it does.&lt;/p&gt;&#xA;&lt;p&gt;Marching the layer in &lt;strong&gt;direct mode&lt;/strong&gt; means being handed the edge velocity and asked for the layer. As the flow approaches separation — as the shear at the wall approaches zero — that problem stops having a solution at all. Not a hard one: none. The equations become singular exactly at the condition you most want to resolve.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;It has a name: Goldstein&amp;rsquo;s singularity, from 1948. It is a property of the &lt;em&gt;direct&lt;/em&gt; formulation, not of the physics, and every scheme that gets past it does so the same way — by making the edge velocity an unknown as well, and solving for the layer and the flow it sits in simultaneously rather than in turn.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;So a scheme like this has to be kept away from the edge, usually by clamping something on the way out of the march. The clamp is not physics, it is scaffolding, and it costs you exactly the region you would most like to be right about — because separation approaching the trailing edge &lt;em&gt;is&lt;/em&gt; the onset of stall.&lt;/p&gt;&#xA;&lt;p&gt;That is the honest limitation of the whole approach, and it is the reason the top of a lift curve is so much harder than the middle of one. The layer knows perfectly well that the flow is coming apart; the direct loop has no way of letting that news reach the lift.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-this-cannot-tell-you&#34;&gt;What this cannot tell you&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#what-this-cannot-tell-you&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;It is worth being as blunt about the limits here as the last post was about the inviscid ones, because this method is the workhorse of practical aerofoil analysis and it has a shape of failure you should expect.&lt;/p&gt;&#xA;&lt;p&gt;It is excellent at attached flow. Give it a section running somewhere in the middle of its range and it will hand you a drag figure good to a few percent, a transition location, and a lift corrected the right way by the right mechanism, in about a millisecond. That is a genuinely remarkable thing to get for the cost.&lt;/p&gt;&#xA;&lt;p&gt;It is at its worst in exactly two places, and they are the two places that decide whether a wing is any good.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The top of the lift curve&lt;/strong&gt;, for the reason in the section above. A method of this kind will tend to go on making lift past the angle at which the real section has given up. That is not a conservative error to have — a wing that never stalls on paper is very much the dangerous direction to be wrong in.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Low Reynolds number&lt;/strong&gt;, where laminar separation bubbles appear and the assumption that the layer stays on the surface stops holding. Below about Re 5×10⁵ that is not a correction to apply, it is a different flow.&lt;/p&gt;&#xA;&lt;p&gt;Both have the same root: the layer and the outer flow are being solved &lt;em&gt;in turn&lt;/em&gt; rather than together. Fixing it properly means giving up the loop entirely and solving every boundary-layer unknown and every edge velocity as one system — which incidentally gives the wake behind the trailing edge somewhere to exist, and at the moment it has nowhere at all. That is a later post in this series.&lt;/p&gt;&#xA;&lt;h2 id=&#34;next&#34;&gt;Next&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#next&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Two posts in, there is now an instrument: shape and angle in, lift and drag and pressure out, fast enough to sweep. What there isn&amp;rsquo;t yet is any way of knowing whether the section it just measured is a good one.&lt;/p&gt;&#xA;&lt;p&gt;That turns out to be a much less obvious question than it sounds, and it is the next post — what a wing section is actually for, why there is no such thing as a good aerofoil, and the one budget that every trade in the field is paid out of.&lt;/p&gt;&#xA;&lt;p&gt;Every number in this post came out of the committed reference data or the solver while I was writing it. If any of it is wrong, I would like to know.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>The question that broke Dave</title>
      <link>https://rmrf.uk/cs/2026-07-27-the-question-that-broke-dave/</link>
      <pubDate>Mon, 27 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/cs/2026-07-27-the-question-that-broke-dave/</guid>
      <category>Computer Broscience</category>
      <description>Can you build a network that behaves like shared memory, with no token, no payment and no accounting of who stores what? I tried in Go two years ago. It worked, and then answering one much smaller question destroyed it. What actually failed, checked against the code — and the three answers the project has produced since.</description>
      <content:encoded>&lt;p&gt;I wrote about &lt;a href=&#34;https://rmrf.uk/cs/2024-05-01-dave/&#34;&gt;Dave&lt;/a&gt; here in May 2024 — a peer-to-peer network that disseminates packets of data, without accounts, without a token, and without anyone in charge. That article carries an edit note admitting it went out of date within months. This is what happened in those months, and why I&amp;rsquo;ve just thrown most of it away.&lt;/p&gt;&#xA;&lt;p&gt;Underneath every version of it there&amp;rsquo;s one question, and it&amp;rsquo;s the reason I keep coming back to this instead of using something that exists:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Can you build a network that behaves like a shared pool of memory — anyone writes, anyone reads, the data stays around — with no payment system, no token, and no accounting of who is storing what? Just an incentive structure simple enough that nothing has to be coordinated at all?&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;The constraint is what makes it interesting. Filecoin, Arweave and Storj all answer the durability half with money and proofs of storage, and they work. Take money off the table and something else has to make a node want to keep your bytes, or the pool is only ever as good as the goodwill sitting in it.&lt;/p&gt;&#xA;&lt;p&gt;Two years on, Dave has produced three different answers to that question. Two of them are in this post because I built them. The third I only recognised while writing it, and it had been sitting in the code since 2024.&lt;/p&gt;&#xA;&lt;p&gt;The question in this article&amp;rsquo;s title is a much smaller one, and much more boring. It&amp;rsquo;s the one that broke it.&lt;/p&gt;&#xA;&lt;p&gt;One thing to be clear about before any of the criticism, because it changes how you should read it. This was months of full-time work, and the decisions in it were deliberate — argued through, mostly written down, often revised more than once. What follows isn&amp;rsquo;t a list of things I hadn&amp;rsquo;t considered. It&amp;rsquo;s largely the opposite: mechanisms I designed carefully, which fail for reasons that only surface when you push a formula to its limit, or think like an attacker for five minutes, or run the thing at a scale I never reached. That&amp;rsquo;s a more useful kind of mistake to read about than carelessness, and a far more common one.&lt;/p&gt;&#xA;&lt;h2 id=&#34;where-the-code-is-now&#34;&gt;Where the code is now&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#where-the-code-is-now&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I&amp;rsquo;ve reset &lt;code&gt;godave&lt;/code&gt;&amp;rsquo;s &lt;code&gt;main&lt;/code&gt; branch back to commit &lt;code&gt;4bc87d5&lt;/code&gt;, from 19 November 2024, and tagged it &lt;code&gt;pre-signed-dats&lt;/code&gt;. Everything after it — seventy-nine commits, three more weeks — lives on a branch called &lt;code&gt;signed-dats&lt;/code&gt;. Nothing is lost, but the default branch is now the version I actually want people to read.&lt;/p&gt;&#xA;&lt;p&gt;That version is 1,074 lines. It builds, and its tests pass. The version I abandoned in December is 3,573 lines across nine packages, and I could no longer hold it in my head.&lt;/p&gt;&#xA;&lt;p&gt;The whole divergence is one commit, and you can see it in a single diff of the protobuf spec:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; enum Op {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    DAT = 2;                    a thing that exists&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    PUT = 2;                    an operation on a named slot&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; message M {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    bytes v = 3;   // value&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    bytes t = 4;   // time&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    bytes s = 5;   // salt&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    bytes w = 6;   // work&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    bytes datKey = 3;           addressable&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    bytes val = 4;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    bytes time = 5;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    bytes salt = 6;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    bytes work = 7;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    bytes pubKey = 8;           ownership&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    bytes sig = 9;              asymmetric crypto&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Addressability, ownership and signatures all arrive together. Before that commit a dat was a value, a time, a salt and a proof of work — content-addressed by its own hash, with no key and no owner. Nobody could update it, because there was no &lt;em&gt;it&lt;/em&gt; to update; there was just the data, and the data was its own name.&lt;/p&gt;&#xA;&lt;p&gt;The rename tells the story on its own. &lt;code&gt;DAT&lt;/code&gt; is a thing that exists. &lt;code&gt;PUT&lt;/code&gt; is an operation on a named slot. I changed one into the other in an afternoon and spent the next three weeks discovering what that cost.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-it-did&#34;&gt;What it did&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#what-it-did&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Each node keeps a table of peers and a table of dats. Every epoch it picks one dat at random and sends it to one peer at random. That&amp;rsquo;s the entire dissemination mechanism, and it&amp;rsquo;s the part I still like.&lt;/p&gt;&#xA;&lt;p&gt;Storage was governed by something I called &lt;em&gt;mass&lt;/em&gt;:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Mass&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;work&lt;/span&gt; []&lt;span style=&#34;color:#66d9ef&#34;&gt;byte&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Time&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;float64&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; float64(&lt;span style=&#34;color:#a6e22e&#34;&gt;nzerobit&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;work&lt;/span&gt;)) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; (&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt; float64(&lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Since&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;Milliseconds&lt;/span&gt;()))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Difficulty divided by age. The idea was that a dat with a harder proof of work should persist longer, and that everything should eventually decay unless somebody keeps re-publishing it. Trust worked the same way: when a peer sent you a dat you didn&amp;rsquo;t already have, its trust score went up by the mass of that dat, and trusted peers got picked more often for gossip.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;That is the first answer to the question at the top: measure what a participant contributes, and pay them in the only currencies the network has — a share of its disk, and a share of its bandwidth.&lt;/strong&gt; No token, no ledger, and a pleasing symmetry between the two halves.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-part-that-worked&#34;&gt;The part that worked&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-part-that-worked&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Before the things that didn&amp;rsquo;t, the part I&amp;rsquo;d write again unchanged — and which survives into the rewrite more or less intact.&lt;/p&gt;&#xA;&lt;p&gt;Peer discovery and liveness are the same pair of messages. You send &lt;code&gt;GETPEER&lt;/code&gt;; a well-behaved node replies &lt;code&gt;PEER&lt;/code&gt; with a couple of addresses. If a peer hasn&amp;rsquo;t been heard from in &lt;code&gt;DROP&lt;/code&gt;, it&amp;rsquo;s deleted. There&amp;rsquo;s no separate heartbeat, no separate bootstrap protocol, and no membership list — the packet that discovers peers is the packet that proves they&amp;rsquo;re alive.&lt;/p&gt;&#xA;&lt;p&gt;That doubling-up turns out to do a third job as well, and it&amp;rsquo;s the one I&amp;rsquo;m most pleased with.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/dave/punch.svg&#34; alt=&#34;Two nodes, A and B, each inside a dashed box labelled home router, with a reachable edge node above them. Faint arrows show each node talking outbound to the edge, and the edge gossiping each to the other. Two bold arrows then cross both router boundaries in opposite directions&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Nothing in the code mentions NAT. The traversal falls out of pinging everything you hear about, on a tick short enough that both sides do it at once.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;Most nodes are behind a home router, and two of them can&amp;rsquo;t exchange UDP without help. The usual answer is a rendezvous server that tells both sides to transmit at the same moment, so each one&amp;rsquo;s outbound packet opens a return path through its own router. Dave has no such server and no code that mentions NAT at all. It gets there anyway, out of three unremarkable decisions:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// a packet from a stranger adds the stranger&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;peers&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;pkfp&lt;/span&gt;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;peers&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;pkfp&lt;/span&gt;] = &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;peer&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;pd&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;pkpd&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fp&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;pkfp&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;added&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Now&lt;/span&gt;(), &lt;span style=&#34;color:#a6e22e&#34;&gt;seen&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Now&lt;/span&gt;()} }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// gossiped addresses are added unconditionally&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dave&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Op_PEER&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;mpd&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;pk&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;msg&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Pds&lt;/span&gt; { &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;peers&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;mpdfp&lt;/span&gt;] = &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;peer&lt;/span&gt;{&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;} } }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// and the tick pings every peer in the table, verified or not&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;pingTick&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;C&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;pid&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;peers&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;edge&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Since&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;seen&lt;/span&gt;) &amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;DROP&lt;/span&gt; { delete(&lt;span style=&#34;color:#a6e22e&#34;&gt;peers&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;pid&lt;/span&gt;) }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Since&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;peermsg&lt;/span&gt;) &amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PING&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;pktout&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;pkt&lt;/span&gt;{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;dave&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;M&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;Op&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;dave&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Op_GETPEER&lt;/span&gt;}, &lt;span style=&#34;color:#a6e22e&#34;&gt;addr&lt;/span&gt;} }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A newly gossiped peer has never sent a &lt;code&gt;PEER&lt;/code&gt; message, so &lt;code&gt;time.Since(p.peermsg)&lt;/code&gt; is enormous and it gets a &lt;code&gt;GETPEER&lt;/code&gt; on the very next tick. Which means: once an edge node has told A about B and B about A, both sides start pinging each other within eight seconds, independently. Each one&amp;rsquo;s outbound &lt;code&gt;GETPEER&lt;/code&gt; opens its own router for the other. Whichever packet arrives once both mappings exist gets through, and the receiver adds the sender on sight.&lt;/p&gt;&#xA;&lt;p&gt;That&amp;rsquo;s a simultaneous open with nobody coordinating it. It works through full-cone and address- or port-restricted routers, which is most domestic kit; symmetric NAT and carrier-grade NAT still defeat it, because there the external port differs per destination and the address the edge saw is no use to anyone else. Good enough that I ran Dave across EC2 instances and Raspberry Pis at home without ever writing traversal code.&lt;/p&gt;&#xA;&lt;p&gt;There&amp;rsquo;s a defence in there too, and it&amp;rsquo;s the detail I&amp;rsquo;d most like to have back if I&amp;rsquo;d lost it. Look at the two constants:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;DROP&lt;/span&gt;        = &lt;span style=&#34;color:#ae81ff&#34;&gt;16&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Second&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Time until silent peers are dropped from the peer table.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;SHARE_DELAY&lt;/span&gt; = &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Second&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Time until new peers are shared. Must be greater than DROP.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When you reply to a &lt;code&gt;GETPEER&lt;/code&gt;, you only offer peers you&amp;rsquo;ve known for at least &lt;code&gt;SHARE_DELAY&lt;/code&gt;. A peer that never answers is deleted at &lt;code&gt;DROP&lt;/code&gt;. Sixteen is less than twenty, so &lt;strong&gt;an address that doesn&amp;rsquo;t respond is dropped before it ever becomes eligible to be passed on&lt;/strong&gt;. You can inject a bogus address into one node&amp;rsquo;s table — anyone can, it&amp;rsquo;s a UDP packet — but it dies there, and no amount of injecting makes it spread. That&amp;rsquo;s peer-table poisoning closed off by an inequality between two constants rather than by any code that knows what poisoning is.&lt;/p&gt;&#xA;&lt;p&gt;The same section has a rate limit on &lt;code&gt;PEER&lt;/code&gt; messages: one accepted per &lt;code&gt;PING&lt;/code&gt; interval, so a malicious peer can&amp;rsquo;t flood you with addresses to drown out the honest ones. I found that hole while writing the 2024 post and fixed it in the same afternoon, which is the only time writing an article has directly found me a bug.&lt;/p&gt;&#xA;&lt;p&gt;Two honest limits on that, before I sound too pleased with myself. It stops addresses that are dead or invented; it does nothing about a live malicious node, which responds and therefore gets shared like anyone else. And discv5&amp;rsquo;s endpoint proof — the grown-up version of this — has exactly the same limitation, so at least the ceiling is in the right place.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;An endpoint proof is the explicit form: before a node will pass your address on, you have to echo back a token it sent to that address, which proves something live is listening there and controls it. Dave gets a weaker version of the same guarantee out of an inequality between two timeouts, and pays no round trip for it.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;I should be calibrated about the rest of it too. Combining discovery with liveness is what Kademlia&amp;rsquo;s &lt;code&gt;FIND_NODE&lt;/code&gt; does. Refusing to gossip reputation is a call several deployed networks got wrong, but it isn&amp;rsquo;t new. Implicit hole punching happens incidentally in most DHTs — what&amp;rsquo;s unusual here is only that it was designed for rather than stumbled into. The delay inequality is the one I&amp;rsquo;d defend as properly neat, and even there the &lt;em&gt;goal&lt;/em&gt; is standard; it&amp;rsquo;s getting it from an ordering between two timeouts rather than a round trip and some cryptography that I haven&amp;rsquo;t seen elsewhere.&lt;/p&gt;&#xA;&lt;p&gt;So: not novel, mostly. I&amp;rsquo;ve stopped thinking that matters much. Landing independently on the same answers as the systems that got it right is the outcome you want from a first attempt at something, and it&amp;rsquo;s a better one than a genuinely new idea that doesn&amp;rsquo;t work.&lt;/p&gt;&#xA;&lt;p&gt;None of this is what went wrong. The dissemination and storage side is where it comes apart, and it does so in three separate ways.&lt;/p&gt;&#xA;&lt;h2 id=&#34;one-packet-is-enough&#34;&gt;One packet is enough&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#one-packet-is-enough&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Look at what happens to mass as age approaches zero.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/dave/mass.svg&#34; alt=&#34;A curve of mass against age, rising steeply towards infinity as age approaches zero, crossing a dashed line marked MAXTRUST = 25 at an age of 0.32 milliseconds&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;The cheapest legal dat is eight leading zero bits, about 256 hashes to find. At any age below 0.32 ms it is worth more than the entire trust cap.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;Trust accumulates like this:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;novel&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;trust&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;peers&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;pkfp&lt;/span&gt;].&lt;span style=&#34;color:#a6e22e&#34;&gt;trust&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;trust&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MAXTRUST&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;trust&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Mass&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;pk&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;msg&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;W&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;Btt&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;pk&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;msg&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;T&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;MAXTRUST&lt;/code&gt; is 25. The cheapest dat the network accepts has eight leading zero bits, which costs about 256 hashes to find — call it free. Mass for that dat is &lt;code&gt;8 / age_in_ms&lt;/code&gt;, so it exceeds 25 at any age below 0.32 milliseconds.&lt;/p&gt;&#xA;&lt;p&gt;An attacker mints a dat, hands it straight to the socket, and it arrives at a peer sub-millisecond on a LAN or from a colocated machine. It is novel by construction, because they invented it. One packet takes them from nothing to the trust ceiling.&lt;/p&gt;&#xA;&lt;p&gt;And if the timestamp on the dat is the current millisecond, &lt;code&gt;time.Since(t).Milliseconds()&lt;/code&gt; is zero, &lt;code&gt;1 / 0&lt;/code&gt; in float64 is &lt;code&gt;+Inf&lt;/code&gt;, and mass is infinite. The cap saves the arithmetic from doing any damage, but only by discarding whatever the number was supposed to mean.&lt;/p&gt;&#xA;&lt;p&gt;The mistake underneath the arithmetic is the interesting one. I was rewarding peers for supplying &lt;em&gt;novel data with valid proof of work&lt;/em&gt;, and I&amp;rsquo;d convinced myself that was evidence of useful contribution. It isn&amp;rsquo;t. It&amp;rsquo;s evidence of having spent CPU, and the attacker has CPU. A reputation metric only works if the thing it rewards is scarce to an adversary and abundant to an honest node, and &amp;ldquo;bytes nobody has seen before&amp;rdquo; is the opposite: trivially abundant to whoever is willing to make some up.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;MAXTRUST&lt;/code&gt; exists precisely because I&amp;rsquo;d thought about farming — an uncapped score is obviously gameable, and the cap bounds what any peer can accumulate no matter how much they send. What defeats it is the interaction between the cap and the divergence, and that only shows up if you take the formula to its limit and ask what mass is worth at an age of zero. A cap you can reach in one packet isn&amp;rsquo;t a reputation system; it&amp;rsquo;s a single bit, and the bit says &amp;ldquo;has sent me something&amp;rdquo;, which every attacker can also say. The defence was real. It was aimed at the wrong axis.&lt;/p&gt;&#xA;&lt;h2 id=&#34;mass-was-recency-wearing-a-costume&#34;&gt;Mass was recency wearing a costume&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#mass-was-recency-wearing-a-costume&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Dats are stored in shards, and the shard is chosen by difficulty:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;keys&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;work&lt;/span&gt; []&lt;span style=&#34;color:#66d9ef&#34;&gt;byte&lt;/span&gt;) (&lt;span style=&#34;color:#66d9ef&#34;&gt;uint8&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;uint64&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; min(&lt;span style=&#34;color:#a6e22e&#34;&gt;MAXWORK&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;nzerobit&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;work&lt;/span&gt;)) &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MINWORK&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;xxhash&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sum64&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;work&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every dat in shard &lt;em&gt;k&lt;/em&gt; has exactly &lt;em&gt;k&lt;/em&gt; + 8 leading zero bits. When the pruner keeps the most massive dats in a shard, the difficulty term in &lt;code&gt;mass&lt;/code&gt; is the same constant for every dat it&amp;rsquo;s comparing — so the comparison collapses to one over age. Within a shard, mass &lt;em&gt;is&lt;/em&gt; recency. The difficulty half of the formula does no work at all.&lt;/p&gt;&#xA;&lt;p&gt;It isn&amp;rsquo;t quite doing nothing globally, and what it &lt;em&gt;is&lt;/em&gt; doing is the best idea in the codebase — which I only worked out properly while writing this.&lt;/p&gt;&#xA;&lt;p&gt;Difficulty picks your shard, and each shard has its own independent capacity. So a harder proof doesn&amp;rsquo;t buy you priority directly; it moves you into a &lt;strong&gt;less contended queue&lt;/strong&gt;. And because every node prunes its own shards independently, the effect compounds across the network into a self-clearing price for permanence. Want a dat to survive? Do more work and land somewhere emptier. As more people do sixteen bits, that tier fills and the price drifts to twenty. Nobody sets the price, nobody verifies anything, no payment changes hands, and there is no ledger, no contract and no challenge-response anywhere in it. It falls out of local pruning decisions alone.&lt;/p&gt;&#xA;&lt;p&gt;That&amp;rsquo;s worth sitting with, given where the project went. Every other way of buying durability in a decentralised store is heavy: Filecoin and Arweave use money and proofs of storage; Freenet uses popularity. The version of Dave I abandoned used &lt;strong&gt;storage challenges&lt;/strong&gt; — the mechanism that dragged in replica tracking and reassignment and eventually ate the whole design. I already had a mechanism that gets a weaker version of the same property for nothing, and I walked past it to build the expensive one.&lt;/p&gt;&#xA;&lt;p&gt;I can&amp;rsquo;t claim it&amp;rsquo;s unprecedented; I know the well-known systems rather than every paper. But I haven&amp;rsquo;t seen proof-of-work difficulty used as a storage tier with per-tier caps anywhere else, and I&amp;rsquo;d have expected to.&lt;/p&gt;&#xA;&lt;p&gt;What defeats it in practice is mundane. Almost nobody does more work than the minimum, because nothing in the interface suggests a reason to, so everything piles into the first shard and fights over the same slots while fifty-five tiers above it sit empty. A market with one participant doesn&amp;rsquo;t clear. Combined with the mass formula collapsing to recency, the effective policy was &amp;ldquo;keep the most recent few thousand dats&amp;rdquo;, which is not what the documentation said. The interaction is subtle enough that I wrote both halves and still didn&amp;rsquo;t see it: partitioning by difficulty and &lt;em&gt;then&lt;/em&gt; ranking by a formula containing difficulty looks like belt and braces, right up until you notice the second term is identical across everything it&amp;rsquo;s being asked to compare.&lt;/p&gt;&#xA;&lt;p&gt;The idea deserves a better implementation than I gave it, and it&amp;rsquo;s separable from all of this — &lt;a href=&#34;#a-loose-end-worth-picking-up&#34;&gt;see below&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-anonymity-claim-was-too-strong&#34;&gt;The anonymity claim was too strong&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-anonymity-claim-was-too-strong&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;From the README on that pinned commit:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Propagating dats in this way ensures that an adversary is unable to create a timing attack to discern the source of a dat, even with a broad view of network traffic.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;The mechanism is sound and I still like it. Because every node emits one random dat to one random peer every epoch, at a constant rate, there&amp;rsquo;s no correlation between when a node &lt;em&gt;received&lt;/em&gt; something and when it passes it on. A forwarder is genuinely indistinguishable from another forwarder.&lt;/p&gt;&#xA;&lt;p&gt;But that&amp;rsquo;s forwarding. Origination is a different act, and the code does it differently:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dave&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Op_DAT&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;put&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;dats&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Dat&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;m&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;V&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;m&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;S&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;m&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;W&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;Btt&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;m&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;T&lt;/span&gt;)})&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;rndpeers&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;peerList&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;trustSum&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;FANOUT&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;pktout&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;pkt&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;m&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;addr&lt;/span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When an application writes a dat, it goes out to five peers immediately, and then repeats on a timer for however many rounds the caller asked for. The originator is the first node to transmit it and the most frequent. Random push hides everyone &lt;em&gt;downstream&lt;/em&gt; of the source. It doesn&amp;rsquo;t hide the source, which is precisely what the sentence claims it does.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The known fix is Dandelion, from Bitcoin&amp;rsquo;s transaction relay: pass a new item along a single random path for a few hops before anybody broadcasts it, so the first node seen shouting is not the one that wrote it. It costs latency on exactly the data you most want to arrive quickly.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;I don&amp;rsquo;t think I was being dishonest when I wrote that. I think I&amp;rsquo;d designed one mechanism carefully, and then let the claim it supported creep one step wider than the evidence.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-one-i-never-solved&#34;&gt;The one I never solved&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-one-i-never-solved&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Those three are things I got wrong. This one is different — it&amp;rsquo;s the problem I worked hardest on, never cracked, and still think is the most interesting thing in the project.&lt;/p&gt;&#xA;&lt;p&gt;Every epoch a node sends one dat to one peer. Which dat?&lt;/p&gt;&#xA;&lt;p&gt;v1&amp;rsquo;s answer is: pick one at random. That works beautifully at the scale I ran it — data moved around exactly as it should across a handful of machines. But the selection probability for any given dat is one in N, so as the store grows a newly-arrived dat waits proportionally longer for its turn. At a thousand dats it&amp;rsquo;s fine. At a million, a new dat is essentially never chosen, and propagation stops working at all.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/dave/select.svg&#34; alt=&#34;Two panels. On the left, one large box of many identical dots with a single arrow leaving it, one dot highlighted and lost among the others. On the right, a small box of a few highlighted dots with several fast arrows leaving, above a large box of many faint dots with a single slow arrow in each direction&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;The left panel is what v1 does. The right is what the problem actually wants — two mechanisms, because the two jobs have opposite requirements.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;I tried a lot of things. The one still in the pinned code is a &lt;strong&gt;ring buffer&lt;/strong&gt;: every novel dat is written to a thousand-entry ring, and each push epoch sends one dat from the ring as well as one from the store at large. That genuinely fixes the acute half — new data propagates immediately, however big the store gets.&lt;/p&gt;&#xA;&lt;p&gt;What it doesn&amp;rsquo;t fix is the tail. A dat that falls out of the ring is back to one in N, so anything a node missed while offline, or anything that existed before it joined, only ever reaches it by luck. I also tried cuckoo filters: track what each peer has already seen, and don&amp;rsquo;t waste a push sending them something they&amp;rsquo;ve got. That work is on a branch called &lt;code&gt;filter&lt;/code&gt; and it isn&amp;rsquo;t in the pinned version, which tells you how it went.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;None of it worked because I was asking one mechanism to do two jobs with opposite requirements.&lt;/strong&gt; Getting new data out fast wants a small working set, aggressive sending, and a short life. Repairing the long tail wants completeness, an unbounded working set, and can afford to be slow. A single random pick from a single pool is a compromise that is bad at both.&lt;/p&gt;&#xA;&lt;p&gt;The literature has known this since 1987. Demers et al., &lt;em&gt;Epidemic Algorithms for Replicated Database Maintenance&lt;/em&gt;, splits it exactly along that line: &lt;strong&gt;rumour mongering&lt;/strong&gt; for new items — push them hard, and each time you push to someone who already has it, probabilistically stop, so the hot set limits itself and stays small — and &lt;strong&gt;anti-entropy&lt;/strong&gt; in the background, periodically reconciling state with a random peer. You run both. My ring buffer is rumour mongering with its stopping rule missing; I had the instinct without the feedback that makes it terminate.&lt;/p&gt;&#xA;&lt;p&gt;And the cuckoo filter was simply the wrong primitive, which is the part I&amp;rsquo;d most want to tell 2024 me. A membership filter answers &lt;em&gt;&amp;ldquo;do you have this one?&amp;rdquo;&lt;/em&gt;, an item at a time. The question you actually need answered is &lt;em&gt;&amp;ldquo;what are you missing?&amp;rdquo;&lt;/em&gt; — in one round trip, in bytes proportional to the size of the difference rather than the size of the set. That&amp;rsquo;s &lt;strong&gt;set reconciliation&lt;/strong&gt;, and it&amp;rsquo;s a different tool entirely: Merkle trees over sorted ranges, as Cassandra and Dynamo use for repair; IBLTs, as Graphene uses for blocks; or PinSketch, which is what Bitcoin&amp;rsquo;s Erlay uses — having hit precisely this problem with transaction relay and solved it by pairing low-fanout flooding with periodic reconciliation.&lt;/p&gt;&#xA;&lt;p&gt;The rewrite changes the shape of the question rather than answering it head-on. Flooding doesn&amp;rsquo;t select: every cell goes to every neighbour exactly once, deduplicated by the seen-set, so for new data there&amp;rsquo;s no choice to make and the problem doesn&amp;rsquo;t arise. What&amp;rsquo;s left is the tail — a node that was offline, or has just joined — and that is now a separate mechanism with its own name, the time-range sync. It&amp;rsquo;s the same split Demers describes, arrived at from the other direction.&lt;/p&gt;&#xA;&lt;p&gt;Which puts the interesting work in exactly one place. That sync is currently naive and re-sends things you already have. Replacing it with real range-based reconciliation is the piece I most want to build, and it&amp;rsquo;ll be the first time in this project that the hard problem in front of me is one with a well-understood answer.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-question&#34;&gt;The question&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-question&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s the one that actually killed it.&lt;/p&gt;&#xA;&lt;p&gt;A dat is addressed by its own work hash. To retrieve one, you send &lt;code&gt;GET&lt;/code&gt; with that hash, and any peer that has it replies. So: how do you get the hash?&lt;/p&gt;&#xA;&lt;p&gt;You don&amp;rsquo;t. Not from the network. There&amp;rsquo;s no index, no enumeration, no namespace, nothing to browse. You can only ask for something you were already told about through some other channel — and if that channel exists, a lot of the reason for the network existing has quietly moved into it.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s worse than that, because &lt;code&gt;GET&lt;/code&gt; doesn&amp;rsquo;t route. A read checks your own store first, and on a hit it returns without sending a single packet:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dave&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Op_GET&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;shardKey&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;datKey&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;keys&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;m&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;W&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;dat&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dats&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;shardKey&lt;/span&gt;][&lt;span style=&#34;color:#a6e22e&#34;&gt;datKey&lt;/span&gt;]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;found&lt;/span&gt; = &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;apprecv&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;dat&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;found&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;rndpeers&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;peerList&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;trustSum&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;FANOUT&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;pktout&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;pkt&lt;/span&gt;{&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;dave&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;M&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;Op&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;dave&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Op_GET&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;W&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;m&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;W&lt;/span&gt;}, &lt;span style=&#34;color:#a6e22e&#34;&gt;addr&lt;/span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/dave/lookup.svg&#34; alt=&#34;Two panels. On the left, a node fires five arrows at randomly chosen peers while the node actually holding the data sits untouched, marked with a cross. On the right, every node in the network is filled in, and the reader reads from itself, marked with a tick&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Left: what godave did on a local miss. Right: what the rewrite does instead. The second panel isn&amp;#39;t a better lookup — it&amp;#39;s the absence of one.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;On a miss it goes to five peers picked at random and repeats on a timer. No key space, no distance metric, nothing pointing the query towards whoever holds the data.&lt;/p&gt;&#xA;&lt;p&gt;That local check is worth dwelling on, because it looks like the answer and isn&amp;rsquo;t. It&amp;rsquo;s a cache lookup, and a cache is only as good as the thing it misses to. What sits in a node&amp;rsquo;s store is a random sample of recent traffic — you receive one random dat per peer per epoch, and the pruner discards everything past the shard capacity every minute. The chance that an arbitrary hash happens to be in there is low, and it falls as the network holds more data. The fallback is doing the real work, and the fallback is the broken part.&lt;/p&gt;&#xA;&lt;p&gt;Which is also why I didn&amp;rsquo;t see this for months. On a dozen nodes on my desk, with capacity far exceeding the data, the local hit rate is close to one and the network path almost never fires — and when it does, five out of twelve peers is most of the network. Dave behaved correctly at every scale I ever ran it at. The lookup only fails once the data in the network exceeds what a single node can hold, which is precisely the point at which you would want it to work. I called Dave a distributed hash table in that first article. It was a distributed hash table with no table, and a small enough network to hide it.&lt;/p&gt;&#xA;&lt;p&gt;So I gave dats a key, and an owner, and a signature — the commit at the top of this piece. Now data had a name you could form without being told it, and a namespace per public key you could enumerate. That is a genuine fix for a genuine problem, and I&amp;rsquo;d make the same call again with the information I had.&lt;/p&gt;&#xA;&lt;p&gt;What followed was mechanical. If dats have keys, the sensible place to put a dat is near its key, so I added XOR distance. If placement is deterministic, some node is &lt;em&gt;supposed&lt;/em&gt; to hold each dat, so I added replica tracking. If replicas are tracked, peers leaving means reassigning them, so I added that. If replicas can be reassigned, I need to know whether a peer is actually storing what it claims, so I added storage challenges. Values then outgrew a UDP datagram, so I added a TCP fallback — a second transport, with its own code path for the same operation.&lt;/p&gt;&#xA;&lt;p&gt;Every one of those follows from the one above it. Not one of them was a bad decision on its own terms. Together they deleted the property the whole thing was for: once placement is deterministic, an observer knows exactly which nodes should hold what, and &amp;ldquo;random push for anonymity&amp;rdquo; no longer hides anything at all.&lt;/p&gt;&#xA;&lt;p&gt;By December I was sketching a third system inside the second — direct messaging, layered on the keyed store, with recipient-keyed namespaces. I never committed it. That&amp;rsquo;s where I stopped.&lt;/p&gt;&#xA;&lt;h2 id=&#34;deleting-the-question&#34;&gt;Deleting the question&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#deleting-the-question&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I&amp;rsquo;m writing it again, in Zig, and the whole design rests on one rule I wrote down before any code:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;A node never needs to know anything about the network beyond its immediate neighbours.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;The point of the rule isn&amp;rsquo;t what it permits. It&amp;rsquo;s what it refuses. &lt;code&gt;GET(key)&lt;/code&gt; means routing a query towards a node you can&amp;rsquo;t see, so there is no lookup — and with no lookup there is no placement, no replica set, no reassignment, and no storage challenge. The entire collapse is rejected by one sentence, and it&amp;rsquo;s rejected &lt;em&gt;automatically&lt;/em&gt;, before I have a chance to talk myself into each step.&lt;/p&gt;&#xA;&lt;p&gt;So how do you read anything, with no lookup?&lt;/p&gt;&#xA;&lt;p&gt;You already have it. Everything floods to everyone, and reading is a query against your own disk. Following someone is a filter over your own store by their key. There&amp;rsquo;s no relay to ask, no query to route, and nothing to place, because nothing was ever anywhere in particular.&lt;/p&gt;&#xA;&lt;p&gt;That is the same first move v1 made, with a completely different guarantee behind it. v1 checked a cache and fell back to a broken query; this checks a replica, and there is no fallback because none is needed. The difference isn&amp;rsquo;t the lookup — both start by looking in the same place. The difference is whether the design is working for or against that lookup succeeding. Random push and mass pruning actively hold a v1 node&amp;rsquo;s store down to a small sample; flooding and a retention window deliberately fill it.&lt;/p&gt;&#xA;&lt;p&gt;That sounds like it can&amp;rsquo;t possibly scale, and it scales further than I expected. At a million users each posting ten times a day, a node carries about 0.6 Mbit/s and 2% of one core. Ingress doesn&amp;rsquo;t bind until several hundred million users on a normal home connection. Storage is the real cost, and it&amp;rsquo;s each node&amp;rsquo;s own choice of how far back to keep — a phone keeps a week, a machine with a spare disk keeps everything and becomes an archive.&lt;/p&gt;&#xA;&lt;p&gt;That last part is deliberately not a protocol feature. There&amp;rsquo;s no mechanism to declare an archive, verify one, reward one or challenge one, and there is never going to be, because that mechanism is exactly what &amp;ldquo;storage challenges&amp;rdquo; were. Data stays available for as long as somebody chooses to keep it, which is how Usenet worked and how Nostr works. Any protocol promising more than that is either lying or has an accounting system that will eventually eat it, and I&amp;rsquo;ve now written the accounting system that ate mine.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;That is the second answer, and it works by refusing the question.&lt;/strong&gt; Nobody is doing anyone a favour, so there is nothing to pay for: you receive everything regardless, and you keep the part you want to read. The simplest possible incentive structure is no incentive structure — just self-interest, and no coordination because none is required.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s an honest answer and I think it&amp;rsquo;s the right one to build on. It also has a hole in it, which took me a while to see. Self-interest keeps the data &lt;em&gt;somebody wants&lt;/em&gt;. It does nothing at all for data that nobody in particular wants yet — the unpopular, the archival, the thing you publish today that matters in five years. Under this scheme that data quietly evaporates, and there is no way to pay to keep it, because paying is the thing we removed.&lt;/p&gt;&#xA;&lt;p&gt;Everything on the wire is one fixed size:&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/dave/cell.svg&#34; alt=&#34;A byte-layout diagram of a 224-byte cell: a one-byte version field, a one-byte hop counter, a 32-byte ephemeral key, a 16-byte authentication tag, and 174 bytes of ciphertext&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Every packet is exactly this shape, so size and structure leak nothing. A message that doesn&amp;#39;t fit is split across several cells, each sealed independently.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;There&amp;rsquo;s no recipient field. Every node tries to open every cell against its own key, and the authentication tag says whether it worked — so nobody, including the node holding a cell, can tell who it&amp;rsquo;s for. On my machine that costs 31 microseconds a cell, essentially all of it the X25519 scalar multiplication.&lt;/p&gt;&#xA;&lt;p&gt;The peer layer comes across almost unchanged: discovery and liveness in one message pair, gossip only in replies, sharing gated behind a delay longer than the drop timeout, and reputation kept strictly local and never gossiped. The one correction is that identity becomes the public key rather than the address, because a table keyed by &lt;code&gt;ip:port&lt;/code&gt; treats a node that moves as a stranger — and on IP, moving is the normal case, not the exception.&lt;/p&gt;&#xA;&lt;p&gt;The traversal needs writing down this time, which it never was. Nothing in v1 records that the ping interval is load-bearing for anything but liveness, and the obvious optimisation — back off the tick, skip unverified peers — is correct for liveness and quietly fatal for hole punching. I nearly made exactly that change while reviewing this code, having read the same three lines and concluded the traversal was accidental. It wasn&amp;rsquo;t; it was the reason the discovery design looks the way it does. The mechanism survived, and the note explaining it didn&amp;rsquo;t.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-zig&#34;&gt;Why Zig&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#why-zig&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Partly because I want to, which is a sufficient reason for a hobby project. But there&amp;rsquo;s a real argument too, and it&amp;rsquo;s about where this code can run.&lt;/p&gt;&#xA;&lt;p&gt;The protocol has no business needing an allocator on the receive path: a cell is a fixed 224 bytes, and opening one is arithmetic on a fixed-size buffer. Zig lets me write that once and compile it for a server or for a microcontroller with no operating system underneath it. I checked rather than assumed — the whole receive path, X25519 and BLAKE3 and ChaCha20-Poly1305 and the framing, cross-compiles to a Cortex-M4 in an eighteen-kilobyte object, with no allocator and no OS.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;m not building the radio version now; I scoped this rewrite to IP so that it might actually get finished. But it means the door stays open, and it&amp;rsquo;s the one thing this project can do that the mature alternatives can&amp;rsquo;t.&lt;/p&gt;&#xA;&lt;p&gt;Zig 0.16 is also recent enough that most of what&amp;rsquo;s written about the language online no longer compiles. &lt;code&gt;main&lt;/code&gt; takes a &lt;code&gt;std.process.Init&lt;/code&gt;, randomness comes through &lt;code&gt;std.Io&lt;/code&gt;, &lt;code&gt;std.time.Timer&lt;/code&gt; is gone. I&amp;rsquo;ve been finding these by compiling rather than by reading.&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-loose-end-worth-picking-up&#34;&gt;A loose end worth picking up&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#a-loose-end-worth-picking-up&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The third answer is the one I didn&amp;rsquo;t recognise until I sat down to write this, and it had been in the code since 2024. It&amp;rsquo;s also the piece that fills the hole in the second: a way to buy permanence for data nobody else wants, paid for in work rather than money, with nothing keeping accounts.&lt;/p&gt;&#xA;&lt;p&gt;I want to write it down properly, because it doesn&amp;rsquo;t need a peer-to-peer network to be interesting and I&amp;rsquo;d like to come back to it.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The mechanism.&lt;/strong&gt; Every stored item carries a proof of work of difficulty &lt;code&gt;d&lt;/code&gt;, in leading zero bits. The store is partitioned into tiers by &lt;code&gt;d&lt;/code&gt;, and each tier has its own capacity. When a tier is full, it evicts within itself — oldest first is fine. A writer chooses &lt;code&gt;d&lt;/code&gt; before publishing, so a writer is choosing which queue to stand in.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Why it should clear.&lt;/strong&gt; Treat each tier as a queue with capacity &lt;code&gt;C&lt;/code&gt; and arrival rate &lt;code&gt;λ&lt;/code&gt;. In steady state, an item&amp;rsquo;s expected residency is &lt;code&gt;T = C / λ&lt;/code&gt; — Little&amp;rsquo;s law, and for evict-oldest it&amp;rsquo;s exact. So the writer faces a straight trade: pay &lt;code&gt;2^d&lt;/code&gt; hashes, receive &lt;code&gt;C_d / λ_d&lt;/code&gt; seconds of retention.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Little&amp;rsquo;s law is &lt;code&gt;L = λW&lt;/code&gt;: the average number of items in any stable queue is the arrival rate times the average time each one spends in it. It assumes nothing whatsoever about the arrival distribution, which is why it is safe to lean on here — a store being filled by strangers is not going to be Poisson.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;That&amp;rsquo;s a market, and it clears without anyone running it. If a tier gets popular its arrival rate climbs, residency falls, and the marginal writer moves up a bit. Cost doubles per bit while capacity stays put, so demand spreads itself across the tiers geometrically. No payment, no ledger, no proof of storage, no contract — just local eviction, on every node independently, adding up to a price.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Finding &lt;code&gt;d&lt;/code&gt; leading zero bits takes &lt;code&gt;2^d&lt;/code&gt; hashes on average, and the distribution is geometric, so the standard deviation is about the same size as the mean. A writer aiming at a tier will sometimes pay several times over and sometimes get in cheap, which is an odd property for a price to have.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;&lt;strong&gt;What was missing, and it&amp;rsquo;s the whole failure.&lt;/strong&gt; A market needs four things. A cost function: proof of work, present. Differentiated goods: the tiers, present. Independent clearing per good: per-tier caps, present. And &lt;strong&gt;a price signal&lt;/strong&gt; — which was entirely absent. Nothing in Dave ever told a writer that tier 0 was full and tier 12 was empty. There was no way to observe occupancy, so there was no way to act on it, so every writer rationally paid the minimum. The market had a price and no way to display it, and a market with one participant doesn&amp;rsquo;t clear.&lt;/p&gt;&#xA;&lt;p&gt;The fix is small: have nodes report how full each of their tiers is, and let a writer sample a few and pick a difficulty from how long they want the thing to live. Occupancy is aggregate information about your own disk, so publishing it leaks very little.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;How to test it with no network at all.&lt;/strong&gt; This is why it&amp;rsquo;s worth doing separately. Simulate a population of writers with some distribution of how much they care about persistence, and one node&amp;rsquo;s store with tiers and caps. Feed arrivals in, evict, and measure whether residency time ends up proportional to cost. That&amp;rsquo;s a discrete-event simulation of a caching policy in a couple of hundred lines, and it answers the question completely — no sockets, no peers, no protocol.&lt;/p&gt;&#xA;&lt;p&gt;Four things I&amp;rsquo;d want it to tell me:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;How to divide capacity across tiers.&lt;/strong&gt; v1 gave every tier the same cap, which wastes most of the disk when demand is concentrated at the bottom. Allocating in proportion to observed demand is self-defeating, because the tier you escape to grows to meet you. There&amp;rsquo;s a right answer here and I don&amp;rsquo;t know what it is.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Whether it oscillates.&lt;/strong&gt; Publish occupancy and everyone can see the same empty tier. They all move there, it fills, they all move again. That&amp;rsquo;s textbook herding, and it probably needs damping or hysteresis to settle.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;What lying buys you.&lt;/strong&gt; A node that reports tier 20 as empty makes writers burn work for nothing. Sampling several nodes and taking a median helps, and a writer can check empirically whether the item actually survived, but this needs thinking about.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Whether the absolute numbers work at all.&lt;/strong&gt; Dave&amp;rsquo;s minimum was eight bits — 256 hashes, which is nothing. Twenty bits is about a million, still only seconds of CPU, so the &amp;ldquo;expensive&amp;rdquo; tiers can be squatted cheaply. The scheme only prices anything if the top tiers are genuinely costly, which means calibrating against current hardware rather than against a number I picked in 2024.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;If it survives that, it&amp;rsquo;s a retention policy any store could use, and it never needed to be part of a network in the first place.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-i-dont-know&#34;&gt;What I don&amp;rsquo;t know&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#what-i-dont-know&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Whether the question at the top has a good answer at all. Three attempts so far. Reward measured contribution — which failed, because anything I could measure locally was something an attacker could manufacture. Refuse the premise and rely on self-interest — which works, and leaves anything unpopular to rot. Price permanence in work and let local eviction clear a market nobody runs — which is untested, and is the only one of the three that&amp;rsquo;s an incentive structure in the sense I originally meant. One is built, one is a cautionary tale, and one is a weekend of simulation away from being either interesting or dead.&lt;/p&gt;&#xA;&lt;p&gt;Whether spam is survivable. Flooding means anyone can inject, everyone must process, and now that data is retained, spam persists as well as propagates. My current answer is to ration rather than reward — per-neighbour rate limits, and a single shared budget that &lt;em&gt;all&lt;/em&gt; unknown authors compete for, so ten thousand fake identities get one stranger&amp;rsquo;s worth of throughput between them. That&amp;rsquo;s an argument, not a result. So was the trust metric.&lt;/p&gt;&#xA;&lt;p&gt;Whether anyone will use it. Almost certainly not many, and that&amp;rsquo;s fine. I want it to be good, and used by a handful of people I know, which is a different target from the one I was implicitly aiming at in 2024 and a much more reachable one.&lt;/p&gt;&#xA;&lt;p&gt;What I do know is the thing I&amp;rsquo;d tell 2024 me. The first version had no rule that could reject a feature. Every addition answered &amp;ldquo;is this good?&amp;rdquo;, and each one honestly was. None of them ever had to answer &amp;ldquo;is this &lt;em&gt;ours&lt;/em&gt;?&amp;rdquo; — and by the time that question would have been useful, there was no longer a clear enough idea of what &amp;ldquo;ours&amp;rdquo; meant to answer it with.&lt;/p&gt;&#xA;&lt;p&gt;The code is at &lt;a href=&#34;https://github.com/intob/godave&#34;&gt;github.com/intob/godave&lt;/a&gt;, pinned where it was still one idea.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>How we built Soaring Spirit</title>
      <link>https://rmrf.uk/cs/2026-07-26-how-we-built-soaring-spirit/</link>
      <pubDate>Sun, 26 Jul 2026 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/cs/2026-07-26-how-we-built-soaring-spirit/</guid>
      <category>Computer Broscience</category>
      <description>Soaring Spirit is my first company, a Swiss GmbH running speedflying trips and coaching. The whole product runs as one Go binary on a Raspberry Pi in my house. Sixteen months in, this is what that choice has cost and what it has bought.</description>
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://soaringspirit.ch&#34;&gt;Soaring Spirit&lt;/a&gt; is my first company. It&amp;rsquo;s a Swiss GmbH, it runs speedflying and paragliding trips and coaching, and the people in it are pilots rather than programmers — Greg cooks and does the osteopathy, Jamie has more speedwing hours than almost anyone alive, Oli drives the bus and keeps the books, Nika gets us into the parts of Georgia nobody else can reach. So when I say &amp;ldquo;we built&amp;rdquo;, I mean the company is a we. The software is mine, and this post is about the software.&lt;/p&gt;&#xA;&lt;p&gt;First commit was 2025-03-13. Sixteen months and 706 commits later it books trips, takes deposits and balances through Stripe, issues invoices, runs a coaching-availability calendar, hosts a flying syllabus, keeps a database of speedflying incidents scraped from the Swiss and French fatality reports, and logs in every user without ever having stored a password.&lt;/p&gt;&#xA;&lt;p&gt;All of it is one Go binary, cross-compiled to arm64 and copied onto a Raspberry Pi in my house.&lt;/p&gt;&#xA;&lt;p&gt;I want to write about the constraints rather than the features, because the constraints are the interesting part. Nearly every decision here was some version of &amp;ldquo;what is the smallest thing that can possibly work&amp;rdquo;, and a few of them were wrong in ways that took months to surface.&lt;/p&gt;&#xA;&lt;h2 id=&#34;one-binary-one-machine&#34;&gt;One binary, one machine&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#one-binary-one-machine&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The deploy script is worth quoting more or less in full, because it is the whole operations story:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;GOOS&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;linux GOARCH&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;arm64 go build -ldflags &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;...&amp;#34;&lt;/span&gt; -o ss_linux_arm64 .&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;scp ss_linux_arm64 &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$HOST&lt;span style=&#34;color:#e6db74&#34;&gt;:~/ss.new&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ssh &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$HOST&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mv ~/ss.new ~/ss &amp;amp;&amp;amp; sudo systemctl restart ss&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;aside class=&#34;note&#34;&gt;The tunnel is the part that makes this respectable rather than reckless. The Pi holds an outbound connection to Cloudflare and nothing else; there is no inbound port open on my home network at all, and no origin address to find.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;That&amp;rsquo;s it. Build, copy, restart. The Pi sits behind a Cloudflare Tunnel, so there is no public IP, no inbound port forwarding, no load balancer and no bill. Postgres runs on the same machine. &lt;code&gt;pgbackrest&lt;/code&gt; ships backups to S3 on a schedule.&lt;/p&gt;&#xA;&lt;p&gt;The frontend is deployed by &lt;code&gt;rsync&lt;/code&gt;, and then the Cloudflare cache is purged. There is no build step for it at all — I&amp;rsquo;ll come back to that.&lt;/p&gt;&#xA;&lt;p&gt;The obvious objection is that this is one machine and one power cut. That&amp;rsquo;s true, and it&amp;rsquo;s a real risk I&amp;rsquo;ve accepted rather than solved. The mitigation is entirely on the data side: the backups are off-site and verified, and the binary can be rebuilt and redeployed onto anything with an ARM chip in about ninety seconds. What I&amp;rsquo;d lose in a hardware failure is availability, not the business. For a company that sells six trips a year to people who mostly book after talking to us in person, that&amp;rsquo;s the right trade. It would be a bad trade for something with real-time revenue.&lt;/p&gt;&#xA;&lt;p&gt;The less obvious cost is that one process serving both the API and the site means a deploy takes the whole site down for the second it takes systemd to restart. I&amp;rsquo;ve never minded this and I doubt anyone has noticed, but it&amp;rsquo;s the kind of thing that&amp;rsquo;s fine until it isn&amp;rsquo;t.&lt;/p&gt;&#xA;&lt;h2 id=&#34;ten-dependencies&#34;&gt;Ten dependencies&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#ten-dependencies&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The &lt;code&gt;go.mod&lt;/code&gt; has ten direct requirements: a Postgres driver, Stripe, WebAuthn, an SMTP pool, three image libraries, and three &lt;code&gt;golang.org/x&lt;/code&gt; packages. No web framework, no ORM, no dependency injection container, no code generation.&lt;/p&gt;&#xA;&lt;p&gt;The router is 154 lines. Here is essentially all of it:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Router&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;ServeHTTP&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ResponseWriter&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;req&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Request&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;path&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;req&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;URL&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Path&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;route&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;routes&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;params&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;matchPath&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;path&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;route&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Segments&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;route&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Method&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;req&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Method&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&#x9;&lt;span style=&#34;color:#75715e&#34;&gt;// ...inject params into the context, wrap in middleware, serve&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;notFoundHandler&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ServeHTTP&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;req&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A linear scan over about 110 routes, splitting the path on &lt;code&gt;/&lt;/code&gt; and comparing segments. It is not clever and it does not need to be — this is a Raspberry Pi answering maybe a few thousand requests a day, and the linear scan is nowhere near the top of any profile I&amp;rsquo;ve taken. I wrote it before Go 1.22 gave the standard library pattern matching, and I&amp;rsquo;ve left it because it works and because the middleware ordering is explicit in a way I like.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;m not going to pretend this is universally the right call. I wrote a router, a session store, an image pipeline and a mail queue that all exist as libraries, and each of them cost me a weekend I could have spent on the actual product. What I got back is that when something breaks at eleven at night before a trip, the stack trace goes through code I wrote, and I can read all of it. That has paid for itself several times.&lt;/p&gt;&#xA;&lt;p&gt;The honest counterweight is the test suite: about 11,000 lines of Go tests against 19,000 lines of everything else, across 31 integration test files that run against a real Postgres in Docker Compose. Writing your own primitives is only defensible if you also write the tests that a library would have shipped with. I didn&amp;rsquo;t understand that at the start and the early code shows it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;no-frontend-build-step&#34;&gt;No frontend build step&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#no-frontend-build-step&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The frontend is about 20,000 lines of hand-written JavaScript. Lit is vendored as a single file. There is no bundler, no transpiler, no &lt;code&gt;npm run build&lt;/code&gt;, no &lt;code&gt;node_modules&lt;/code&gt; in the deploy. Files are served from disk, so an edit is live on reload.&lt;/p&gt;&#xA;&lt;p&gt;Routes are plain objects with a dynamic import:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;path&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;trip/:id&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;component&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ss-trip-detail&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;action&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; () =&amp;gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;./components/app/trip/trip-detail.js&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The browser does the code splitting, because ES modules already are code splitting. Every component is a &lt;code&gt;LitElement&lt;/code&gt; with its styles in a tagged template literal next to its markup.&lt;/p&gt;&#xA;&lt;p&gt;What this buys is that the edit-to-live loop is a file save, and that there is no build to break. What it costs is that &lt;code&gt;node --check&lt;/code&gt; is the entire compile step — nothing catches a typo in a property name until it renders wrong. Over sixteen months, that has cost me less than I expected. Shadow DOM turns out to do most of what a build step&amp;rsquo;s CSS scoping would.&lt;/p&gt;&#xA;&lt;p&gt;It has one wart I still haven&amp;rsquo;t fixed properly. The server pre-renders the SPA shell&amp;rsquo;s &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, so it keeps its own table of route titles and descriptions in &lt;code&gt;app/prerender.go&lt;/code&gt;, mirroring the one in &lt;code&gt;site/index.js&lt;/code&gt;. Two hand-kept copies of the same data. It&amp;rsquo;s documented in three places and I&amp;rsquo;ve still got it wrong once.&lt;/p&gt;&#xA;&lt;h2 id=&#34;money-is-where-the-discipline-goes&#34;&gt;Money is where the discipline goes&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#money-is-where-the-discipline-goes&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Everything above is &amp;ldquo;how little can I get away with&amp;rdquo;. Bookings are where that stops.&lt;/p&gt;&#xA;&lt;p&gt;A booking moves through ten states — &lt;code&gt;DRAFT&lt;/code&gt;, &lt;code&gt;IN_REVIEW&lt;/code&gt;, &lt;code&gt;AWAITING_DEPOSIT&lt;/code&gt;, &lt;code&gt;DEPOSIT_PAID&lt;/code&gt;, &lt;code&gt;DEPOSIT_TO_REFUND&lt;/code&gt;, &lt;code&gt;DEPOSIT_REFUNDED&lt;/code&gt;, &lt;code&gt;AWAITING_BALANCE&lt;/code&gt;, &lt;code&gt;FINALISED&lt;/code&gt;, &lt;code&gt;VOID&lt;/code&gt;, &lt;code&gt;REJECTED&lt;/code&gt; — and the legal transitions are an explicit switch, not a convention:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;switch&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;current&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;BookingStateInReview&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;BookingStateAwaitingDeposit&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;BookingStateRejected&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;BookingStateVoid&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;BookingStateDepositPaid&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;BookingStateDepositToRefund&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;BookingStateAwaitingBalance&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;errInvalidTransition&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every transition goes through one function, which opens a transaction, takes &lt;code&gt;SELECT state FROM app.bookings WHERE id = $1 FOR UPDATE&lt;/code&gt;, validates, and writes with the old state in the &lt;code&gt;WHERE&lt;/code&gt; clause. The row lock is there because the check-then-write without it is a textbook TOCTOU: two concurrent callers both read &lt;code&gt;AWAITING_DEPOSIT&lt;/code&gt;, both decide the transition is legal, both proceed.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The transaction on its own wouldn&amp;rsquo;t have saved it. Postgres defaults to READ COMMITTED, where both callers can read &lt;code&gt;AWAITING_DEPOSIT&lt;/code&gt; and both carry on quite happily. &lt;code&gt;FOR UPDATE&lt;/code&gt; is the part that serialises them: the second one blocks on the row until the first commits, and then reads what it actually wrote.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;There&amp;rsquo;s one flag on it that took me a while to get right. A transition to the state you&amp;rsquo;re already in is treated as a no-op rather than an error, because Stripe redelivers webhooks and a retried handler re-observing its own applied state is normal. But a caller that&amp;rsquo;s about to issue an actual refund on the strength of that transition cannot accept a no-op — two callers racing there means refunding twice. So &lt;code&gt;RequireStateChange&lt;/code&gt; turns the no-op back into a conflict for exactly those callers:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Set this for callers about to perform an external side effect (e.g. a&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Stripe refund) on the strength of this transition, so a second,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// concurrent caller that raced past the same check doesn&amp;#39;t repeat the&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// side effect.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;the-retry-loop&#34;&gt;The retry loop&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-retry-loop&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The bug I&amp;rsquo;m most glad to have found is not in that machine, it&amp;rsquo;s in what surrounds it.&lt;/p&gt;&#xA;&lt;p&gt;Stripe redelivers any webhook that doesn&amp;rsquo;t return 2xx, backing off over several days. That&amp;rsquo;s correct for a transient failure — database down, network blip — where a later attempt might work. It is actively harmful for a deterministic rejection.&lt;/p&gt;&#xA;&lt;p&gt;During the host merge in July, a past &lt;code&gt;invoice.paid&lt;/code&gt; event got resent for a booking that had already reached &lt;code&gt;FINALISED&lt;/code&gt;. The handler read it as a deposit payment and attempted &lt;code&gt;FINALISED&lt;/code&gt; → &lt;code&gt;DEPOSIT_PAID&lt;/code&gt;. The state machine rejected it, correctly. The handler returned 500. Stripe retried. The state machine rejected it again, identically, because the state machine is deterministic — that&amp;rsquo;s the entire point of it.&lt;/p&gt;&#xA;&lt;p&gt;So it retried for days.&lt;/p&gt;&#xA;&lt;p&gt;The fix is a distinction rather than a patch: an invalid-transition error is now a sentinel that the webhook handler acknowledges with a 2xx, because &amp;ldquo;received, nothing to do&amp;rdquo; is the honest answer when the booking is already in its terminal state. Only genuinely transient errors get a 5xx. It&amp;rsquo;s obvious in hindsight and it was not obvious at all in advance, and it&amp;rsquo;s the clearest lesson the project has taught me — &lt;em&gt;retry semantics are a property of the error, not of the endpoint.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;let-the-database-say-no&#34;&gt;Let the database say no&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#let-the-database-say-no&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The application checks that two coaching sessions don&amp;rsquo;t overlap. That check is a race, always, no matter how carefully it&amp;rsquo;s written, because two requests can pass it concurrently before either commits.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;&lt;code&gt;btree_gist&lt;/code&gt; is needed because the constraint mixes two kinds of comparison: plain equality on &lt;code&gt;contact_id&lt;/code&gt; and overlap on the date range. A stock gist index handles the range; the extension is what lets an ordinary scalar sit beside it in the same index.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;Postgres will just do this for you:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CREATE&lt;/span&gt; EXTENSION &lt;span style=&#34;color:#66d9ef&#34;&gt;IF&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;EXISTS&lt;/span&gt; btree_gist;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;ALTER&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;TABLE&lt;/span&gt; app.coaching_sessions&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;ADD&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;CONSTRAINT&lt;/span&gt; excl_coaching_sessions_overlap&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    EXCLUDE &lt;span style=&#34;color:#66d9ef&#34;&gt;USING&lt;/span&gt; gist (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        contact_id &lt;span style=&#34;color:#66d9ef&#34;&gt;WITH&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        daterange(window_start, window_end, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;[]&amp;#39;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;WITH&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#66d9ef&#34;&gt;WHERE&lt;/span&gt; (status &lt;span style=&#34;color:#66d9ef&#34;&gt;IN&lt;/span&gt; (&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;PENDING&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;CONFIRMED&amp;#39;&lt;/span&gt;));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The application check stays, because it produces a friendly error message at the right moment in the UI. The constraint is the thing that&amp;rsquo;s actually true. I&amp;rsquo;ve come round to thinking this is the correct shape for most validation: the readable error in the app, the authority in the schema.&lt;/p&gt;&#xA;&lt;p&gt;The same instinct runs through the rest of the schema — seventy-odd numbered migration files, currently up to &lt;code&gt;0193&lt;/code&gt;. Images are content-addressed by SHA-256 and stored as &lt;code&gt;bytea&lt;/code&gt;, so uploading the same photo twice is a no-op; they&amp;rsquo;re resized and encoded to both JPEG and AVIF in-process, on the Pi, which is slower than I&amp;rsquo;d like and has never been slow enough to matter.&lt;/p&gt;&#xA;&lt;p&gt;My favourite migration is &lt;code&gt;0130_remove_passwords.sql&lt;/code&gt;, which drops six columns and is the entire content of the file. Login is passkeys, or a six-digit code by email. Nothing to leak.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Nothing to leak &lt;em&gt;from my database&lt;/em&gt;, which is not the same as nothing to leak. The six-digit code makes the mail provider the root of every account, so the security of the whole thing is now somebody else&amp;rsquo;s password reset rather than mine. That is a much better place for it to sit, and it is not nowhere.&lt;/aside&gt;&#xA;&#xA;&lt;h2 id=&#34;two-ways-to-get-email-wrong&#34;&gt;Two ways to get email wrong&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#two-ways-to-get-email-wrong&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Until three weeks ago, sending mail went straight to SMTP inline, and it managed to be wrong in two opposite directions at once.&lt;/p&gt;&#xA;&lt;p&gt;Most call sites sent from a detached goroutine and logged the error. A momentary SMTP outage meant a user silently never learned their booking was approved — no record, no retry, no way to answer &amp;ldquo;did they actually get told?&amp;rdquo;.&lt;/p&gt;&#xA;&lt;p&gt;The Stripe webhook did the opposite: it returned the send error up, so a failed email failed the whole webhook, and Stripe retried the hook and re-ran every side effect around it. A bounced email could void an invoice twice.&lt;/p&gt;&#xA;&lt;p&gt;Both are the same bug — treating delivery as part of the transaction that caused it. The fix is a transactional outbox: write the intent to a table, drain it from a worker with backoff and a max attempt count, and key rows on an idempotency key so a redelivered webhook produces one receipt rather than two. Delivered rows are kept for ninety days rather than deleted, because &amp;ldquo;did that user get told?&amp;rdquo; should be a query.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-thing-that-stopped-running&#34;&gt;The thing that stopped running&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-thing-that-stopped-running&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;For eleven days in July, every page on the site told Google it was a duplicate of the homepage.&lt;/p&gt;&#xA;&lt;p&gt;The SPA ships one generic &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, description and canonical in &lt;code&gt;index.html&lt;/code&gt;, and rewrites them in JavaScript once the router resolves. That&amp;rsquo;s invisible to anything that doesn&amp;rsquo;t execute JavaScript, so to fix it for crawlers there was a Cloudflare Pages Function — &lt;code&gt;site/functions/_middleware.js&lt;/code&gt; — which filled the head in server-side. It worked. It had worked since the day I wrote it.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Added 2026-07-01 with the rest of the SEO work — sitemap, &lt;code&gt;robots.txt&lt;/code&gt;, per-route meta. All of that survived the move. The middleware was the only piece that depended on &lt;em&gt;where&lt;/em&gt; the site was served from.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;Then on 15 July I moved site hosting off Pages and onto the Pi, so the site would deploy by &lt;code&gt;rsync&lt;/code&gt; and sit behind the same cache rules as everything else. That was a good change and I&amp;rsquo;d make it again. It also switched the middleware off, because a Pages Function is a thing Pages does, not a file that runs wherever it happens to sit.&lt;/p&gt;&#xA;&lt;p&gt;Nothing errored. Nothing logged. &lt;code&gt;site/functions/_middleware.js&lt;/code&gt; stayed in the repo looking exactly as maintained as it had the week before, and every page quietly went back to shipping &lt;code&gt;&amp;lt;link rel=&amp;quot;canonical&amp;quot; href=&amp;quot;https://soaringspirit.ch/&amp;quot;&amp;gt;&lt;/code&gt; — each one declaring itself a duplicate of the homepage.&lt;/p&gt;&#xA;&lt;p&gt;Eleven days is about as cheap as this class of bug gets, and I only caught it because I was planning the host merge and went to check what the middleware would do afterwards. What bothers me isn&amp;rsquo;t the eleven days, it&amp;rsquo;s that nothing could have caught it. There&amp;rsquo;s no test for &amp;ldquo;this file is still in a position to execute&amp;rdquo;. The deploy got faster and simpler, and what broke was a capability rather than a line of code.&lt;/p&gt;&#xA;&lt;p&gt;The fix folded into a merge I wanted anyway. &lt;code&gt;api.soaringspirit.ch&lt;/code&gt; and &lt;code&gt;soaringspirit.ch&lt;/code&gt; were two processes from the same binary on the same machine behind the same tunnel, and the split existed for no reason beyond history — while costing a CORS middleware, a preflight round-trip on every non-simple request, and a sitemap on the wrong host. Moving the API under &lt;code&gt;/api&lt;/code&gt; on the site origin deleted the CORS code entirely and let the head for &lt;code&gt;/trip/:id&lt;/code&gt; be rendered from a direct database query instead of an HTTP call to ourselves.&lt;/p&gt;&#xA;&lt;p&gt;Sixteen route collisions had to be resolved to do it, and &lt;code&gt;http.StripPrefix&lt;/code&gt; meant all 110 route registrations stayed exactly as written. Everyone gets the same correct head now — no user-agent sniffing, because varying HTML by user agent would fragment the edge cache and there&amp;rsquo;s no upside.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-id-tell-myself-at-the-first-commit&#34;&gt;What I&amp;rsquo;d tell myself at the first commit&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#what-id-tell-myself-at-the-first-commit&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Write the integration tests before the third feature, not after the thirtieth. They&amp;rsquo;re the only reason the no-framework approach is defensible, and I built up months of debt before I understood that.&lt;/p&gt;&#xA;&lt;p&gt;Push correctness into the schema earlier. Every exclusion constraint and foreign key I added late replaced application code I&amp;rsquo;d been quietly trusting.&lt;/p&gt;&#xA;&lt;p&gt;And when you move a platform, inventory what the old one was doing for you. Migration notes list what you&amp;rsquo;re carrying across, and I wrote good ones. What they don&amp;rsquo;t list is the work the old platform did for free, because nobody had to think about it while it worked — that&amp;rsquo;s exactly the part that leaves no diff, throws no error, and shows up weeks later in a search result. A file can keep being correct long after it has stopped being &lt;em&gt;reachable&lt;/em&gt;, and those two things fail completely independently.&lt;/p&gt;&#xA;&lt;p&gt;The company is sixteen months old and the software has never lost a booking. For a first attempt at both, I&amp;rsquo;ll take it.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Two stories about air</title>
      <link>https://rmrf.uk/cs/2026-04-28-two-stories-about-air/</link>
      <pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/cs/2026-04-28-two-stories-about-air/</guid>
      <category>Computer Broscience</category>
      <description>The first in a series about aerofoil design, written while building a simulation backend called Foilworks. Air behaves in two completely different ways depending on how close to the wing you are, and nearly every practical tool in the field is built on that split. This is the half with no friction in it — where lift comes from, and why the hardest part is a single missing equation.</description>
      <content:encoded>&lt;p&gt;I&amp;rsquo;ve been flying soft wings for years without really understanding the sections they&amp;rsquo;re built from. Paragliders, speedwings, kites — they all live or die on the shape of a slice through the wing, and I&amp;rsquo;ve mostly taken that shape on trust. So I&amp;rsquo;ve started building something to fix that: a simulation backend called &lt;strong&gt;Foilworks&lt;/strong&gt;, which takes a wing section, an angle, and an airspeed, and tells you what the air does about it.&lt;/p&gt;&#xA;&lt;p&gt;This is the first of what I expect to be quite a few posts. The early ones are about the field rather than about my code — the physics has to come first, or none of the rest means anything. My maths is not strong, so I&amp;rsquo;ve had to build the intuition first and let the equations follow, and that&amp;rsquo;s how I&amp;rsquo;m going to explain it. If you can picture air moving, you can follow all of this.&lt;/p&gt;&#xA;&lt;h2 id=&#34;where-friction-is-and-where-it-isnt&#34;&gt;Where friction is, and where it isn&amp;rsquo;t&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#where-friction-is-and-where-it-isnt&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Air behaves in two completely different ways depending on how close to the wing you are, and almost every practical aerodynamics tool is built on that split.&lt;/p&gt;&#xA;&lt;p&gt;Nearly everywhere, air is so slippery that friction may as well not exist. It just has to flow &lt;em&gt;around&lt;/em&gt; the shape rather than through it. Solve that, and you get the pressure everywhere on the surface — and pressure is where lift comes from.&lt;/p&gt;&#xA;&lt;p&gt;Then there&amp;rsquo;s a film glued to the surface, about one percent of the chord thick, where friction is the &lt;em&gt;only&lt;/em&gt; thing that matters. That film is where drag comes from, and where stall comes from.&lt;/p&gt;&#xA;&lt;p&gt;This post is entirely about the first story. It&amp;rsquo;s the easier one, it runs in about a millisecond, and it gets you lift and pressure. The second one is much harder, I&amp;rsquo;m still fighting it, and it gets its own post.&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-wing-is-a-list-of-points&#34;&gt;A wing is a list of points&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#a-wing-is-a-list-of-points&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The first job is turning &amp;ldquo;NACA 4412&amp;rdquo; into something a computer can chew on. You walk all the way around the section and write down coordinates, then join consecutive points with straight lines. Those straight segments are the &lt;strong&gt;panels&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The digits are the section: 4% maximum camber, sitting at 40% of the chord, 12% thick. That is the entire specification, which is why a 4-digit NACA section has a closed form and the code can generate the coordinates from a polynomial. Most sections worth flying are published as a table of ordinates and nothing else.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/panels.svg&#34; alt=&#34;An aerofoil drawn as 42 flat panels, with nodes marked at the corners and outward normals drawn along the surface&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Chord is always normalised to 1, so the nose is at x=0 and the tail at x=1, whatever the real wing measures.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;Two details in that picture do real work.&lt;/p&gt;&#xA;&lt;p&gt;The points aren&amp;rsquo;t evenly spaced. They&amp;rsquo;re bunched at the nose and the tail using a cosine distribution. Over the middle of a section the flow does almost nothing interesting — the pressure changes gently and slowly. Around the nose it goes berserk: air comes to a dead stop, then accelerates to well above freestream speed within a couple of percent of chord. Even spacing would spend most of your panels describing the boring part and smear the most important feature on the whole surface.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Cosine spacing is points at equal angles around a circle, projected down onto the chord. Equal steps in angle, unequal steps in x — dense at both ends where the circle is steepest, loose through the middle. One line of trigonometry, and the cheapest accuracy in the whole solver.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;And the direction you walk matters. I go clockwise — tail, along the bottom, round the nose, back along the top. Every panel computes its outward normal as &amp;ldquo;the direction I&amp;rsquo;m travelling, rotated ninety degrees&amp;rdquo;. Walk the other way and every normal points &lt;em&gt;into&lt;/em&gt; the wing, and the sign of the lift flips. I have this written in capital letters in my notes.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-trick-throw-the-wing-away&#34;&gt;The trick: throw the wing away&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-trick-throw-the-wing-away&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s where it gets clever, and where I had to stop and think for a while.&lt;/p&gt;&#xA;&lt;p&gt;You&amp;rsquo;d assume you solve the flow around a solid object. You don&amp;rsquo;t. You delete the object entirely and replace its surface with a &lt;strong&gt;vortex sheet&lt;/strong&gt; — imagine laying a carpet of tiny spinning rollers along where the skin used to be. Each roller drags the air around with it.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/vortex-sheet.svg&#34; alt=&#34;An aerofoil outline covered in small rotation markers, with freestream arrows approaching from the left and a red no-entry symbol over an arrow trying to cross the surface&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;The rollers have no physical existence. They&amp;#39;re a device for producing a flow that happens to follow the shape you care about.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;Now ask one question: &lt;em&gt;how hard does each roller have to spin so that the wind plus all the rollers together never pushes air through the line where the skin used to be?&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Answer that, and you&amp;rsquo;ve solved the flow — because a flow that doesn&amp;rsquo;t cross the skin is indistinguishable from a flow around a solid wing.&lt;/p&gt;&#xA;&lt;p&gt;Why spinning rollers, specifically? Because spin is what makes lift. You could lay down a carpet of little sprinklers instead, blowing air outwards, and they&amp;rsquo;d make the shape appear fatter — but they&amp;rsquo;d generate exactly zero lift no matter how you tuned them. Net circulation around the section &lt;em&gt;is&lt;/em&gt; lift. Vorticity is the only ingredient that can produce it.&lt;/p&gt;&#xA;&lt;p&gt;Chop the surface into N panels, let the spin vary linearly along each one, and the unknowns become the values at the N+1 corners. Then write one equation per panel: &lt;em&gt;at this panel&amp;rsquo;s midpoint, the flow perpendicular to the surface is zero.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;That&amp;rsquo;s N equations and N+1 unknowns. One short.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-bit-where-nature-has-to-choose&#34;&gt;The bit where nature has to choose&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-bit-where-nature-has-to-choose&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;That missing equation isn&amp;rsquo;t a bookkeeping mistake. It&amp;rsquo;s the most interesting thing in the whole method.&lt;/p&gt;&#xA;&lt;p&gt;Frictionless flow theory genuinely does not have a unique answer. There&amp;rsquo;s an entire family of valid solutions around any wing section — one for every possible amount of circulation — and every single one satisfies &amp;ldquo;no air through the skin&amp;rdquo; perfectly. In one of them the air whips around the sharp trailing edge from below to above. In another it goes the other way. In exactly one, the flow leaves the trailing edge smoothly, both streams merging and departing together.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/kutta.svg&#34; alt=&#34;Two trailing-edge close-ups side by side: on the left, marked with a cross and drawn in red, a streamline whipping around the sharp corner; on the right, marked with a tick and drawn in blue, two streams merging and leaving smoothly&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Both are mathematically valid. Only the right-hand one is ever observed.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;Nature picks the smooth one. It picks it &lt;em&gt;because&lt;/em&gt; real air has viscosity, and viscosity flatly refuses to let flow turn a sharp corner at infinite speed. The first attempt to do so sheds a vortex downstream — you can watch this happen in a water tank — and that departing vortex leaves behind precisely the circulation needed to make the trailing edge smooth ever after.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The bookkeeping is Kelvin&amp;rsquo;s circulation theorem: around a large circuit enclosing the wing and everything it has shed, total circulation cannot change. It was zero before the wing moved, so it is zero afterwards — whatever circulation the wing keeps, the starting vortex has to carry away exactly.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;So the missing equation is a fact about &lt;em&gt;sticky&lt;/em&gt; air, smuggled into a model that has no friction in it anywhere. Written down, it is one line — the &lt;strong&gt;Kutta condition&lt;/strong&gt;:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;γ₀ + γₙ = 0&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;The spin strength just under the tail, plus the spin strength just above it, equals zero. Since the sheet strength turns out to &lt;em&gt;be&lt;/em&gt; the local surface speed, that says the speeds on the two sides of the trailing edge match: no jump, no infinite turn.&lt;/p&gt;&#xA;&lt;p&gt;This is where the lift comes from. Everything else is bookkeeping. When I had a sign error early on and my sections produced negative lift, this line was where the bug was.&lt;/p&gt;&#xA;&lt;h2 id=&#34;its-all-one-linear-system&#34;&gt;It&amp;rsquo;s all one linear system&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#its-all-one-linear-system&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;With that last equation in place you have a square system: a matrix, a vector of unknown spin strengths, and a right-hand side.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/linear-system.svg&#34; alt=&#34;A schematic of the linear system: a square influence matrix with its final row highlighted as the Kutta condition, multiplied by the unknown vector, equalling the freestream vector&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;N rows of “no air through this panel”, plus one row of physics.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;Each entry of the matrix answers a very concrete question: &lt;em&gt;if roller j were spinning at unit strength, how much air would it push through panel i?&lt;/em&gt; That&amp;rsquo;s pure geometry — there&amp;rsquo;s a closed-form expression involving a logarithm and an arctangent, and it&amp;rsquo;s the same on every panel method ever written.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Both terms fall out of integrating a point vortex&amp;rsquo;s velocity along a straight segment: the logarithm is the radial part, the arctangent the angular one. Worth deriving once. That every panel method in print carries the same two expressions is the best evidence you&amp;rsquo;ll get that yours is right.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;The bit worth noticing is what &lt;em&gt;isn&amp;rsquo;t&lt;/em&gt; in the matrix. The angle of attack appears nowhere in it. It only touches the right-hand side. So the expensive half — factorising the matrix, which is the cubic-time part — gets done once per shape, and each new angle is a cheap back-substitution. Running a whole polar sweep across twenty angles costs roughly the same as solving one.&lt;/p&gt;&#xA;&lt;h2 id=&#34;getting-the-answer-back-out&#34;&gt;Getting the answer back out&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#getting-the-answer-back-out&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Once you&amp;rsquo;ve solved for the spin strengths, you might expect to have to sum up every roller&amp;rsquo;s contribution at every point to find the surface speed. You don&amp;rsquo;t, and this is my favourite part.&lt;/p&gt;&#xA;&lt;p&gt;A vortex sheet is &lt;em&gt;defined&lt;/em&gt; as a jump in tangential velocity across it. The solution was constructed so the air inside the imaginary body is perfectly still. So the jump across the sheet is simply the speed outside it. The strength you solved for &lt;strong&gt;is&lt;/strong&gt; the speed of the air sliding over the skin.&lt;/p&gt;&#xA;&lt;p&gt;Then Bernoulli turns speed into pressure — fast air means low pressure — and the whole thing collapses to one line, evaluated at each panel&amp;rsquo;s midpoint:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;Cp = 1 − γ²&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;Cp&lt;/code&gt; is the pressure coefficient: local pressure minus ambient, divided by the pressure you&amp;rsquo;d get by bringing the freestream to a dead stop. Dividing through like that means it doesn&amp;rsquo;t depend on how fast you&amp;rsquo;re going or how thick the air is — the same curve holds at 10 m/s at sea level and 30 m/s at three thousand metres. It&amp;rsquo;s the universal currency of this field.&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s what actually comes out:&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/foilworks/cp-4412.svg&#34; alt=&#34;Pressure distribution over a NACA 4412 at six degrees, upper and lower surface curves plotted with negative Cp upward, showing the stagnation point, suction peak, and pressure recovery&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Straight out of the solver: 198 panels, Cl = 1.2386. Everyone plots Cp upside down, so that the area between the curves reads as lift.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;p&gt;Three things to read off it. &lt;strong&gt;Cp = +1&lt;/strong&gt;, bottom left, is air brought completely to rest — the stagnation point, and the highest value physically possible. The big negative spike just behind the nose is the &lt;strong&gt;suction peak&lt;/strong&gt;, where air is moving far faster than freestream and pressure has dropped well below ambient; most of the lift lives there. And the long climb from the peak back towards zero at the tail is the &lt;strong&gt;pressure recovery&lt;/strong&gt; — air being asked to move into rising pressure. That climb is the whole ballgame for the second story, because it&amp;rsquo;s what eventually pushes the boundary layer into separating.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-drag-that-isnt-drag&#34;&gt;The drag that isn&amp;rsquo;t drag&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-drag-that-isnt-drag&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The solver reports a drag number. It&amp;rsquo;s a lie, and I&amp;rsquo;ve had to be quite disciplined about saying so.&lt;/p&gt;&#xA;&lt;p&gt;In genuinely frictionless flow, a closed body experiences no drag at all. The pressure pushing back on the front is exactly cancelled by pressure pushing forward on the rear. This is d&amp;rsquo;Alembert&amp;rsquo;s paradox, and it bothered people for about a century before boundary layers were understood.&lt;/p&gt;&#xA;&lt;p&gt;So the drag my panel solver computes is not physical. It&amp;rsquo;s the residue of chopping a smooth curve into flat segments — a measure of how good my panelling is, nothing more. On the run above it&amp;rsquo;s 0.000172, which is a couple of orders of magnitude smaller than the real drag of that section. If it ever gets large, that tells me my resolution is bad, not that I&amp;rsquo;ve designed a draggy wing.&lt;/p&gt;&#xA;&lt;p&gt;I have a rule written into the project: never report this number as drag. It would be very easy to ship something that looks like a working aerodynamics tool and is quietly nonsense.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-check-that-keeps-me-honest&#34;&gt;The check that keeps me honest&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-check-that-keeps-me-honest&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The pressure integration gives lift. But there&amp;rsquo;s a completely independent route to the same number — Kutta–Joukowski, which says lift is just twice the total circulation. Two different calculations from the same solution:&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Kutta–Joukowski is &lt;code&gt;L = ρVΓ&lt;/code&gt;. The &lt;em&gt;twice&lt;/em&gt; is an artefact of the coefficient form: divide through by &lt;code&gt;½ρV²c&lt;/code&gt;, set &lt;code&gt;V&lt;/code&gt; and &lt;code&gt;c&lt;/code&gt; to 1, and &lt;code&gt;Cl = 2Γ&lt;/code&gt;. Nearly every stray factor of two in this field is somebody&amp;rsquo;s &lt;code&gt;½ρV²&lt;/code&gt; in disguise.&lt;/aside&gt;&#xA;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;cl      = 1.2386&#xA;cl_circ = 1.2387&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If those ever diverge by more than a percent, the solver attaches a warning to its own output. It&amp;rsquo;s a cheap and brutal test: the two agree only if the solution is actually right, and it costs almost nothing because the second number falls out of the solution you already have.&lt;/p&gt;&#xA;&lt;p&gt;One result that looks like a bug and isn&amp;rsquo;t: my lift slope comes out at 6.86 per radian, against the textbook thin-aerofoil value of 2π ≈ 6.28. That&amp;rsquo;s 9.2% high. It&amp;rsquo;s meant to be. Thin-aerofoil theory linearises the thickness away, and thickness genuinely adds lift. Every thick-section panel method overshoots the textbook line by about this much. It took me an embarrassingly long time to stop trying to &amp;ldquo;fix&amp;rdquo; it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-it-cant-do&#34;&gt;What it can&amp;rsquo;t do&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#what-it-cant-do&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;This is worth being blunt about, because a plausible-looking wrong answer is worse than no answer.&lt;/p&gt;&#xA;&lt;p&gt;There is no viscosity in any of this. So: no real drag, no stall, no separation, no transition from laminar to turbulent flow, no compressibility. You cannot get a glide ratio out of it, because you cannot get drag out of it. You cannot find the stall angle, because nothing in the model knows how to stall — push the angle up and the lift just keeps rising happily, forever, which real wings famously do not.&lt;/p&gt;&#xA;&lt;p&gt;What it does give you, and gives you fast and accurately, is lift and the pressure distribution for attached flow. That turns out to be exactly the input the second story needs.&lt;/p&gt;&#xA;&lt;h2 id=&#34;next&#34;&gt;Next&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#next&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The other story: the boundary layer, which is where drag and stall actually live. It&amp;rsquo;s a much nastier problem, and not only because friction is harder than no friction — the thin film and the outer flow each need the other&amp;rsquo;s answer before either can start, so the two have to be talked into agreeing.&lt;/p&gt;&#xA;&lt;p&gt;The diagrams in this post were generated from the solver itself rather than drawn by hand, which felt like the right way to do it. If any of it is wrong, I&amp;rsquo;d genuinely like to know.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Progress on the Peregrine HTTP server</title>
      <link>https://rmrf.uk/cs/2025-01-09-progress-on-peregrine/</link>
      <pubDate>Thu, 09 Jan 2025 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/cs/2025-01-09-progress-on-peregrine/</guid>
      <category>Computer Broscience</category>
      <description>As a way for me to dive deeper into Zig, I'm writing a HTTP server from scratch. This article describes some of the work I've done recently, and a few lessons that I've learned.</description>
      <content:encoded>&lt;p&gt;I&amp;rsquo;m implementing a HTTP server from scratch in Zig, as a fun way to learn the Zig systems programming language, and also to get more comfortable and familiar with some modern kernel features that make highly performant network IO possible.&lt;/p&gt;&#xA;&lt;p&gt;I started this project on the 24th December 2024, and within a couple of days, I had a bare-bones TCP server that could parse and respond to HTTP requests. Initially, I followed Karl Seguin&amp;rsquo;s excellent &lt;a href=&#34;https://www.openmymind.net/TCP-Server-In-Zig-Part-1-Single-Threaded/&#34;&gt;guide to creating a TCP server in Zig&lt;/a&gt;. If you&amp;rsquo;re interested in learning Zig, after solving some &lt;a href=&#34;https://codeberg.org/ziglings/exercises/&#34;&gt;Ziglings&lt;/a&gt;, I suggest that you look at Karl&amp;rsquo;s blog posts on Zig.&lt;/p&gt;&#xA;&lt;p&gt;After some light optimisation work, I wrote some example applications because I know that the best way to test the usability of your library is to use it.&lt;/p&gt;&#xA;&lt;p&gt;Initially, the server was initialised with an &lt;code&gt;on_request(request, response) void&lt;/code&gt; handler function. I found that this approach made it difficult to create a stateful request handler. After trying a VTable-based interface, I settled on the more idiomatic Zig generic pattern, leveraging Zig&amp;rsquo;s comptime metaprogramming. Basically, you just call &lt;code&gt;Server(MyHandler).init(config)&lt;/code&gt;, where Server is a function that returns a type of Server that has your handler built in at compile time. You then simply define your handler as the implementation of an interface, containing at least the required methods. If the user messes anything up, the program will simply fail to compile, and some useful error will be output.&lt;/p&gt;&#xA;&lt;p&gt;Anyway, with the nicer API, I implemented a simple counter server where the handler increments a counter on each request, and responds with the current value. I then wanted to see my server doing something useful, so I wrote a directory server, and served this website locally. To my delight, the server was working great, and the implementation of the DirServer helper required no significant changes to the server. It was fun to see the server working well, serving binaries and text files that were rendered correctly by the browser.&lt;/p&gt;&#xA;&lt;p&gt;At this point, the server was slightly more performant than Go&amp;rsquo;s stdlib HTTP server. Go&amp;rsquo;s HTTP server is a good benchmark, because it&amp;rsquo;s well written and used extensively in production. Also, it supports HTTP/2 and TLS out of the box, which is really great. Unfortunately, Peregrine does not yet support HTTP/2 or TLS. However, Peregrine does beat Go&amp;rsquo;s HTTP server in one regard, and that is built in support for WebSockets. Peregrine features full support for WebSockets, and adding WebSocket support to your application is as easy as handling the connection upgrade by calling the upgrade helper function, and implementing the WS handler methods in your handler. It really could not be easier, and I&amp;rsquo;m quite happy with how that turned out.&lt;/p&gt;&#xA;&lt;p&gt;Slightly more performant than Go&amp;rsquo;s HTTP server? There is no way that we can be happy with that&amp;hellip; after all, many HTTP servers outperform Go&amp;rsquo;s.&lt;/p&gt;&#xA;&lt;p&gt;From my own tests, NGINX only marginally outperforms Go&amp;rsquo;s, even with some tuning such as enabling Kqueue/Epoll and setting the thread count equal to the CPU core count. That surprised me, because as far as I know, Go relies on its runtime scheduler, and does not use Kqueue/Epoll for socket readiness notifications, which should eliminate wasted CPU time spent polling the sockets.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;It does use them. Go&amp;rsquo;s runtime netpoller is epoll on Linux and kqueue on BSD and Darwin; what the scheduler adds on top is parking the goroutine while the poller waits, so you write blocking code and get readiness notification underneath. Which removes the surprise — NGINX and Go are doing the same thing.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;At the front of the pack is &lt;a href=&#34;https://github.com/zigzap/zap&#34;&gt;Zap&lt;/a&gt;, built on &lt;a href=&#34;https://facil.io&#34;&gt;Facil.io&lt;/a&gt;. Zap is basically Zig bindings for the HTTP server part of Facil.io. Facil.io is a marvellous networking library written in C. The more I study and benchmark Facil.io, the more I&amp;rsquo;ve come to appreciate its stability and performance. On my M2 Pro, Zap will process in excess of 170K static GET requests per second with 1000 connections. I&amp;rsquo;ve not yet found a HTTP server that outperforms this with 1000 connections. Please let me know if there is one.&lt;/p&gt;&#xA;&lt;p&gt;As a baseline, on 28th December, 4 days after the start of the project, Peregrine was doing around 86K static GET requests per second with 1000 connections. Similar to Go&amp;rsquo;s stdlib HTTP server and also similar to (tuned) NGINX. At this point, Zap/Facil.io does more than twice the throughput. We have a lot of work to do&amp;hellip;&lt;/p&gt;&#xA;&lt;h2 id=&#34;step-1-improve-the-handler&#34;&gt;Step 1: Improve the handler&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#step-1-improve-the-handler&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;As I mentioned earlier, the nature of the library&amp;rsquo;s on_request hook made implementing a stateful handler inconvenient at best. This was at the top of my list. I implemented a VTable-based interface, but I didn&amp;rsquo;t like that the user needed to declare the VTable in their handler:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-zig&#34; data-lang=&#34;zig&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// commit 0beb0019df043abb43e924d4aa8234f479eba607&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ./example/basic.zig&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; MyHandler &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; vtable: pereg.HandlerVTable &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; .{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .handle &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; handle,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;handle&lt;/span&gt;(ptr: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;anyopaque&lt;/span&gt;, req: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;pereg.Request, resp: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;pereg.Response) &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; self &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; @as(&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;@This(), @alignCast(@ptrCast(ptr)));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self.&lt;span style=&#34;color:#a6e22e&#34;&gt;handleWithError&lt;/span&gt;(req, resp) &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;err&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            std.debug.&lt;span style=&#34;color:#a6e22e&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;error handling request: {any}&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;, .{err});&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;inline&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;handleWithError&lt;/span&gt;(_: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;@This(), _: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;pereg.Request, resp: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;pereg.Response) &lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _ &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; resp.&lt;span style=&#34;color:#a6e22e&#34;&gt;setBody&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Kawww&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; len_header &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; pereg.Header.&lt;span style=&#34;color:#a6e22e&#34;&gt;init&lt;/span&gt;(.{ .key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Content-Length&amp;#34;&lt;/span&gt;, .value &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;6&amp;#34;&lt;/span&gt; });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; resp.headers.&lt;span style=&#34;color:#a6e22e&#34;&gt;append&lt;/span&gt;(len_header);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This version actually performed quite well, doing a little over 160K requests per second. That&amp;rsquo;s nearly a 2x improvement from our baseline! In retrospect, I&amp;rsquo;m surprised that this version performs so well, because the indirection of the VTable lookup should cost at least a few nanoseconds, not to mention that this indirection should increase CPU cache misses for calls to handler methods. It seems that function pointers are even more costly than VTables. Anyway, this performs well, but it&amp;rsquo;s quite ugly, and does not make use of what Zig has to offer. We need a better way to &amp;lsquo;inject&amp;rsquo; our handler into the server.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;An indirect call whose target never changes gets predicted almost perfectly, so the jump itself costs close to nothing in a hot loop. What a vtable actually costs is the inlining the compiler can no longer do — which bites hardest when the callee is tiny, and a handler that writes six bytes is tiny.&lt;/aside&gt;&#xA;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-zig&#34; data-lang=&#34;zig&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// commit 2d13a14cd5ed77de040bea003ad37a55020fb54d&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ./example/basic.zig&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; std &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; @import(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;std&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; pereg &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; @import(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;peregrine&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; MyHandler &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;init&lt;/span&gt;(allocator: std.mem.Allocator) &lt;span style=&#34;color:#f92672&#34;&gt;!*&lt;/span&gt;@This() {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; allocator.&lt;span style=&#34;color:#a6e22e&#34;&gt;create&lt;/span&gt;(@This());&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;deinit&lt;/span&gt;(_: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;@This()) &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;handle&lt;/span&gt;(self: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;@This(), req: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;pereg.Request, resp: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;pereg.Response) &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self.&lt;span style=&#34;color:#a6e22e&#34;&gt;handleWithError&lt;/span&gt;(req, resp) &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;err&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            std.debug.&lt;span style=&#34;color:#a6e22e&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;error handling request: {any}&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;, .{err});&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;handleWithError&lt;/span&gt;(_: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;@This(), _: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;pereg.Request, resp: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;pereg.Response) &lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _ &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; resp.&lt;span style=&#34;color:#a6e22e&#34;&gt;setBody&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Kawww&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; len_header &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; pereg.Header.&lt;span style=&#34;color:#a6e22e&#34;&gt;init&lt;/span&gt;(.{ .key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Content-Length&amp;#34;&lt;/span&gt;, .value &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;6&amp;#34;&lt;/span&gt; });&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; resp.headers.&lt;span style=&#34;color:#a6e22e&#34;&gt;append&lt;/span&gt;(len_header);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; gpa &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; std.heap.&lt;span style=&#34;color:#a6e22e&#34;&gt;GeneralPurposeAllocator&lt;/span&gt;(.{}){};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; allocator &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; gpa.&lt;span style=&#34;color:#a6e22e&#34;&gt;allocator&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;defer&lt;/span&gt; _ &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; gpa.&lt;span style=&#34;color:#a6e22e&#34;&gt;deinit&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; srv &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; pereg.&lt;span style=&#34;color:#a6e22e&#34;&gt;Server&lt;/span&gt;(MyHandler).&lt;span style=&#34;color:#a6e22e&#34;&gt;init&lt;/span&gt;(allocator, &lt;span style=&#34;color:#ae81ff&#34;&gt;3000&lt;/span&gt;, .{});&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    std.debug.&lt;span style=&#34;color:#a6e22e&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;listening on 0.0.0.0:3000&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;, .{});&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; srv.&lt;span style=&#34;color:#a6e22e&#34;&gt;start&lt;/span&gt;(); &lt;span style=&#34;color:#75715e&#34;&gt;// Blocks if there is no error&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By the way, sorry for &lt;code&gt;@This()&lt;/code&gt; in the method signatures, this was pasted as committed. I&amp;rsquo;ve since started declaring &lt;code&gt;const Self = @This();&lt;/code&gt; to clean up method signatures. This looks much better. Our handler simply implements the 3 required methods; init, deinit, and handle. Our handler is then given to the Server by making Server a function that returns a type. Our handler is passed to the &amp;lsquo;constructor&amp;rsquo; as a parameter. Under the hood:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-zig&#34; data-lang=&#34;zig&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// commit 2d13a14cd5ed77de040bea003ad37a55020fb54d&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ./src/server.zig&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Server&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;comptime&lt;/span&gt; Handler: &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;comptime&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;@hasDecl(Handler, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;init&amp;#34;&lt;/span&gt;)) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                @compileError(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Handler must implement init(std.mem.Allocator) !*@This()&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;@hasDecl(Handler, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;deinit&amp;#34;&lt;/span&gt;)) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                @compileError(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Handler must implement deinit(*@This()) void&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;@hasDecl(Handler, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;handle&amp;#34;&lt;/span&gt;)) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                @compileError(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Handle must implement handle(*@This(), *Request, *Response) void&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        handler: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;Handler,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;// ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pretty simple, right? As it should be. As you can see, we will get a compile error if our handler is missing any of the 3 required methods.&lt;/p&gt;&#xA;&lt;h2 id=&#34;step-2-optimise-the-request-parser&#34;&gt;Step 2: optimise the request parser&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#step-2-optimise-the-request-parser&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Being a text protocol, efficient parsing of a HTTP request comes down to well-implemented buffered reading from the socket, and optimised parsing operations. The parts of a HTTP request are delimited by CRLF (sometimes clients may use bare LF line endings). The first line contains the request method, path, and HTTP version. The following lines contain headers, until an empty line. If present, the request body would follow. The length would be specified in a Content-Length header. For example (without body):&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;GET /path/to/resource HTTP/1.1\r\n&#xA;Host: localhost\r\n&#xA;User-Agent: Microsoft Internet Explorer 6\r\n&#xA;Accept: */*\r\n&#xA;Connection: keep-alive\r\n&#xA;\r\n&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;As a side note, the Connection header is the client&amp;rsquo;s request for the server to either close or keep-alive the TCP connection. HTTP/1.1 introduced the keep-alive mechanism, allowing connections to be reused. This greatly improves HTTP&amp;rsquo;s efficiency, especially as modern websites typically consist of many resources that are requested individually. In HTTP/1.0, clients had to create a new connection for each request. Our server must handle both HTTP/1.0 and HTTP/1.1 as a bare minimum. HTTP/2 and HTTP/3 would be great to have, but these are binary protocols, and must be upgraded to from a HTTP/1.1 connection. Additionally, HTTP/3 is not over TCP, but QUIC, which is over UDP. Support for HTTP/2 and 3 may come later.&lt;/p&gt;&#xA;&lt;p&gt;So, how to parse this request? Logically, we should read it line-by-line. So far, I&amp;rsquo;ve found that efficient request parsing is about minimising search and copy operations.&lt;/p&gt;&#xA;&lt;p&gt;If you&amp;rsquo;re interested, check out &lt;a href=&#34;https://github.com/intob/peregrine/blob/main/src/reader.zig&#34;&gt;./src/reader.zig&lt;/a&gt;. I&amp;rsquo;ll spare you a complicated code-dump.&lt;/p&gt;&#xA;&lt;h3 id=&#34;method-path-and-version&#34;&gt;Method, path and version&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#method-path-and-version&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;In essence, the method and version are parsed first, at the beginning and the end of the first line. Parsing the method was originally done using a compile-time generated lookup table of 8-byte masks. SIMD operations were then used to compare the first 8 bytes of the request with the lookup table. In practice, I was not happy with how some methods were parsed fairly quickly (9ns), but other methods further down the lookup table took considerably longer (over 30ns) due them requiring more comparisons.&lt;/p&gt;&#xA;&lt;p&gt;While a few nanoseconds may seem negligible, I&amp;rsquo;m trying to keep request parsing under 2000 CPU cycles. That&amp;rsquo;s around 600ns on my M2 Pro.&lt;/p&gt;&#xA;&lt;p&gt;I also tried a switch of the method length before byte comparisons, but this necessitated finding the first &amp;rsquo; &amp;lsquo;, which cost more than the saving of narrowing down the number of possible methods.&lt;/p&gt;&#xA;&lt;p&gt;Today, I settled on the following:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-zig&#34; data-lang=&#34;zig&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; Method &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;enum&lt;/span&gt;(u4) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    GET,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    PUT,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    HEAD,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    POST,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    PATCH,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    TRACE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    DELETE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    CONNECT,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    OPTIONS,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;parse&lt;/span&gt;(bytes: []&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;u8&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;Method {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (bytes.len &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;.UnsupportedMethod;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;switch&lt;/span&gt; (bytes[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;G&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; .GET,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;P&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;switch&lt;/span&gt; (bytes[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;O&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; .POST,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;U&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; .PUT,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;A&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; .PATCH,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;.UnsupportedMethod,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;H&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; .HEAD,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;T&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; .TRACE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;D&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; .DELETE,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;C&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; .CONNECT,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;O&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; .OPTIONS,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;.UnsupportedMethod,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;toLength&lt;/span&gt;(self: Method) &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;switch&lt;/span&gt; (self) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .GET &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .PUT &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .HEAD &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .POST &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .PATCH &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .TRACE &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .DELETE &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;6&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .CONNECT &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;7&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .OPTIONS &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;7&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As beautiful as the comptime-generated lookup table was, I found that the overhead of initialising SIMD vectors was too expensive for this use-case. The above solution is much faster, with methods parsed between 6ns and 9ns. We simply give it at least 3 bytes, and in the fewest operations possible, it will return the method.&lt;/p&gt;&#xA;&lt;p&gt;We then parse the HTTP version. Not worthy of a code-dump, we simply take the last 8 bytes of the request line, and check that the 6th byte is &amp;lsquo;1&amp;rsquo; (HTTP/1), the 7th byte is &amp;lsquo;.&amp;rsquo;, and switch over the 8th byte to return the version enum 1.0 or 1.1. You can see it in &lt;a href=&#34;https://github.com/intob/peregrine/blob/main/src/version.zig&#34;&gt;./src/version.zig&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Next is extracting the path. You probably noticed the toLength method in the Method enum. We know that everything between the method and the version (minus the spaces) is the path and query. We also know that the version is 8 bytes in length. Therefore, by calling toLength() on the method, we have the position of the beginning of the path without a single scalar search for the delimiting spaces (&amp;rsquo; &amp;lsquo;).&lt;/p&gt;&#xA;&lt;p&gt;But what about separating the query from the path? We could simply defer this until (if ever) the query is needed, omitting the search for &amp;lsquo;?&amp;rsquo;, but I found that this hurt the usability of the library. I may change this again because searching for &amp;lsquo;?&amp;rsquo; is relatively expensive, and not always required. For now, the path and query are separated, but the query is never parsed, and instead stored in its raw URL-encoded form. Calling request.parseQuery() will populate a hash map in the request.&lt;/p&gt;&#xA;&lt;h3 id=&#34;parsing-headers&#34;&gt;Parsing headers&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#parsing-headers&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Here, we simply read the headers line-by-line, parsing them using a scalar search for &amp;lsquo;:&amp;rsquo; which is the delimiter for the key-value pair. The header is simply a struct with key and value fields. If you&amp;rsquo;re looking at &lt;a href=&#34;https://github.com/intob/peregrine/blob/main/src/header.zig&#34;&gt;./src/header.zig&lt;/a&gt; and wondering why Header contains fixed-sized arrays and length fields&amp;hellip; Requests are pre-allocated with a fixed-sized array of 32 Headers. This means that we don&amp;rsquo;t need to allocate headers for each request.&lt;/p&gt;&#xA;&lt;p&gt;Some of the most important optimisations can manifest as quite ugly &amp;lsquo;hacky&amp;rsquo; tricks. Such is the case for the Connection header. The Connection header is a little special, because it isn&amp;rsquo;t really for an application/library user to think about, but a client request for the server to handle the connection in a specific way. The value is normally either &amp;ldquo;keep-alive&amp;rdquo; or &amp;ldquo;close&amp;rdquo;. The server may still choose to close the connection, for example, if the limit of connection requests has been reached. Therefore, when we respond to a HTTP/1.1 request, we most often need to evaluate the Connection header. Initially, I was searching for the Connection header while generating the response, but I noticed that we could save a few nanoseconds by moving this into the header parsing step, as we&amp;rsquo;re already iterating over the headers. This was as simple as adding a boolean flag to the request &lt;code&gt;keep_alive&lt;/code&gt;. Looking at the implementation at time of writing, it&amp;rsquo;s so ugly that I don&amp;rsquo;t want to dump the code here! If you want, check it out in the parseHeaders method in &lt;a href=&#34;https://github.com/intob/peregrine/blob/main/src/reader.zig&#34;&gt;./src/reader.zig&lt;/a&gt;. Basically, we do a quick process of elimination by raw length, before checking if the value starts with &amp;lsquo;cl&amp;rsquo; for &amp;lsquo;close&amp;rsquo;. Notice the &lt;code&gt;conn_header_found&lt;/code&gt; flag in the method to prevent wasted operations after we&amp;rsquo;ve found it.&lt;/p&gt;&#xA;&lt;h3 id=&#34;simd-for-readerreadline&#34;&gt;SIMD for &lt;code&gt;reader.readLine&lt;/code&gt;&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#simd-for-readerreadline&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;How could I forget? &lt;code&gt;reader.readLine(fd: posix.socket_t)&lt;/code&gt; is right in the critical path. See &lt;a href=&#34;https://github.com/intob/peregrine/blob/main/src/reader.zig&#34;&gt;./src/reader.zig&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;One important optimisation here is the use of SIMD (vectors/chunks) when searching for line-endings. Rather than check byte-by-byte if we have a new-line character, we can leverage the SIMD (single-instruction-multiple-data) support of modern CPUs. This allows us to fill a mask with new-line characters, and check whether each 16-byte chunk contains the new-line. If it does, we simply call the built-in @ctz (count trailing zeros) to calculate its offset (position) in the chunk.&lt;/p&gt;&#xA;&lt;p&gt;For any remaining bytes (if the line-length is not divisible by 16), we simply iterate over them byte-by-byte.&lt;/p&gt;&#xA;&lt;p&gt;The use of SIMD is often integral to efficient data processing, and you will see it a lot.&lt;/p&gt;&#xA;&lt;h2 id=&#34;step-3-write-iovecs&#34;&gt;Step 3: Write iovecs&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#step-3-write-iovecs&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Finally for this article, when the &lt;a href=&#34;https://github.com/intob/peregrine/blob/main/src/worker.zig&#34;&gt;./src/worker.zig&lt;/a&gt; responds to a request, making a syscall to write each part of the response, or concatenating the response status line, headers, and body buffers, we can make use of &lt;code&gt;posix.writev&lt;/code&gt; to write a slice of iovecs (IO vectors) with a single syscall. This is much more efficient. Thanks to Karl Seguin for his &lt;a href=&#34;https://www.openmymind.net/TCP-Server-In-Zig-Part-3-Minimizing-Writes-and-Reads/&#34;&gt;guide to writing iovecs in Zig&lt;/a&gt;. I&amp;rsquo;d like to reiterate just how helpful I&amp;rsquo;ve found his guides, so thank you Karl.&lt;/p&gt;&#xA;&lt;h2 id=&#34;wrap-up&#34;&gt;Wrap up&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#wrap-up&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I hope you found some of this interesting. I quite enjoy writing these, as it&amp;rsquo;s a good way for me to reflect on some of the decisions that I&amp;rsquo;ve made while. While writing this, I even found a couple of little things that I can improve.&lt;/p&gt;&#xA;&lt;p&gt;Thanks to Bo (author of Facil.io), for his advice for optimising the server implementation. Facil.io still outperforms Peregrine by around 10%, and shows better stability of response times under load. I think that this is due to Bo&amp;rsquo;s superior solution for distributing and scheduling the computation.&lt;/p&gt;&#xA;&lt;p&gt;Today, I implemented a fairly performant ring buffer queue (50ns for one read and one write). Maybe I can still improve this by a factor of 2, as a hardware lock should only take a single cycle.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The mutex isn&amp;rsquo;t where the 50 ns is. An uncontended lock is a handful of cycles; the cost is the condition variable, which may put a thread to sleep and wake it again. The way past it isn&amp;rsquo;t a faster lock but no lock — one producer and one consumer need two atomic indices and nothing else.&lt;/aside&gt;&#xA;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-zig&#34; data-lang=&#34;zig&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; std &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; @import(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;std&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RingQueue&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;comptime&lt;/span&gt; T: &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; Self &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; @This();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        allocator: std.mem.Allocator,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        buffer: []T,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        read_index: &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        write_index: &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        mutex: std.Thread.Mutex &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; .{},&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        not_empty: std.Thread.Condition &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; .{},&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        not_full: std.Thread.Condition &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; .{},&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;init&lt;/span&gt;(allocator: std.mem.Allocator, capacity: &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;!*&lt;/span&gt;Self {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; self &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; allocator.&lt;span style=&#34;color:#a6e22e&#34;&gt;create&lt;/span&gt;(Self);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self&lt;span style=&#34;color:#f92672&#34;&gt;.*&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; .{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .allocator &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; allocator,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .buffer &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; allocator.&lt;span style=&#34;color:#a6e22e&#34;&gt;alloc&lt;/span&gt;(T, capacity),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; self;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;deinit&lt;/span&gt;(self: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;Self) &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.allocator.&lt;span style=&#34;color:#a6e22e&#34;&gt;free&lt;/span&gt;(self.buffer);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.allocator.&lt;span style=&#34;color:#a6e22e&#34;&gt;destroy&lt;/span&gt;(self);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;isFull&lt;/span&gt;(self: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;Self) &lt;span style=&#34;color:#66d9ef&#34;&gt;bool&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; (self.write_index &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; self.buffer.len &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; self.read_index;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;isEmpty&lt;/span&gt;(self: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;Self) &lt;span style=&#34;color:#66d9ef&#34;&gt;bool&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; self.write_index &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; self.read_index;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;write&lt;/span&gt;(self: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;Self, data: T) &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.mutex.&lt;span style=&#34;color:#a6e22e&#34;&gt;lock&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;defer&lt;/span&gt; self.mutex.&lt;span style=&#34;color:#a6e22e&#34;&gt;unlock&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; (self.&lt;span style=&#34;color:#a6e22e&#34;&gt;isFull&lt;/span&gt;()) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                self.not_full.&lt;span style=&#34;color:#a6e22e&#34;&gt;wait&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;self.mutex);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.buffer[self.write_index] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; data;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.write_index &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (self.write_index &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; self.buffer.len;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.not_empty.&lt;span style=&#34;color:#a6e22e&#34;&gt;signal&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;read&lt;/span&gt;(self: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;Self) T {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.mutex.&lt;span style=&#34;color:#a6e22e&#34;&gt;lock&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;defer&lt;/span&gt; self.mutex.&lt;span style=&#34;color:#a6e22e&#34;&gt;unlock&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; (self.&lt;span style=&#34;color:#a6e22e&#34;&gt;isEmpty&lt;/span&gt;()) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                self.not_empty.&lt;span style=&#34;color:#a6e22e&#34;&gt;wait&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;self.mutex);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self.buffer[self.read_index];&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.read_index &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (self.read_index &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; self.buffer.len;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.not_full.&lt;span style=&#34;color:#a6e22e&#34;&gt;signal&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; data;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This queue will make it possible to distribute work (requests to handle) to a thread pool on a FIFO basis. This would prevent long-running handlers from blocking an entire worker, which could block multiple connections.&lt;/p&gt;&#xA;&lt;p&gt;I also learned that Zig supports async/await as this could be an even better way for each worker in the pool to handle multiple requests concurrently.&lt;/p&gt;&#xA;&lt;p&gt;Peace &amp;amp; love!&#xA;J&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Elegance in Global Cooperation: Engineering Simplicity, Purity and Correctness</title>
      <link>https://rmrf.uk/reflections/2025-01-09-elegance-in-global-cooperation/</link>
      <pubDate>Thu, 09 Jan 2025 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/reflections/2025-01-09-elegance-in-global-cooperation/</guid>
      <category>Reflections</category>
      <description>I draw parallels between engineering and social systems, suggesting that some fundamental engineering principles and concepts of game theory may be applied to improve the fairness and productivity of human cooperation at scale.</description>
      <content:encoded>&lt;p&gt;Today, after reading a good chunk of Serious Cryptography, by J. P. Aumasson, I was lamenting over the likeness of computer science and society, and the way that game theory and complexity theory are interlinked. Reflecting on my day&amp;rsquo;s study, and my view of the world in general, I found myself drawing many comparisons that seemed insightful.&lt;/p&gt;&#xA;&lt;p&gt;For example, the security of a given cryptographic scheme is in practice dependent on its understandability, for if an algorithm is difficult to understand, there is a high likelihood that it will be implemented poorly or misused. That is to say that the number of possible attack vectors scales with complexity.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The sharpest instance is ECDSA, which is sound on paper and hands over the private key if you ever sign twice with the same nonce. Sony shipped a PlayStation 3 that used a constant. The algorithm was not broken; the requirement was simply not visible enough in the shape of the thing.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;A system that requires many details to be properly defined is inherently harder to understand. I do not suggest that complexity is bad, but merely a result of solving hard problems. However, the complexity of a solution does not imply its efficacy, but rather increases the probability that there is a flaw. This notion may be reduced to the idea that together simplicity and efficacy is virtuous. One might say that an elegant solution is simplified correctness.&lt;/p&gt;&#xA;&lt;p&gt;To me, this is one of the most fundamental engineering principles. Our intuition for what is good and what is bad is rooted in the elegance we see in the idea. To me, elegance is purity, simplicity and correctness. Nothing more, and nothing less than fit for purpose.&lt;/p&gt;&#xA;&lt;p&gt;When solving hard problems to deliver value to the world, we reduce our solution to its essence. Such is a proper definition of our solution.&lt;/p&gt;&#xA;&lt;p&gt;My view of humanity is an ecosystem of beautiful souls that must cooperate for survival. Productive and fair cooperation is tantamount to our survival.&lt;/p&gt;&#xA;&lt;p&gt;Before I continue, I must state that I am an optimist, and I believe that in general, humans have made progress toward cooperating on a global scale. As our capabilities of travel and communication have grown, so have the numbers of people we attempt to organise, and the geographic size of these clusters of organised cooperation. I suggest that recent technological progress has not been matched by organisational progress when it comes to organising large groups of people. As such, my feeling is that many of the social constructs that we depend on for productive and fair cooperation are no-longer fit for purpose. I also suggest that in engineering terms, our current legal, political and economic mechanisms are monolithic patchworks of progressive solutions mixed with outdated and sometimes irrelevant requirements. To simplify, I suggest that in order to progress further as a civilisation, we must aim for simpler and clearer terms by which we define fair and productive cooperation.&lt;/p&gt;&#xA;&lt;p&gt;I suggest that the definition of an ideal societal system is akin to a mathematical formula, just as you can devise a proof of correctness in mathematics. Game theory is about creating a structure of incentives such that the sum of incentives to play fairly is greater than the sum of incentives to cheat.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Designing the incentives rather than analysing them has its own name: mechanism design, game theory played backwards, from the outcome you want to the rules that would produce it. Hurwicz, Maskin and Myerson took the 2007 Nobel for it. Its central results are discouraging ones — for several natural sets of requirements, no mechanism exists at all.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;Rather than hopelessly wishing that everyone would take only what they need and contribute on their own accord, we would better ensure that the incentives of the individual align with the incentives of the masses. In other words, ensuring that the system makes cheating less rewarding than fair play.&lt;/p&gt;&#xA;&lt;p&gt;That is, of course, what the current mechanisms attempt to do, however they have become overly complex, inefficient, and riddled with vulnerabilities. You could liken it to a very insecure cryptographic scheme that is exploited whenever deployed. The forces at play are the same; there is an arbitrage between the risk and reward for cheating because the formal specification by which we govern ourselves is inadequate.&lt;/p&gt;&#xA;&lt;p&gt;At this point, a reader could feel that the critique of a system has no value in the absence of proposed solutions. However, the precursor to positive change is the understanding of contemporary challenges and issues. It would be equally naive to suggest that the solutions to such hard problems could or should be defined by a single person. Rather, a sound and logical consensus that fosters fair and productive cooperation must be a collaborative effort. Many perspectives and experiences must be considered.&lt;/p&gt;&#xA;&lt;p&gt;I believe that many principles of engineering, mathematics and game theory may be applied in the conception and definition of an ideal societal fabric by which we may better cooperate fairly and productively. Let us empathise with one-another, and believe that we are headed towards a healthy, compassionate and productive future.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>The beginning of project Peregrine - a bleeding fast HTTP server</title>
      <link>https://rmrf.uk/cs/2024-12-26-starting-peregrine/</link>
      <pubDate>Thu, 26 Dec 2024 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/cs/2024-12-26-starting-peregrine/</guid>
      <category>Computer Broscience</category>
      <description>As a way for me to dive deeper into Zig, I'm writing a HTTP server from scratch.</description>
      <content:encoded>&lt;p&gt;If you&amp;rsquo;re very familiar with Zig, then you&amp;rsquo;ll know that the standard library HTTP server is not written to be highly performant.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/zigzap/zap&#34;&gt;Zap&lt;/a&gt; is an alternative. Zap is built on the fantastic &lt;a href=&#34;https://facil.io&#34;&gt;Facil.io&lt;/a&gt; networking library written in C.&lt;/p&gt;&#xA;&lt;p&gt;Now, while there are plenty of fast HTTP servers, at time of writing, I couldn&amp;rsquo;t find a production-ready high-performance HTTP server written purely in Zig. There is &lt;a href=&#34;https://github.com/karlseguin/http.zig&#34;&gt;http.zig&lt;/a&gt;, but according to Karl, the developer, it must sit behind a reverse-proxy because it&amp;rsquo;s not resilient to DoS attack.&lt;/p&gt;&#xA;&lt;p&gt;As I need a real project to get deeper into the language, and having never written a HTTP server from scratch before, I felt that this would be a great opportunity to have fun learning about Zig, async IO, vectored IO, memory alignment, zero-alloc, zero-copy and a bunch of other systems stuff. I&amp;rsquo;ve also found that working with constraints (such as following a spec) is great for forcing one to find a solution, rather than skirting around an issue. Also, writing a library is a particularly good way to learn a language because it forces you to write reusable (and usable) software.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/intob/peregrine&#34;&gt;Peregrine - a bleeding fast HTTP server&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;My goal with this project is to write a robust, performant and simple HTTP server. In doing so, I will learn much of the Zig language, and a bunch about systems programming.&lt;/p&gt;&#xA;&lt;p&gt;I started this project on 2024-12-24, two days ago at time of writing. So how far have I got in two days?&lt;/p&gt;&#xA;&lt;p&gt;Currently, we have the main components of the server implemented. The server is written in a library form, and exposes a clean API to create a server and handle requests.&lt;/p&gt;&#xA;&lt;p&gt;There is a worker pool. Each worker runs on its own thread. For now, connecting sockets are assigned to a worker using round-robin distribution. This means that we don&amp;rsquo;t need a scheduler.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Round-robin balances connections, not work. Nothing stops all the slow clients landing on one worker while the others idle, and with one request in flight per worker that worker stalls completely. &lt;a href=&#34;https://rmrf.uk/cs/2025-01-09-progress-on-peregrine/&#34;&gt;A queue to fix it turns up two weeks later&lt;/a&gt;, which is a scheduler after all.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;Kqueue is the FreeBSD/MacOS solution for async IO. It&amp;rsquo;s a kernel event system. Basically, you register interest in a type of event. You then process the events. We have one of these event buses for our server&amp;rsquo;s main thread, where we accept connections. Each connecting socket is then assigned to a worker by registering interest in the client socket&amp;rsquo;s events with the worker&amp;rsquo;s event bus. This means that when clients write to the socket, the appropriate worker is notified and will process the event. In this case, the worker processing the event means reading from the socket.&lt;/p&gt;&#xA;&lt;p&gt;For the Linux kernel, there is a different solution, called epoll. In some ways Kqueue is superior to epoll because Kqueue allows registering interest in multiple events with a single system call. Conversely, with epoll, each interest must be registered with separate sys calls. In our case, this won&amp;rsquo;t make any difference because we make a sys call every time we accept a new connection. If we didn&amp;rsquo;t, clients would have to wait until their socket was registered. Otherwise, epoll is very similar to kqueue, as far as I know. In the next days, I will implement epoll to support Linux.&lt;/p&gt;&#xA;&lt;p&gt;When each worker starts, it does all of its heap allocations that it needs for normal operation. Ideally, no further heap allocations will be made per-request. This zero-alloc approach greatly improves performance because we don&amp;rsquo;t need to wait for memory to be allocated on the heap before progressing with processing each request.&lt;/p&gt;&#xA;&lt;p&gt;Each worker can handle one request at a time. This constraint allows us to reuse the buffers and data structures that it allocates. The only thing we have to ensure is that they are properly reset before reuse.&lt;/p&gt;&#xA;&lt;p&gt;Getting deeper into the guts of the server, each worker creates a request reader. This request reader reads from a given socket, and maintains a buffer of read-but-unprocessed data. When we call readRequest, unprocessed data in the buffer will be read before compacting the buffer and making another syscall to get more data. This approach means that fewer syscalls are used to read each request, while not needing a particularly large buffer.&lt;/p&gt;&#xA;&lt;p&gt;One optimisation that I&amp;rsquo;ve made already is that buffers are properly aligned. This means that the data&amp;rsquo;s memory address is a multiple of its size. Memory alignment is crucial for high-performance software because the CPU accesses memory most efficiently when data is naturally aligned. This allows the processor to fetch memory in a single operation, whereas misaligned data may require multiple accesses. Additionally, aligned data structures help to optimise cache efficiency. When data crosses cache line boundaries, it can result in additional cache misses, resulting in performance degradation.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The cache-line half of that is a different number. A line is typically 64 bytes, and it is alignment to &lt;em&gt;that&lt;/em&gt; which decides whether a structure straddles two of them. A &lt;code&gt;u64&lt;/code&gt; at an address divisible by 8 is naturally aligned and can still sit across a line boundary, if the buffer around it isn&amp;rsquo;t.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;Below is the critical path of the worker&amp;rsquo;s event loop. Each kernel event is processed as follows.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-zig&#34; data-lang=&#34;zig&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ./src/worker.zig&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;handleKevent&lt;/span&gt;(self: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;Worker, socket: posix.socket_t, reader: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;request.RequestReader) &lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;defer&lt;/span&gt; posix.&lt;span style=&#34;color:#a6e22e&#34;&gt;close&lt;/span&gt;(socket);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self.req.&lt;span style=&#34;color:#a6e22e&#34;&gt;reset&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; reader.&lt;span style=&#34;color:#a6e22e&#34;&gt;readRequest&lt;/span&gt;(socket, self.req);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self.resp.&lt;span style=&#34;color:#a6e22e&#34;&gt;reset&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    self.&lt;span style=&#34;color:#a6e22e&#34;&gt;on_request&lt;/span&gt;(self.req, self.resp);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;self.resp.hijacked) &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; self.&lt;span style=&#34;color:#a6e22e&#34;&gt;respond&lt;/span&gt;(socket);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;respond&lt;/span&gt;(self: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;Worker, socket: posix.socket_t) &lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; headers_len &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; self.resp.&lt;span style=&#34;color:#a6e22e&#34;&gt;serialiseHeaders&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;self.resp_buf);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (self.resp.body_len &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; iovecs &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [_]posix.iovec_const{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .{ .base &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; @ptrCast(self.resp_buf[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;..&lt;/span&gt;headers_len]), .len &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; headers_len },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            .{ .base &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; @ptrCast(self.resp.body[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;..&lt;/span&gt;self.resp.body_len]), .len &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self.resp.body_len },&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; written &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; posix.&lt;span style=&#34;color:#a6e22e&#34;&gt;writev&lt;/span&gt;(socket, &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;iovecs);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (written &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; headers_len &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; self.resp.body_len) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;.WriteError;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;writeAll&lt;/span&gt;(socket, self.resp_buf[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;..&lt;/span&gt;headers_len]);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One optimisation you can see above is in the respond method. This is called after the user&amp;rsquo;s on_request handler is called, providing that they did not hijack the response. More on that later. If the response has a body, vectored IO is used to write the headers and body in a single syscall, without needing to concatenate the buffers. If there is no body, the response is written by repeatedly writing to the socket until all bytes are written. Without a body, this is likely to be achieved in a single syscall.&lt;/p&gt;&#xA;&lt;p&gt;Making use of x86&amp;rsquo;s SIMD and ARM&amp;rsquo;s NEON capabilities allow us to speed up a variety of operations. One example that you will see often is chunking. This is where slices are processed in chunks, rather than operating on single bytes. This is important because operating on single bytes does not make good use of the cache. A good example of this is in Zig&amp;rsquo;s stdlib: &lt;code&gt;std.mem.indexOfScalarPos&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;This searches a slice for a specific value. Normally this would be a slice of bytes, and we&amp;rsquo;d be searching for a byte. If we were to search the slice byte-by-byte, the CPU would have to individually load each byte into a register for comparison. In &lt;code&gt;std.mem.indexOfScalarPos&lt;/code&gt;, you can see how a bit mask is used to efficiently test if the target byte is somewhere in the chunk.&#xA;The naive and much slower implementation would be as follows:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-zig&#34; data-lang=&#34;zig&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;indexOfPos&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;comptime&lt;/span&gt; T: &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;, slice []&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; T, start_index: &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt;, value T) ?&lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (start_index &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;=&lt;/span&gt; slice.len) &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; i: &lt;span style=&#34;color:#66d9ef&#34;&gt;usize&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; start_index;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; (i &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt; slice.len) : (i &lt;span style=&#34;color:#f92672&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (slice[i] &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; value) &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; i;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Often, adding some room for complexity allows us to solve problems more efficiently. Good engineering is partly about balancing performance gains with simplicity. A good engineer knows what to optimise, and what not to. Tight loops in critical paths tend to offer the greatest reward for optimisation.&lt;/p&gt;&#xA;&lt;p&gt;Anyway, hopefully that was interesting. After a couple of days, we have the bare bones of a high-performance HTTP server, written purely in Zig. Considering that I&amp;rsquo;m new to systems programming and Zig, I feel that I&amp;rsquo;m doing alright. It surely is a very fun and expressive language. I&amp;rsquo;m enjoying the build speed and the quality of the compiler&amp;rsquo;s errors.&lt;/p&gt;&#xA;&lt;p&gt;Stay tuned for my progress, as I will be working on this to make it production-ready. I&amp;rsquo;m not sure how long it will take, but I am sure that I&amp;rsquo;ll continue learning a lot.&lt;/p&gt;&#xA;&lt;p&gt;Peace and love!&#xA;J&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>First thoughts on Zig</title>
      <link>https://rmrf.uk/cs/2024-12-23-zig-first-thoughts/</link>
      <pubDate>Mon, 23 Dec 2024 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/cs/2024-12-23-zig-first-thoughts/</guid>
      <category>Computer Broscience</category>
      <description>I describe what I like about the Zig systems programming language, and what I don't like after a couple of weeks of tinkering.</description>
      <content:encoded>&lt;p&gt;I&amp;rsquo;ve done very little systems programming. I wrote Conway&amp;rsquo;s Game of Life in C as a teenager. I also did a bit of assembly at GE Aviation Systems. A few years ago, I completed the Rust programming book, but struggled with the language beyond that, and never grew to love it.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;m currently diving quite deep into systems programming in Zig. While there are many things I love about the language, namely its elegant approach to generics and meta-programming, I do feel that the language is lacking in some ways.&lt;/p&gt;&#xA;&lt;h2 id=&#34;simplicity&#34;&gt;Simplicity&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#simplicity&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;What I like most about the language is its simplicity. Similar to Go in this regard, it didn&amp;rsquo;t take me long to become comfortable with the syntax.&lt;/p&gt;&#xA;&lt;h2 id=&#34;errors-lack-context&#34;&gt;Errors lack context&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#errors-lack-context&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;One of the most obvious flaws with the language is its error type. It&amp;rsquo;s impossible to add context or details to errors. In contrast, an error in Go for example, is just a value. This allows you to add context as the error is passed up the call stack.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;m not sure if Zig will offer a solution for this.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The idiom that emerged is a separate diagnostics struct the callee writes into, alongside the plain error — &lt;code&gt;std.json&lt;/code&gt; and the compiler&amp;rsquo;s own parser both do it. It keeps the error set exhaustive and switchable, which is the property Zig is protecting, and puts the prose somewhere else.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;Despite this, errors in Zig are otherwise great, and very easy to handle or throw up the stack.&lt;/p&gt;&#xA;&lt;h2 id=&#34;powerful-build-system&#34;&gt;Powerful build system&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#powerful-build-system&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Zig exposes a very powerful yet usable build API. Linking C libraries is quite effortless, and I&amp;rsquo;m a noob to this.&lt;/p&gt;&#xA;&lt;p&gt;While I&amp;rsquo;m not yet familiar with much of the build system, I have the feeling that it is probably the best build system that I&amp;rsquo;ve seen yet. For me, it was also the hardest part of Zig to become comfortable with. I&amp;rsquo;m still not really comfortable with it. In addition, as the language is so young and volatile, much of the 3rd party documentation for the build system is out of date.&lt;/p&gt;&#xA;&lt;p&gt;Fortunately, the API is very clean, and so it&amp;rsquo;s not so difficult to figure things out on your own.&lt;/p&gt;&#xA;&lt;h2 id=&#34;lack-of-interfaces&#34;&gt;Lack of interfaces&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#lack-of-interfaces&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Zig&amp;rsquo;s philosophy is to provide low-level building blocks, rather than high-level abstractions. As such, the Zig team felt that interfaces are an unnecessary abstraction.&lt;/p&gt;&#xA;&lt;p&gt;While I understand this view, I feel that the language&amp;rsquo;s lack of built-in support for interfaces can make interface implementations feel quite clunky. This became particularly apparent to me when I began using the standard library&amp;rsquo;s filesystem types. I&amp;rsquo;m sure that this could be greatly improved as the language server matures.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;I tried both answers a couple of weeks later while writing an HTTP server: an explicit vtable the user has to declare, and the comptime-generic &lt;code&gt;Server(MyHandler)&lt;/code&gt; pattern. The generic one won on readability and on speed — &lt;a href=&#34;https://rmrf.uk/cs/2025-01-09-progress-on-peregrine/&#34;&gt;the comparison is here&lt;/a&gt;.&lt;/aside&gt;&#xA;&#xA;&lt;h2 id=&#34;expressive-language&#34;&gt;Expressive language&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#expressive-language&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I am often surprised by Zig because it tends to work exactly as you&amp;rsquo;d hope. This is particularly noticeable when implementing polymorphic types and platform-dependent behaviour. This makes writing cross-platform software remarkably easy. For me, Zig really shines in this regard.&lt;/p&gt;&#xA;&lt;h2 id=&#34;generics-are-beautiful&#34;&gt;Generics are beautiful&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#generics-are-beautiful&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Thanks to Zig&amp;rsquo;s expressive compile-time metaprogramming, generics are very easy to implement, and are perfectly type-safe.&lt;/p&gt;&#xA;&lt;p&gt;The following is a generic thread-safe message queue:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-zig&#34; data-lang=&#34;zig&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; std &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; @import(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;std&amp;#34;&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Queue&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;comptime&lt;/span&gt; T: &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; Node &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; { data: T, next: ?&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;Node };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        mutex: std.Thread.Mutex,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        head: ?&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;Node,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        tail: ?&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;Node,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        allocator: std.mem.Allocator,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;init&lt;/span&gt;(allocator: std.mem.Allocator) &lt;span style=&#34;color:#f92672&#34;&gt;!*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Queue&lt;/span&gt;(T) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; queue &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; allocator.&lt;span style=&#34;color:#a6e22e&#34;&gt;create&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;Queue&lt;/span&gt;(T));&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            queue&lt;span style=&#34;color:#f92672&#34;&gt;.*&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; .{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .mutex &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; std.Thread.Mutex{},&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .head &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .tail &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .allocator &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; allocator,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; queue;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;deinit&lt;/span&gt;(self: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Queue&lt;/span&gt;(T)) &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.mutex.&lt;span style=&#34;color:#a6e22e&#34;&gt;lock&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;defer&lt;/span&gt; self.mutex.&lt;span style=&#34;color:#a6e22e&#34;&gt;unlock&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;while&lt;/span&gt; (self.head) &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;node&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; next &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; node.next;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                self.allocator.&lt;span style=&#34;color:#a6e22e&#34;&gt;destroy&lt;/span&gt;(node);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                self.head &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; next;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.tail &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;write&lt;/span&gt;(self: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Queue&lt;/span&gt;(T), data: T) &lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; new_node &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; self.allocator.&lt;span style=&#34;color:#a6e22e&#34;&gt;create&lt;/span&gt;(Node);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            new_node&lt;span style=&#34;color:#f92672&#34;&gt;.*&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; .{&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; data,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                .next &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.mutex.&lt;span style=&#34;color:#a6e22e&#34;&gt;lock&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;defer&lt;/span&gt; self.mutex.&lt;span style=&#34;color:#a6e22e&#34;&gt;unlock&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (self.tail) &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;tail&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                tail.next &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; new_node;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                self.tail &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; new_node;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            } &lt;span style=&#34;color:#66d9ef&#34;&gt;else&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                self.head &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; new_node;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                self.tail &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; new_node;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pub&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;read&lt;/span&gt;(self: &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Queue&lt;/span&gt;(T)) ?T {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            self.mutex.&lt;span style=&#34;color:#a6e22e&#34;&gt;lock&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;defer&lt;/span&gt; self.mutex.&lt;span style=&#34;color:#a6e22e&#34;&gt;unlock&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (self.head) &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt;node&lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; data &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; node.data;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                self.head &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; node.next;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (self.head &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    self.tail &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                self.allocator.&lt;span style=&#34;color:#a6e22e&#34;&gt;destroy&lt;/span&gt;(node);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; data;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    };&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;test&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;basic usage&amp;#34;&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; gpa &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; std.heap.&lt;span style=&#34;color:#a6e22e&#34;&gt;GeneralPurposeAllocator&lt;/span&gt;(.{}){};&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; allocator &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; gpa.&lt;span style=&#34;color:#a6e22e&#34;&gt;allocator&lt;/span&gt;();&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; queue &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Queue&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;u8&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;init&lt;/span&gt;(allocator);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; queue.&lt;span style=&#34;color:#a6e22e&#34;&gt;write&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; queue.&lt;span style=&#34;color:#a6e22e&#34;&gt;write&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; queue.&lt;span style=&#34;color:#a6e22e&#34;&gt;write&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; std.testing.&lt;span style=&#34;color:#a6e22e&#34;&gt;expect&lt;/span&gt;(queue.&lt;span style=&#34;color:#a6e22e&#34;&gt;read&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; std.testing.&lt;span style=&#34;color:#a6e22e&#34;&gt;expect&lt;/span&gt;(queue.&lt;span style=&#34;color:#a6e22e&#34;&gt;read&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; std.testing.&lt;span style=&#34;color:#a6e22e&#34;&gt;expect&lt;/span&gt;(queue.&lt;span style=&#34;color:#a6e22e&#34;&gt;read&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; std.testing.&lt;span style=&#34;color:#a6e22e&#34;&gt;expect&lt;/span&gt;(queue.&lt;span style=&#34;color:#a6e22e&#34;&gt;read&lt;/span&gt;() &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;);&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In Go, I very rarely found myself using generics. Generics in Go feel like an after-thought, and that&amp;rsquo;s probably because they are.&lt;/p&gt;&#xA;&lt;p&gt;Conversely, in Zig, I&amp;rsquo;ve used this comptime struct pattern many times to write reusable components.&lt;/p&gt;&#xA;&lt;h2 id=&#34;standard-library-is-still-rough-around-the-edges&#34;&gt;Standard library is still rough around the edges&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#standard-library-is-still-rough-around-the-edges&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Some packages in the standard library are not entirely production-ready. For example, the Blake3 implementation uses the reference rather than the optimised implementation.&lt;/p&gt;&#xA;&lt;p&gt;There are faster and more robust HTTP servers than the one in the standard library, namely &lt;a href=&#34;https://github.com/zigzap/zap&#34;&gt;Zap&lt;/a&gt; built on facil.io (written in C).&lt;/p&gt;&#xA;&lt;p&gt;As Zig is still young, it seems only natural that the standard library is not yet polished. Obviously, the language should mature and stabilise before polishing the standard library can become a priority.&lt;/p&gt;&#xA;&lt;h2 id=&#34;memory-management-made-easy&#34;&gt;Memory management made easy&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#memory-management-made-easy&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;When I learned Rust (or tried to), I never became comfortable with it, and I was constantly fighting the compiler.&lt;/p&gt;&#xA;&lt;p&gt;Possibly, I&amp;rsquo;m simply a better programmer these days, but I find it more likely that Zig&amp;rsquo;s approach to memory management is just simpler and more explicit.&lt;/p&gt;&#xA;&lt;p&gt;Zig is not a fancy language, it really does feel close to C. I feel that this inherently makes it easier to learn than Rust.&lt;/p&gt;&#xA;&lt;p&gt;Mostly writing Go recently, I expected that Zig&amp;rsquo;s memory management would be a really big hurdle. While it has taken me a few days to become comfortable allocating memory on the heap, I was surprised how easy I found it.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;m still not fully familiar with each of the standard library&amp;rsquo;s allocators, and I mostly use the GeneralPurposeAllocator.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;&lt;code&gt;GeneralPurposeAllocator&lt;/code&gt; has since become &lt;code&gt;std.heap.DebugAllocator&lt;/code&gt;, which is the honest name — it was always a debugging allocator rather than a fast default. The release-build answer is now &lt;code&gt;std.heap.smp_allocator&lt;/code&gt;. Zig moves like this constantly, and it is the main reason anything written about it goes stale inside a year.&lt;/aside&gt;&#xA;&#xA;&lt;h1 id=&#34;conclusion&#34;&gt;Conclusion&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#conclusion&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;p&gt;I&amp;rsquo;m only a couple of weeks into my journey with Zig.&lt;/p&gt;&#xA;&lt;p&gt;Would I use Zig for work? If the project would benefit from optimal performance and reliability, absolutely. For bits such as tooling for managing infrastructure or automated key rotation, where performance is not a concern, I&amp;rsquo;d likely opt for Go. At time of writing, Zig&amp;rsquo;s ecosystem is still very small compared to Go&amp;rsquo;s.&lt;/p&gt;&#xA;&lt;p&gt;My reason for beginning with Zig was to get into systems programming, and I&amp;rsquo;m really enjoying it. I remember faffing with header files in C as a teenager, and I know that building C projects can be a real pain. So far I&amp;rsquo;m quite happy with Zig, and I&amp;rsquo;m optimistic that a lot of the kinks will be ironed out as the language matures.&lt;/p&gt;&#xA;&lt;p&gt;There will never be a perfect language for everyone for all use-cases. As I&amp;rsquo;ve been learning Zig, I&amp;rsquo;ve come to appreciate how hard it is to design a language that is fit for decades of mission-critical software engineering. It&amp;rsquo;s incredible how good C is, despite its (lack of) build system. I&amp;rsquo;ve also come to realise how much of Go they got right. Writing concurrent software in Zig has left me with an appreciation for Go&amp;rsquo;s channels. The only thing that I would change about Go is the verbose error handling.&lt;/p&gt;&#xA;&lt;p&gt;I feel that learning a new programming language allows us to open our minds to alternative paradigms. Questioning what we already know is fundamental to making progress.&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>Dave</title>
      <link>https://rmrf.uk/cs/2024-05-01-dave/</link>
      <pubDate>Wed, 01 May 2024 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/cs/2024-05-01-dave/</guid>
      <category>Computer Broscience</category>
      <description>Distributed KV store.</description>
      <content:encoded>&lt;p&gt;EDIT: In September &amp;amp; October I made significant changes to the DHT, and so much of this article is out of date. I have not yet found time to update this article, and I&amp;rsquo;m not currently working on the project.&lt;/p&gt;&#xA;&lt;p&gt;So&amp;hellip; what am I doing? When I started this project (first commit 2024-04-05), I didn&amp;rsquo;t know exactly, beyond designing a peer-to-peer network application that disseminates information. Now my vision is finally becoming clear, and I&amp;rsquo;m able to describe it.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;m writing a peer-to-peer application that allows anyone, or any device, to read and write information without a value transaction of any kind. A writer pays in CPU cycles and electricity, while a reader pays in network participation.&lt;/p&gt;&#xA;&lt;p&gt;Simplicity, anonymity, efficiency, performance, and high availability are key aspects that I&amp;rsquo;m aiming for.&lt;/p&gt;&#xA;&lt;p&gt;Simplicity will come at the cost of pushing much application-specific complexity up the stack. We must be free to re-imagine a broken game, if it turns out to be broken&amp;hellip; At this time, I feel that this approach ensures a modular and upgradeable stack of protocols, while maximising the probability of success of the dave protocol.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;m aware that such projects have been attempted and failed. I&amp;rsquo;m also aware that such a challenging project may not be an ideal start for a budding p2p network researcher. With that in mind, I do need a worthy challenge and vision to hold my interest.&lt;/p&gt;&#xA;&lt;h1 id=&#34;dave---distributed-kv-store&#34;&gt;Dave - distributed KV store&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#dave---distributed-kv-store&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;p&gt;Dave is a protocol for continuously disseminating information in a decentralised, anonymous, and censorship-resistant manner, without the need for a native token or value transaction.&lt;/p&gt;&#xA;&lt;p&gt;New values are available nearly instantly, with sub-second lookup time. Old values have a significantly faster lookup time, because more nodes will likely have the data. Real-world speed depends on the bandwidth provisioned by the nodes. Most likely, the response time is better than average network ping, even with a naive implementation.&lt;/p&gt;&#xA;&lt;p&gt;Use-cases: Decentralised social media applications, serverless forms, communication layer for any decentralised application.&lt;/p&gt;&#xA;&lt;h2 id=&#34;design&#34;&gt;Design&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#design&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The protocol is designed around a single message format with an enumerated operation code. There are 4 operation codes, as follows; GETPEER, PEER, PUT, GET.&lt;/p&gt;&#xA;&lt;p&gt;Execution is of a cyclic mode, with the minimum period defined by constant EPOCH. The program sends a uniform stream of packets, minimising packet loss and maximising efficiency. This allows the protocol to be adjusted safely, and in a way that could preserve interoperability with a network running variations for different bandwidth ideals &amp;amp; constraints.&lt;/p&gt;&#xA;&lt;h3 id=&#34;getpeer--peer-messages&#34;&gt;GETPEER &amp;amp; PEER Messages&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#getpeer--peer-messages&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;These are the first two op-codes that I defined, and initially the only operations that the network performed. These two messages allow nodes on the network to discover peers, and to verify their availability.&lt;/p&gt;&#xA;&lt;p&gt;For every PING EPOCH, iterate over the peer table. If a peer is found that has not been seen in the last PING EPOCH, and has not been pinged within the last PING EPOCH, send to the peer a message with op-code GETPEER.&lt;/p&gt;&#xA;&lt;p&gt;A protocol-following peer will reply with op-code PEER, a message containing NPEER addresses for other peers.&lt;/p&gt;&#xA;&lt;p&gt;I often refer to these addresses as peer descriptors, as in future, they may not necessarily be IP addresses. I would like the possibility to cleanly implement interoperable transports. This would allow nodes to serve as bridges. As software-defined radio is a field of interest to me, I imagine that the dave protocol could used to create alternative low-energy networks that could bridge our existing infrastructure built on IP. Imagine a free and open internet that does not require an internet service provider, or any knowledge of its users. Know that I have not yet adjusted the protobuf specification to support this, as I prefer to focus on a building working proof over UDP for now.&lt;/p&gt;&#xA;&lt;p&gt;If a peer does not respond with any valid message, after DROP * EPOCH has elapsed, the peer is deleted from the peer table. Unresponsive peers are no-longer advertised after OPEN * EPOCH has elapsed without message, so as to ensure that unresponsive peers are not re-added from latent gossip.&lt;/p&gt;&#xA;&lt;h3 id=&#34;put-message&#34;&gt;PUT Message&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#put-message&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;A message with op-code PUT is a packet of data containing a Key, Value, Time, Salt, Work, Sig and PubKey. Salt and Work are outputs of the cost function, into which Key, Value and Time are passed.&lt;/p&gt;&#xA;&lt;p&gt;Every EPOCH, each node sends one randomly selected dat to one randomly selected peer.&lt;/p&gt;&#xA;&lt;p&gt;This ensures that information propagates and re-propagates through the network reliably, until it is eventually no-longer stored by any node. I describe the selection algorithm later.&lt;/p&gt;&#xA;&lt;p&gt;This provides a good level of anonymity for original senders, because it is virtually impossible to discern the origin of a dat, even with a broad view of network traffic.&lt;/p&gt;&#xA;&lt;p&gt;Anonymity is achieved by ensuring no correlation between the timing of a dat being received for the first time, and its eventual propagation to other nodes. This happens at random, but at a constant interval.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;This claim is too strong, and I retracted it later. Constant-rate random push does hide every node that &lt;em&gt;forwards&lt;/em&gt; a dat. It does not hide the node that wrote it, because origination takes a different path through the code — straight out to five peers, immediately, and again on a timer. &lt;a href=&#34;https://rmrf.uk/cs/2026-07-27-the-question-that-broke-dave/&#34;&gt;The full retraction is here&lt;/a&gt;.&lt;/aside&gt;&#xA;&#xA;&lt;h3 id=&#34;get-message&#34;&gt;GET Message&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#get-message&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;A GET message is a packet with op-code GET, containing a Key. If the remote has the dat, the remote should reply with a message with op-code PUT, containing the data. A node may request the same dat from many peers simultaneously. This is up to the application to optimise, although the reference implementation should be pretty good.&lt;/p&gt;&#xA;&lt;h3 id=&#34;dat-selection-by-mass&#34;&gt;DAT Selection by Mass&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#dat-selection-by-mass&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;h4 id=&#34;proof-of-work&#34;&gt;Proof-of-work&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#proof-of-work&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;A Salt is found, that when combined with the Key, Value &amp;amp; Time fields using a hash function, the output begins with some desired number of leading zero bits. The number of leading zeros (or some other constraint) probabilistically accurately reflects the energetic cost equivalent of computing the proof. This is commonly known as proof-of-work, and is well known for its use in Bitcoin mining.&lt;/p&gt;&#xA;&lt;p&gt;The number of leading zero bits is referred to as the &amp;ldquo;difficulty&amp;rdquo; throughout the remainder of this document.&lt;/p&gt;&#xA;&lt;p&gt;We compute a salty hash from an initial hash of key, value and time. For large values, this performs better than a single hash.&lt;/p&gt;&#xA;&lt;h5 id=&#34;work--blake3salt-blake3key-value-time&#34;&gt;work = blake3(salt, blake3(key, value, time))&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#work--blake3salt-blake3key-value-time&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h5&gt;&#xA;&lt;h4 id=&#34;mass-calculation&#34;&gt;Mass Calculation&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#mass-calculation&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;As each machine has limited resources, we need a mechanism by which the software can select which dats should be stored, at the expense of others being dropped. In a peer-to-peer application without any central coordination or authority, each node must be able to decide which dats to keep on its own.&lt;/p&gt;&#xA;&lt;p&gt;So how can we give priority to some dats over others?&lt;/p&gt;&#xA;&lt;p&gt;As the cryptographic proof contains the value, and time, neither may be modified without invalidating the proof. As such, we can use the time, and the difficulty in the calculation of a score, referred to here as mass.&lt;/p&gt;&#xA;&lt;h5 id=&#34;mass--difficulty--1--millisecondssinceadded&#34;&gt;mass = difficulty * (1 / millisecondsSinceAdded)&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#mass--difficulty--1--millisecondssinceadded&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h5&gt;&#xA;&lt;p&gt;The mass tends to zero over time. Dats with a harder proof of work persist longer in the network. In addition, difficulty scales exponentially (each added zero byte increases the difficulty by 256 times.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The difficulty term does less than it looks. Dats are pruned within a shard, and the shard is &lt;em&gt;chosen&lt;/em&gt; by difficulty — so every dat being compared already has the same difficulty, and mass collapses to one over age. What difficulty really buys is a less contended queue, which turns out to be the better idea. &lt;a href=&#34;https://rmrf.uk/cs/2026-07-27-the-question-that-broke-dave/&#34;&gt;Two years on&lt;/a&gt;.&lt;/aside&gt;&#xA;&#xA;&lt;h3 id=&#34;peer-trust-mechanism&#34;&gt;Peer Trust Mechanism&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#peer-trust-mechanism&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;A decentralised network depends on a trust system that incentivises fair play. It&amp;rsquo;s much more challenging than operating in a private network, as most web applications do today. So why do it? It allows us to build more powerful software that cannot be controlled by any single entity.&lt;/p&gt;&#xA;&lt;p&gt;The goal of the trust mechanism is to ensure that energy is not lost to malicious or protocol-deviating peers.&lt;/p&gt;&#xA;&lt;h4 id=&#34;earning-trust&#34;&gt;Earning Trust&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#earning-trust&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;Each time a packet is received containing a dat &lt;strong&gt;not already stored&lt;/strong&gt;, the remote peer&amp;rsquo;s trust value is incremented by the mass of the dat.&lt;/p&gt;&#xA;&lt;p&gt;If a peer is dropped, and then re-joins the network, they will begin with a trust score of zero.&lt;/p&gt;&#xA;&lt;p&gt;Trust scores are not gossiped, as this implies additional attack surface and complexity.&lt;/p&gt;&#xA;&lt;h4 id=&#34;use-of-trust&#34;&gt;Use of Trust&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#use-of-trust&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;The trust score is weighed in to the random peer selection. A random threshold between the maximum trust score and zero is chosen. A peer with a trust score greater than the random threshold is selected at random.&lt;/p&gt;&#xA;&lt;p&gt;Therefore, peers with a higher trust score are more likely to be selected for gossip messages. This in turn increases the chance for the peer to learn of new dats earlier, reinforcing the pattern.&lt;/p&gt;&#xA;&lt;p&gt;In essence, the longer a peer (ip-port) remains in an other&amp;rsquo;s peer table, the higher the trust score will likely be, and therefore more bandwidth is allocated to that peer.&lt;/p&gt;&#xA;&lt;h2 id=&#34;resilience-to-attack&#34;&gt;Resilience to Attack&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#resilience-to-attack&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;sybil-attack&#34;&gt;Sybil Attack&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#sybil-attack&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;A Sybil attack is one where an attacker creates many peers that join the network. Failing to consider this would result in the network losing energy to these numerous byzantine nodes that are inexpensive to spawn.&lt;/p&gt;&#xA;&lt;h3 id=&#34;eclipse-attack&#34;&gt;Eclipse Attack&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#eclipse-attack&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;An Eclipse attack is one where an attacker attempts to poison the peer tables of peers that it discovers on the network. This poisoning may be either byzantine nodes controlled by the attacker, or simply bogus peer descriptors.&lt;/p&gt;&#xA;&lt;h3 id=&#34;denial-of-service&#34;&gt;Denial of Service&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#denial-of-service&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;This attack is the simplest to orchestrate, and yet one of the hardest to deal with. A denial of service attack is one where an attacker sends packets that induce the maximum amount of computation on the remote. HTTP servers are commonly the recipients of such attacks, which is why we use CDNs where possible to shield our origin servers. This is obviously not possible for stateful APIs and databases. That&amp;rsquo;s why we put databases in private networks, and employ efficient filtering of API requests, often using an IP-based rate limiter. Obviously we can&amp;rsquo;t run our peer-to-peer application in a private network unless we only want to use it in a datacenter.&lt;/p&gt;&#xA;&lt;p&gt;Unfortunately, if an attacker has more bandwidth than any given node, the node can simply be denied service by exhausting their bandwidth. If the attacker has more bandwidth than the entire network combined, the whole network is denied service. So if you advocate for a given peer-to-peer network, the best way that you can support the network is by giving your bandwidth. Run a node.&lt;/p&gt;&#xA;&lt;h3 id=&#34;attack-mitigations&#34;&gt;Attack Mitigations&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#attack-mitigations&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;h4 id=&#34;dat-delay&#34;&gt;Dat Delay&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#dat-delay&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;A node must wait some time before receiving dats. They must participate in the gossip of peers without receiving any dats, until the defined period has elapsed. They will then start to receive dats very slowly, based on the PROBE constant.&lt;/p&gt;&#xA;&lt;h4 id=&#34;trust&#34;&gt;Trust&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#trust&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;A node normally selects only trusted peers to receive dats, but occasionally an untrusted peer is probed. Over time, a node is able to build trust with peers, therefore receiving dats more frequently. As a peer earns trust, they are more likely to be selected, thereby having more bandwidth allocated to them.&lt;/p&gt;&#xA;&lt;h4 id=&#34;npeer-limit&#34;&gt;NPEER Limit&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#npeer-limit&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h4&gt;&#xA;&lt;p&gt;We only accept PEER messages containing up to NPEER peer descriptors. I recommend that this value is either 2 or 3. I err on the side of caution with 2, because there is little benefit to speeding up the peer discovery in this way. It would be more secure to ask for peers more often.&lt;/p&gt;&#xA;&lt;p&gt;Funny! While writing this (2024-05-29), I discovered a vulnerability to eclipse attack. A byzantine peer may send many PEER messages, limited only by the packet filter. This made address poisoning trivial. This was resolved by recording the last time a PEER message was received, and only accepting a PEER message after the expected duration has elapsed since the last.&lt;/p&gt;&#xA;&lt;h2 id=&#34;storing-large-files&#34;&gt;Storing Large Files&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#storing-large-files&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;As mentioned earlier, all application-specific complexity is pushed up the stack. Dave is purely a packet-sharing protocol, with no built-in features for storing large files. That said, I have considered the need for storing large files in the network.&lt;/p&gt;&#xA;&lt;h3 id=&#34;linked-list&#34;&gt;Linked List&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#linked-list&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;The simplest approach to storing large files, and the earliest proof that I implemented, is a linked-list. Simply prepend or append the hash of the previous dat in next dat. This simple approach has the disadvantage that reading is slow because GETs cannot be made in parallel.&lt;/p&gt;&#xA;&lt;h3 id=&#34;merkle-tree&#34;&gt;Merkle Tree&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#merkle-tree&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;An optimised approach is to write the large file as a sequence of dats, and concurrently build a merkle tree, where additional pointer dats are used to reference dats containing the file&amp;rsquo;s content. The hash of the root of the tree may then be used as the reference to the file. A reader may then traverse the tree, collecting all of the dats in parallel, and re-assemble the file. Reads of this nature are significantly faster than the linked-list approach.&lt;/p&gt;&#xA;&lt;h2 id=&#34;some-repositories&#34;&gt;Some Repositories&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#some-repositories&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;godave-is-the-protocol-implementation-in-library-form-written-in-go&#34;&gt;godave is the protocol implementation in library form, written in Go.&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#godave-is-the-protocol-implementation-in-library-form-written-in-go&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Protocol &lt;a href=&#34;https://github.com/intob/godave/&#34;&gt;https://github.com/intob/godave/&lt;/a&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;daved-is-a-program-that-executes-the-protocol-as-any-other-application-that-may-join-the-network&#34;&gt;daved is a program that executes the protocol, as any other application that may join the network.&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#daved-is-a-program-that-executes-the-protocol-as-any-other-application-that-may-join-the-network&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;A tiny cli &lt;a href=&#34;https://github.com/intob/daved/&#34;&gt;https://github.com/intob/daved/&lt;/a&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;garry-is-a-http-gateway&#34;&gt;garry is a HTTP gateway.&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#garry-is-a-http-gateway&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Anything can work with dave. &lt;a href=&#34;https://github.com/intob/garry/&#34;&gt;https://github.com/intob/garry/&lt;/a&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;state-of-operations&#34;&gt;State of Operations&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#state-of-operations&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I&amp;rsquo;m no-longer running public edge (bootstrap) nodes myself. I was using AWS, with a bunch of t4g.nano arm64 VMs running Debian 12. I used scripts &amp;amp; programs to control groups of additional machines as I needed. I view logs by grepping the linux system journal. Logs begin with a short path prefix /fn/proc/action, allowing me to efficiently grep logs without need for typing quotes around the query.&lt;/p&gt;&#xA;&lt;p&gt;Thank you for reading. I value advice and ideas. If you have any, please do reach me.&lt;/p&gt;&#xA;&lt;h3 id=&#34;run-as-node&#34;&gt;Run as Node&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#run-as-node&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Executing with no command (just flags) puts the program in its default mode of operation, participating in the network.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;daved &lt;span style=&#34;color:#75715e&#34;&gt;# listen on all network interfaces, port 1618&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;daved -v &lt;span style=&#34;color:#75715e&#34;&gt;# verbose logging, use grep to filter logs&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;daved -v | grep /d/pr &lt;span style=&#34;color:#75715e&#34;&gt;# verbose logging, with only the daemon&amp;#39;s prune procedure logs output&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;daved -l :2024 &lt;span style=&#34;color:#75715e&#34;&gt;# listen on all network interfaces, port 2024. Same as -l [::]:2024&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;daved -e :1969 &lt;span style=&#34;color:#75715e&#34;&gt;# bootstrap to peer on local machine, port 1969&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;daved -e 12.34.56.78:1234 &lt;span style=&#34;color:#75715e&#34;&gt;# bootstrap to peer at address&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;commands&#34;&gt;Commands&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#commands&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;daved set hello_dave &lt;span style=&#34;color:#75715e&#34;&gt;# write hello_dave to the network with default difficulty of 16 bits&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;daved -d &lt;span style=&#34;color:#ae81ff&#34;&gt;32&lt;/span&gt; set hello_world &lt;span style=&#34;color:#75715e&#34;&gt;# write hello_world to the network with difficulty of 32 bits&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;daved get $HASH &lt;span style=&#34;color:#75715e&#34;&gt;# get the dat with the given work hash from the network&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;daved setf myfile.txt &lt;span style=&#34;color:#75715e&#34;&gt;# write a very small file to the network&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content:encoded>
    </item>
    <item>
      <title>logd</title>
      <link>https://rmrf.uk/cs/2024-03-15-logd/</link>
      <pubDate>Fri, 15 Mar 2024 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/cs/2024-03-15-logd/</guid>
      <category>Computer Broscience</category>
      <description>Real-time log tailing over UDP: a map of ring buffers, a protocol that ended up in production, and three wrong answers to replay protection before WireGuard showed me the right one.</description>
      <content:encoded>&lt;p&gt;Started around December 2023. &lt;code&gt;logd&lt;/code&gt; is a log daemon: services fire log lines at it over UDP, and you tail or query them live from anywhere. It ran in production, where it did more for our observability than anything else we had, and it is still the most fun I have had building something.&lt;/p&gt;&#xA;&lt;p&gt;I had been building protocols for fun since I was young. This is the one that ended up in production — security flaws and all, as it turned out. UDP is most of why it exists. TCP hands you a stream and hides the machinery. UDP hands you a datagram and a socket and leaves the rest to you — who is connected, what arrived, what to do about the packet that didn&amp;rsquo;t, all of it becomes a decision you have to make on purpose. I wanted to make those decisions.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-tail&#34;&gt;The tail&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-tail&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The part I still like best: one command, every service, live.&lt;/p&gt;&#xA;&lt;p&gt;Log lines are written under a path — &lt;code&gt;/prod/my-app/http&lt;/code&gt;, &lt;code&gt;/prod/my-app/udp&lt;/code&gt;, &lt;code&gt;/debug&lt;/code&gt;. A query carries a &lt;strong&gt;key prefix&lt;/strong&gt; rather than a key, so the store walks every ring whose name starts with what you asked for:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;rings&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;strings&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;HasPrefix&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;keyPrefix&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;matchedPrefix&lt;/span&gt; = &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;d&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Read&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;offset&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;limit&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;count&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;out&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;d&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&#x9;&lt;span style=&#34;color:#75715e&#34;&gt;// ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which means tailing &lt;code&gt;/prod&lt;/code&gt; merges every service beneath it into a single stream. Watching a whole deployment scroll past in one terminal, interleaved in real time, is a genuinely different way of seeing a system than opening one log file per box. That was the feature people actually used.&lt;/p&gt;&#xA;&lt;h2 id=&#34;ring-buffers-all-the-way-down&#34;&gt;Ring buffers all the way down&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#ring-buffers-all-the-way-down&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The store is a &lt;code&gt;map[string]*ring.Ring&lt;/code&gt; — one fixed-size ring per key, sized in config, plus a fallback ring for anything unrecognised. A write is an index bump and nothing else:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;b&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Ring&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;Write&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; []&lt;span style=&#34;color:#66d9ef&#34;&gt;byte&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;head&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;b&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;head&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Load&lt;/span&gt;()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;b&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;values&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;head&lt;/span&gt;] = &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;b&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;head&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Store&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;head&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;%&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;b&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;size&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Memory is decided at startup and never moves. There is no rotation, no compaction, no disk, no vacuum job at 3am. Old lines fall off the back. For &lt;em&gt;what is happening right now&lt;/em&gt; — the only question I ever asked it — that is the entire feature set.&lt;/p&gt;&#xA;&lt;p&gt;The other half of that decision is UDP itself. A logger that blocks the thing it is logging is worse than no logger, and a write here is a &lt;code&gt;sendto&lt;/code&gt; that cannot apply backpressure to the application even if the daemon is gone.&lt;/p&gt;&#xA;&lt;h2 id=&#34;nobody-is-connected&#34;&gt;Nobody is connected&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#nobody-is-connected&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;UDP has no connections, so &lt;em&gt;who is currently tailing?&lt;/em&gt; is a question the socket cannot answer. The server keeps a map of tails, each client sends a &lt;code&gt;PING&lt;/code&gt; every two seconds, and missing three drops you. A client that dies stops pinging and is cleaned up by absence.&lt;/p&gt;&#xA;&lt;p&gt;I like this more than I expected to. A TCP server learns about a dead client from an error on a write; a UDP one just stops hearing from it. Same outcome, less machinery.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-hard-part&#34;&gt;The hard part&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-hard-part&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Every packet has to authenticate itself, alone. There is no session to establish trust once and then ride on.&lt;/p&gt;&#xA;&lt;p&gt;The wire format has been the same since v0 — a 32-byte SHA-256 sum, 15 bytes of marshalled time, then the Protobuf payload — and the sum is taken over the secret, the time and the payload together. The time is what bounds the damage: a packet more than 200 ms old is rejected before anything else happens, so a captured packet is only worth replaying for 200 ms.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;Which quietly makes the sender&amp;rsquo;s clock a dependency. NTP normally holds a machine inside a few milliseconds, so this never came up — but a box with a drifting clock cannot log at all, and the symptom is packets vanishing, which reads as a network problem for as long as you let it.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;Which leaves the actual question: &lt;strong&gt;have I seen this packet before?&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;attempt-one-remember-every-hash&#34;&gt;Attempt one: remember every hash&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#attempt-one-remember-every-hash&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;A ring of the last few thousand sums, compared one at a time under a mutex.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;history&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Do&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{}) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;b&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;.([]&lt;span style=&#34;color:#66d9ef&#34;&gt;byte&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;bytes&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Equal&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;b&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;sum&lt;/span&gt;) {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&#x9;&lt;span style=&#34;color:#a6e22e&#34;&gt;found&lt;/span&gt; = &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This gets worse exactly where it needs to get better. Every packet costs a full scan of the ring, so making the history longer makes each packet more expensive — and because the ring holds a fixed &lt;em&gt;number&lt;/em&gt; of packets rather than a fixed &lt;em&gt;duration&lt;/em&gt;, the span of time it covers shrinks as traffic rises. The busier the server, the less it remembers. It was backwards in both directions at once.&lt;/p&gt;&#xA;&lt;h3 id=&#34;attempt-two-a-cuckoo-filter&#34;&gt;Attempt two: a cuckoo filter&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#attempt-two-a-cuckoo-filter&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Constant time, comfortably over 200,000 packets a second, and about four lines of code:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Guard&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;replay&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;sum&lt;/span&gt; []&lt;span style=&#34;color:#66d9ef&#34;&gt;byte&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;bool&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;g&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;filter&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;InsertUnique&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;sum&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two things wrong with it, and I only saw one at the time.&lt;/p&gt;&#xA;&lt;p&gt;The one I saw: a filter has no notion of &lt;em&gt;when&lt;/em&gt;, so it cannot expire anything. The only way to stop it filling is to reset the whole thing on a timer — ten seconds, in the shipped config — and at every reset all memory is dropped at once, so a replay from just before the tick sails straight through. There is a comment in &lt;code&gt;main.go&lt;/code&gt; where I worked out the fix and never wrote it:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;FilterCap&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;16000000&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;// balance for memory, larger size reduces false positives&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// now I know, we need 2 smaller filters, A &amp;amp; B.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Each must pass, and each is reset off-kilter to prevent&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// replay immediately after filter reset.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The one I didn&amp;rsquo;t see: &lt;code&gt;InsertUnique&lt;/code&gt; returns false when the item is already present &lt;strong&gt;and&lt;/strong&gt; when the filter is too full to accept it. So &lt;code&gt;replay()&lt;/code&gt; reports a replay in both cases. Fill the filter before its timer comes round and the server rejects every packet until it does.&lt;/p&gt;&#xA;&lt;p&gt;And the sizing tells the story on its own. Packets are valid for 200 ms. The filter holds ten seconds — fifty times the window — in 16 MB of fingerprints, to answer a question about the last fifth of a second.&lt;/p&gt;&#xA;&lt;h3 id=&#34;the-answer&#34;&gt;The answer&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-answer&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;I found it in the WireGuard spec, which is worth reading whether or not you are ever going to write a protocol. A sequence counter, incremented per packet and included in each one. The server tracks the highest it has seen: anything far behind is rejected outright, without a lookup, and anything close is checked against a small bitmap of which of the recent numbers have already arrived.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;WireGuard didn&amp;rsquo;t invent this either — it is the anti-replay window from IPsec. RFC 4303 defines it, and RFC 6479 gives the implementation that avoids shifting the bitmap on every packet. Both are short, and between them they are the clearest description of the mechanism I have found.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;The reason it works is that &lt;strong&gt;a counter is ordered and a set is not&lt;/strong&gt;. Ordering makes forgetting free — once the counter has moved on, everything behind it is rejected by arithmetic, with nothing to reset, clear or time out. My filters had to remember far more than they needed to, because the only alternative on offer was remembering nothing at all.&lt;/p&gt;&#xA;&lt;p&gt;&lt;figure class=&#34;diagram&#34;&gt;&#xA;  &lt;img src=&#34;https://rmrf.uk/img/cs/logd/replay-window.svg&#34; alt=&#34;Three bands drawn against a timeline running from ten seconds ago on the left to now on the right. The ring of hashes covers a short, dashed span whose width moves with load; the cuckoo filter covers the entire ten seconds; the counter and window cover only a thin sliver at the right-hand end, the two hundred milliseconds in which a packet is still valid.&#34; loading=&#34;lazy&#34; /&gt;&#xA;  &lt;figcaption&gt;Two of the three remember the wrong amount. The filter is not too slow — it is too broad, and it cannot be anything else.&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;and-it-wasnt-hmac&#34;&gt;And it wasn&amp;rsquo;t HMAC&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#and-it-wasnt-hmac&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I described this, in the README and in the first version of this post, as hash-based message authentication. It isn&amp;rsquo;t, quite. HMAC is a specific construction — two nested hashes with padded keys — and it exists precisely because the obvious thing, &lt;code&gt;H(secret || message)&lt;/code&gt;, is not safe with SHA-256.&lt;/p&gt;&#xA;&lt;p&gt;SHA-256 is a Merkle–Damgård hash, and its output &lt;em&gt;is&lt;/em&gt; its internal state. Given &lt;code&gt;H(secret || time || payload)&lt;/code&gt; and a guess at the length of the secret, you can resume the hash from that state and compute &lt;code&gt;H(secret || time || payload || padding || anything)&lt;/code&gt; without ever knowing the secret. My verifier splits the packet at a fixed offset and recomputes over &lt;code&gt;secret || time || payload&lt;/code&gt;, so an attacker who appends to the payload and supplies the extended sum passes the check.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The flaw belongs to the Merkle–Damgård family specifically — MD5, SHA-1, SHA-2. A sponge like SHA-3, or BLAKE2 and BLAKE3, does not hand you its whole internal state in the digest, so &lt;code&gt;H(secret || message)&lt;/code&gt; is genuinely fine with those. That is a poor reason to choose one. HMAC works with all of them.&lt;/aside&gt;&#xA;&#xA;&lt;p&gt;What saves this particular deployment is narrow and accidental. The appended bytes have to include the hash&amp;rsquo;s own padding, and Protobuf will not parse the result — so the forgery is thrown out by the &lt;em&gt;parser&lt;/em&gt;, one step after the check it defeated. And the 200 ms validity window means you have 200 ms to capture a packet, extend it and land it.&lt;/p&gt;&#xA;&lt;p&gt;Neither of those is a defence. &lt;code&gt;hmac.New(sha256.New, secret)&lt;/code&gt; is the same amount of code as the thing I wrote instead.&lt;/p&gt;&#xA;&lt;h2 id=&#34;looking-back&#34;&gt;Looking back&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#looking-back&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;em&gt;2024-05.&lt;/em&gt; This was an enjoyable project to rediscover writing software. It&amp;rsquo;s amazing to return to something after a change of mind. A new mind has more fun, and is more productive. The same thing happened when I re-imagined speedflying after my spinal-cord injury. Some years after my recovery, I rediscovered a lost joy with a new mind.&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;2024-12.&lt;/em&gt; I find it amusing that I struggled so long with replays. The sequence counter is simpler than everything I tried before it, and once you have seen it, a membership filter looks like the wrong shape for the job.&lt;/p&gt;&#xA;&lt;p&gt;I would build it differently now — for most of what this did, a process reading the systemd journal is the honest answer, and the logger abstraction is a layer I would not reach for again. But the daemon did its job for a real team on real traffic, and the parts of it I got wrong are the parts I now understand. You don&amp;rsquo;t learn why HMAC exists by importing it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;repo&#34;&gt;Repo&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#repo&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/intob/logd&#34;&gt;https://github.com/intob/logd&lt;/a&gt;&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>arpload</title>
      <link>https://rmrf.uk/cs/2024-03-09-arpload/</link>
      <pubDate>Sat, 09 Mar 2024 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/cs/2024-03-09-arpload/</guid>
      <category>Computer Broscience</category>
      <description>I wanted to put a 4K video on Arweave. The web wallet burned my AR without telling me, ArDrive insisted on bundling, so I wrote the uploader — and then made it resumable.</description>
      <content:encoded>&lt;p&gt;Arweave sells one thing: you pay once, and the network stores your data for at least two hundred years. The bet behind it is that storage keeps getting cheaper, so a one-off fee paid into an endowment outruns the cost of holding the bytes forever. Whether that holds for two centuries is a question for another post. I wanted to upload a 4K video.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-web-wallet&#34;&gt;The web wallet&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#the-web-wallet&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;I started with the &lt;a href=&#34;https://arweave.app&#34;&gt;Arweave web wallet&lt;/a&gt;, which would have worked. The upload sat there for a long time with no visible progress — I hadn&amp;rsquo;t opened dev tools — so I assumed it had failed, and tried again. It had not failed. I had paid for every one of those transactions.&lt;/p&gt;&#xA;&lt;p&gt;That lesson isn&amp;rsquo;t really about Arweave. An upload with no progress indicator, on a network where every attempt costs money, will get retried until the user runs out of money.&lt;/p&gt;&#xA;&lt;h2 id=&#34;ardrive&#34;&gt;ArDrive&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#ardrive&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Then ArDrive, because what I actually wanted was a nice interface and a large file safely uploaded, and the web interface genuinely is pleasant.&lt;/p&gt;&#xA;&lt;p&gt;But ArDrive bundles. Your data goes into a bundle transaction alongside other people&amp;rsquo;s, which is cheaper and faster and entirely reasonable, and not what I was after. I wanted a standalone transaction: one id that is mine, that resolves to my file and nothing else.&lt;/p&gt;&#xA;&lt;aside class=&#34;note&#34;&gt;The distinction is real rather than aesthetic. A bundle is one transaction on the chain carrying many &lt;em&gt;data items&lt;/em&gt; inside it, under ANS-104. Your file still gets an id and a gateway still resolves it — but the thing the network agreed to store is somebody else&amp;rsquo;s transaction, and your file is a row in its index.&lt;/aside&gt;&#xA;&#xA;&lt;h2 id=&#34;so-build-it&#34;&gt;So, build it&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#so-build-it&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/everFinance/goar&#34;&gt;goar&lt;/a&gt;, from everFinance, does the heavy lifting, which left the uploader small enough to be worth writing rather than looking for.&lt;/p&gt;&#xA;&lt;p&gt;The piece worth having is resume. A 4K video is a lot of chunks, and an upload that starts again from zero every time the connection hiccups is an upload that never finishes — which, in a roundabout way, was the same problem I&amp;rsquo;d started with. &lt;code&gt;arpload&lt;/code&gt; tracks how far it got and picks the transaction back up.&lt;/p&gt;&#xA;&lt;h2 id=&#34;demo&#34;&gt;Demo&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#demo&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Tx: &lt;a href=&#34;https://viewblock.io/arweave/tx/TK60GE39cvuTh_DlLK5ZCnhcoOkN42YFQjsJfzwvrk4&#34;&gt;TK60GE39cvuTh_DlLK5ZCnhcoOkN42YFQjsJfzwvrk4&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://jsxligcn7vzpxe4h6dsszlszbj4fzihjbxrwmbkchmex6pbpvzha.arweave.net/TK60GE39cvuTh_DlLK5ZCnhcoOkN42YFQjsJfzwvrk4&#34; alt=&#34;The 4K image uploaded to Arweave by arpload in this demo, served directly from the transaction linked above&#34; loading=&#34;lazy&#34; /&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;repo&#34;&gt;Repo&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#repo&#34; aria-label=&#34;Permalink to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/intob/arpload&#34;&gt;https://github.com/intob/arpload&lt;/a&gt;&lt;/p&gt;&#xA;</content:encoded>
    </item>
    <item>
      <title>First time in Madeira</title>
      <link>https://rmrf.uk/reflections/2023-12-madeira/</link>
      <pubDate>Tue, 19 Dec 2023 00:00:00 +0000</pubDate>
      <guid>https://rmrf.uk/reflections/2023-12-madeira/</guid>
      <category>Reflections</category>
      <description>Considering the Maritime alps, I looked further afield, to warmer climes. I almost went to Corsica...</description>
      <content:encoded>&lt;p&gt;Flare have given me a Line 8 (cheers Beni), and they asked me if I&amp;rsquo;d kindly take it flying.&lt;/p&gt;&#xA;&lt;p&gt;Considering the Maritime alps, I looked further afield, to warmer climes. I almost went to Corsica.&lt;/p&gt;&#xA;&lt;p&gt;After hearing of the Bitcoin conference and arranging with a couple of friends to go together, I saw photos &amp;amp; videos of this incredible island.&lt;/p&gt;&#xA;&lt;p&gt;My imagination ran wild as I explored 3D maps, marvelling at the topography.&lt;/p&gt;&#xA;&lt;p&gt;The island is inhabited by one of the friendliest populations I&amp;rsquo;ve ever seen.&lt;/p&gt;&#xA;&lt;p&gt;I felt the people to be especially beautiful, relaxed &amp;amp; kind. Very refreshing.&lt;/p&gt;&#xA;&lt;p&gt;We enjoyed almost every sunrise &amp;amp; sunset on the mountains or by the sea.&lt;/p&gt;&#xA;&lt;p&gt;Almost every day, we swam in the sea.&lt;/p&gt;&#xA;&lt;p&gt;We ate only one meal per day. Each meal was incredible.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://youtu.be/8CO--11bj_E&#34;&gt;Enjoy the video&lt;/a&gt; of my flying with this interesting wing.&lt;/p&gt;&#xA;</content:encoded>
    </item>
  </channel>
</rss>
