<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.2.2">Jekyll</generator><link href="inacsb.com/feed.xml" rel="self" type="application/atom+xml" /><link href="inacsb.com/" rel="alternate" type="text/html" /><updated>2025-11-20T20:14:25+00:00</updated><id>inacsb.com/feed.xml</id><title type="html">Not a Computer Scientist, But…</title><subtitle>Blog about math and code, often with almost rigorous proofs.</subtitle><entry><title type="html">Deploy Simple PWA Locally with Tailscale</title><link href="inacsb.com/tailscale-pwa/" rel="alternate" type="text/html" title="Deploy Simple PWA Locally with Tailscale" /><published>2025-11-12T00:00:00+00:00</published><updated>2025-11-12T00:00:00+00:00</updated><id>inacsb.com/tailscale-pwa</id><content type="html" xml:base="inacsb.com/tailscale-pwa/"><![CDATA[<p>In my last post, I explored a way to generate a large word list for passphrase
generation. But I couldn’t find any Android app that allows me to actually take
this list and generate passphrases! That’s a pretty good excuse for me to play
around with progressive webapps (PWA) to see if I could get an LLM to write me a
simple app to install and run on my phone.</p>

<h1 id="writing-generating-the-pwa">Writing (Generating) the PWA</h1>

<p>These days it is extremely easy to create a simple web app. I just prompted
GPT-OSS with my requirements and got what I wanted in a few iterations. Since
the word list is huge, instead of directly reading and generating the standalone
html file, it is better to prompt the LLM to generate a script, which takes the
word list as an input and generates the html file.</p>

<p>For a PWA, in addition to the html, you also need a service worker that
specifies how to cache assets, a manifest file with the app’s metadata, and some
icon files. I just took some existing generic app icon files from the internet,
but it wouldn’t be hard to generate it with local AI either.</p>

<p>Very quickly I got all the files ready: I have index.html, service-worker.js,
manifest.json, icon-512.png and icon-192.png in a folder locally.</p>

<h1 id="tailscale-serve">Tailscale Serve</h1>

<p>To deploy a PWA to my phone, there is a step that is usually a little tricky to
figure out, which is that I need to serve the web app with https. But luckily
this is a very easy problem to solve these days with Tailscale.</p>

<p>You need to first sign up for an account, then run a Tailscale client on both
the development machine and your phone and add both devices to the same tailnet.
All of this is free, although slightly annoyingly this step relies on using
third party services like Google and Tailscale for authentication. But at least
it is free and none of the data goes through their servers.</p>

<p>Here is one extra step that took me a while to figure out: now, you should go to
the Tailscale admin console and change your machine’s name to a name unique to
the app you’re trying to deploy. This allows you to deploy multiple apps using
the same method, otherwise chrome on Android thinks they’re all one app and
refuses to install it multiple times. So let’s say you renamed the machine to
<code class="language-plaintext highlighter-rouge">passphrase</code>.</p>

<p>Then, you can find some way to serve the website locally, like running <code class="language-plaintext highlighter-rouge">python3
-m http.server</code> in the folder with the static assets. With that running (say on
port 8000), you can then use <code class="language-plaintext highlighter-rouge">sudo tailscale serve --https 8001 8000</code> to serve
the same content to port 8001 but with SSL certs magically figured out, without
chrome complaining that your cert is broken in some way. Having dealt with certs
before, I am extremely grateful that this is such a simple and seamless step.</p>

<p>That command would give you a url that you can open in chrome on Android. Now
chrome will prompt you to install the PWA. Install it and that’s it! As far as I
can tell you can just kill the commands on your machine, delete all the files if
you want, and the app will work just fine offline on your phone. It just looks
and works like any other app.</p>

<p>As a final clean up step, you probably want to change the machine name in
Tailscale back to the original name. If you’d rather not deal with Tailscale
anymore, you can also get rid of all your clients and delete your account, but
I’ve found Tailscale to be quite useful for things like this.</p>

<h1 id="update-tailscale-service">Update: Tailscale Service</h1>

<p>That weird step of renaming the machine just to deploy an app can be eliminated
now with Tailscale Service! Incredible. I’m going to deploy so many apps.</p>]]></content><author><name></name></author><category term="PWA" /><category term="Tailscale" /><summary type="html"><![CDATA[In my last post, I explored a way to generate a large word list for passphrase generation. But I couldn’t find any Android app that allows me to actually take this list and generate passphrases! That’s a pretty good excuse for me to play around with progressive webapps (PWA) to see if I could get an LLM to write me a simple app to install and run on my phone.]]></summary></entry><entry><title type="html">Generating Pronounceable Gibberish for Passphrases</title><link href="inacsb.com/gibberish-passphrase/" rel="alternate" type="text/html" title="Generating Pronounceable Gibberish for Passphrases" /><published>2025-09-11T00:00:00+00:00</published><updated>2025-09-11T00:00:00+00:00</updated><id>inacsb.com/gibberish-passphrase</id><content type="html" xml:base="inacsb.com/gibberish-passphrase/"><![CDATA[<p>I was looking for a way to generate strong and easy to type passwords, and ended
up with a simple method of generating a large word list. At the end of the post
I’ll provide the code, which makes it easy for you to bring in your own input
text corpus and generate your own word list according to your needs.</p>

<h1 id="passphrase">Passphrase</h1>

<p>As far as I can tell, this <a href="https://xkcd.com/936/">relevant xkcd</a> popularized
the use of a passphrase as a means of generating passwords. Unfortunately, 44
bits of entropy is now considered insufficient. From a cursory search, proton
recommends &gt;100 bits of entropy for a “strong” password. Using the xkcd method,
we’d need at least 9 english words, which is a lot of letters to type!</p>

<p>One might ask: why do we need easy to type passwords if I’m already using a
password manager with random password generation and autofill? That’s great, but
there are still going to be some passwords to be manually typed on a regular
basis, e.g. computer login or the master password of the password manager.</p>

<p>Is it possible to have a high entropy password that isn’t crazy long?</p>

<h1 id="pronounceable-gibberish">Pronounceable Gibberish</h1>

<p>I don’t know whom to credit, but the idea behind pronounceable gibberish is to
increase the entropy per “word” by not actually using words, but using a string
of letters that look somewhat like a word instead. So, like, “batim”, “phrux”
and whatnot. Still a lot easier to memorize and type than random letters like
“bFxls”, but much higher entropy per letter than using real English words.</p>

<p>Ideally, I’d like to have a word list of words no longer than 5 letters,
containing at least 2^20 words (around 1 million), then I can plug it into
KeePassXC to generate 80 bit passwords for normal passwords and 100 bit
passwords for the master password. I looked around online but couldn’t really
find anything like this.</p>

<h1 id="crafting-a-generator">Crafting a generator</h1>

<p>I spent two days trying to craft a gibberish generator using GPT-OSS-120B, using
techniques like alternating consonants and vowels, and stretching the
definitions of each as much as I can. In the end I mostly failed - I couldn’t
get past 500k words without starting to generate words that are unpronounceable.</p>

<p>Then I came up with the following algorithm, which was simple and good enough
that I got what I wanted in 20 mins. The idea is to generate all possible 5
letter strings, run them through some scoring function, then take the words with
the highest score. The scoring is a bit more involved but mostly GPT-OSS took
care of it: take some large text corpus as input (e.g. a large list of uncommon
english words), generate some log-likelihood of trigrams (i.e. how likely it is
for a given three letter combination to appear in a word), then compute the log
likelihood of the candidate accordingly. I’m honestly not sure how it worked in
full - I didn’t bother to read the code. Since it’s just for my own use, I
didn’t do anything like filter out bad words or filter out common passwords.</p>

<p>Here is the code if you’d like to run it yourself:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
generate_pseudowords.py

Usage
-----
    python generate_pseudowords.py &lt;corpus.txt&gt; [output.txt]
"""</span>

<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">math</span>
<span class="kn">import</span> <span class="nn">itertools</span>
<span class="kn">import</span> <span class="nn">heapq</span>
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">defaultdict</span><span class="p">,</span> <span class="n">Counter</span>

<span class="n">ALPHABET</span> <span class="o">=</span> <span class="s">"abcdefghijklmnopqrstuvwxyz"</span>
<span class="n">START</span> <span class="o">=</span> <span class="s">"&lt;"</span>
<span class="n">END</span>   <span class="o">=</span> <span class="s">"&gt;"</span>
<span class="n">N_TOP</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">**</span> <span class="mi">20</span>
<span class="n">SMOOTHING_ALPHA</span> <span class="o">=</span> <span class="mf">0.001</span>
<span class="n">MAX_LEN</span> <span class="o">=</span> <span class="mi">5</span>

<span class="k">def</span> <span class="nf">clean_word</span><span class="p">(</span><span class="n">word</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="s">"""Return a lower‑cased word containing only alphabetic chars."""</span>
    <span class="k">return</span> <span class="s">''</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">ch</span> <span class="k">for</span> <span class="n">ch</span> <span class="ow">in</span> <span class="n">word</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">if</span> <span class="n">ch</span> <span class="ow">in</span> <span class="n">ALPHABET</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">build_trigram_counts</span><span class="p">(</span><span class="n">corpus_path</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="s">"""
    Scan the corpus and count every trigram, including start/end markers.
    Returns:
        trigram_counts: dict{(a,b,c): int}
        bigram_counts : dict{(a,b): int}   (needed for conditional probs)
    """</span>
    <span class="n">trigram_counts</span> <span class="o">=</span> <span class="n">Counter</span><span class="p">()</span>
    <span class="n">bigram_counts</span>  <span class="o">=</span> <span class="n">Counter</span><span class="p">()</span>

    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">corpus_path</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s">'utf-8'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">f</span><span class="p">:</span>
            <span class="c1"># split on whitespace – Wikipedia already tokenises fairly well
</span>            <span class="k">for</span> <span class="n">raw</span> <span class="ow">in</span> <span class="n">line</span><span class="p">.</span><span class="n">split</span><span class="p">():</span>
                <span class="n">w</span> <span class="o">=</span> <span class="n">clean_word</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span>
                <span class="k">if</span> <span class="ow">not</span> <span class="n">w</span><span class="p">:</span>                <span class="c1"># skip empty tokens
</span>                    <span class="k">continue</span>
                <span class="c1"># pad with start/end markers
</span>                <span class="n">padded</span> <span class="o">=</span> <span class="n">START</span> <span class="o">+</span> <span class="n">w</span> <span class="o">+</span> <span class="n">END</span>
                <span class="c1"># slide a window of three characters
</span>                <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">padded</span><span class="p">)</span> <span class="o">-</span> <span class="mi">2</span><span class="p">):</span>
                    <span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span> <span class="o">=</span> <span class="n">padded</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">padded</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">],</span> <span class="n">padded</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">2</span><span class="p">]</span>
                    <span class="n">trigram_counts</span><span class="p">[(</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">,</span><span class="n">c</span><span class="p">)]</span> <span class="o">+=</span> <span class="mi">1</span>
                    <span class="n">bigram_counts</span><span class="p">[(</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">)]</span>     <span class="o">+=</span> <span class="mi">1</span>   <span class="c1"># count of the context
</span>
    <span class="k">return</span> <span class="n">trigram_counts</span><span class="p">,</span> <span class="n">bigram_counts</span>

<span class="k">def</span> <span class="nf">trigram_logprob</span><span class="p">(</span><span class="n">trigram</span><span class="p">,</span> <span class="n">trigram_counts</span><span class="p">,</span> <span class="n">bigram_counts</span><span class="p">,</span> <span class="n">vocab_size</span><span class="p">,</span> <span class="n">alpha</span><span class="p">):</span>
    <span class="s">"""Return log P(c | a,b) with add‑α smoothing."""</span>
    <span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">,</span><span class="n">c</span> <span class="o">=</span> <span class="n">trigram</span>
    <span class="n">num</span> <span class="o">=</span> <span class="n">trigram_counts</span><span class="p">.</span><span class="n">get</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">,</span><span class="n">c</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="n">alpha</span>
    <span class="n">den</span> <span class="o">=</span> <span class="n">bigram_counts</span><span class="p">.</span><span class="n">get</span><span class="p">((</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="n">alpha</span> <span class="o">*</span> <span class="n">vocab_size</span>
    <span class="c1"># Guard against log(0) – denominator is never zero because of smoothing
</span>    <span class="k">return</span> <span class="n">math</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">num</span> <span class="o">/</span> <span class="n">den</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">score_word</span><span class="p">(</span><span class="n">word</span><span class="p">,</span> <span class="n">trigram_counts</span><span class="p">,</span> <span class="n">bigram_counts</span><span class="p">,</span> <span class="n">vocab_size</span><span class="p">,</span> <span class="n">alpha</span><span class="p">):</span>
    <span class="s">"""
    Compute the log‑probability score of a word (without start/end markers).
    The word must already be cleaned (lower‑case, a‑z only).
    """</span>
    <span class="n">padded</span> <span class="o">=</span> <span class="n">START</span> <span class="o">+</span> <span class="n">word</span> <span class="o">+</span> <span class="n">END</span>
    <span class="n">score</span> <span class="o">=</span> <span class="mf">0.0</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">padded</span><span class="p">)</span> <span class="o">-</span> <span class="mi">2</span><span class="p">):</span>
        <span class="n">tri</span> <span class="o">=</span> <span class="p">(</span><span class="n">padded</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">padded</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">],</span> <span class="n">padded</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">2</span><span class="p">])</span>
        <span class="n">score</span> <span class="o">+=</span> <span class="n">trigram_logprob</span><span class="p">(</span><span class="n">tri</span><span class="p">,</span> <span class="n">trigram_counts</span><span class="p">,</span> <span class="n">bigram_counts</span><span class="p">,</span>
                                 <span class="n">vocab_size</span><span class="p">,</span> <span class="n">alpha</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">score</span>

<span class="k">def</span> <span class="nf">generate_all_candidates</span><span class="p">():</span>
    <span class="s">"""Yield every possible string of length 1‑5 over ALPHABET."""</span>
    <span class="k">for</span> <span class="n">length</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">MAX_LEN</span><span class="o">+</span><span class="mi">1</span><span class="p">):</span>
        <span class="k">for</span> <span class="n">tup</span> <span class="ow">in</span> <span class="n">itertools</span><span class="p">.</span><span class="n">product</span><span class="p">(</span><span class="n">ALPHABET</span><span class="p">,</span> <span class="n">repeat</span><span class="o">=</span><span class="n">length</span><span class="p">):</span>
            <span class="k">yield</span> <span class="s">''</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">tup</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">corpus_path</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">out_path</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"pseudowords.txt"</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"[*] Building trigram counts from corpus …"</span><span class="p">)</span>
    <span class="n">trigram_counts</span><span class="p">,</span> <span class="n">bigram_counts</span> <span class="o">=</span> <span class="n">build_trigram_counts</span><span class="p">(</span><span class="n">corpus_path</span><span class="p">)</span>

    <span class="c1"># Vocabulary for the trigram model = all symbols that can appear:
</span>    <span class="n">vocab</span> <span class="o">=</span> <span class="nb">set</span><span class="p">(</span><span class="n">ALPHABET</span><span class="p">)</span> <span class="o">|</span> <span class="p">{</span><span class="n">START</span><span class="p">,</span> <span class="n">END</span><span class="p">}</span>
    <span class="n">V</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">vocab</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"    #unique trigrams : </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">trigram_counts</span><span class="p">)</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"    vocab size      : </span><span class="si">{</span><span class="n">V</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

    <span class="n">heap</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="n">total</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"[*] Enumerating candidates and keeping the best ones …"</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">cand</span> <span class="ow">in</span> <span class="n">generate_all_candidates</span><span class="p">():</span>
        <span class="n">total</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="n">sc</span> <span class="o">=</span> <span class="n">score_word</span><span class="p">(</span><span class="n">cand</span><span class="p">,</span> <span class="n">trigram_counts</span><span class="p">,</span> <span class="n">bigram_counts</span><span class="p">,</span> <span class="n">V</span><span class="p">,</span> <span class="n">SMOOTHING_ALPHA</span><span class="p">)</span>

        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">heap</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">N_TOP</span><span class="p">:</span>
            <span class="n">heapq</span><span class="p">.</span><span class="n">heappush</span><span class="p">(</span><span class="n">heap</span><span class="p">,</span> <span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">cand</span><span class="p">))</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="c1"># heap[0] is the worst (smallest) score in the current top set
</span>            <span class="k">if</span> <span class="n">sc</span> <span class="o">&gt;</span> <span class="n">heap</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">]:</span>
                <span class="n">heapq</span><span class="p">.</span><span class="n">heapreplace</span><span class="p">(</span><span class="n">heap</span><span class="p">,</span> <span class="p">(</span><span class="n">sc</span><span class="p">,</span> <span class="n">cand</span><span class="p">))</span>

        <span class="c1"># occasional progress report
</span>        <span class="k">if</span> <span class="n">total</span> <span class="o">%</span> <span class="mi">1_000_000</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"    processed </span><span class="si">{</span><span class="n">total</span><span class="si">:</span><span class="p">,</span><span class="si">}</span><span class="s"> candidates … "</span>
                  <span class="sa">f</span><span class="s">"heap size = </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">heap</span><span class="p">)</span><span class="si">}</span><span class="s"> (worst score = </span><span class="si">{</span><span class="n">heap</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="mi">0</span><span class="p">]</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>

    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"[+] Finished scoring </span><span class="si">{</span><span class="n">total</span><span class="si">:</span><span class="p">,</span><span class="si">}</span><span class="s"> candidates."</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"[+] Keeping the top </span><span class="si">{</span><span class="n">N_TOP</span><span class="si">:</span><span class="p">,</span><span class="si">}</span><span class="s"> words."</span><span class="p">)</span>

    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"[*] Writing results to '</span><span class="si">{</span><span class="n">out_path</span><span class="si">}</span><span class="s">' …"</span><span class="p">)</span>
    <span class="n">top_words</span> <span class="o">=</span> <span class="n">heapq</span><span class="p">.</span><span class="n">nlargest</span><span class="p">(</span><span class="n">N_TOP</span><span class="p">,</span> <span class="n">heap</span><span class="p">)</span>  <span class="c1"># each entry = (score, word)
</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">out_path</span><span class="p">,</span> <span class="s">"w"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p">)</span> <span class="k">as</span> <span class="n">out</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">score</span><span class="p">,</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">top_words</span><span class="p">:</span>
            <span class="n">out</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">word</span><span class="si">}</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>

    <span class="k">print</span><span class="p">(</span><span class="s">"[+] Done!"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">2</span> <span class="ow">or</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">3</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="n">__doc__</span><span class="p">)</span>
        <span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

    <span class="n">corpus_file</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">output_file</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">==</span> <span class="mi">3</span> <span class="k">else</span> <span class="s">"pseudowords.txt"</span>
    <span class="n">main</span><span class="p">(</span><span class="n">corpus_file</span><span class="p">,</span> <span class="n">output_file</span><span class="p">)</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="Cybersecurity" /><category term="Vibecoding" /><summary type="html"><![CDATA[I was looking for a way to generate strong and easy to type passwords, and ended up with a simple method of generating a large word list. At the end of the post I’ll provide the code, which makes it easy for you to bring in your own input text corpus and generate your own word list according to your needs.]]></summary></entry><entry><title type="html">Bit Reversal in Concurrent Data Structures</title><link href="inacsb.com/bit-reversal-in-concurrent-data-structures/" rel="alternate" type="text/html" title="Bit Reversal in Concurrent Data Structures" /><published>2025-03-16T00:00:00+00:00</published><updated>2025-03-16T00:00:00+00:00</updated><id>inacsb.com/bit-reversal-in-concurrent-data-structures</id><content type="html" xml:base="inacsb.com/bit-reversal-in-concurrent-data-structures/"><![CDATA[<p>Combing through old papers on concurrent data structures, I was delighted to see
the bit reversal subroutine being used in 2 of the designs in ingenious ways. In
this post I will try to capture the essence of them.</p>

<h2 id="bit-reversal-concurrent-data-structures">Bit reversal? Concurrent data structures?</h2>

<p>Bit reversal is where we take an int (say, 64 bits) and reverse it, e.g.
reversing 8 bits may look like 00110101 -&gt; 10101100. There is no CPU instruction
to do so, but it can be implemented relatively efficiently in a few ways, e.g.
by masking + shifting, bswap (instruction for swapping bytes), lookup tables
etc.</p>

<p>Concurrent data structures are data structures that are safe to use in
shared-memory parallelism. For performance, modern computers don’t guarantee
strong memory consistency, and programs that need to access the same memory
locations need to explicitly do so, e.g. via mutexes and atomic operations.</p>

<h2 id="concurrent-priority-queue-by-hunt-et-al-1994">Concurrent priority queue by Hunt et. al. (1994)</h2>

<p>This section covers the paper An Efficient Algorithm for Concurrent Priority
Queue Heaps. The problem is: let’s say we want to have a heap (supports
inserting an element with priority and removing the element with highest
priority) and let a bunch of threads access it. The most naive thing to do is
probably an array-based binary heap that is put behind a lock. The ith node has
children 2i+1 and 2i+2, parent has higher priority than children, inserts happen
at the bottom and bubble up to the root, removals swap the last element to the
root and bubbles down, etc. Any thread that attempts to read/write needs to take
the lock, and only one thread can proceed at a time.</p>

<p>What if we put a lock at each node? Before you swap two nodes in a bubble-up
process, you’d only need to lock those two nodes. That way, you wouldn’t need to
lock the entire data structure, giving other threads a chance to proceed.</p>

<p>Unfortunately this results in high contention. If two threads are inserting at
the same time, one will be at node i, and the other will be at node i+1. Chances
are they have the same parent, so they immediately start contending for the same
lock. Even if they have different parents, their parents’ parents are probably
the same, and so on.</p>

<p>This paper’s main insight is: what if after inserting at node i, we don’t insert
at node i+1, but somewhere far away in the tree? Imagine the following binary tree:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    (0)
   /   \
 (1)   (2)
 / \   / \
3   4 5   6
</code></pre></div></div>

<p>Only nodes 0-2 are occupied. Inserting in the order (3, 4, 5, 6) will likely
cause high contention. But if we insert in the order (3, 5, 4, 6), then we might
avoid some of the most common lock contentions, since neighboring pairs don’t
share the same parent.</p>

<p>More generally, when a low bit of the heap size changes, you want to choose a
different sub-tree to insert; when a high bit changes, we have inserted a lot of
nodes, so it’s fine to choose a closer sub-tree.</p>

<p>What this ends up looking like is: the high bits of the node at which to insert
depends on the low bits of the heap size. We could literally implement that by
reversing the order of bits after the leading 1 bit. So the insert order would
look like:</p>

<p>0, 1; 10, 11; 100, 110, 101, 111; 1000, 1100, 1010, 1110, 1001, 1101, 1011,
1111…</p>

<p>This idea is simple but clever. Unfortunately this paper didn’t quite stand the
test of time, most importantly there’s still global contention to mutate the
heap size and to pop the heap at the root. Skip-list based designs are now
considered a better choice.</p>

<h2 id="split-ordered-lists-by-shalev-et-al-2006">Split-ordered lists by Shalev et. al. (2006)</h2>

<p>This section covers the paper Split-Ordered Lists: Lock-Free Extensible Hash
Tables.</p>

<p>Compared to priority queues, hash tables are undoubtedly more ubiquitous.
Generally they can be open/closed - open meaning each bucket contains a
resizable container (e.g. linked list) and closed meaning all data lives in the
array, and collisions are handled by probing.</p>

<p>A significant challenge in designing a concurrent hash table is the resizing
operation. Typically, resizes are done by doubling the size of the array and
moving all old data over to the new one. During this migration, no
readers/writers can proceed.</p>

<p>This paper asks: what if we don’t move the data into buckets, but move the
buckets to point to the data in the right way? If we’re only creating a new
index of the same underlying data, then the old index is still valid and can be
used in the meanwhile.</p>

<p>More concretely, we want to put all the items in a linked list, so that all the
hashes that are equal mod 2^k (for any k) are consecutive in the list. This way,
we can maintain a hash table by having an array where the ith bucket points to
the first node where hash mod 2^k = i.</p>

<p>It turns out you get this property by ordering the nodes by the reverse of the
hash. Thinking about it more, this isn’t that surprising - at first, you want
all the nodes with the hashes having 0 as the last bit to be in front of the
rest; then, within those, you want all the 0 as the second last bit to be in
front…</p>

<p>As a quick example, say we have 8 items with hashes 0-7. Sorted by this order,
we have:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>000 100 010 110 001 101 011 111
^               ^               | 2 buckets (hash mod 2)
^       ^       ^       ^       | 4 buckets (hash mod 4)
^   ^   ^   ^   ^   ^   ^   ^   | 8 buckets (hash mod 8)
</code></pre></div></div>

<p>Notice how across different number of buckets, the chains of each bucket is
still valid, even though the underlying linked list never changed.</p>

<p>From what I can tell, this design is still not obsolete in 2025, even as new
improvements have appeared and computers evolved.</p>]]></content><author><name></name></author><category term="Data Structures" /><summary type="html"><![CDATA[Combing through old papers on concurrent data structures, I was delighted to see the bit reversal subroutine being used in 2 of the designs in ingenious ways. In this post I will try to capture the essence of them.]]></summary></entry><entry><title type="html">HTB CTF Try Out</title><link href="inacsb.com/htb-ctf-try-out/" rel="alternate" type="text/html" title="HTB CTF Try Out" /><published>2024-10-20T00:00:00+00:00</published><updated>2024-10-20T00:00:00+00:00</updated><id>inacsb.com/htb-ctf-try-out</id><content type="html" xml:base="inacsb.com/htb-ctf-try-out/"><![CDATA[<p>I just finished all challenges for <a href="https://ctf.hackthebox.com/event/details/ctf-try-out-1434">HTB CTF Try
Out</a>, which was my
first CTF event. This post serves both as a summary for what I’ve learned and as
guides for beginners like myself.</p>

<h2 id="background">Background</h2>

<p>If you’re a total beginner to cyber security like myself, Capture The Flag
events are something like puzzles that often involve extracting obscured
information, writing bespoke interactive scripts or hacking designated processes
to eventually get access to a string, i.e. the flag. It’s a bit like puzzle
hunts in that challenges can take many forms, and the difficulty is mostly
figuring out what the rules are, rather than following them.</p>

<p>Below, I’ll go through each category. I won’t detail the solutions, only
describe general knowledge needed to solve the challenges.</p>

<h2 id="web-timekorp-flag-command-labyrinth-linguist">Web: TimeKORP, Flag Command, Labyrinth Linguist</h2>

<p>There are 2 flavors of exploits here - client side and server side.</p>

<p>On the client side, I find the chrome dev tools (source code, console, debugger)
more than sufficient to understand what the program does or read data out of
memory.</p>

<p>On the server side, there are various types of injections. The relevant ones are
php and server-side template injection here. TimeKORP’s source code was given
which makes it pretty easy, as long as you know how to <code class="language-plaintext highlighter-rouge">cat</code> a file. Labyrinth
Linguist’s source code was given as an encrypted zip. This encrypted zip could
be <a href="https://github.com/kimci86/bkcrack">decrypted</a>, but note that the cracker
only works for uncompressed plaintext of at least 12 bytes (there is exactly one
file in the zip that matches the conditions). You actually need to figure this
out for a later challenge. But even if you didn’t, you can still see what
entries are in the zip and therefore what SSTI payloads are likely to work. If
you have Burp Suite Professional Edition, your life is a lot easier as it just
tells you the vulnerability. From there it was still difficult to get remote
code execution because none of the top google results for the payloads worked.
The hint here is that in addition to getClassLoader, there is another function
that achieves similar functionality that gives you the java runtime for RCE.</p>

<h2 id="forensics-an-unusual-sighting-phreaky">Forensics: An Unusual Sighting, Phreaky</h2>

<p>Similar to Labyrinth Linguist, you’re given an encrypted file for Phreaky. Refer
to the above to decrypt the zip (john the ripper wasn’t the right tool). Then
Wireshark can be used to reveal the next step, and NetworkMiner can get the job
done.</p>

<h2 id="reversing-lootstash">Reversing: LootStash</h2>

<p>Just install <a href="https://github.com/NationalSecurityAgency/ghidra">ghidra</a>. It’ll
be useful later.</p>

<h2 id="misc-character-stop-drop-and-roll">Misc: Character, Stop Drop and Roll</h2>

<p>LLMs make these coding problems very easy. I did struggle with Stop Drop and
Roll a little bit until I changed the approach to use a regex.</p>

<h2 id="crypto-dynastic">Crypto: Dynastic</h2>

<p>This is like a Caesar cipher (not sure if there’s a name for this variant).</p>

<h2 id="hardware-critical-flight-debug">Hardware: Critical Flight, Debug</h2>

<p>You just have to figure out which softwares to use to open these files. I used
KiCad Gerber Viewer and Saleae Logic-2.</p>

<h2 id="pwn-getting-started-labyrinth-void">Pwn: Getting Started, Labyrinth, Void</h2>

<p>Ghidra was very helpful for these, as well as
<a href="https://docs.pwntools.com/en/latest/">pwntools</a>. For Labyrinth, you need to
learn something called <a href="https://book.hacktricks.xyz/binary-exploitation/rop-return-oriented-programing">Return Oriented
Programming</a>.
Embarrassingly I had to peek at a solution for Void, but I just didn’t read the
linked website closely enough. <a href="https://book.hacktricks.xyz/binary-exploitation/rop-return-oriented-programing/ret2dlresolve">This code
snippet</a>
basically worked after filling in the details shown by ghidra.</p>

<h2 id="closing-thoughts">Closing Thoughts</h2>

<p>There are quite a few libraries, tools and concepts to go through even in this
“beginner-friendly” CTF. But adding these to my personal toolbox has been quite
rewarding.</p>

<p>As a metapoint, it can feel like cheating to get hint by searching for the
challenge name online. But all of this is for fun anyway, so if you’re going to
have more fun knowing what to do than getting stuck, then that’s more important.</p>]]></content><author><name></name></author><category term="Capture The Flag" /><category term="Hack The Box" /><summary type="html"><![CDATA[I just finished all challenges for HTB CTF Try Out, which was my first CTF event. This post serves both as a summary for what I’ve learned and as guides for beginners like myself.]]></summary></entry><entry><title type="html">The Monty Hall Game</title><link href="inacsb.com/the-monty-hall-game/" rel="alternate" type="text/html" title="The Monty Hall Game" /><published>2024-05-27T00:00:00+00:00</published><updated>2024-05-27T00:00:00+00:00</updated><id>inacsb.com/the-monty-hall-game</id><content type="html" xml:base="inacsb.com/the-monty-hall-game/"><![CDATA[<p>A friend brought up the classic Monty Hall problem. During our discussions, I
realized it’s interesting to put the problem under the lens of game theory.</p>

<h2 id="refresher">Refresher</h2>

<p>You’re on a game show. The host shows you three doors, and you win a car iff you
open the right door. After you name your choice (door 1), the host opens door 2,
showing that it’s not the right one. Now you’re given the choice to switch to
door 3. Should you do it?</p>

<p>Person A says it doesn’t matter, since 1 and 3 are still equally likely to be
the right door. Why would eliminating door 2 make that untrue?</p>

<p>Person B says we should switch. Before the host opened door 2, door 1 has 1/3
chance to be the right one, i.e. there is 2/3 chance that you’re wrong. If you
were wrong, switching would make you right, so door 3 now has 2/3 chance of
being right!</p>

<p>This apparent paradox has caused heated debates throughout the years.</p>

<h2 id="a-two-player-zero-sum-game">A two-player zero-sum game</h2>

<p>It turns out that what you should do depends on what the host was thinking when
they opened door 2.</p>

<p>Let us reframe the game as a two-player zero-sum game. The contestant picks a
door (name it 1), then the host either opens door 1 or another door (name it 2),
showing that it’s not the correct door. Now the contestant can pick one of the
two unopened doors.</p>

<p>The contestant’s strategy can be characterized with a single probability <code class="language-plaintext highlighter-rouge">p</code>. If
the host opens door 1, obviously the contestant can only guess among the other
two doors, getting it right 1/2 of the time. If the host opens door 2, the
contestant can switch with probability <code class="language-plaintext highlighter-rouge">p</code>.</p>

<p>The host’s strategy can also be characterized with a single probability <code class="language-plaintext highlighter-rouge">q</code>. If
door 1 is correct, then the host doesn’t have any choice but to open one of the
remaining two doors at random. Otherwise, the host can open door 1 with
probability <code class="language-plaintext highlighter-rouge">q</code>, forcing the contestant to guess.</p>

<p>The contestant’s probability of winning would then be:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P(door 1 is right) * P(contestant doesn't switch)
+ P(door 1 is wrong) * P(host opens door 1) * P(contestant guesses right)
+ P(door 1 is wrong) * P(host opens door 2 or 3) * P(constestant switches)
= 1/3 * (1 + p + q - 2pq)
</code></pre></div></div>

<p>If the host wants to minimize this probability by changing <code class="language-plaintext highlighter-rouge">q</code>, then it depends
the sign of <code class="language-plaintext highlighter-rouge">1-2p</code>. If <code class="language-plaintext highlighter-rouge">p &lt; 1/2</code>, the host would want <code class="language-plaintext highlighter-rouge">q</code> to be minimized,
otherwise maximized. The contestant faces an analogous situation: if <code class="language-plaintext highlighter-rouge">q &lt; 1/2</code>,
<code class="language-plaintext highlighter-rouge">p</code> should be maximized, otherwise minimized. In other words, if <code class="language-plaintext highlighter-rouge">p</code> is small,
<code class="language-plaintext highlighter-rouge">q</code> should be small, which should make <code class="language-plaintext highlighter-rouge">p</code> large, which should make <code class="language-plaintext highlighter-rouge">q</code> large…
The only equilibrium is when <code class="language-plaintext highlighter-rouge">p = q = 1/2</code>, which makes the probability of
winning 1/2. This intuitively makes sense: either player is just randomly
guessing, and the result is the same as flipping a coin.</p>

<h2 id="the-game-show-hypothetically">The game show, hypothetically</h2>

<p>In the actual game show, the host never opens the contestant’s chosen door, i.e.
<code class="language-plaintext highlighter-rouge">q = 0</code>. By the above analysis, <code class="language-plaintext highlighter-rouge">p</code> should be 1, resulting in a winning
probability of 2/3. One way to think about this classic result is that the host
is playing a bad strategy and getting exploited by the contestant.</p>

<p>But as a thought experiment, what if you’re the first ever contestant, and you
don’t know what the host is thinking when you see door 2 being opened?</p>

<ul>
  <li>If <code class="language-plaintext highlighter-rouge">q = 0</code> then switching is 2/3 of a prize;</li>
  <li>But if <code class="language-plaintext highlighter-rouge">q = 1</code> then switching is 1/3;</li>
  <li>What if the host only opens the second door if you were correct on the first
try? Then switching always loses.</li>
  <li>What are the host’s incentives? Does the show want to save on the prizes, or
is giving more prizes better for ratings?</li>
  <li>What is the average contestant expected to do? Does that affect the design of
the game?</li>
</ul>

<p>How do we stay sane when playing games when the rules are unknown?</p>]]></content><author><name></name></author><category term="Game Theory" /><summary type="html"><![CDATA[A friend brought up the classic Monty Hall problem. During our discussions, I realized it’s interesting to put the problem under the lens of game theory.]]></summary></entry><entry><title type="html">When, If Not Now?</title><link href="inacsb.com/when-if-not-now/" rel="alternate" type="text/html" title="When, If Not Now?" /><published>2023-04-08T00:00:00+00:00</published><updated>2023-04-08T00:00:00+00:00</updated><id>inacsb.com/when-if-not-now</id><content type="html" xml:base="inacsb.com/when-if-not-now/"><![CDATA[<p>Everybody procrastinates, and everyone hates it. Much ink has been spilled to
teach people how to stop delaying work. Here, I want to offer a logician’s view.</p>

<p>There are certain tasks that I want to do, but there aren’t clear deadlines, for
example working on a hobby project, reading a book, or learning how to play a
new song. I might write those down in my notes, thinking to myself: I’ll get to
them when I get some free time.</p>

<p>But when I finally have some free time, I start to find excuses. I’m a bit
tired right now. Maybe in an hour. Why not tomorrow?</p>

<p>The thing is, if at time t, I have the chance to do a thing, but I delay that to
t+1, then by induction I am never going to do it. In other words, if I’m ever
going to do it, it might as well be now.</p>

<p>It is always now or never.</p>]]></content><author><name></name></author><category term="Nontechnical" /><summary type="html"><![CDATA[Everybody procrastinates, and everyone hates it. Much ink has been spilled to teach people how to stop delaying work. Here, I want to offer a logician’s view.]]></summary></entry><entry><title type="html">This Sudoku Must Be Solvable</title><link href="inacsb.com/this-sudoku-must-be-solvable/" rel="alternate" type="text/html" title="This Sudoku Must Be Solvable" /><published>2023-02-04T00:00:00+00:00</published><updated>2023-02-04T00:00:00+00:00</updated><id>inacsb.com/this-sudoku-must-be-solvable</id><content type="html" xml:base="inacsb.com/this-sudoku-must-be-solvable/"><![CDATA[<h2 id="unique-rectangle">Unique Rectangle</h2>

<p>In this post I’ll share my favorite Sudoku trick, called the Unique Rectangle. It is really clever and comes up in actual puzzles. I’ve been solving the New York Times hard Sudoku every night for years, and I’ve used this trick on maybe 10-20% of them.</p>

<p>Imagine you’re solving a Sudoku and you’ve penciled in:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     1   2   3
   +===+===+===+
A  |12 |12 | ? |
   +---+---+---+
B  | ? | ? | ? |
   +---+---+---+
C  | ? | ? | ? |
   +===+===+===+
D  |12 |123| ? |
   +---+---+---+
</code></pre></div></div>
<p>You’ve eliminated all possibilities except <code class="language-plaintext highlighter-rouge">1</code> and <code class="language-plaintext highlighter-rouge">2</code> for grids <code class="language-plaintext highlighter-rouge">A1</code>, <code class="language-plaintext highlighter-rouge">A2</code> and <code class="language-plaintext highlighter-rouge">D1</code>, and you know <code class="language-plaintext highlighter-rouge">D2</code> can only be <code class="language-plaintext highlighter-rouge">1</code>, <code class="language-plaintext highlighter-rouge">2</code> or <code class="language-plaintext highlighter-rouge">3</code>.</p>

<p>The Unique Rectangle rule says that <code class="language-plaintext highlighter-rouge">D2</code> must be <code class="language-plaintext highlighter-rouge">3</code>.</p>

<p>“Why?” You object. “There are only 3 rules in Sudoku - all 9 3x3 blocks, 9 rows and 9 columns must all contain numbers <code class="language-plaintext highlighter-rouge">1-9</code>. If <code class="language-plaintext highlighter-rouge">D2</code> is <code class="language-plaintext highlighter-rouge">1</code> or <code class="language-plaintext highlighter-rouge">2</code>, we can still fill in <code class="language-plaintext highlighter-rouge">A1</code>, <code class="language-plaintext highlighter-rouge">A2</code> and <code class="language-plaintext highlighter-rouge">D1</code> without violating the rules!”</p>

<p>You are right that the normal constraints of Sudoku aren’t enough. This trick relies on a leap of faith: we assume that <em>the puzzle has a unique solution</em>. This is true for all valid Sudokus – if there are more than one solutions, then the puzzle will not be solvable using logic, and is therefore invalid. (Using the fact that the puzzle is solvable to solve the puzzle might feel like cheating, but I have no personal issues with it.)</p>

<p>In the above example, if <code class="language-plaintext highlighter-rouge">D2</code> isn’t <code class="language-plaintext highlighter-rouge">3</code>, we have 2 possibilities:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(I)       1   2   3     (II)      1   2   3
        +===+===+===+           +===+===+===+
     A  | 1 | 2 | ? |        A  | 2 | 1 | ? |
        +---+---+---+           +---+---+---+
     ...                     ...
        +===+===+===+           +===+===+===+
     D  | 2 | 1 | ? |        D  | 1 | 2 | ? |
        +---+---+---+           +---+---+---+
</code></pre></div></div>
<p>The only constraints that can be used to solve these 4 grids are columns <code class="language-plaintext highlighter-rouge">1</code> and <code class="language-plaintext highlighter-rouge">2</code>, rows <code class="language-plaintext highlighter-rouge">A</code> and <code class="language-plaintext highlighter-rouge">D</code> and the 3x3 blocks, but in all these constraints, <code class="language-plaintext highlighter-rouge">(I)</code> and <code class="language-plaintext highlighter-rouge">(II)</code> are indistinguishable, and we are left with an unsolvable puzzle. So <code class="language-plaintext highlighter-rouge">D2</code> has to be <code class="language-plaintext highlighter-rouge">3</code>.</p>

<p>More generally, the Unique Rectangle rule states that if you have 4 grids in 2 3x3 blocks forming a rectangle, and 3 of them have only 2 choices, then the fourth grid cannot be either of those choices.</p>

<p>There are some extensions to this trick. One is chaining which I have also used:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     1   2   3
   +===+===+===+
A  |12 |12 | ? |
   +---+---+---+
...
   +===+===+===+
D  |23 |23 | ? |
   +---+---+---+
...
   +===+===+===+
G  |13 |134| ? |    G2 cannot be 1 or 3
   +---+---+---+
</code></pre></div></div>

<p>Another one that I recently figured out during solves:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     1   2   3
   +===+===+===+
A  |12 |12 | ? |
   +---+---+---+
B  | ? | ? | ? |
   +---+---+---+
C  | ? | ? | ? |
   +===+===+===+
D  |123|123|34 |
   +---+---+---+

Either D1 or D2 must be 3, so we know D3 must be 4.
</code></pre></div></div>

<p>Here is a <a href="https://www.learn-sudoku.com/advanced-techniques.html">website with other advanced Sudoku techniques</a>. I have not found the other tricks to be as interesting or as applicable to NYT puzzles.</p>

<h2 id="leap-of-faith">Leap of Faith</h2>

<p>This sort of <em>“if there is a solution, it must be this”</em> thinking comes up in other occasions as well. Here are a few that I can immediately think of.</p>

<p>In minesweeper, it is very common to run into unsolvable boards, but the same logic can still apply. In this example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     1   2   3   4
   +===+===+===+===+ ...
A  |   |   | 1 | 0 | 
   +---+---+---+---+
B  |   |   | 2 | 1 |
   +---+---+---+---+
C  | 1 | 2 | |&gt;| 1 |
   +---+---+---+---+
D  | 0 | 1 | 1 | 1 |
   +---+---+---+---+
...
</code></pre></div></div>
<p>There are three possibilities:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(I)            (II)          (III)
+===+===+ ...  +===+===+ ... +===+===+ ...
| X |   |      |   | X |     |   |   |
+---+---+      +---+---+     +---+---+
|   | X |      | X |   |     |   | X |
+---+---+      +---+---+     +---+---+
...            ...           ...            
</code></pre></div></div>
<p>When you finish the rest of the board, you can see how many mines are left. If it’s 1, you know it must be <code class="language-plaintext highlighter-rouge">(III)</code>; but you can’t tell between <code class="language-plaintext highlighter-rouge">(I)</code> and <code class="language-plaintext highlighter-rouge">(II)</code>. Either way, clicking on <code class="language-plaintext highlighter-rouge">A2</code> or <code class="language-plaintext highlighter-rouge">B1</code> is never wrong, so you can do it without solving the rest of the board. (In minesweeper, if you must guess, it is better to guess earlier so that you don’t waste time in an unsolvable game).</p>

<p>Another example is <a href="https://en.wikipedia.org/wiki/Induction_puzzles#Alice_at_the_Convention_of_Logicians">Alice at the Convention of Logicians</a>. Everything on this wiki page is worth a read, so I won’t bother explaining the puzzle here.</p>

<p>I’ve also found that this line of thinking is useful in solving math puzzles in general, or even problems that aren’t necessarily designed to be solvable. It is like a less blasphemous form of Pascal’s wager – if you’re right, then great, otherwise it doesn’t matter.</p>]]></content><author><name></name></author><category term="Game" /><summary type="html"><![CDATA[Unique Rectangle]]></summary></entry><entry><title type="html">Left Shoot, Right Shoot: A Rock Paper Scissors Variant</title><link href="inacsb.com/left-shoot-right-shoot/" rel="alternate" type="text/html" title="Left Shoot, Right Shoot: A Rock Paper Scissors Variant" /><published>2022-09-19T21:31:00+00:00</published><updated>2022-09-19T21:31:00+00:00</updated><id>inacsb.com/left-shoot-right-shoot</id><content type="html" xml:base="inacsb.com/left-shoot-right-shoot/"><![CDATA[<p>In Hong Kong, there’s a variant of rock paper scissors called 左一拳右一拳, which can be roughly translated as “left shoot, right shoot”. In this post, I’ll walk through how it works and how to play optimally.</p>

<h2 id="rules">Rules</h2>

<p>There are three steps in this game. First, both players play rock paper scissors with one hand. Second, both players play rock paper scissors with the other hand. Third, both players take back one hand, and the winner is determined by comparing the remaining hands using normal rock paper scissors rules.</p>

<p>Let’s run through a quick example. Say players A/B play ✊/🖐, then 🖐/✌. Now since A can only pick between ✊ and 🖐, if B keeps 🖐 and retracts ✌, B will never lose. If A anticipates B to play 🖐 and also plays 🖐, then they will tie.</p>

<p>But if B anticipates that, B can sometimes pick ✌ which beats 🖐. But if A anticipates that, maybe A will also sometimes play ✊… In true rock paper scissors spirit, there always seems to be a better strategy.</p>

<h2 id="strategy">Strategy</h2>

<p>But of course, all games have optimal strategies, when we allow strategies to incorporate randomness. The optimal play is:</p>
<ol>
  <li>Pick anything for the first hand with equal probability;</li>
  <li>follow your opponent’s first hand for your second hand with 2/3 probability, unless that’s the same as your first hand, in which case pick the one that beats that;</li>
  <li>Keep the hand that both players have in common with 2/3 probability, unless both players have the same two choices, in which case pick the one that doesn’t lose.</li>
</ol>

<p>Below, let’s go through an outline of the math involved. First, we have to define what both players are maximizing.</p>

<h2 id="objective">Objective</h2>

<p>It might not be immediately obvious that it is nontrivial to define the goal of the game - of course you want to win instead of lose! But this is not always the case. For example, some people might really want to avoid losing, instead of trying too hard to win.</p>

<p>Rock paper scissors is commonly played as a way to fairly decide a binary outcome between two people (e.g. who gets to pick the restaurant). It is reasonable to apply the same to this game, meaning players will repeat the game until a winner is determined, allowing no ties. Playing optimally then means maximizing the probability of winning in a repeated game.</p>

<p>Since the game is fair, in the event of a tie, your chance of winning is still 1/2. So, we’re maximizing <code class="language-plaintext highlighter-rouge">P(win)+P(tie)*1/2</code>, which is the same as minimizing of <code class="language-plaintext highlighter-rouge">P(lose)+P(tie)*1/2</code>. Equivalently we can put those together and maximize <code class="language-plaintext highlighter-rouge">P(win)-P(lose)</code> for each game. In other words, we can pretend the loser pays $x to the winner, and maximize the expected value of winnings.</p>

<h2 id="analysis">Analysis</h2>

<p>First we can analyze step 3 - both players have to pick one out of two given choices.</p>

<p>Let’s run through the boring cases quickly. Boring case 1: either player has the same choice for both hands (dumb). Boring case 2: both players have the same two choices (the optimal pick is obvious).</p>

<p>Now to analyze the remaining case where both players have different choices. Let’s say A has ✊, 🖐 and B has 🖐, ✌ to pick from. Let’s say the winner of the game wins $3, the loser loses $3. We have four outcomes after both players take back one hand:</p>

<table>
  <thead>
    <tr>
      <th>A \ B</th>
      <th>🖐</th>
      <th>✌</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>✊</td>
      <td>-3 \ 3</td>
      <td>3 \ -3</td>
    </tr>
    <tr>
      <td>🖐</td>
      <td>0 \ 0</td>
      <td>-3 \ 3</td>
    </tr>
  </tbody>
</table>

<p>Since this is a zero sum game, to maximize your winning, you want to make your opponent’s best option as bad as possible. This happens when both of their options are equally bad.</p>

<p>When A (B) plays 🖐 with 2/3 probability, B (A) gets $1 (-1) in expectation no matter which hand they take back. Hence, the optimal strategy for step 3 is to pick the option that can lead to a tie 2/3 of the time (🖐 in this case).</p>

<p>Now that we’ve established step 3’s strategy, let’s do the same to step 2. Say A picked ✊ and B picked 🖐, and both have to pick their second hand. Since we already worked out the expected value for all possibilities at step 3, we can again tabulate the expected value of all scenarios for A and B here.</p>

<table>
  <tbody>
    <tr>
      <td>A \ B</td>
      <td>🖐 , ✊</td>
      <td>🖐 , ✌</td>
    </tr>
    <tr>
      <td>✊ , ✌</td>
      <td>-1 \ 1</td>
      <td>1 \ -1</td>
    </tr>
    <tr>
      <td>✊ , 🖐</td>
      <td>0 \ 0</td>
      <td>-1 \ 1</td>
    </tr>
  </tbody>
</table>

<p>If you paid attention, you’ll realize that this is just the previous table from step 3 but with winnings scaled down by 1/3. This means that at step 2, we’re playing the exact same game! So the optimal strategy must also be the same - we play the option that leads to a tie with probability 2/3.</p>

<h2 id="summary">Summary</h2>

<p>As a bonus fact, we can calculate the probability of a tie. If both players end up with the same two choices after step 2, the game must end in a tie; otherwise there’s still a 2/3*2/3 chance of a tie. This yields 1/3 + 2/3 * (4/9 + 5/9 * 4/9) = 193/243 = 79.4% chance of a tie.</p>

<p>This concludes the analysis of the game. In Cantonese, both players would both say something like “Xxx, Xxx, xxxxXxx” (inscrutable Chinese) where the uppercase letters indicate when the steps happen. I wonder how this gameplay can be translated into English exactly.</p>]]></content><author><name></name></author><category term="Strategy" /><category term="Game" /><summary type="html"><![CDATA[In Hong Kong, there’s a variant of rock paper scissors called 左一拳右一拳, which can be roughly translated as “left shoot, right shoot”. In this post, I’ll walk through how it works and how to play optimally.]]></summary></entry><entry><title type="html">Unexpected Chinese Remainder Theorem</title><link href="inacsb.com/unexpected-chinese-remainder-theorem/" rel="alternate" type="text/html" title="Unexpected Chinese Remainder Theorem" /><published>2022-05-28T00:00:00+00:00</published><updated>2022-05-28T00:00:00+00:00</updated><id>inacsb.com/unexpected-chinese-remainder-theorem</id><content type="html" xml:base="inacsb.com/unexpected-chinese-remainder-theorem/"><![CDATA[<p>Last week, we had an incident at work. Both the bug itself and the debugging process were mildly interesting, and I’ll describe both briefly below, and discuss some lessons.</p>

<h2 id="the-setup-and-the-incident">The Setup and the Incident</h2>

<p>We have a system that basically subscribes to a whole bunch of data, does a bunch of computations on them, and publishes output in real time. The computations are split into tasks identifiable by unique names. For both CPU &amp; memory reasons, the system spawns up to a few dozens of workers (Linux processes) across a bunch of computers, and assigns each task to one process by the hash of its name, modulus the number of workers.</p>

<p>For redundancy and latency, we run a few replicas of the whole thing across multiple data centers, all doing roughly the same computations.</p>

<p>One day, during a routine system upgrade, all replicas crashed one after another. This got us into panic mode.</p>

<h2 id="the-debugging-process">The Debugging Process</h2>

<p>To be clear, this is bad - this is what we specifically tried to prevent by running replicas, and stagger their upgrade schedule.</p>

<p>To find out the root cause, the first thing as always is to inspect the logs. There was a single error message saying which worker crashed first, and the exception that crashed it. The exception suggested that one of the values that came from a data subscription was too large and caused a buffer overflow.</p>

<p>OK, that’s something, but we have hundreds of thousands of data subscriptions, so we need more cleverness to narrow it down so we can find the problematic data.</p>

<p>Well, we know the worker number is X out of N total workers from the logs. If we get a list of all data scriptions and their task names (a data subscription is also considered a task), then we can compute the hash of each name and see which ones are running on worker X. This will narrow it down by a factor of N, which is on the order of 50. Which is not nearly enough.</p>

<p>But it happens that due to whatever reason, some replicas run with a different number of workers. That means we can gather a bunch of X<sub>i</sub> and N<sub>i</sub> pairs, and narrow down the set of suspected tasks further.</p>

<p>If you have a few equations of the form <code class="language-plaintext highlighter-rouge">A mod Ni = Xi</code>, you can merge them together to get <code class="language-plaintext highlighter-rouge">A mod N* = X*</code> where N* is the LCM of all N<sub>i</sub> using the Chinese Remainder Theorem. The larger we can make N*, the fewer tasks will satisfy the equation, and the better chance we have to pin down exactly the one task we’re looking for.</p>

<p>So we gathered 4 pairs of X and N, and ended up shrinking the number of suspected tasks by a factor of a few thousand, leaving us with only a few dozen options. Poring over the task names one by one, we finally found the one subscription that caused the crashes.</p>

<h2 id="the-bug">The Bug</h2>

<p>The bug itself is fairly simple, but the mechanism in which it crashed all replicas is a bit subtle.</p>

<p>There was a recent code change that changes the behavior when the system gets erroneous values from data subscriptions. In the past, when a worker gets an error, it just passes the error value to downstream computations. The code change was to append metadata to these errors to help track down where they came from.</p>

<p>This change seems innocent enough, but the issue manifests when the system is configured to subscribe to data published by itself. Let’s say such a self loop exists in a task. When this task first computes, it subscribes to data that hasn’t been published yet, so it will result in an error. In the old code, this error will then be fed in to the task again, but the output will not change. However, in the new code, each round of feedback leads to a bigger error value due to the additional metadata, eventually overflowing buffers and crashing the process and also clients consuming the value.</p>

<p>This also explains why despite rolling out the new executable to only a subset of replicas, all of them crashed. When subscribing to a data, you have to specify some sort of url, and this url points to one of the replicas. In other words, only one replica has the self loop, and other replicas are consumers of the output of the loop. So when the loop is completed in the roll process, all replicas will crash upon receiving the large value, regardless of whether they contain the code change.</p>

<h2 id="reflections">Reflections</h2>

<p>This incident didn’t end up causing too much headache because it was fixable with a rollback. Either way, it’s a good exercise to think through it to learn the maximum amount of lessons out of it.</p>

<p>Pinning down the issue this time required some amount of luck. In particular, we had replicas running with different number of workers. This did not have to be the case, and it even seems undesirable to have different enviroments. One change we could make here is to change the hashing scheme of task names to worker. We could hash the task name and the replica ID together (i.e. use the ID to salt the hash). This way, we don’t have to rely on the numbers of workers being coprime with each other to narrow down tasks using worker IDs.</p>

<p>This incident is not the first time that snowballing error values caused hiccups. I’ve also heard of stories where parsing and appending to error values lead to accidentally quadratic time complexity. Perhaps we should be a bit careful when dealing with error values, because they can sometimes be unexpectedly large. (I’m not sure how much this makes sense in various programming languages; some languages might not have the concept of a error value object that can be manipulated at runtime.)</p>

<p>Another thing that is less clear is that perhaps we could just outright ban self loops, as these are probably just footguns. But this might or might be reasonable depending on the actual situation.</p>

<p>One might also be tempted to think that instead of relying on clever filtering based on hashes, we should just improve the error message in the logs to show exactly what caused the crash. I think practically this would not have helped in this case. Sometimes you just don’t know where the system could crash - if we had anticipated it, we would’ve fixed it. Wrapping every single part of code with error tagging just seems excessive and infeasible.</p>

<p>In the end, I didn’t think anyone made a mistake in the process, and there was not much we could’ve done to avoid it. Testing couldn’t have caught it because the loop would only exist in production, due to all testing systems also subscribing to the url that points to production; this bug was hard to anticipate in code review; and careful deployment wouldn’t have prevented it.</p>

<p>Sometimes, incidents are just a cost of business, even if they happen in production.</p>]]></content><author><name></name></author><category term="Bug" /><category term="Debugging" /><summary type="html"><![CDATA[Last week, we had an incident at work. Both the bug itself and the debugging process were mildly interesting, and I’ll describe both briefly below, and discuss some lessons.]]></summary></entry><entry><title type="html">A Tale of Two Zeros</title><link href="inacsb.com/a-tale-of-two-zeros/" rel="alternate" type="text/html" title="A Tale of Two Zeros" /><published>2022-04-07T00:00:00+00:00</published><updated>2022-04-07T00:00:00+00:00</updated><id>inacsb.com/a-tale-of-two-zeros</id><content type="html" xml:base="inacsb.com/a-tale-of-two-zeros/"><![CDATA[<p>One day I came across some code that looked like this (paraphrased):</p>

<div class="language-ocaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">sign__old</span> <span class="n">f</span> <span class="o">=</span>
  <span class="k">assert</span> <span class="p">(</span><span class="nn">Float</span><span class="p">.</span><span class="n">is_finite</span> <span class="n">f</span><span class="p">);</span>
  <span class="k">if</span> <span class="nn">String</span><span class="p">.</span><span class="n">is_prefix</span> <span class="p">(</span><span class="nn">Float</span><span class="p">.</span><span class="n">to_string</span> <span class="n">f</span><span class="p">)</span> <span class="o">~</span><span class="n">prefix</span><span class="o">:</span><span class="s2">"-"</span>
  <span class="k">then</span> <span class="nt">`Negative</span>
  <span class="k">else</span> <span class="nt">`Nonnegative</span>
</code></pre></div></div>

<p>Naturally, I replaced it with the following:</p>

<div class="language-ocaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="n">sign__new</span> <span class="n">f</span> <span class="o">=</span>
  <span class="k">assert</span> <span class="p">(</span><span class="nn">Float</span><span class="p">.</span><span class="n">is_finite</span> <span class="n">f</span><span class="p">);</span>
  <span class="k">if</span> <span class="nn">Float</span><span class="p">.</span><span class="n">is_negative</span> <span class="n">f</span> <span class="k">then</span> <span class="nt">`Negative</span> <span class="k">else</span> <span class="nt">`Nonnegative</span>
</code></pre></div></div>

<p>A few days later, this caused an issue in prod. How is it possible? These two functions are obviously equivalent, right?</p>

<p>It turns out there is exactly one edge case for which these two functions behave differently: -0., aka negative float zero.</p>

<h2 id="positive-vs-negative-zero">Positive vs Negative Zero</h2>

<p>In the <a href="https://en.wikipedia.org/wiki/IEEE_754">IEEE floating point standard</a>, numbers are represented as sign and magnitude. This means it is technically possible to have both a positive and a negative zero. While these two values are numerically equal, both are treated as valid floats, and they behave differently when passed into different functions.</p>

<p>In this case, <code class="language-plaintext highlighter-rouge">sign__new</code> sees negative zero as <code class="language-plaintext highlighter-rouge">`Nonnegative</code>, because it is not numerically smaller than zero, despite having a negative sign. On the other hand, <code class="language-plaintext highlighter-rouge">Float.to_string (-0.)</code> produces <code class="language-plaintext highlighter-rouge">"-0."</code>, so <code class="language-plaintext highlighter-rouge">sign__old</code> thinks it is <code class="language-plaintext highlighter-rouge">`Negative</code>.</p>

<p>I think it’s likely uncommon that the existence of negative zero leads to bugs in code, because typically programs see these two values as having the same behavior. In my case, the code is constructing an AST that represents the float value. The old code produces a unary negation applied to positive zero immediate, while the new code proces a negative zero immediate. This change caused an exception in prod because the code generation and parsing process no longer round trips.</p>

<p>The first time I learned about negative zero, I thought it was a misfeature. But it actually makes sense, considering infinities are also signed. With all four values representable, we can have <code class="language-plaintext highlighter-rouge">1/-inf = -0</code>, <code class="language-plaintext highlighter-rouge">1/-0 = -inf</code> and so on, which is nice.</p>

<p>With the bug identified, the fix is easy: use <code class="language-plaintext highlighter-rouge">Float.ieee_negative</code> instead of <code class="language-plaintext highlighter-rouge">Float.is_negative</code>.</p>

<p>If you enjoyed this post, try another: <a href="/precisely-compare-ints-and-floats">Precisely Compare Ints and Floats</a></p>]]></content><author><name></name></author><category term="Bug" /><category term="Floating Point" /><category term="IEEE" /><summary type="html"><![CDATA[One day I came across some code that looked like this (paraphrased):]]></summary></entry></feed>