<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://alinush.github.io//feed.xml" rel="self" type="application/atom+xml" /><link href="https://alinush.github.io//" rel="alternate" type="text/html" hreflang="en" /><updated>2026-09-10T18:15:02+00:00</updated><id>https://alinush.github.io//feed.xml</id><title type="html">Alin Tomescu</title><subtitle>You taught me the courage of stars before you left...</subtitle><author><name>Alin Tomescu</name></author><entry><title type="html">Confidentiality on Aptos vs. Arc vs. Canton</title><link href="https://alinush.github.io//aptos-arc-canton" rel="alternate" type="text/html" title="Confidentiality on Aptos vs. Arc vs. Canton" /><published>2026-08-18T00:00:00+00:00</published><updated>2026-08-18T00:00:00+00:00</updated><id>https://alinush.github.io//hot-take-on-aptos-vs-arc-vs-canton-confidentiality</id><content type="html" xml:base="https://alinush.github.io//aptos-arc-canton"><![CDATA[<p class="info"><strong>tl;dr:</strong> This is an <strong>incomplete</strong> and <strong>opinionated</strong> take on three different approaches to confidential payments (and beyond).</p>

<!--more-->

<!-- Here you can define LaTeX macros -->
<div style="display: none;">$
$</div>
<p><!-- $ --></p>

<h2 id="aptos">Aptos</h2>

<ul>
  <li><span style="color:green"><strong>Good</strong></span>: Encrypts balances under the owning user’s encryption key $\Rightarrow$ no one can see the balance, except the owning user – neither validators nor full nodes.</li>
  <li><span style="color:green"><strong>Good</strong></span>: Encrypts transferred amounts under the sending and receiving users’ encryption keys $\Rightarrow$ same guarantees</li>
  <li><span style="color:red"><strong>Bad</strong></span>: Auditing functionality is somewhat restricted
    <ul>
      <li>Although governance-based auditors get full visibility into transferred amounts and encrypted balances<sup id="fnref:available-balances"><a href="#fn:available-balances" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, this is only <em>after</em> auditing is enabled.</li>
      <li>Retroactive auditing of previous transferred amounts is not possible. (This is a good or a bad thing, depending on whom you ask.)</li>
    </ul>
  </li>
  <li><span style="color:red"><strong>Bad</strong></span>: Trickier to implement key management for users inside wallets $\Rightarrow$ must do extra careful cryptographic work to enable seamless confidentiality UX in wallets like <a href="https://petra.app">Petra</a>
    <ul>
      <li>Banks/customers may not like this, especially if they are clueless about blockchains</li>
    </ul>
  </li>
  <li><span style="color:red"><strong>Bad</strong></span>: 30x higher gas cost than public payments. (See <a href="/confidential-assets#gas-benchmarks-for-confidential_asset-v11-move-module">gas benchmarks here</a>.)</li>
</ul>

<p class="note">If confidentiality is what you care about most, go with Aptos.
If it is full privacy, of course, go with <a href="https://z.cash">Zcash</a>.</p>

<h2 id="canton">Canton</h2>

<p class="note">I understand that Canton was built for permissioned enterprise networks where participants are known and regulated. 
Thus, operator visibility is viewed not as a bug but a feature for compliance.</p>

<ul>
  <li><span style="color:red"><strong>Bad</strong></span>: User’s balances are stored in plaintext on their chosen validator/operator $\Rightarrow$ that operator/validator can see the balance $\Rightarrow$ attacker who breaks in can see it too $\Leftrightarrow$ you are trusting your validator operator $\Leftrightarrow$ weak privacy
    <ul>
      <li>Suggesting that validators/operators can encrypt balances under their own keys is no better, since the operator still holds the key</li>
      <li>Suggesting that trusted hardware can make it better is tenuous (see <a href="#arc">Arc</a> discussion on attacks)</li>
    </ul>
  </li>
  <li><span style="color:red"><strong>Bad</strong></span>: Amounts and balances are only encrypted “in flight” between full nodes / validators $\Rightarrow$ high trust assumption on these intermediaries</li>
  <li><span style="color:green"><strong>Good</strong></span>: Easier to do key management
    <ul>
      <li>Banks/customers should like this</li>
    </ul>
  </li>
  <li><span style="color:green"><strong>Good</strong></span>: Easier to implement more complex auditing policies, precisely because validators see everything / the privacy guarantees are weaker
    <ul>
      <li>Banks/customers should also like this</li>
    </ul>
  </li>
</ul>

<h3 id="claudes-take">Claude’s take</h3>

<blockquote>
  <p>Aptos confidential assets encrypt balances on the public ledger under keys the user owns, so secrecy is a cryptographic guarantee: only the key holder can read amounts, and no operator can.</p>

  <p>Canton keeps balances in plaintext in each participant node’s local database and gets secrecy from routing — the protocol simply never sends the data to non-stakeholders, with inter-node encryption under node keys covering transport only.</p>

  <p>So the trust anchor differs:</p>
  <ul>
    <li>Aptos trusts math and the user’s ability to protect their decryption keys.</li>
    <li>Canton trusts your hosting participant operator, plus whatever leaks through divulgence, timing, and workflow metadata.</li>
  </ul>
</blockquote>

<h2 id="arc">Arc<sup id="fnref:BGGplus26"><a href="#fn:BGGplus26" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></h2>

<ul>
  <li><span style="color:red"><strong>Bad</strong></span>: Leverages <strong>trusted hardware</strong> to encrypt blockchain state on validators under a <strong>master secret key (MSK)</strong>
    <ul>
      <li><em>Unfortunately</em>, trusted hardware is much easier to break <strong>in practice</strong> than <em>in theory</em></li>
      <li>It has been fully (and partially) broken several times in the past. See just a few recent attacks:
        <ul>
          <li><a href="https://batteringram.eu/batteringram.pdf">batteringram.eu/batteringram.pdf</a>: <em>“arbitrary plaintext read/write access and extracting SGX’s platform provisioning key, thereby dismantling trust in remote attestation”</em></li>
          <li><a href="https://tee.fail/files/paper.pdf">tee.fail/files/paper.pdf</a>: <em>“extract secret key material (such as attestation keys in some cases) from machines in fully trusted status”</em></li>
          <li><a href="https://wiretap.fail/files/wiretap.pdf">wiretap.fail/files/wiretap.pdf</a>: <em>“extract an SGX attestation key from a machine in fully trusted status”</em> $\Rightarrow$ <em>“end-to-end attacks on both confidentiality and integrity guarantees of deployments with multi-million dollar market caps, allowing attackers to disclose confidential transactions or illegitimately obtain transaction rewards”</em></li>
        </ul>
      </li>
    </ul>
  </li>
  <li><span style="color:red"><strong>Bad</strong></span>: Uses a secret-shared MSK on validators (or, more loosely, their operators)
    <ul>
      <li>$\Rightarrow$ subject to collusion attacks by validators $\Rightarrow$ the MSK could be revealed $\Rightarrow$ privacy guarantees would be broken without even having to break the trusted hardware</li>
      <li>The typical counterargument here is that <em>“more trusted hardware on each validator’s KMS will fix this.”</em>
        <ul>
          <li>But will it <code class="language-plaintext highlighter-rouge">¯\_(ツ)_/¯</code>? (See attacks above!)</li>
        </ul>
      </li>
    </ul>
  </li>
  <li><span style="color:green"><strong>Good</strong></span>: Supports arbitrary privacy-preserving* computations, beyond just payments
    <ul>
      <li>*Of course, under the big caveat of relying on (repeatedly-broken-into) trusted hardware.</li>
    </ul>
  </li>
  <li><span style="color:green"><strong>Good</strong></span>: Easy key management</li>
  <li><span style="color:green"><strong>Good</strong></span>: Very flexible auditing</li>
</ul>

<h3 id="but-exists-secure-trusted-hardware">But $\exists$ secure trusted hardware!</h3>

<p>One reasonable objection to <em>“trusted hardware keeps being broken into”</em> is that such Intel SGX attacks<sup id="fnref:signal-sgx"><a href="#fn:signal-sgx" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> will not transfer to other trusted hardware platforms.</p>

<p>I doubt it<sup id="fnref:ZGWplus24"><a href="#fn:ZGWplus24" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>$^,$<sup id="fnref:DWOplus25"><a href="#fn:DWOplus25" class="footnote" rel="footnote" role="doc-noteref">5</a></sup>$^,$<sup id="fnref:MHHW18"><a href="#fn:MHHW18" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>$^,$<sup id="fnref:TSS17"><a href="#fn:TSS17" class="footnote" rel="footnote" role="doc-noteref">7</a></sup>.</p>

<p>Although there are ways to reduce the attack surface of trusted hardware (e.g., see Sanctum<sup id="fnref:CLD15e"><a href="#fn:CLD15e" class="footnote" rel="footnote" role="doc-noteref">8</a></sup> from my group at MIT back in the day), in practice, they tend to come with lower performance and/or less functionality.
And it so happens that trusted hardware companies mainly compete on performance, functionality and (I hope) developer-friendliness.
They hardly compete on security, because it’s hard to: there’s always someone else who can (mis)claim more security.
And such claims are hard to adjudicate by the market.</p>

<p>Thus, what ends up happening is a race to the bottom, sacrificing security for extra performance and functionality.</p>

<h3 id="claudes-take-1">Claude’s take</h3>

<blockquote>
  <p>Aptos protects balances with pure cryptography: ciphertexts sit on-chain under the user’s own key, so an attacker who compromises every validator still learns nothing about anyone’s balance.</p>

  <p>Arc protects balances with a master secret key (MSK) housed inside trusted hardware on the validators. This gives Arc two failure modes Aptos doesn’t have: (1) hardware-level attacks that extract the MSK from the enclave, which were demonstrated repeatedly in practice (see links above), and (2) validator collusion that reconstructs the secret-shared MSK without touching the hardware at all.</p>

  <p>The trade-off is expressivity: Arc’s enclaves can run arbitrary confidential computation, not just payments, while Aptos’s on-chain approach is limited to payments. But that expressivity assumes trusted hardware won’t get broken into. So you’re trading a <em>mathematical</em> guarantee for a <em>physical</em> one, and the physical one has a poor track record.</p>
</blockquote>

<p class="note">If I had to pick between Canton and Arc, I’d pick Arc due to its simpler design, clearer trust model (IMHO), and higher expressivity.</p>

<h2 id="references">References</h2>

<p>For cited works, see below 👇👇</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:available-balances">
      <p>Specifically, only the available balance, not the pending. See <a href="https://aptos.dev/build/smart-contracts/confidential-asset#confidential-balance">documentation</a>. <a href="#fnref:available-balances" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:BGGplus26">
      <p><strong>Arc Privacy Sector: Keeping Blockchain State, Transactions, and Accounts Private and Quantum Safe</strong>, by Mira Belenkiy and Guy Garcia and Sergey Gorbunov and Christian Gorenflo and Jay Logelin and Christopher Ries and Dragos Rotaru and Abinesh Sarvepalli and Milap Sheth, 2026, <a href="https://6778953.fs1.hubspotusercontent-na1.net/hubfs/6778953/PDFs/Whitepapers/Arc_Privacy_Sector%20(5).pdf">[URL]</a> <a href="#fnref:BGGplus26" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:signal-sgx">
      <p><a href="https://x.com/v12sec/status/2092664320547254388">Signal’s SGX-based contact discovery gets broken</a> <a href="#fnref:signal-sgx" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:ZGWplus24">
      <p><strong>CacheWarp}: Software-based Fault Injection using Selective State Reset</strong>, by Zhang, Ruiyi and Gerlach, Lukas and Weber, Daniel and Hetterich, Lorenz and Lü, Youheng and Kogler, Andreas and Schwarz, Michael, <em>in 33rd USENIX Security Symposium (USENIX Security 24)</em>, 2024 <a href="#fnref:ZGWplus24" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:DWOplus25">
      <p><strong>BadRAM}: Practical Memory Aliasing Attacks on Trusted Execution Environments</strong>, by De Meulemeester, Jesse and Wilke, Luca and Oswald, David and Eisenbarth, Thomas and Verbauwhede, Ingrid and Van Bulck, Jo, <em>in 46th {IEEE} Symposium on Security and Privacy ({S\&amp;P})</em>, 2025 <a href="#fnref:DWOplus25" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:MHHW18">
      <p><strong>SEVered: Subverting AMD’s Virtual Machine Encryption</strong>, by Mathias Morbitzer and Manuel Huber and Julian Horsch and Sascha Wessel, 2018, <a href="https://arxiv.org/abs/1805.09604">[URL]</a> <a href="#fnref:MHHW18" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:TSS17">
      <p><strong>CLKSCREW}: Exposing the Perils of {Security-Oblivious} Energy Management</strong>, by Adrian Tang and Simha Sethumadhavan and Salvatore Stolfo, <em>in 26th USENIX Security Symposium (USENIX Security 17)</em>, 2017, <a href="https://www.usenix.org/conference/usenixsecurity17/technical-sessions/presentation/tang">[URL]</a> <a href="#fnref:TSS17" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:CLD15e">
      <p><strong>Sanctum: Minimal Hardware Extensions for Strong Software Isolation</strong>, by Victor Costan and Ilia Lebedev and Srinivas Devadas, <em>in Cryptology {ePrint} Archive, Paper 2015/564</em>, 2015, <a href="https://eprint.iacr.org/2015/564">[URL]</a> <a href="#fnref:CLD15e" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Alin Tomescu</name></author><summary type="html"><![CDATA[tl;dr: This is an incomplete and opinionated take on three different approaches to confidential payments (and beyond).]]></summary></entry><entry><title type="html">Formal verification in Lean</title><link href="https://alinush.github.io//lean" rel="alternate" type="text/html" title="Formal verification in Lean" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>https://alinush.github.io//formal-verification-in-lean</id><content type="html" xml:base="https://alinush.github.io//lean"><![CDATA[<p class="info"><strong>tl;dr:</strong> A bunch of resources I hope to get to.</p>

<!--more-->

<!-- Here you can define LaTeX macros -->
<div style="display: none;">$
$</div>
<p><!-- $ --></p>

<h2 id="cryptography">Cryptography</h2>

<ul>
  <li><a href="https://github.com/Verified-zkEVM">Verified-zkEVM</a>
    <ul>
      <li><a href="https://github.com/Verified-zkEVM/VCVio">VCVio</a></li>
      <li><a href="https://github.com/Verified-zkEVM/ArkLib">ArkLib</a></li>
      <li><a href="https://github.com/Verified-zkEVM/clean">clean zkDSL</a></li>
    </ul>
  </li>
  <li><a href="https://www.galois.com/articles/zklean-a-dsl-for-zk-statement-verification">zkLean: A DSL for ZK statement verification</a></li>
</ul>

<h2 id="software-engineering-in-lean">Software engineering in Lean</h2>

<ul>
  <li><a href="https://kim-em.github.io/blog/2026-7-24-why-lean-is-faster-than-rust/">Why lean is faster than Rust</a>
    <ul>
      <li><a href="https://kirancodes.me/posts/log-who-watches-the-watchers.html">Lean proved this program was correct; then I found a bug.</a></li>
    </ul>
  </li>
  <li><a href="https://just-lean.mitscha-baude.at/">Just Lean: a verified, fast sort</a></li>
  <li><a href="https://github.com/alinush/sort-in-rust-prove-in-lean-example">Sort in Rust, prove in Lean example</a></li>
  <li><a href="https://paulbutcher.com/lean1.html">Lean-ing into Software Engineering</a></li>
  <li><a href="https://paulbutcher.com/lean-markdown.html">A (somewhat) formally verified implementation of Markdown</a></li>
  <li><a href="https://paulbutcher.com/lean2.html">Formally verified CRUD</a></li>
  <li><a href="https://powdr.org/blog/formally-verified-autoprecompiles">Formally Verified [zkVM] Autoprecompiles</a></li>
  <li><a href="https://georgwiese.github.io/posts/formal-verification-ai/">A new software engineering paradigm</a></li>
</ul>

<h2 id="rust-non-lean">Rust (Non-Lean)</h2>

<ul>
  <li><a href="https://github.com/verus-lang/verus">Verus</a>: Verified Rust for low-level systems code</li>
  <li><a href="https://creusot.rs/">Creusot</a>: a deductive verifier for the Rust programming language</li>
  <li><a href="https://github.com/AeneasVerif/aeneas">Aeneas</a>: translation from Rust’s MIR internal language to a pure lambda calculus
    <ul>
      <li>Often <a href="https://lean-lang.org/use-cases/aeneas/">used to “compile” a subset of Rust to Lean</a></li>
    </ul>
  </li>
</ul>

<h2 id="logic">Logic</h2>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/Propositional_logic">Propositional logic</a>, $p \wedge \neg p$ type of thing; no quantifiers; no predicates</li>
  <li><a href="https://en.wikipedia.org/wiki/First-order_logic">First-order logic</a>, $\exists y, \forall x, P(x, y)$; quantifiers and predicates</li>
  <li><a href="https://en.wikipedia.org/wiki/Second-order_logic">Second-order logic</a>, $\exists P, \forall x. P(x)$; more expressive than first-order logic (quantifies over predicates too)</li>
</ul>

<h2 id="docs">Docs</h2>

<ul>
  <li><a href="https://leanprover-community.github.io/mathlib4_docs/">mathlib4 docs</a></li>
  <li><a href="https://leansearch.net/?q=is+sorted+array">LeanSearch</a></li>
  <li><a href="https://loogle.lean-lang.org/?q=sort">Loogle</a></li>
</ul>

<h2 id="misc">Misc</h2>

<ul>
  <li><a href="https://www.cslib.io/">CSLib</a></li>
  <li><a href="https://github.com/cameronfreer/lean4-skills">lean4-skills</a></li>
  <li><a href="https://adam.math.hhu.de/">Lean game server</a></li>
  <li><a href="https://raw.githubusercontent.com/blanchette/logical_verification_2023/main/hitchhikers_guide.pdf">Hitchhiker’s guide to formal verification</a> [in Lean]</li>
  <li><a href="https://leanprover-community.github.io/lean4-metaprogramming-book/">Lean metaprogramming</a></li>
  <li><a href="https://lean-lang.org/functional_programming_in_lean/Programming___-Proving___-and-Performance/Insertion-Sort-and-Array-Mutation/">Insertion sort in Lean with termination proof</a></li>
  <li><a href="https://hashcloak.com/blog/tutorial-introduction-to-formal-verification-with-lean-(part-1)">Tutorial: Introduction to Formal Verification with Lean (Part 1)</a></li>
  <li><a href="https://cameronfreer.github.io/slides/202608-aitp/#/title">From Prompts to Protocols: lean4-skills for AI-Assisted Lean Formalization</a></li>
</ul>

<h2 id="questions">Questions</h2>

<h3 id="probability">Probability</h3>

<p>How good is Lean at modeling probabilistic games in cryptography?</p>

<h3 id="high-performance-code">High-performance code</h3>

<p>Need two things that are in tension:</p>
<ul>
  <li>high performance code</li>
  <li>code that is provable against the spec</li>
</ul>

<p>e.g., if the code is in (some subset of) Rust, we can translate it to Lean using Aeneas (see above). But not sure how well this works in practice. Also, not sure how much TCB this involves.</p>

<h2 id="references">References</h2>

<p>For cited works, see below 👇👇</p>]]></content><author><name>Alin Tomescu</name></author><category term="Lean" /><summary type="html"><![CDATA[tl;dr: A bunch of resources I hope to get to.]]></summary></entry><entry><title type="html">Baby-step giant-step (BSGS) discrete log algorithms</title><link href="https://alinush.github.io//bsgs" rel="alternate" type="text/html" title="Baby-step giant-step (BSGS) discrete log algorithms" /><published>2026-05-31T00:00:00+00:00</published><updated>2026-05-31T00:00:00+00:00</updated><id>https://alinush.github.io//bsgs</id><content type="html" xml:base="https://alinush.github.io//bsgs"><![CDATA[<p class="info"><strong>tl;dr:</strong> When the discrete log $a$ of $a\cdot G$ is known to lie in a small range $[m)$, the <strong>baby-step giant-step (BSGS)</strong> algorithm recovers $a$ in $\ceil{\sqrt{m}}$ $\Gr$ additions using only a precomputed table of exactly $\ceil{\sqrt{m}}$ compressed points, trading the $O(1)$ time of the naive $m$-sized lookup table for much less memory.
<br />
This post describes <strong>vanilla BSGS</strong>, and two Ristretto255-optimized variants: <strong>BSGS-$k$</strong>, which batches $k$ giant steps to amortize the expensive point compression and <strong>truncated BSGS-$k$ (TBSGS-$k$)</strong>, which stores only 8-byte truncated keys to shrink the table by 75% with negligible performance impact.</p>

<!--more-->

<!-- Here you can define LaTeX macros -->
<div style="display: none;">$
\def\table{\mathsf{tbl}}
\def\dict{\mathsf{T}}
\def\jG#1{\green{#1\cdot G}}
% BSGS
\def\bsgsPrecompute{\mathsf{BSGS.Precompute}}
\def\bsgsSolve{\mathsf{BSGS.Solve}}
\def\sG{\green{-s \cdot G}}
\def\twojG#1{\green{2#1\cdot G}}
%
% BSGS-k
\def\trunc#1{\mathsf{Trunc}\left(#1\right)}
\def\compress#1{\mathsf{Compress}\left(#1\right)}
\def\doubleAndCompressBatch#1{\mathsf{DoubleAndCompressBatch}\left(#1\right)}
\def\ctwojG#1{\compress{\twojG{#1}}}
%
% TBSGS-k
\def\tctwomjG#1{\trunc{\ctwojG{#1}}}
\def\sqm{\ceil{\sqrt{m}}}
$</div>
<p><!-- $ --></p>

<div style="display: none;">$
%
% Field operations
%
% #1 is the number of field additions
\def\Fadd#1{#1\ \green{\F^+}}
% #1 is the number of field multiplications
\def\Fmul#1{#1\ \red{\F}^\red{\times}}
%
% Abstract group
%
% #1 is the group
% #2 is the # of group additions
\def\Gadd#1#2{#2\ \green{#1}^\green{+}}
% #2 is the # of scalar muls
\def\Gmul#1#2{#2\ \orange{#1}^\orange{\times}}
% #2 is the MSM size
\def\msm#1#2{\red{#1}^{#2}} % do not use directly use either \fmsm or \vmsm
\def\vmsm#1#2{\red{\mathsf{var}}\text{-}\msm{#1}{#2}}
\def\fmsm#1#2{\msm{#1}{#2}}
\def\fmsmSmall#1#2#3{\fmsm{#1}{#2}/{#3}}
% ...#3 is the max scalar size
\def\vmsmSmall#1#2#3{\vmsm{#1}{#2}/{#3}}
%
% \mathbb{G} group
%
\def\GaddG#1{\Gadd{\Gr}{#1}}
\def\GmulG#1{\Gmul{\Gr}{#1}}
\def\msmG#1{\msm{\Gr}{#1}}
\def\vmsmG#1{\vmsm{\Gr}{#1}}
\def\fmsmG#1{\fmsm{\Gr}{#1}}
\def\fmsmGSmall#1#2{\fmsmSmall{\Gr}{#1}/{#2}}
\def\vmsmGSmall#1#2{\vmsmSmall{\Gr}{#1}/{#2}}
%
% G_1 group
%
% Note: replicating the colors here because cannot get subscript to align with superscript (e.g., $\msmOne{n}$ would render akwardly)
\def\GaddOne#1{\Gadd{\Gr}{#1}_\green{1}}
\def\GmulOne#1{\Gmul{\Gr}{#1}_\orange{1}}
\def\msmOne#1{\msm{\Gr}{#1}_\red{1}}
\def\vmsmOne#1{\vmsm{\Gr}{#1}_\red{1}}
\def\fmsmOne#1{\fmsm{\Gr}{#1}_\red{1}}
\def\fmsmOneSmall#1#2{\fmsmSmall{\Gr}{#1}_\red{1}/{#2}}
\def\vmsmOneSmall#1#2{\vmsmSmall{\Gr}{#1}_\red{1}/{#2}}
%
% G_2 group
%
% Note: same replication as for G_1
\def\GaddTwo#1{\Gadd{\Gr}{#1}_\green{2}}
\def\GmulTwo#1{\Gmul{\Gr}{#1}_\orange{2}}
\def\msmTwo#1{\msm{\Gr}{#1}_\red{2}}
\def\vmsmTwo#1{\vmsm{\Gr}{#1}_\red{2}}
\def\fmsmTwo#1{\fmsm{\Gr}{#1}_\red{2}}
\def\fmsmTwoSmall#1#2{\fmsmSmall{\Gr}{#1}_\red{2}/{#2}}
\def\vmsmTwoSmall#1#2{\vmsmSmall{\Gr}{#1}_\red{2}/{#2}}
%
% Target group
%
% Note: same replication as for G_1
\def\GaddTarget#1{\Gadd{\Gr}{#1}_\green{T}}
\def\GmulTarget#1{\Gmul{\Gr}{#1}_\orange{T}}
\def\msmTarget#1{\msm{\Gr}{#1}_\red{T}}
\def\vmsmTarget#1{\vmsm{\Gr}{#1}_\red{T}}
\def\fmsmTarget#1{\fmsm{\Gr}{#1}_\red{T}}
\def\fmsmTargetSmall#1#2{\fmsmSmall{\Gr}{#1}_\red{T}/{#2}}
\def\vmsmTargetSmall#1#2{\vmsmSmall{\Gr}{#1}_\red{T}/{#2}}
%
% A single pairing
\def\pairing{\mathbb{P}}
% #1 is the # of pairings
\def\multipair#1{\mathbb{P}^{#1}}
$</div>
<p><!-- $ --></p>

<h2 id="notation">Notation</h2>

<ul>
  <li>We assume a prime-order group $\term{\Gr}$ of prime order $\term{p}$</li>
  <li>We use additive group notation: $a \cdot G$ denotes scalar multiplication in $\Gr$, where $a\in \Zp$ and $G\in\Gr$</li>
</ul>

<h2 id="naive-discrete-log-algorithm">Naive discrete log algorithm</h2>

<p>Naively, computing the <strong>discrete logarithm (DL)</strong> $a$ on $a \cdot G$ when $a\in[m)$ can be done in constant-time via a single lookup in an $m$-sized <strong>precomputed table</strong>:</p>

<p>\begin{align}
(\jG{j})_{j\in [m)}
\end{align}</p>

<p>This works great for small enoguh $m$, depending on how much memory is available.
e.g.,</p>
<ul>
  <li>2 MiB for $m=2^{16} \Rightarrow$ 512 KiB with <a href="#truncation">truncation</a></li>
  <li>32 MiB for $m=2^{20} \Rightarrow$ 4 MiB with truncation</li>
</ul>

<h2 id="baby-step-giant-step-bsgs-discrete-log-algorithm">Baby-step giant step (BSGS) discrete log algorithm</h2>

<p>The BSGS algorithm compute a DL $a\in[m)$ on $a\cdot G$ with less memory but more computation.
Specifically:</p>
<ul>
  <li>it <strong>reduces the table size</strong> from $m$ to $\sqm$</li>
  <li>it <em>increases the solving time</em> from $O(1)$ to $\GaddG{(\sqm-1)}$.</li>
</ul>

<p>Let us define a <strong>base</strong>, $s$, as:</p>

\[\term{s}\bydef\sqm\]

<p>The key idea is that we can represent the value $a$ as a 2-digit number $(i,j)$ in base-$\emph{s}$:
\begin{align}
a = i\cdot s + j,\ \text{where}\ i,j\in[s)
\end{align}
As a result finding the discrete log $a$ of $H\bydef a\cdot G$, can be reduced to finding its two digits $i,j\in[s)$ such that:
\begin{align}
H &amp;= (i \cdot s + j)\cdot G\Leftrightarrow\\<br />
H &amp;= i \cdot (s \cdot G) + j \cdot G\Leftrightarrow\\<br />
\label{eq:bsgs-check}
H + i \cdot (\sG) &amp;= \jG{j}
\end{align}</p>

<p>Now, imagine we have all $(\jG{j})_{j\in[s)}$ and $\sG$ precomputed.
Then, finding $a$ can be reduced to computing all the left hand sides (LHS) of Eq. $\ref{eq:bsgs-check}$ for all possible $i\in[s)$ and checking if there exists $j\in[s)$ such that the LHS equals the right hand side (RHS).
If it does, then $a = i\cdot s+ j$!</p>

<p>More concretely, we compute:
\begin{align}
V_0 &amp;\gets H = H + 0 \cdot (\sG)\\<br />
V_1 &amp;\gets V_0 \sG = H + 1 \cdot (\sG)\\<br />
V_2 &amp;\gets V_1 \sG = H + 2 \cdot (\sG)\\<br />
 &amp;\hspace{.7em}\vdots\\<br />
V_i &amp;\gets V_{i-1} \sG = H + i \cdot (\sG)\\<br />
 &amp;\hspace{.7em}\vdots\\<br />
V_{s-1} &amp;\gets V_{s-2} \sG = H + (s-1) \cdot (\sG)\\<br />
\end{align}
Then, for each computed $V_i$, we check (in constant-time) whether there exists a $j\in[s)$ such that $V_i = \jG{j}$.
In other words, we check if Eq. \ref{eq:bsgs-check} holds for some $i,j\in[s)$.
If it does, then we solved for the correct DL $a = i\cdot s + j$!</p>

<p>Note that this algorithm will take at most $s-1$ group additions in $\Gr$, so it is very efficient!</p>

<p class="note">The maximum value $a$ can take in this base-$s$ representation is $(s-1) \cdot s + (s-1) = (s-1)(s+1) = s^2 - 1$.
Since $\sqm \ge \sqrt{m}$, by squaring it, it follows that $\sqm^2 \ge \sqrt{m}^2\Leftrightarrow s^2 \ge m$.
This means $a=m-1$ can be represented in base-$s$, so the algorithm will be able to solve DL for all $a\in[m)$.
(Not only: it may also be able to solve it for slightly higher values, e.g., $s=4$ for both $m = 15,16$).</p>

<p>We give formal algorithms for BSGS below.</p>

<h3 id="mathsfbsgsprecomputemin-n-gingr-rightarrow-table">$\mathsf{BSGS.Precompute}(m\in \N, G\in\Gr) \rightarrow \table$</h3>

<p>Recall that $\term{s}\bydef\sqm$.</p>

<p>Using $\GaddG{(s-1)}$ time, build a <strong>dictionary</strong> $\term{\dict}$ (e.g., a hash map) that maps each baby step to its index:
\begin{align}
\dict[\jG{j}] \gets j,\quad \forall j\in[s)
\end{align}
By convention, $\dict[P] = \bot$ for any $P$ that is not a key (i.e., for any $P\notin \{\jG{j}\}_{j\in[s)}$).</p>

<p>Return the table $\table \gets (s, \dict, \sG)$.</p>

<h3 id="mathsfbsgssolvetable-hingr-rightarrow-a-in-mtimes-bot">$\mathsf{BSGS.Solve}(\table, H\in\Gr) \rightarrow a \in [m)\times \{\bot\}$</h3>

<p>Parse the table:
\begin{align}
(s, \dict, \sG) \parse \table
\end{align}</p>

<p>Let $V_0 = H$.</p>

<p>For each $i\in[0, s)$:</p>
<ul>
  <li>$j \gets \dict[V_i]$</li>
  <li><strong>if</strong> $j \ne \bot$, <strong>then</strong> return $i\cdot s + j$</li>
  <li>$V_{i+1} \gets V_i + (\sG)$</li>
</ul>

<p>If we reached this point, this means no $i,j\in[s)$ were found.
This, in turn, means $a \ge s^2 \ge m$.</p>

<p>Therefore, return $\bot$.</p>

<h2 id="bsgs-k-discrete-log-algorithm">BSGS-$k$ discrete log algorithm</h2>

<p>BSGS-$k$ is a variant of BSGS that <strong>dramatically speeds up</strong> the solve algorithm on certain elliptic curves by batching $k$ giant steps together.</p>

<p>This addresses a key performance issue: when using BSGS on Ristretto255 curves, its main bottleneck is the expensive point compression required to index into the precomputed table after every giant step.
BSGS-$k$ avoids this by first computing $k$ giant steps via $k-1$ group additions and then compressing all $k$ points at once.
Unfortunately, batch compression is <strong>not</strong> supported in Ristretto255.
(Feel free to open that can of worms on your own.)
But, fortunately, “doubled-point” batch compression <strong>is</strong> supported via a $\doubleAndCompressBatch{\cdot}$ algorithm.
As a result, we observe that the BSGS algorithm can be adjusted to work over these doubled points!</p>

<p class="note">This algorithm was LLM-discovered.
Upon first glance at the Ristretto255 APIs in <code class="language-plaintext highlighter-rouge">curve25519-dalek</code>, I could not find any batch compression algorithms.
So I just threw the problem to Claude, with very little hope, thinking <em>“Ha! There is no way it’ll find anything I couldn’t find. Let me watch it fail…“</em>
Then, Claude found the $\doubleAndCompressBatch{\cdot}$ API I would have otherwise missed.</p>

<p>We give formal algorithms for BSGS-$k$ below, building on the <a href="#baby-step-giant-step-bsgs-discrete-log-algorithm">BSGS algorithms above</a>.</p>

<h3 id="mathsfcompresshin-gr-rightarrow-01256">$\mathsf{Compress}(H\in \Gr) \rightarrow \{0,1\}^{256}$</h3>

<p>Returns a 32-byte (256-bit) compressed canonical representation of the group element $H$.</p>

<h3 id="mathsfdoubleandcompressbatchlefth_i_iinkin-grright-rightarrow-left01256rightk">$\mathsf{DoubleAndCompressBatch}\left((H_i)_{i\in[k]}\in \Gr\right) \rightarrow \left(\{0,1\}^{256}\right)^k$</h3>

<p>Doubles all $H_i$’s and compress the results in batch, more efficiently than repeatedly calling $\compress{2\cdot H_i}$ for all $i\in [k]$.</p>

<h3 id="mathsfbsgstext-kprecomputemin-n-gingr-rightarrow-table">$\mathsf{BSGS\text{-}k.Precompute}(m\in \N, G\in\Gr) \rightarrow \table$</h3>

<p>Recall that $\term{s}\bydef\sqm$.</p>

<p>Using $\GaddG{(s-1)}$ time, build a dictionary $\dict$ that maps each <strong>doubled</strong> baby step to its index:
\begin{align}
\dict[\ctwojG{j}] \gets j,\quad \forall j\in[s)
\end{align}
(As before, $\dict[c] = \bot$ for any $c$ that is not a key.)</p>

<p>Return the table $\table \gets (s, \dict, \sG)$.</p>

<p class="note">Note that the <strong>baby steps</strong> are stored <em>doubled</em> (as $\ctwojG{j}$), so that we can compare against them using $\doubleAndCompressBatch{\cdot}$.
The <strong>giant step</strong>, however, remains $\sG$ (<strong>not</strong> doubled): as we’ll see below, the doubling is applied to each running point $V_{i+\ell}$ at compression time, not to the giant step itself.</p>

<h3 id="bsgs-k-solve">$\mathsf{BSGS\text{-}k.Solve}(\table, H\in\Gr) \rightarrow a \in [m)\times \{\bot\}$</h3>

<p>Parse the table:
\begin{align}
(s, \dict, \sG) \parse \table
\end{align}</p>

<p>Let $V_0 = H$.</p>

<p>For each $i\in\{0,k,2k,\ldots\}$ subject to $i &lt; s$:</p>
<ul>
  <li>Compute $V_{i+1}, \ldots, V_{i+k-1}$ via $k-1$ additions of $\sG$, such that $\term{V_{i+\ell}} \bydef H + (i+\ell)\cdot (\sG)$</li>
  <li>Compute $\doubleAndCompressBatch{V_i, V_{i+1}, \ldots, V_{i+k-1}}$ to get compressed points $(c_i, \ldots, c_{i+k-1})$, where $c_{i+\ell} \bydef \compress{2\cdot V_{i+\ell}}$</li>
  <li>For each $\ell \in [0, k)$, run <strong>the match check</strong>:
    <ul>
      <li>$j \gets \dict[c_{i+\ell}]$</li>
      <li><strong>if</strong> $j \ne \bot$, <strong>then</strong> return $(i+\ell)\cdot s + j$</li>
    </ul>
  </li>
  <li>$V_{i+k} \gets V_{i+k-1} + (\sG)$</li>
</ul>

<p>If we reached this point, return $\bot$.</p>

<p class="note">For simplicity, we describe the last batch as always having $k$ points, even when $i + k &gt; s$.
As a result, we may compute and look up a few extra points $V_n$ whose giant-step index $i + \ell$ is $\ge s$.
This is harmless.</p>

<h2 id="truncation">Truncated BSGS-$k$ (TBSGS-$k$) discrete log algorithm</h2>

<p>TBSGS-$k$ is a variant of BSGS that dramatically <strong>reduces the table size</strong> from $\sqm \cdot 32$ bytes to $\sqm \cdot 8$ bytes (a 75% reduction) while maintaining nearly-identical performance for large secrets.</p>

<p>The key idea is: instead of keying the dictionary on the full doubled baby steps $\ctwojG{j}$, we key it on their 8-byte truncations $\tctwomjG{j}$,
where $\term{\mathsf{Trunc}}$ returns the first 8 bytes and $\term{\mathsf{Compress}}$ is the standard Ristretto255 point compression.</p>

<p>We give formal algorithms for TBSGS-$k$ below, building on the <a href="#bsgs-k-discrete-log-algorithm">BSGS-$k$ algorithms above</a>.</p>

<h3 id="mathsftbsgstext-kprecomputemin-n-gingr-rightarrow-table">$\mathsf{TBSGS\text{-}k.Precompute}(m\in \N, G\in\Gr) \rightarrow \table$</h3>

<p>Recall that $\term{s}\bydef\sqm$.</p>

<p>Using $\GaddG{(s-1)}$ time, build a dictionary $\dict$ that maps each <strong>truncated</strong>, doubled baby step to its index:
\begin{align}
\dict[\tctwomjG{j}] \gets j,\quad \forall j\in[s)
\end{align}
(As before, $\dict[t] = \bot$ for any $t$ that is not a key.)
The dictionary’s keys are now only 8 bytes each, hence the $\sqm \cdot 8$-byte table size.</p>

<p>Return the table $\table \gets (s, \dict, \sG)$.</p>

<h3 id="mathsftbsgstext-ksolvetable-hingr-rightarrow-a-in-mtimes-bot">$\mathsf{TBSGS\text{-}k.Solve}(\table, H\in\Gr) \rightarrow a \in [m)\times \{\bot\}$</h3>

<p>TBSGS-$k$.Solve is identical to <a href="#bsgs-k-solve">BSGS-$k$.Solve</a> above, except for <strong>the match check</strong>.
Since the table is now keyed by 8-byte truncations (which can collide), a truncated hit must be <em>verified</em> via a scalar multiplication before we trust it.
So the match check becomes:</p>
<ul>
  <li>$j \gets \dict[\trunc{c_{i+\ell}}]$</li>
  <li><strong>if</strong> $j \ne \bot$ <strong>and</strong> $V_{i+\ell} \equals \jG{j}$ (verify the match), <strong>then</strong> return $(i+\ell)\cdot s + j$</li>
</ul>

<h2 id="application-to-aptos-confidential-assets">Application to Aptos confidential assets</h2>

<p>We use the TBSGS-k algorithm to decrypt confidential balances quickly for the <a href="/confidential-assets">Aptos confidential assets</a> features.</p>

<h2 id="appendix-benchmarks">Appendix: Benchmarks</h2>

<p>All benchmarks are run <strong>single-threaded</strong> on an Apple Macbook Pro M4 Max.</p>

<p>All algorithms are implemented for Ristretto255 in this <a href="https://github.com/aptos-labs/ristretto255-dlog">ristretto255-dlog</a> repo.</p>

<h3 id="discrete-log-precomputed-table-sizes">Discrete log precomputed table sizes</h3>

<table>
  <thead>
    <tr>
      <th>Algorithm</th>
      <th>Size</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>TBSGS-k 32-bit</td>
      <td>512 KiB</td>
    </tr>
    <tr>
      <td>BSGS 32-bit</td>
      <td>2.00 MiB</td>
    </tr>
    <tr>
      <td>BSGS-k 32-bit</td>
      <td>2.00 MiB</td>
    </tr>
    <tr>
      <td>BL12 32-bit</td>
      <td>258 KiB</td>
    </tr>
  </tbody>
</table>

<p class="note">TBSGS-k is the default algorithm in <a href="/confidential-assets">Aptos confidential assets</a>, offering the best balance between WASM size and DL solving time.</p>

<!--

#### MPHF-based table sizes (not used)

| Algorithm           | HashMap  | MPHF-based | Reduction |
|---------------------|----------|------------|-----------|
| BSGS 32-bit         | 2.50 MiB | 534 KiB    | 79%       |
| BSGS-k 32-bit       | 2.50 MiB | 534 KiB    | 79%       |
| Naive Lookup 16-bit | 2.50 MiB | 534 KiB    | 79%       |

{: .error}
This was an experiment gone wrong, because MPHFs cannot detect "key not in set" - it always returns some index. For BSGS where most lookups are misses, this is unacceptable without storing keys alongside (defeating the space savings).
Plus, truncating the table is even more effective.
But this may be due to the initially-naive MPHF implementation: may have stored values in `u64`'s?
-->

<h3 id="rust-dlp-bl12">Rust DLP: [BL12]<sup id="fnref:BL12"><a href="#fn:BL12" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[BL12] 32-bit secrets   time:   [4.6197 ms 4.8080 ms 5.0194 ms]
</code></pre></div></div>

<p class="note">On my old M1 Max, for 48 bits, BL12 times were: <code class="language-plaintext highlighter-rouge">[763.90 ms 1.1598 s 1.6174 s]</code>.</p>

<h3 id="rust-dlp-truncated-bsgs-with-batch-size-k">Rust DLP: (Truncated-)BSGS with batch size $k$</h3>

<p class="info"><strong>Summary:</strong>
For 32-bit secrets, TBSGS-k performs identically to BSGS-k (~11 ms).
For smaller secrets, TBSGS-k is ~3x slower due to its verification overhead (i.e., one scalar multiplication per truncated match)
However, this is negligible in absolute terms (35 µs vs 12 µs).
<strong>TBSGS-k should be preferred</strong> for its dramatically smaller table size with minimal performance impact.</p>

<p>For discrete logs on 32-bit values with tables for 32-bits:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[BSGS-k1], 32-bit secrets
                        time:   [61.928 ms 67.501 ms 77.980 ms]
[TBSGS-k1], 32-bit secrets
                        time:   [64.498 ms 69.626 ms 74.543 ms]

[BSGS-k2], 32-bit secrets
                        time:   [40.716 ms 42.236 ms 44.658 ms]
[TBSGS-k2], 32-bit secrets
                        time:   [41.303 ms 43.666 ms 46.133 ms]

[BSGS-k4], 32-bit secrets
                        time:   [24.406 ms 25.578 ms 26.663 ms]
[TBSGS-k4], 32-bit secrets
                        time:   [25.164 ms 26.253 ms 27.185 ms]

[BSGS-k8], 32-bit secrets
                        time:   [16.921 ms 17.727 ms 18.506 ms]
[TBSGS-k8], 32-bit secrets
                        time:   [17.375 ms 18.174 ms 18.895 ms]

[BSGS-k16], 32-bit secrets
                        time:   [12.398 ms 13.299 ms 14.351 ms]
[TBSGS-k16], 32-bit secrets
                        time:   [12.851 ms 13.537 ms 14.255 ms]

[BSGS-k32], 32-bit secrets
                        time:   [11.833 ms 12.347 ms 12.783 ms]
[TBSGS-k32], 32-bit secrets
                        time:   [11.379 ms 12.037 ms 12.533 ms]

[BSGS-k64], 32-bit secrets
                        time:   [11.760 ms 12.257 ms 12.909 ms]
[TBSGS-k64], 32-bit secrets
                        time:   [10.717 ms 11.302 ms 11.772 ms]

[BSGS-k128], 32-bit secrets
                        time:   [10.264 ms 10.677 ms 11.013 ms]
[TBSGS-k128], 32-bit secrets
                        time:   [10.363 ms 10.843 ms 11.202 ms]

[BSGS-k256], 32-bit secrets
                        time:   [10.674 ms 11.057 ms 11.478 ms]
[TBSGS-k256], 32-bit secrets
                        time:   [10.512 ms 10.976 ms 11.631 ms]

[BSGS-k512], 32-bit secrets
                        time:   [9.8338 ms 10.664 ms 11.260 ms]
[TBSGS-k512], 32-bit secrets
                        time:   [10.967 ms 11.461 ms 12.073 ms]

[BSGS-k1024], 32-bit secrets
                        time:   [10.329 ms 11.076 ms 11.718 ms]
[TBSGS-k1024], 32-bit secrets
                        time:   [11.144 ms 11.453 ms 11.714 ms]

[BSGS-k2048], 32-bit secrets
                        time:   [10.394 ms 10.811 ms 11.373 ms]
[TBSGS-k2048], 32-bit secrets
                        time:   [10.800 ms 11.248 ms 11.783 ms]

[BSGS-k4096], 32-bit secrets
                        time:   [10.686 ms 11.277 ms 11.681 ms]
[TBSGS-k4096], 32-bit secrets
                        time:   [10.911 ms 11.467 ms 12.043 ms]

[BSGS-k8192], 32-bit secrets
                        time:   [11.520 ms 12.029 ms 12.341 ms]
[TBSGS-k8192], 32-bit secrets
                        time:   [11.663 ms 12.255 ms 12.650 ms]

[BSGS-k16384], 32-bit secrets
                        time:   [12.228 ms 13.168 ms 14.037 ms]
[TBSGS-k16384], 32-bit secrets
                        time:   [13.009 ms 13.615 ms 14.469 ms]
</code></pre></div></div>

<p>For discrete log on 17-24 bit values but with the same tables for 32 bits, demonstrating that smaller values are solved for faster:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[BSGS-k32], 17-bit secrets (32-bit table)
                        time:   [12.096 µs 12.118 µs 12.144 µs]
[TBSGS-k32], 17-bit secrets (32-bit table)
                        time:   [35.155 µs 35.252 µs 35.368 µs]

[BSGS-k32], 18-bit secrets (32-bit table)
                        time:   [12.144 µs 12.166 µs 12.189 µs]
[TBSGS-k32], 18-bit secrets (32-bit table)
                        time:   [35.003 µs 35.073 µs 35.164 µs]

[BSGS-k32], 19-bit secrets (32-bit table)
                        time:   [12.211 µs 12.234 µs 12.261 µs]
[TBSGS-k32], 19-bit secrets (32-bit table)
                        time:   [35.060 µs 35.131 µs 35.218 µs]

[BSGS-k32], 20-bit secrets (32-bit table)
                        time:   [12.286 µs 12.307 µs 12.331 µs]
[TBSGS-k32], 20-bit secrets (32-bit table)
                        time:   [35.063 µs 35.098 µs 35.137 µs]

[BSGS-k32], 21-bit secrets (32-bit table)
                        time:   [12.424 µs 12.444 µs 12.466 µs]
[TBSGS-k32], 21-bit secrets (32-bit table)
                        time:   [35.119 µs 35.153 µs 35.190 µs]

[BSGS-k32], 22-bit secrets (32-bit table)
                        time:   [18.917 µs 18.954 µs 18.999 µs]
[TBSGS-k32], 22-bit secrets (32-bit table)
                        time:   [41.477 µs 41.553 µs 41.628 µs]

[BSGS-k32], 23-bit secrets (32-bit table)
                        time:   [31.782 µs 31.871 µs 31.971 µs]
[TBSGS-k32], 23-bit secrets (32-bit table)
                        time:   [54.167 µs 54.306 µs 54.453 µs]

[BSGS-k32], 24-bit secrets (32-bit table)
                        time:   [57.521 µs 57.709 µs 57.893 µs]
[TBSGS-k32], 24-bit secrets (32-bit table)
                        time:   [79.229 µs 79.522 µs 79.798 µs]
</code></pre></div></div>

<p>For varying K values with 18-bit secrets:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[BSGS-k64], 18-bit secrets (32-bit table)
                        time:   [22.715 µs 22.763 µs 22.816 µs]
[TBSGS-k64], 18-bit secrets (32-bit table)
                        time:   [45.064 µs 45.104 µs 45.144 µs]

[BSGS-k128], 18-bit secrets (32-bit table)
                        time:   [43.323 µs 43.387 µs 43.456 µs]
[TBSGS-k128], 18-bit secrets (32-bit table)
                        time:   [65.512 µs 65.641 µs 65.812 µs]

[BSGS-k1024], 18-bit secrets (32-bit table)
                        time:   [338.10 µs 338.59 µs 339.12 µs]
[TBSGS-k1024], 18-bit secrets (32-bit table)
                        time:   [354.13 µs 354.48 µs 354.88 µs]

[BSGS-k2048], 18-bit secrets (32-bit table)
                        time:   [673.72 µs 674.26 µs 674.87 µs]
[TBSGS-k2048], 18-bit secrets (32-bit table)
                        time:   [681.17 µs 681.61 µs 682.10 µs]
</code></pre></div></div>

<p class="info"><strong>Decision for Aptos:</strong>
Stick with 16-bit chunks and use the <a href="/bsgs#naive-discrete-log-algorithm">naive DL algorithm</a>: store all solutions in tables of $2^{16}$ group elements ($2^{16}\times 32$ bytes $\Rightarrow 2$ MiB) and use naive lookup to compute the DL in constant time, when chunks are small!
Note that the TBSGS-$k$ table can be reused for this naive lookup approach!
When chunks are big, resort to TBSGS-$k$ (or, in the future, to [BL12]<sup id="fnref:BL12:1"><a href="#fn:BL12" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>).
<!-- 16 chunks of 8-bit each give $2^4$ additions to decrypt: so, 3.20 $\mu$s per chunk $\Rightarrow $ 51.2 $\mu$s per TXN $\Rightarrow$ 19,500 TXN decryptions per second in Rust (or 1000 in the browser).-->
<br />
<strong>Rationale:</strong>
We have to be conservative because a user may be using multiple confidential apps at the same time and/or the browser may be busy doing other things.</p>

<h3 id="rust-dlp-bsgs-compression-after-every-step">Rust DLP: BSGS (compression after every step)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[BSGS] 32-bit secrets   time:   [63.673 ms 69.557 ms 75.040 ms]
</code></pre></div></div>

<h3 id="wasm-dlp-bl12">WASM DLP: [BL12]<sup id="fnref:BL12:2"><a href="#fn:BL12" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></h3>

<p>These were run on an older version of the TS SDK repo via: <code class="language-plaintext highlighter-rouge">pnpm jest discrete-log</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>WASM [BL12] 16-bit: avg=0.24ms, min=0.19ms, max=0.30ms
WASM [BL12] 32-bit: avg=42.59ms, min=34.17ms, max=56.06ms
</code></pre></div></div>

<h3 id="wasm-dlp-tbsgs-k-and-naivetruncateddoubledlookup">WASM DLP: TBSGS-$k$ and NaiveTruncatedDoubledLookup</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>WASM NaiveTruncatedDoubledLookup 16-bit: avg=1.50ms, min=1.33ms, max=2.29ms

WASM TBSGS-k32 32-bit: avg=20.15ms, min=1.84ms, max=39.55ms
</code></pre></div></div>

<h3 id="typescript-dlp-bsgs">TypeScript DLP: BSGS</h3>

<p>These were run in the TS SDK repo via: <code class="language-plaintext highlighter-rouge">pnpm jest discrete-log</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TS BSGS 16-bit: avg=7.47ms, min=5.82ms, max=8.24ms
TS BSGS 32-bit: avg=2252.36ms, min=1539.60ms, max=3359.79ms
</code></pre></div></div>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:BL12">
      <p><strong>Computing small discrete logarithms faster</strong>, by Daniel Bernstein and Tanja Lange, 2012, <a href="https://cr.yp.to/dlog/cuberoot-20120919.pdf">[URL]</a> <a href="#fnref:BL12" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:BL12:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a> <a href="#fnref:BL12:2" class="reversefootnote" role="doc-backlink">&#8617;<sup>3</sup></a></p>
    </li>
  </ol>
</div>]]></content><author><name>Alin Tomescu</name></author><category term="discrete logarithm" /><category term="baby-step giant-step (BSGS)" /><category term="Ristretto255" /><summary type="html"><![CDATA[tl;dr: When the discrete log $a$ of $a\cdot G$ is known to lie in a small range $[m)$, the baby-step giant-step (BSGS) algorithm recovers $a$ in $\ceil{\sqrt{m}}$ $\Gr$ additions using only a precomputed table of exactly $\ceil{\sqrt{m}}$ compressed points, trading the $O(1)$ time of the naive $m$-sized lookup table for much less memory. This post describes vanilla BSGS, and two Ristretto255-optimized variants: BSGS-$k$, which batches $k$ giant steps to amortize the expensive point compression and truncated BSGS-$k$ (TBSGS-$k$), which stores only 8-byte truncated keys to shrink the table by 75% with negligible performance impact.]]></summary></entry><entry><title type="html">Benchmarking MSMs over secp256k1</title><link href="https://alinush.github.io//secp256k1-msm" rel="alternate" type="text/html" title="Benchmarking MSMs over secp256k1" /><published>2026-05-01T00:00:00+00:00</published><updated>2026-05-01T00:00:00+00:00</updated><id>https://alinush.github.io//libsecp256k1-msm-benchmark</id><content type="html" xml:base="https://alinush.github.io//secp256k1-msm"><![CDATA[<p class="info"><strong>tl;dr:</strong> Running some secp256k1 MSM benchmarks.</p>

<!--more-->

<!-- Here you can define LaTeX macros -->
<div style="display: none;">$
$</div>
<p><!-- $ --></p>

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

<p>For ECDSA verification and pubkey recovery, the relevant MSM size is $n=2$.
For <strong>(modified) ECDSA batch verification</strong>, we will be interested in $n \ge 4$.</p>

<h3 id="glv">GLV</h3>

<p>secp256k1 has a GLV endomorphism $\phi(x,y)=(\beta x, y)$, where $\beta$ is a primitive cube root of 1 over the base field $\F_p$.
This lets you split a 256-bit scalar into two ~128-bit halves, speeding up scalar multiplication arithmetic.</p>

<p>The four libraries benchmarked below differ in where they apply GLV:</p>

<table>
  <thead>
    <tr>
      <th>Library</th>
      <th style="text-align: center">Single-mul GLV</th>
      <th style="text-align: center">MSM GLV</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">p256k1</code> (FFI to libsecp256k1)</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gnark-crypto</code></td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ark-secp256k1</code></td>
      <td style="text-align: center">❌</td>
      <td style="text-align: center">❌</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">halo2curves</code></td>
      <td style="text-align: center">❌</td>
      <td style="text-align: center">❌</td>
    </tr>
  </tbody>
</table>

<p>For <code class="language-plaintext highlighter-rouge">p256k1</code>, both <a href="https://github.com/bitcoin-core/secp256k1/blob/master/src/ecmult_impl.h">Strauss-WNAF and Pippenger-WNAF in <code class="language-plaintext highlighter-rouge">libsecp256k1</code></a> call <code class="language-plaintext highlighter-rouge">secp256k1_scalar_split_lambda</code> (resp. <code class="language-plaintext highlighter-rouge">secp256k1_ecmult_endo_split</code>) to feed Pippenger $2n$ pairs of half-length scalars instead of $n$ pairs of full-length scalars.</p>

<p>For <code class="language-plaintext highlighter-rouge">gnark-crypto</code>, only the per-point <a href="https://github.com/Consensys/gnark-crypto/blob/master/ecc/secp256k1/g1.go"><code class="language-plaintext highlighter-rouge">mulGLV</code></a> path uses the endomorphism; <code class="language-plaintext highlighter-rouge">MultiExp</code> runs Pippenger over the full 256-bit scalars (no <code class="language-plaintext highlighter-rouge">glv</code>/<code class="language-plaintext highlighter-rouge">phi</code>/<code class="language-plaintext highlighter-rouge">endomorph</code> references in <code class="language-plaintext highlighter-rouge">multiexp*.go</code>).</p>

<p>For <code class="language-plaintext highlighter-rouge">ark-secp256k1</code>, the <a href="https://docs.rs/ark-ec/latest/ark_ec/scalar_mul/glv/trait.GLVConfig.html"><code class="language-plaintext highlighter-rouge">GLVConfig</code></a> trait exists in <code class="language-plaintext highlighter-rouge">ark-ec</code>, but no curve in <a href="https://github.com/arkworks-rs/algebra"><code class="language-plaintext highlighter-rouge">arkworks-rs/algebra</code></a> implements it (<code class="language-plaintext highlighter-rouge">grep -rln "impl GLVConfig\|GLVConfig for"</code> is empty). secp256k1 only implements <code class="language-plaintext highlighter-rouge">SWCurveConfig</code>, so both <code class="language-plaintext highlighter-rouge">mul</code> and <code class="language-plaintext highlighter-rouge">VariableBaseMSM::msm</code> use the generic double-and-add over 256-bit scalars.</p>

<p>For <code class="language-plaintext highlighter-rouge">halo2curves</code>, the <a href="https://github.com/privacy-ethereum/halo2curves/blob/main/src/arithmetic.rs"><code class="language-plaintext highlighter-rouge">CurveEndo</code></a> trait exists and the <a href="https://github.com/privacy-ethereum/halo2curves/blob/main/src/derive/curve.rs"><code class="language-plaintext highlighter-rouge">endo!</code></a> macro provides a <code class="language-plaintext highlighter-rouge">decompose_scalar</code> impl for curves like <code class="language-plaintext highlighter-rouge">bn256</code> (<code class="language-plaintext highlighter-rouge">endo!(G1, Fr, ENDO_PARAMS_BN);</code>). But <a href="https://github.com/privacy-ethereum/halo2curves/blob/main/src/secp256k1/curve.rs"><code class="language-plaintext highlighter-rouge">secp256k1/curve.rs</code></a> never invokes the macro, so <code class="language-plaintext highlighter-rouge">Secp256k1</code> doesn’t implement <code class="language-plaintext highlighter-rouge">CurveEndo</code>. Independently, <a href="https://github.com/privacy-ethereum/halo2curves/blob/main/src/msm.rs"><code class="language-plaintext highlighter-rouge">msm.rs</code></a> doesn’t reference <code class="language-plaintext highlighter-rouge">CurveEndo::decompose_scalar</code> at all — <code class="language-plaintext highlighter-rouge">msm_serial</code>/<code class="language-plaintext highlighter-rouge">msm_parallel</code>/<code class="language-plaintext highlighter-rouge">msm_best</code> run Pippenger (with Booth encoding) over full 256-bit scalars regardless of whether the curve has GLV.</p>

<h2 id="p256k1">p256k1</h2>

<p>Bitcoin Core’s <a href="https://github.com/bitcoin-core/secp256k1"><code class="language-plaintext highlighter-rouge">libsecp256k1</code></a> C library ships a Pippenger-WNAF <a href="https://en.wikipedia.org/wiki/Exponentiation_by_squaring#Multi-scalar">multi-scalar multiplication (MSM)</a> routine, but neither the Rust <a href="https://crates.io/crates/secp256k1"><code class="language-plaintext highlighter-rouge">secp256k1</code></a> crate nor the pure-Rust <a href="https://crates.io/crates/libsecp256k1"><code class="language-plaintext highlighter-rouge">libsecp256k1</code></a> port expose it. i
The <a href="https://github.com/Trust-Machines/p256k1"><code class="language-plaintext highlighter-rouge">p256k1</code></a> crate does.</p>

<p>Below I benchmark its MSM.</p>

<p><code class="language-plaintext highlighter-rouge">libsecp256k1</code> exposes <code class="language-plaintext highlighter-rouge">secp256k1_ecmult</code> for individual signature verication (and for pubkey recovery) and exposes <code class="language-plaintext highlighter-rouge">secp256k1_ecmult_multi_var</code> for $n$-element MSM, dispatched to either Strauss-WNAF (small $n$) or Pippenger-WNAF (larger $n$). The cutoff is <code class="language-plaintext highlighter-rouge">ECMULT_PIPPENGER_THRESHOLD = 88</code> (<a href="https://github.com/bitcoin-core/secp256k1/blob/master/src/ecmult_impl.h#L55">line 55 of <code class="language-plaintext highlighter-rouge">ecmult_impl.h</code></a>): for $n &lt; 88$ the library calls <code class="language-plaintext highlighter-rouge">secp256k1_ecmult_strauss_wnaf</code>, and for $n \ge 88$ it calls <code class="language-plaintext highlighter-rouge">secp256k1_ecmult_pippenger_wnaf</code>. So in the table below, the rows for $n \in \{2, 4, 8, 16, 32, 64\}$ measure Strauss-WNAF + GLV, and the rows for $n \in \{128, 256, 512, 1024\}$ measure Pippenger-WNAF + GLV.</p>

<h3 id="run-the-benchmark">Run the benchmark</h3>

<p>I <a href="https://github.com/alinush/p256k1/tree/msm-benchmark">forked</a> the <code class="language-plaintext highlighter-rouge">p256k1</code> crate and added</p>
<ol>
  <li>a <a href="https://github.com/bheisler/criterion.rs">Criterion</a> benchmark that times <code class="language-plaintext highlighter-rouge">Point::multimult</code></li>
  <li>a naive <code class="language-plaintext highlighter-rouge">for i in 0..n { p += scalars[i] * points[i] }</code> loop for $n \in \{2, 4, \ldots, 1024\}$,</li>
  <li>a <code class="language-plaintext highlighter-rouge">run-benches.sh</code> script that runs these, parses Criterion’s <code class="language-plaintext highlighter-rouge">estimates.json</code> files and outputs a Markdown table.</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="nt">--branch</span> msm-benchmark https://github.com/alinush/p256k1.git
<span class="nb">cd </span>p256k1
<span class="c"># The script needs `cargo`, `jq`, `awk`, and `bc`.</span>
./run-benches.sh
</code></pre></div></div>

<p>Benchmarks shoud take ~1-2 minutes to run.</p>

<h3 id="results">Results</h3>

<p>Run on an Apple M4 Max, release build:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">$n$</th>
      <th style="text-align: right">MSM (µs)</th>
      <th style="text-align: right">Naive (µs)</th>
      <th style="text-align: right">Speedup</th>
      <th style="text-align: right">µs per scalar mul (MSM)</th>
      <th style="text-align: right">µs per scalar mul (Naive)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">2</td>
      <td style="text-align: right">13.130</td>
      <td style="text-align: right">19.994</td>
      <td style="text-align: right">1.52x</td>
      <td style="text-align: right">6.565</td>
      <td style="text-align: right">9.997</td>
    </tr>
    <tr>
      <td style="text-align: right">4</td>
      <td style="text-align: right">48.409</td>
      <td style="text-align: right">47.073</td>
      <td style="text-align: right">0.97x</td>
      <td style="text-align: right">12.102</td>
      <td style="text-align: right">11.768</td>
    </tr>
    <tr>
      <td style="text-align: right">8</td>
      <td style="text-align: right">69.829</td>
      <td style="text-align: right">94.050</td>
      <td style="text-align: right">1.34x</td>
      <td style="text-align: right">8.728</td>
      <td style="text-align: right">11.756</td>
    </tr>
    <tr>
      <td style="text-align: right">16</td>
      <td style="text-align: right">111.468</td>
      <td style="text-align: right">189.588</td>
      <td style="text-align: right">1.70x</td>
      <td style="text-align: right">6.966</td>
      <td style="text-align: right">11.849</td>
    </tr>
    <tr>
      <td style="text-align: right">32</td>
      <td style="text-align: right">201.065</td>
      <td style="text-align: right">379.832</td>
      <td style="text-align: right">1.88x</td>
      <td style="text-align: right">6.283</td>
      <td style="text-align: right">11.869</td>
    </tr>
    <tr>
      <td style="text-align: right">64</td>
      <td style="text-align: right">377.774</td>
      <td style="text-align: right">763.196</td>
      <td style="text-align: right">2.02x</td>
      <td style="text-align: right">5.902</td>
      <td style="text-align: right">11.924</td>
    </tr>
    <tr>
      <td style="text-align: right">128</td>
      <td style="text-align: right">641.421</td>
      <td style="text-align: right">1521.647</td>
      <td style="text-align: right">2.37x</td>
      <td style="text-align: right">5.011</td>
      <td style="text-align: right">11.887</td>
    </tr>
    <tr>
      <td style="text-align: right">256</td>
      <td style="text-align: right">1133.065</td>
      <td style="text-align: right">3059.156</td>
      <td style="text-align: right">2.69x</td>
      <td style="text-align: right">4.426</td>
      <td style="text-align: right">11.949</td>
    </tr>
    <tr>
      <td style="text-align: right">512</td>
      <td style="text-align: right">1966.887</td>
      <td style="text-align: right">6151.611</td>
      <td style="text-align: right">3.12x</td>
      <td style="text-align: right">3.841</td>
      <td style="text-align: right">12.014</td>
    </tr>
    <tr>
      <td style="text-align: right">1024</td>
      <td style="text-align: right">3626.145</td>
      <td style="text-align: right">12418.983</td>
      <td style="text-align: right">3.42x</td>
      <td style="text-align: right">3.541</td>
      <td style="text-align: right">12.127</td>
    </tr>
  </tbody>
</table>

<p class="info"><strong>Note (size-2 row uses the dedicated path):</strong> The $n=2$ row measures the <strong>specialized size-2 multiexp</strong> that ECDSA verify and pubkey recovery actually call (<code class="language-plaintext highlighter-rouge">secp256k1_ecmult</code> in libsecp256k1, exposed in my fork as <code class="language-plaintext highlighter-rouge">Point::ecmult</code>).
This leverages precomputed odd-multiples table for the fixed generator $G$. 
For reference, passing $n=2$ to the <em>generic</em> <code class="language-plaintext highlighter-rouge">secp256k1_ecmult_multi_var</code> instead, which treats $G$ as a random base, would give ≈24.6 µs, about <strong>2× slower</strong>.</p>

<p class="info"><strong>Note (batch-normalize patch in my fork):</strong> The numbers above are from a <a href="https://github.com/alinush/p256k1/tree/msm-benchmark">small patch</a> to <code class="language-plaintext highlighter-rouge">Point::multimult</code>: a batch-inversion replacement of $n$ inversions that gives a clean <strong>~30% speedup at $n=1024$</strong> (from 4734 µs down to 3644 µs).</p>

<h3 id="future-optimizations">Future optimizations</h3>

<p>For large $n$, <code class="language-plaintext highlighter-rouge">blst</code> and <code class="language-plaintext highlighter-rouge">halo2curves</code> use Pippenger with <strong>batched affine addition</strong>: they keep Pippenger’s bucket sums in <em>affine</em> coordinates and amortize many independent slope-inversions via Montgomery’s simultaneous-inversion trick. 
To understand the potential speed-ups, I vibe-coded a small Rust <a href="https://github.com/alinush/p256k1/tree/msm-benchmark">proof-of-concept</a> timing $K$ independent affine additions with (1) $K$ independent inversions vs. (2) one shared inversion.
(There’s a unit test that verifies both paths agree element-wise.)
Run from the same fork via:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="nt">--branch</span> msm-benchmark https://github.com/alinush/p256k1.git
<span class="nb">cd </span>p256k1
cargo bench <span class="nt">--bench</span> affine_add_bench
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th style="text-align: right">$K$</th>
      <th style="text-align: right">Naive (µs)</th>
      <th style="text-align: right">Batched (µs)</th>
      <th style="text-align: right">Speedup</th>
      <th style="text-align: right">Naive µs / add</th>
      <th style="text-align: right">Batched µs / add</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">2</td>
      <td style="text-align: right">4.99</td>
      <td style="text-align: right">3.79</td>
      <td style="text-align: right">1.32x</td>
      <td style="text-align: right">2.50</td>
      <td style="text-align: right">1.90</td>
    </tr>
    <tr>
      <td style="text-align: right">4</td>
      <td style="text-align: right">7.35</td>
      <td style="text-align: right">4.23</td>
      <td style="text-align: right">1.74x</td>
      <td style="text-align: right">1.84</td>
      <td style="text-align: right">1.06</td>
    </tr>
    <tr>
      <td style="text-align: right">8</td>
      <td style="text-align: right">12.52</td>
      <td style="text-align: right">5.20</td>
      <td style="text-align: right">2.41x</td>
      <td style="text-align: right">1.57</td>
      <td style="text-align: right">0.65</td>
    </tr>
    <tr>
      <td style="text-align: right">16</td>
      <td style="text-align: right">22.65</td>
      <td style="text-align: right">6.74</td>
      <td style="text-align: right">3.36x</td>
      <td style="text-align: right">1.42</td>
      <td style="text-align: right">0.42</td>
    </tr>
    <tr>
      <td style="text-align: right">32</td>
      <td style="text-align: right">42.81</td>
      <td style="text-align: right">10.07</td>
      <td style="text-align: right">4.25x</td>
      <td style="text-align: right">1.34</td>
      <td style="text-align: right">0.32</td>
    </tr>
    <tr>
      <td style="text-align: right">64</td>
      <td style="text-align: right">83.45</td>
      <td style="text-align: right">16.69</td>
      <td style="text-align: right">5.00x</td>
      <td style="text-align: right">1.30</td>
      <td style="text-align: right">0.26</td>
    </tr>
    <tr>
      <td style="text-align: right">128</td>
      <td style="text-align: right">163.94</td>
      <td style="text-align: right">30.41</td>
      <td style="text-align: right">5.39x</td>
      <td style="text-align: right">1.28</td>
      <td style="text-align: right">0.24</td>
    </tr>
    <tr>
      <td style="text-align: right">256</td>
      <td style="text-align: right">328.90</td>
      <td style="text-align: right">57.49</td>
      <td style="text-align: right">5.72x</td>
      <td style="text-align: right">1.29</td>
      <td style="text-align: right">0.23</td>
    </tr>
  </tbody>
</table>

<p class="info"><strong>What this means for Pippenger MSMs:</strong> The relevant comparison is batched-affine (≈0.23 µs/add) vs. <code class="language-plaintext highlighter-rouge">libsecp256k1</code>’s current mixed Jacobian+affine add (<code class="language-plaintext highlighter-rouge">secp256k1_gej_add_ge_var</code>, no inversion, ≈0.5-0.6 µs/add) $\Rightarrow$ a <strong>~2-2.5× per-add speedup</strong>. 
Since bucket-fill is roughly half of Pippenger’s work, plumbing this into <code class="language-plaintext highlighter-rouge">secp256k1_ecmult_pippenger_wnaf</code> should give ~25-30% more on top of the batch-normalize patch.
<strong>Caveat:</strong> This only helps the Pippenger path ($n \ge 88$).
For $n &lt; 88$ (i.e., ECDSA verify, recovery, and most small instances of batch-verify), <code class="language-plaintext highlighter-rouge">libsecp256k1</code> uses Strauss-WNAF, whose serial-accumulator inner loop has no parallel adds to batch.</p>

<h2 id="ark-secp256k1">ark-secp256k1</h2>

<p>The pure-Rust <a href="https://crates.io/crates/ark-secp256k1"><code class="language-plaintext highlighter-rouge">ark-secp256k1</code></a> crate (part of <a href="https://github.com/arkworks-rs/algebra">arkworks</a>) exposes a generic Pippenger MSM via the <a href="https://docs.rs/ark-ec/latest/ark_ec/scalar_mul/variable_base/trait.VariableBaseMSM.html"><code class="language-plaintext highlighter-rouge">VariableBaseMSM</code></a> trait: <code class="language-plaintext highlighter-rouge">Projective::msm(&amp;bases, &amp;scalars)</code>.</p>

<p>Unlike <code class="language-plaintext highlighter-rouge">p256k1</code>, this is pure Rust: no FFI, no hand-tuned assembly.
As a result, things are slower.</p>

<h3 id="run-the-benchmark-1">Run the benchmark</h3>

<p>In the same <a href="https://github.com/alinush/p256k1/tree/msm-benchmark">fork</a>, I added:</p>
<ol>
  <li>a Criterion benchmark that times <code class="language-plaintext highlighter-rouge">&lt;Projective as VariableBaseMSM&gt;::msm(&amp;bases, &amp;scalars)</code>,</li>
  <li>a naive <code class="language-plaintext highlighter-rouge">for i in 0..n { p += bases[i] * scalars[i] }</code> loop for $n \in \{2, 4, \ldots, 1024\}$,</li>
  <li>a <code class="language-plaintext highlighter-rouge">run-arkworks-benches.sh</code> script that runs these and outputs a Markdown table.</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="nt">--branch</span> msm-benchmark https://github.com/alinush/p256k1.git
<span class="nb">cd </span>p256k1
<span class="c"># The script needs `cargo`, `jq`, `awk`, and `bc`.</span>
./run-arkworks-benches.sh
</code></pre></div></div>

<p>Benchmarks should take ~1-2 minutes to run.</p>

<h3 id="results-1">Results</h3>

<p>Run on the same Apple M4 Max, release build:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">$n$</th>
      <th style="text-align: right">MSM (µs)</th>
      <th style="text-align: right">Naive (µs)</th>
      <th style="text-align: right">Speedup</th>
      <th style="text-align: right">µs per scalar mul (MSM)</th>
      <th style="text-align: right">µs per scalar mul (Naive)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">2</td>
      <td style="text-align: right">88.712</td>
      <td style="text-align: right">88.666</td>
      <td style="text-align: right">0.99x</td>
      <td style="text-align: right">44.356</td>
      <td style="text-align: right">44.333</td>
    </tr>
    <tr>
      <td style="text-align: right">4</td>
      <td style="text-align: right">131.302</td>
      <td style="text-align: right">173.934</td>
      <td style="text-align: right">1.32x</td>
      <td style="text-align: right">32.825</td>
      <td style="text-align: right">43.483</td>
    </tr>
    <tr>
      <td style="text-align: right">8</td>
      <td style="text-align: right">187.550</td>
      <td style="text-align: right">355.432</td>
      <td style="text-align: right">1.89x</td>
      <td style="text-align: right">23.443</td>
      <td style="text-align: right">44.429</td>
    </tr>
    <tr>
      <td style="text-align: right">16</td>
      <td style="text-align: right">292.609</td>
      <td style="text-align: right">775.267</td>
      <td style="text-align: right">2.64x</td>
      <td style="text-align: right">18.288</td>
      <td style="text-align: right">48.454</td>
    </tr>
    <tr>
      <td style="text-align: right">32</td>
      <td style="text-align: right">491.861</td>
      <td style="text-align: right">1799.569</td>
      <td style="text-align: right">3.65x</td>
      <td style="text-align: right">15.370</td>
      <td style="text-align: right">56.236</td>
    </tr>
    <tr>
      <td style="text-align: right">64</td>
      <td style="text-align: right">842.343</td>
      <td style="text-align: right">3720.552</td>
      <td style="text-align: right">4.41x</td>
      <td style="text-align: right">13.161</td>
      <td style="text-align: right">58.133</td>
    </tr>
    <tr>
      <td style="text-align: right">128</td>
      <td style="text-align: right">1373.081</td>
      <td style="text-align: right">7650.410</td>
      <td style="text-align: right">5.57x</td>
      <td style="text-align: right">10.727</td>
      <td style="text-align: right">59.768</td>
    </tr>
    <tr>
      <td style="text-align: right">256</td>
      <td style="text-align: right">2531.880</td>
      <td style="text-align: right">15444.088</td>
      <td style="text-align: right">6.09x</td>
      <td style="text-align: right">9.890</td>
      <td style="text-align: right">60.328</td>
    </tr>
    <tr>
      <td style="text-align: right">512</td>
      <td style="text-align: right">4649.651</td>
      <td style="text-align: right">31023.133</td>
      <td style="text-align: right">6.67x</td>
      <td style="text-align: right">9.081</td>
      <td style="text-align: right">60.592</td>
    </tr>
    <tr>
      <td style="text-align: right">1024</td>
      <td style="text-align: right">8119.509</td>
      <td style="text-align: right">62420.595</td>
      <td style="text-align: right">7.68x</td>
      <td style="text-align: right">7.929</td>
      <td style="text-align: right">60.957</td>
    </tr>
  </tbody>
</table>

<h2 id="gnark-crypto">gnark-crypto</h2>

<p><a href="https://github.com/Consensys/gnark-crypto"><code class="language-plaintext highlighter-rouge">gnark-crypto</code></a> is Consensys’ Go cryptography library. 
Its <code class="language-plaintext highlighter-rouge">ecc/secp256k1</code> package is code-generated (per-curve specialization, including modulus-specific Montgomery arithmetic) and exposes <a href="https://pkg.go.dev/github.com/consensys/gnark-crypto/ecc/secp256k1#G1Jac.MultiExp"><code class="language-plaintext highlighter-rouge">MultiExp</code></a>, implementing the Pippenger variant from <a href="https://eprint.iacr.org/2012/549.pdf">eprint 2012/549</a>.</p>

<p>For a fair comparison, I run <code class="language-plaintext highlighter-rouge">MultiExp</code> with <code class="language-plaintext highlighter-rouge">MultiExpConfig{NbTasks: 1}</code> (single goroutine), since <code class="language-plaintext highlighter-rouge">p256k1</code> and <code class="language-plaintext highlighter-rouge">ark-secp256k1</code> above are also single-threaded.</p>

<h3 id="run-the-benchmark-2">Run the benchmark</h3>

<p>I <a href="https://github.com/alinush/gnark-crypto/tree/secp256k1-msm-benchmark">forked</a> <code class="language-plaintext highlighter-rouge">gnark-crypto</code> and added:</p>
<ol>
  <li>a Go benchmark <code class="language-plaintext highlighter-rouge">BenchmarkMSMSizes</code> in <code class="language-plaintext highlighter-rouge">ecc/secp256k1</code> that times <code class="language-plaintext highlighter-rouge">(*G1Affine).MultiExp(points, scalars, ecc.MultiExpConfig{NbTasks: 1})</code>,</li>
  <li>a naive <code class="language-plaintext highlighter-rouge">for i := 0; i &lt; n; i++ { tmp.ScalarMultiplication(&amp;p[i], s[i]); acc.AddAssign(&amp;tmp) }</code> loop for $n \in \{2, 4, \ldots, 1024\}$,</li>
  <li>a <code class="language-plaintext highlighter-rouge">run-gnark-benches.sh</code> script that runs these and parses <code class="language-plaintext highlighter-rouge">go test -bench</code> output into a Markdown table.</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="nt">--branch</span> secp256k1-msm-benchmark https://github.com/alinush/gnark-crypto.git
<span class="nb">cd </span>gnark-crypto
<span class="c"># The script needs `go` and `awk`.</span>
./run-gnark-benches.sh
</code></pre></div></div>

<p>Benchmarks should take ~1-2 minutes to run.</p>

<h3 id="results-2">Results</h3>

<p>Run on the same Apple M4 Max, release build (Go’s default; optimizations + inlining are on unless you explicitly pass <code class="language-plaintext highlighter-rouge">-gcflags='all=-N -l'</code>):</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">$n$</th>
      <th style="text-align: right">MSM (µs)</th>
      <th style="text-align: right">Naive (µs)</th>
      <th style="text-align: right">Speedup</th>
      <th style="text-align: right">µs per scalar mul (MSM)</th>
      <th style="text-align: right">µs per scalar mul (Naive)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">2</td>
      <td style="text-align: right">162.226</td>
      <td style="text-align: right">53.448</td>
      <td style="text-align: right">0.33x</td>
      <td style="text-align: right">81.113</td>
      <td style="text-align: right">26.724</td>
    </tr>
    <tr>
      <td style="text-align: right">4</td>
      <td style="text-align: right">242.297</td>
      <td style="text-align: right">125.605</td>
      <td style="text-align: right">0.52x</td>
      <td style="text-align: right">60.574</td>
      <td style="text-align: right">31.401</td>
    </tr>
    <tr>
      <td style="text-align: right">8</td>
      <td style="text-align: right">317.981</td>
      <td style="text-align: right">273.368</td>
      <td style="text-align: right">0.86x</td>
      <td style="text-align: right">39.748</td>
      <td style="text-align: right">34.171</td>
    </tr>
    <tr>
      <td style="text-align: right">16</td>
      <td style="text-align: right">442.898</td>
      <td style="text-align: right">561.851</td>
      <td style="text-align: right">1.27x</td>
      <td style="text-align: right">27.681</td>
      <td style="text-align: right">35.116</td>
    </tr>
    <tr>
      <td style="text-align: right">32</td>
      <td style="text-align: right">654.000</td>
      <td style="text-align: right">1229.385</td>
      <td style="text-align: right">1.88x</td>
      <td style="text-align: right">20.438</td>
      <td style="text-align: right">38.418</td>
    </tr>
    <tr>
      <td style="text-align: right">64</td>
      <td style="text-align: right">975.187</td>
      <td style="text-align: right">2856.921</td>
      <td style="text-align: right">2.93x</td>
      <td style="text-align: right">15.237</td>
      <td style="text-align: right">44.639</td>
    </tr>
    <tr>
      <td style="text-align: right">128</td>
      <td style="text-align: right">1683.287</td>
      <td style="text-align: right">5882.025</td>
      <td style="text-align: right">3.49x</td>
      <td style="text-align: right">13.151</td>
      <td style="text-align: right">45.953</td>
    </tr>
    <tr>
      <td style="text-align: right">256</td>
      <td style="text-align: right">2877.386</td>
      <td style="text-align: right">12113.487</td>
      <td style="text-align: right">4.21x</td>
      <td style="text-align: right">11.240</td>
      <td style="text-align: right">47.318</td>
    </tr>
    <tr>
      <td style="text-align: right">512</td>
      <td style="text-align: right">4718.920</td>
      <td style="text-align: right">24435.621</td>
      <td style="text-align: right">5.18x</td>
      <td style="text-align: right">9.217</td>
      <td style="text-align: right">47.726</td>
    </tr>
    <tr>
      <td style="text-align: right">1024</td>
      <td style="text-align: right">8077.244</td>
      <td style="text-align: right">50120.264</td>
      <td style="text-align: right">6.21x</td>
      <td style="text-align: right">7.888</td>
      <td style="text-align: right">48.946</td>
    </tr>
  </tbody>
</table>

<p class="note">From speaking with <a href="https://yelhousni.github.io/">Youssef El Housni</a>, it seems like their secp256k1 implementation is unoptimized; it’s just there for some witness generation when proving ECDSA signatures.</p>

<h2 id="halo2curves">halo2curves</h2>

<p>The <a href="https://github.com/privacy-ethereum/halo2curves/blob/main/src/msm.rs"><code class="language-plaintext highlighter-rouge">msm</code></a> module exposes:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">msm_serial</code> (single-threaded),</li>
  <li><code class="language-plaintext highlighter-rouge">msm_parallel</code> (rayon-based),
    <ul>
      <li>simple <em>“chunk the input across threads, run <code class="language-plaintext highlighter-rouge">msm_serial</code> on each chunk, sum”</em> pattern</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">msm_best</code> (picks based on input size):
    <ul>
      <li>for $\lceil \ln(n) \rceil &lt; 10$ (i.e., $n \lesssim 22{,}000$, including all sizes in this post) it calls <code class="language-plaintext highlighter-rouge">msm_parallel</code>, which is rayon-parallelized but runs the <em>same</em> Pippenger as <code class="language-plaintext highlighter-rouge">msm_serial</code> per thread.
        <ul>
          <li>$\Rightarrow$ in our $n \leq 1024$ range, <code class="language-plaintext highlighter-rouge">msm_best</code> would use <code class="language-plaintext highlighter-rouge">msm_parallel</code>, which we do not want.</li>
        </ul>
      </li>
      <li>for $\lceil \ln(n) \rceil \ge 10$ it switches to a window-parallelized variant with a different memory layout.
        <ul>
          <li>this variant is not exposed as a <code class="language-plaintext highlighter-rouge">msm_*</code> function, apparently</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p>The MSM is generic over <code class="language-plaintext highlighter-rouge">CurveAffine</code> and uses Pippenger with Booth signed-digit encoding.</p>

<p>As noted in the <a href="#glv">GLV table</a> above, halo2curves’ secp256k1 module does <strong>not</strong> plug into the library’s <code class="language-plaintext highlighter-rouge">CurveEndo</code> trait, and <code class="language-plaintext highlighter-rouge">msm.rs</code> ignores <code class="language-plaintext highlighter-rouge">CurveEndo</code> regardless. So this is the slowest of the four implementations: pure-Rust, no GLV, <em>and</em> no curve-specific field arithmetic tuning for secp256k1’s pseudo-Mersenne prime $p = 2^{256} - 2^{32} - 977$.</p>

<p>We benchmark <strong><code class="language-plaintext highlighter-rouge">msm_serial</code></strong> (not <code class="language-plaintext highlighter-rouge">msm_best</code>) so the comparison is apples-to-apples with the other single-threaded benches in this post.</p>

<h3 id="run-the-benchmark-3">Run the benchmark</h3>

<p>I <a href="https://github.com/alinush/halo2curves/tree/secp256k1-msm-benchmark">forked</a> <code class="language-plaintext highlighter-rouge">halo2curves</code> and added:</p>
<ol>
  <li>a Criterion benchmark <code class="language-plaintext highlighter-rouge">benches/secp256k1_msm_sizes.rs</code> that times <code class="language-plaintext highlighter-rouge">msm_serial(&amp;scalars, &amp;bases, &amp;mut acc)</code>,</li>
  <li>a naive <code class="language-plaintext highlighter-rouge">for i in 0..n { acc += bases[i] * scalars[i] }</code> loop for $n \in \{2, 4, \ldots, 1024\}$,</li>
  <li>a <code class="language-plaintext highlighter-rouge">run-halo2curves-benches.sh</code> script that runs these and outputs a Markdown table.</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="nt">--branch</span> secp256k1-msm-benchmark https://github.com/alinush/halo2curves.git
<span class="nb">cd </span>halo2curves
<span class="c"># The script needs `cargo`, `jq`, `awk`, and `bc`.</span>
./run-halo2curves-benches.sh
</code></pre></div></div>

<p>Benchmarks should take ~1-2 minutes to run.</p>

<h3 id="results-3">Results</h3>

<p>Run on the same Apple M4 Max, release build:</p>

<table class="table-display">
  <thead>
    <tr>
      <th style="text-align: right">$n$</th>
      <th style="text-align: right">MSM (µs)</th>
      <th style="text-align: right">Naive (µs)</th>
      <th style="text-align: right">Speedup</th>
      <th style="text-align: right">µs per scalar mul (MSM)</th>
      <th style="text-align: right">µs per scalar mul (Naive)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">2</td>
      <td style="text-align: right">238.247</td>
      <td style="text-align: right">266.017</td>
      <td style="text-align: right">1.11x</td>
      <td style="text-align: right">119.123</td>
      <td style="text-align: right">133.008</td>
    </tr>
    <tr>
      <td style="text-align: right">4</td>
      <td style="text-align: right">272.565</td>
      <td style="text-align: right">531.791</td>
      <td style="text-align: right">1.95x</td>
      <td style="text-align: right">68.141</td>
      <td style="text-align: right">132.947</td>
    </tr>
    <tr>
      <td style="text-align: right">8</td>
      <td style="text-align: right">377.622</td>
      <td style="text-align: right">1064.668</td>
      <td style="text-align: right">2.81x</td>
      <td style="text-align: right">47.202</td>
      <td style="text-align: right">133.083</td>
    </tr>
    <tr>
      <td style="text-align: right">16</td>
      <td style="text-align: right">581.653</td>
      <td style="text-align: right">2131.086</td>
      <td style="text-align: right">3.66x</td>
      <td style="text-align: right">36.353</td>
      <td style="text-align: right">133.192</td>
    </tr>
    <tr>
      <td style="text-align: right">32</td>
      <td style="text-align: right">883.423</td>
      <td style="text-align: right">4256.997</td>
      <td style="text-align: right">4.81x</td>
      <td style="text-align: right">27.606</td>
      <td style="text-align: right">133.031</td>
    </tr>
    <tr>
      <td style="text-align: right">64</td>
      <td style="text-align: right">1412.361</td>
      <td style="text-align: right">8522.399</td>
      <td style="text-align: right">6.03x</td>
      <td style="text-align: right">22.068</td>
      <td style="text-align: right">133.162</td>
    </tr>
    <tr>
      <td style="text-align: right">128</td>
      <td style="text-align: right">2436.448</td>
      <td style="text-align: right">17030.856</td>
      <td style="text-align: right">6.99x</td>
      <td style="text-align: right">19.034</td>
      <td style="text-align: right">133.053</td>
    </tr>
    <tr>
      <td style="text-align: right">256</td>
      <td style="text-align: right">4065.745</td>
      <td style="text-align: right">34042.049</td>
      <td style="text-align: right">8.37x</td>
      <td style="text-align: right">15.881</td>
      <td style="text-align: right">132.976</td>
    </tr>
    <tr>
      <td style="text-align: right">512</td>
      <td style="text-align: right">6999.182</td>
      <td style="text-align: right">68019.231</td>
      <td style="text-align: right">9.71x</td>
      <td style="text-align: right">13.670</td>
      <td style="text-align: right">132.850</td>
    </tr>
    <tr>
      <td style="text-align: right">1024</td>
      <td style="text-align: right">12951.051</td>
      <td style="text-align: right">136053.208</td>
      <td style="text-align: right">10.50x</td>
      <td style="text-align: right">12.647</td>
      <td style="text-align: right">132.864</td>
    </tr>
  </tbody>
</table>

<h2 id="references">References</h2>

<p>For cited works, see below 👇👇</p>]]></content><author><name>Alin Tomescu</name></author><category term="ecdsa" /><category term="elliptic curves" /><summary type="html"><![CDATA[tl;dr: Running some secp256k1 MSM benchmarks.]]></summary></entry><entry><title type="html">Notes on NEAR’s MPC</title><link href="https://alinush.github.io//near" rel="alternate" type="text/html" title="Notes on NEAR’s MPC" /><published>2026-05-01T00:00:00+00:00</published><updated>2026-05-01T00:00:00+00:00</updated><id>https://alinush.github.io//near-mpc</id><content type="html" xml:base="https://alinush.github.io//near"><![CDATA[<p class="info"><strong>tl;dr:</strong>
<em>The good:</em> Audit went well. Lúcás Meier’s Cait-Sith threshold ECDSA protocol seems like a reasonable, conservative choice.
<em>The bad:</em> Near’s MPC currently works in a 5 out of 8 setting, without any proactive refresh.</p>

<h2 id="notes">Notes</h2>

<h3 id="good">Good</h3>

<ul>
  <li>MPC’s configuration is transparent, on-chain $\Rightarrow$ can monitor for suspicious membership changes</li>
  <li><em>“uses the Cait-Sith protocol in a secure manner”</em><sup id="fnref:DIM25"><a href="#fn:DIM25" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></li>
  <li><em>“we did not identify any issues related to Beaver triple or nonce reuse that could compromise the security of a shared key”</em><sup id="fnref:DIM25:1"><a href="#fn:DIM25" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></li>
  <li>NEAR currently only uses Cait-Sith on mainnet (May 1st, 2026)
    <ul>
      <li>(The MPC committee can change this over time, of course.)</li>
    </ul>
  </li>
</ul>

<h3 id="unclear">Unclear</h3>

<ul>
  <li>There is a proof for Cait-Sith but it’s in a new framework called MPS<sup id="fnref:Meie23e"><a href="#fn:Meie23e" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> which is <a href="/files/cait-sith/mps-is-uc-like.png">claimed to be UC-like</a><sup id="fnref:cronokirby-overview"><a href="#fn:cronokirby-overview" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>
    <ul>
      <li>(For all I know, this framework could be much better than UC!)</li>
      <li><a href="https://cronokirby.com/notes/cait-sith-security.html">Overview</a>
        <ul>
          <li><a href="https://cronokirby.com/notes/cait-sith-security-0-preliminaries.html">(0): Preliminaries</a></li>
          <li><a href="https://cronokirby.com/notes/cait-sith-security-1-echo-broadcast.html">(1): Echo Broadcast</a></li>
          <li><a href="https://cronokirby.com/notes/cait-sith-security-2-key-sharing.html">(2): Key Sharing</a></li>
          <li><a href="https://cronokirby.com/notes/cait-sith-security-3-multiplication-and-triples.html">(3): Multiplication and Triples</a></li>
          <li><a href="https://cronokirby.com/notes/cait-sith-security-4-signing.html">(4): Signing</a></li>
          <li><a href="https://cronokirby.com/notes/cait-sith-security-x-cheat-sheet.html">(X): Cheat Sheet</a></li>
        </ul>
      </li>
      <li>Initially, I thought the proof was <a href="https://github.com/cronokirby/cait-sith/issues/23">missing</a></li>
    </ul>
  </li>
  <li>❓ No audit for the robust threshold ECDSA scheme by Damgård et al.<sup id="fnref:DJNplus20e"><a href="#fn:DJNplus20e" class="footnote" rel="footnote" role="doc-noteref">4</a></sup></li>
</ul>

<h3 id="somewhat-concerning">Somewhat concerning</h3>

<ul>
  <li>Claude Code identified four unaddressed items from the Trails of Bits audit; they are not high-severity though:
    <ul>
      <li>#4 Ciphertext swapping (DB AAD missing) – Informational, Cryptography</li>
      <li>#5 Hash function used as KDF (derive_tweak) – Informational, Cryptography</li>
      <li>#6 P2P identity misbinding (duplicate p2p public keys) – Medium, Data Validation</li>
      <li>#10 AES-GCM nonce reuse (96-bit random, no rotation) – Medium, Cryptography</li>
    </ul>
  </li>
  <li>NEAR modified the Cait-Sith scheme<sup id="fnref:DIM25:2"><a href="#fn:DIM25" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>
    <ul>
      <li>multiplicative rerandomization of pre-signatures</li>
    </ul>
  </li>
</ul>

<h3 id="concerning">Concerning</h3>

<ul>
  <li>TEE-backed MPC nodes are <strong>off</strong> on mainnet (as of May 1st, 2026)</li>
  <li>No robustness and no identifiable abort (AFACCT)
    <ul>
      <li>a malicious participant who deviates can make signing fail (abort the protocol) and do so without being identifiable by honest parties $\Rightarrow$ 1 participant can DoS</li>
    </ul>
  </li>
  <li>NEAR had a lot of GitHub CI security issues in the Trail of Bits report (#12, #13, #14<sup id="fnref:DIM25:3"><a href="#fn:DIM25" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>)
    <ul>
      <li>#14 could’ve allowed an attacker to release a completely malicious binary (was rated “difficult” though)</li>
    </ul>
  </li>
</ul>

<h3 id="deeply-concerning">Deeply concerning</h3>

<ul>
  <li><strong>5 out of 8</strong> secret-sharing on mainnet (see <a href="#fetch-participants">here</a>)
    <ul>
      <li>The <a href="https://levex.com/en/blog/ronin-bridge-hack-explained">Ronin Bridge attack</a> compromised exactly $t=5$ out of $n=9$</li>
    </ul>
  </li>
  <li><strong>No proactive refresh</strong>, even though re-sharing (for nodes leaving and joining) is implemented and would be trivial to call periodically
    <ul>
      <li>Without proactive refresh, an attacker can slowly take its time and compromise all 5 keys</li>
      <li>The last re-sharing (and thus refresh) was on March 3rd, 2026
        <ul>
          <li>$\Rightarrow$ any progress an attacker made in the last 2 months (as of May 1st, 2026) is still good progress</li>
        </ul>
      </li>
      <li><strong>Why not?</strong> Because <a href="#proactive-refresh">proactive refresh must invalidate the ECDSA pre-signatures</a>, which lowers performance.</li>
    </ul>
  </li>
  <li>The Cait-Sith threshold ECDSA library was not part of the audit; only its use by NEAR’s MPC node implementation was audited<sup id="fnref:DIM25:4"><a href="#fn:DIM25" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>
    <ul>
      <li>This library may not have been production-ready.</li>
    </ul>
  </li>
</ul>

<h2 id="resources">Resources</h2>

<h3 id="links-and-notes">Links and notes</h3>

<ul>
  <li>NEAR actually implemented two different threshold ECDSA protocols: Cait-Sith<sup id="fnref:CS"><a href="#fn:CS" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> and Damgård et al.<sup id="fnref:DJNplus20e:1"><a href="#fn:DJNplus20e" class="footnote" rel="footnote" role="doc-noteref">4</a></sup></li>
  <li><a href="https://github.com/cronokirby/cait-sith/tree/main/docs">Documentation on Cait-Sith library and design</a></li>
  <li><a href="https://github.com/near/mpc">Near MPC repo</a></li>
</ul>

<h3 id="mpc-node-operators">MPC node operators</h3>

<p>Operators:</p>

<ul>
  <li>lacksandtech.near,</li>
  <li>mpc-lgns.near,</li>
  <li>multichain-mainnet-aurora.near,</li>
  <li>near-mpc-staking4all-01.near,</li>
  <li>nodemonster.near,</li>
  <li>n1-multichain.near,</li>
  <li>everstake-mpc-1.near,</li>
  <li>stakin-mpc.near</li>
</ul>

<p>NEAR-affiliated names visible:</p>
<ul>
  <li>n1-multichain.near (NEAR One)</li>
  <li>multichain-mainnet-aurora.near (Aurora)</li>
</ul>

<p>The rest are independent operators (Everstake, Stakin, etc.).</p>

<h3 id="proactive-refresh">Why no proactive refresh?</h3>

<p>First, Beaver triple secret sharing do <strong>not</strong> need to be refreshed: theft of such sharings do not lead to signature forgeries.
Furthermore, $t$-out-of-$n$ secret-shared beaver triples continue to be usable after refresh.</p>

<p>Second, recall that a pre-signature for $k \cdot G$ is a secret sharing:</p>

\[([k]_i, [k\cdot \sk]_i)_{i\in[n]},\]

<p>where $[k]_i$ is player $i$’s ($t$-out-of-$n$) share of the nonce $k$ (and, similarly, $[k\cdot \sk]_i$ is $k\cdot \sk$’s share).</p>

<p>While pre-signatures are indifferent to the secret-sharing of $\sk$ changing, theft of $t$ pre-signature shares would lead to recovery of the $\sk$: the adversary can reconstruct $k$ and $k\cdot \sk$ and then obtain $\sk$.</p>

<p>Hence, for refresh to be meaningful, pre-signatures have to be securely erased/discarded and new ones need to be generated.</p>

<h3 id="why-presignature-rerandomization-exists">Why presignature rerandomization exists</h3>

<p class="info"><strong>tl;dr:</strong> Rerandomization adapts a master-key presignature to sign under $\mathsf{sk} + e$ in 1 online round, with a guardrail against accidental presignature reuse.</p>

<p>NEAR’s MPC supports <strong>additive HD key derivation</strong>: derived keys are:</p>

\[\mathsf{sk}_{\mathsf{app}} = \mathsf{sk} + e\]

<p>where $e = H(\mathsf{account_id}, \mathsf{path})$ is public.</p>

<p>But cached presignatures $(R, [k]_i, [\sigma]_i)$ are generated with respect to the master $\mathsf{sk}$ (i.e. $\sigma = k \cdot \mathsf{sk}$).</p>

<p>To sign for $\mathsf{sk}_{\mathsf{app}}$ instead of $\mathsf{sk}$, each party must locally adjust their $\sigma$-share to:</p>

\[[\sigma_{\mathsf{app}}]_i = [\sigma]_i + [k]_i \cdot e\]

<p>This $\sigma$-adjustment is why rerandomization exists.</p>

<p>NEAR additionally multiplies through by a public scalar $\delta = \mathsf{HKDF}(\mathsf{pk}, \mathsf{tweak}, \mathsf{msg_hash}, R, \mathsf{participants}, \mathsf{entropy})$, yielding:</p>

\[\bigl(\delta R, \delta [k]_i,\ \delta ([\sigma]_i + [k]_i \cdot e)\bigr)\]

<p>This multiplicative step is <strong>not</strong> needed for nonce freshness: each signing request already consumes a fresh presignature (<code class="language-plaintext highlighter-rouge">crates/node/src/providers/ecdsa/sign.rs:73</code>, <code class="language-plaintext highlighter-rouge">take_owned()</code>), so the underlying $k$ is already per-request.</p>

<p>What $\delta$ buys is <strong>defense-in-depth</strong>: if a bug or retry path ever served the same presignature twice across different $(\mathsf{msg}, \mathsf{tweak})$ pairs, which would normally leak $\mathsf{sk}$, the request-bound $\delta$ ensures the effective nonces still differ.
ToB’s audit<sup id="fnref:DIM25:5"><a href="#fn:DIM25" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> also notes NEAR picked the multiplicative form (vs. Groth–Shoup’s additive<sup id="fnref:GS21e"><a href="#fn:GS21e" class="footnote" rel="footnote" role="doc-noteref">6</a></sup>) because the security proof was easier to write that way.</p>

<p class="todo">Was not able to find the proof in their repo.</p>

<h3 id="fetch-participants">Script: Fetch all MPC participants</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -s -X POST https://rpc.mainnet.near.org -H "Content-Type: application/json" -d '{
    "jsonrpc":"2.0","id":"1","method":"query",
    "params":{"request_type":"call_function","finality":"final",
              "account_id":"v1.signer","method_name":"state","args_base64":"e30="}
  }' | jq -r '.result.result | implode' | jq .
</code></pre></div></div>

<h3 id="script-mpc-membership-over-time">Script: MPC membership over time</h3>

<p>Based on visible on-chain history, no pure refresh (same set) has happened on mainnet.
Every reshare was triggered by a  membership change.</p>

<p>Here’s what each epoch’s <code class="language-plaintext highlighter-rouge">vote_new_parameters</code> proposal actually contained (winning proposals only, ordered by epoch):</p>

<table>
  <thead>
    <tr>
      <th>Epoch</th>
      <th>Finalized</th>
      <th>n / threshold</th>
      <th>Membership delta from previous</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3</td>
      <td>2025-08-25</td>
      <td>8 / 5</td>
      <td>(earliest <code class="language-plaintext highlighter-rouge">vote_new_parameters</code> call against this contract — see note)</td>
    </tr>
    <tr>
      <td>4</td>
      <td>2025-09-17</td>
      <td>10 / 7</td>
      <td>+ everstake-mpc-1.near, + stakin-mpc.near</td>
    </tr>
    <tr>
      <td>5</td>
      <td>2025-11-21</td>
      <td>9 / 6</td>
      <td>− lifted-mainnet.near</td>
    </tr>
    <tr>
      <td>6</td>
      <td>2026-03-03</td>
      <td>8 / 5</td>
      <td>− chain-signatures-hot.near ← current</td>
    </tr>
  </tbody>
</table>

<p class="note">Run <a href="https://github.com/alinush/scripts/blob/e200f3911926f4c5e46dbdac0f59bbaf51897563/src/aptos/near-mpc-epoch-history.py">this vibe-coded script</a> to reproduce these results.</p>

<h2 id="references">References</h2>

<p>For cited works, see below 👇👇</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:DIM25">
      <p><strong>NEAR One MPC Chain Signatures</strong>, by Fredrik Dahlgren, Marc Ilunga, and Jim Miller, 2025, <a href="https://github.com/trailofbits/publications/blob/master/reviews/2025-03-near-one-mpc-chain-signatures-securityreview.pdf">[URL]</a> <a href="#fnref:DIM25" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:DIM25:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a> <a href="#fnref:DIM25:2" class="reversefootnote" role="doc-backlink">&#8617;<sup>3</sup></a> <a href="#fnref:DIM25:3" class="reversefootnote" role="doc-backlink">&#8617;<sup>4</sup></a> <a href="#fnref:DIM25:4" class="reversefootnote" role="doc-backlink">&#8617;<sup>5</sup></a> <a href="#fnref:DIM25:5" class="reversefootnote" role="doc-backlink">&#8617;<sup>6</sup></a></p>
    </li>
    <li id="fn:Meie23e">
      <p><strong>Towards Modular Foundations for Protocol Security</strong>, by Lúcás Críostóir Meier, <em>in Cryptology {ePrint} Archive, Paper 2023/187</em>, 2023, <a href="https://eprint.iacr.org/2023/187">[URL]</a> <a href="#fnref:Meie23e" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:cronokirby-overview">
      <p><a href="https://cronokirby.com/notes/cait-sith-security.html">Cait-Sith: Overview</a>, Lucas Meier, April 16th, 2023 <a href="#fnref:cronokirby-overview" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:DJNplus20e">
      <p><strong>Fast Threshold {ECDSA} with Honest Majority</strong>, by Ivan Damgård and Thomas Pelle Jakobsen and Jesper Buus Nielsen and Jakob Illeborg Pagter and Michael Bæksvang Østergård, <em>in Cryptology {ePrint} Archive, Paper 2020/501</em>, 2020, <a href="https://eprint.iacr.org/2020/501">[URL]</a> <a href="#fnref:DJNplus20e" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:DJNplus20e:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:CS">
      <p>Cait-Sith threshold ECDSA signatures, by Lúcás Meier, <a href="https://github.com/cronokirby/cait-sith">GitHub repo</a> <a href="#fnref:CS" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:GS21e">
      <p><strong>On the security of ECDSA with additive key derivation and presignatures</strong>, by Jens Groth and Victor Shoup, <em>in Cryptology ePrint Archive, Report 2021/1330</em>, 2021, <a href="https://ia.cr/2021/1330">[URL]</a> <a href="#fnref:GS21e" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Alin Tomescu</name></author><category term="ecdsa" /><summary type="html"><![CDATA[tl;dr: The good: Audit went well. Lúcás Meier’s Cait-Sith threshold ECDSA protocol seems like a reasonable, conservative choice. The bad: Near’s MPC currently works in a 5 out of 8 setting, without any proactive refresh. Notes Good MPC’s configuration is transparent, on-chain $\Rightarrow$ can monitor for suspicious membership changes “uses the Cait-Sith protocol in a secure manner”1 “we did not identify any issues related to Beaver triple or nonce reuse that could compromise the security of a shared key”1 NEAR currently only uses Cait-Sith on mainnet (May 1st, 2026) (The MPC committee can change this over time, of course.) Unclear There is a proof for Cait-Sith but it’s in a new framework called MPS2 which is claimed to be UC-like3 (For all I know, this framework could be much better than UC!) Overview (0): Preliminaries (1): Echo Broadcast (2): Key Sharing (3): Multiplication and Triples (4): Signing (X): Cheat Sheet Initially, I thought the proof was missing ❓ No audit for the robust threshold ECDSA scheme by Damgård et al.4 Somewhat concerning Claude Code identified four unaddressed items from the Trails of Bits audit; they are not high-severity though: #4 Ciphertext swapping (DB AAD missing) – Informational, Cryptography #5 Hash function used as KDF (derive_tweak) – Informational, Cryptography #6 P2P identity misbinding (duplicate p2p public keys) – Medium, Data Validation #10 AES-GCM nonce reuse (96-bit random, no rotation) – Medium, Cryptography NEAR modified the Cait-Sith scheme1 multiplicative rerandomization of pre-signatures Concerning TEE-backed MPC nodes are off on mainnet (as of May 1st, 2026) No robustness and no identifiable abort (AFACCT) a malicious participant who deviates can make signing fail (abort the protocol) and do so without being identifiable by honest parties $\Rightarrow$ 1 participant can DoS NEAR had a lot of GitHub CI security issues in the Trail of Bits report (#12, #13, #141) #14 could’ve allowed an attacker to release a completely malicious binary (was rated “difficult” though) Deeply concerning 5 out of 8 secret-sharing on mainnet (see here) The Ronin Bridge attack compromised exactly $t=5$ out of $n=9$ No proactive refresh, even though re-sharing (for nodes leaving and joining) is implemented and would be trivial to call periodically Without proactive refresh, an attacker can slowly take its time and compromise all 5 keys The last re-sharing (and thus refresh) was on March 3rd, 2026 $\Rightarrow$ any progress an attacker made in the last 2 months (as of May 1st, 2026) is still good progress Why not? Because proactive refresh must invalidate the ECDSA pre-signatures, which lowers performance. The Cait-Sith threshold ECDSA library was not part of the audit; only its use by NEAR’s MPC node implementation was audited1 This library may not have been production-ready. Resources Links and notes NEAR actually implemented two different threshold ECDSA protocols: Cait-Sith5 and Damgård et al.4 Documentation on Cait-Sith library and design Near MPC repo MPC node operators Operators: lacksandtech.near, mpc-lgns.near, multichain-mainnet-aurora.near, near-mpc-staking4all-01.near, nodemonster.near, n1-multichain.near, everstake-mpc-1.near, stakin-mpc.near NEAR-affiliated names visible: n1-multichain.near (NEAR One) multichain-mainnet-aurora.near (Aurora) The rest are independent operators (Everstake, Stakin, etc.). Why no proactive refresh? First, Beaver triple secret sharing do not need to be refreshed: theft of such sharings do not lead to signature forgeries. Furthermore, $t$-out-of-$n$ secret-shared beaver triples continue to be usable after refresh. Second, recall that a pre-signature for $k \cdot G$ is a secret sharing: \[([k]_i, [k\cdot \sk]_i)_{i\in[n]},\] where $[k]_i$ is player $i$’s ($t$-out-of-$n$) share of the nonce $k$ (and, similarly, $[k\cdot \sk]_i$ is $k\cdot \sk$’s share). While pre-signatures are indifferent to the secret-sharing of $\sk$ changing, theft of $t$ pre-signature shares would lead to recovery of the $\sk$: the adversary can reconstruct $k$ and $k\cdot \sk$ and then obtain $\sk$. Hence, for refresh to be meaningful, pre-signatures have to be securely erased/discarded and new ones need to be generated. Why presignature rerandomization exists tl;dr: Rerandomization adapts a master-key presignature to sign under $\mathsf{sk} + e$ in 1 online round, with a guardrail against accidental presignature reuse. NEAR’s MPC supports additive HD key derivation: derived keys are: \[\mathsf{sk}_{\mathsf{app}} = \mathsf{sk} + e\] where $e = H(\mathsf{account_id}, \mathsf{path})$ is public. But cached presignatures $(R, [k]_i, [\sigma]_i)$ are generated with respect to the master $\mathsf{sk}$ (i.e. $\sigma = k \cdot \mathsf{sk}$). To sign for $\mathsf{sk}_{\mathsf{app}}$ instead of $\mathsf{sk}$, each party must locally adjust their $\sigma$-share to: \[[\sigma_{\mathsf{app}}]_i = [\sigma]_i + [k]_i \cdot e\] This $\sigma$-adjustment is why rerandomization exists. NEAR additionally multiplies through by a public scalar $\delta = \mathsf{HKDF}(\mathsf{pk}, \mathsf{tweak}, \mathsf{msg_hash}, R, \mathsf{participants}, \mathsf{entropy})$, yielding: \[\bigl(\delta R, \delta [k]_i,\ \delta ([\sigma]_i + [k]_i \cdot e)\bigr)\] This multiplicative step is not needed for nonce freshness: each signing request already consumes a fresh presignature (crates/node/src/providers/ecdsa/sign.rs:73, take_owned()), so the underlying $k$ is already per-request. What $\delta$ buys is defense-in-depth: if a bug or retry path ever served the same presignature twice across different $(\mathsf{msg}, \mathsf{tweak})$ pairs, which would normally leak $\mathsf{sk}$, the request-bound $\delta$ ensures the effective nonces still differ. ToB’s audit1 also notes NEAR picked the multiplicative form (vs. Groth–Shoup’s additive6) because the security proof was easier to write that way. Was not able to find the proof in their repo. Script: Fetch all MPC participants curl -s -X POST https://rpc.mainnet.near.org -H "Content-Type: application/json" -d '{ "jsonrpc":"2.0","id":"1","method":"query", "params":{"request_type":"call_function","finality":"final", "account_id":"v1.signer","method_name":"state","args_base64":"e30="} }' | jq -r '.result.result | implode' | jq . Script: MPC membership over time Based on visible on-chain history, no pure refresh (same set) has happened on mainnet. Every reshare was triggered by a membership change. Here’s what each epoch’s vote_new_parameters proposal actually contained (winning proposals only, ordered by epoch): Epoch Finalized n / threshold Membership delta from previous 3 2025-08-25 8 / 5 (earliest vote_new_parameters call against this contract — see note) 4 2025-09-17 10 / 7 + everstake-mpc-1.near, + stakin-mpc.near 5 2025-11-21 9 / 6 − lifted-mainnet.near 6 2026-03-03 8 / 5 − chain-signatures-hot.near ← current Run this vibe-coded script to reproduce these results. References For cited works, see below 👇👇 NEAR One MPC Chain Signatures, by Fredrik Dahlgren, Marc Ilunga, and Jim Miller, 2025, [URL] &#8617; &#8617;2 &#8617;3 &#8617;4 &#8617;5 &#8617;6 Towards Modular Foundations for Protocol Security, by Lúcás Críostóir Meier, in Cryptology {ePrint} Archive, Paper 2023/187, 2023, [URL] &#8617; Cait-Sith: Overview, Lucas Meier, April 16th, 2023 &#8617; Fast Threshold {ECDSA} with Honest Majority, by Ivan Damgård and Thomas Pelle Jakobsen and Jesper Buus Nielsen and Jakob Illeborg Pagter and Michael Bæksvang Østergård, in Cryptology {ePrint} Archive, Paper 2020/501, 2020, [URL] &#8617; &#8617;2 Cait-Sith threshold ECDSA signatures, by Lúcás Meier, GitHub repo &#8617; On the security of ECDSA with additive key derivation and presignatures, by Jens Groth and Victor Shoup, in Cryptology ePrint Archive, Report 2021/1330, 2021, [URL] &#8617;]]></summary></entry><entry><title type="html">Groth21 PVSS</title><link href="https://alinush.github.io//groth21" rel="alternate" type="text/html" title="Groth21 PVSS" /><published>2026-04-15T00:00:00+00:00</published><updated>2026-04-15T00:00:00+00:00</updated><id>https://alinush.github.io//groth21-pvss-algorithms</id><content type="html" xml:base="https://alinush.github.io//groth21"><![CDATA[<p class="info"><strong>tl;dr:</strong> Groth’s non-interactive distributed key generation paper<sup id="fnref:Grot21e"><a href="#fn:Grot21e" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, which uses a novel approximate ZK range proofs to argue correct chunking, but inadvertantly increases share decryption time.</p>

<!--more-->

<!-- Here you can define LaTeX macros -->
<div style="display: none;">$
%
\def\ek{\mathsf{ek}}
\def\dk{\mathsf{dk}}
\def\Gone{G_1}
\def\Gtwo{G_2}
%
\def\ProveSh{\mathsf{NIZK.ProveSh}}
\def\VerSh{\mathsf{NIZK.VerSh}}
\def\ProveChunk{\mathsf{NIZK.ProveChunk}}
\def\VerChunk{\mathsf{NIZK.VerChunk}}
%
\def\pvss{\mathsf{PVSS}}
\def\deal{\mathsf{Deal}}
\def\verify{\mathsf{Verify}}
\def\decrypt{\mathsf{DecryptShare}}
\def\setup{\mathsf{Setup}}
%
\def\trx{\mathsf{trx}}
$</div>
<p><!-- $ --></p>

<h2 id="overview">Overview</h2>

<p class="info">$\term{t}$-out-of-$\term{n}$ means you need $t$ shares to reconstruct out of all $n$ shares.</p>

<p><strong>Step 1:</strong> Commit to shares $\rightarrow$ split each share into $\term{m}$ <strong>chunks</strong> $\rightarrow$ <a href="/elgamal">ElGamal</a>-encrypt chunks:</p>

<ul>
  <li>Pick random degree $t-1$ polynomial</li>
  <li>Commits to its $t$ coefficients</li>
  <li>Vanilla ElGamal batch encrypt the $nm$ chunks of the shares for each player</li>
</ul>

<p><strong>Step 2</strong>: Proofs:</p>

<ol>
  <li><strong>Proof of correct secret sharing:</strong> i.e., ciphertexts encrypt the chunks of the $n$ evaluations of the committed degree-$t$ polynomial</li>
  <li><strong>Proof of correct chunking:</strong> i.e., ciphertexts encrypt small enough chunks</li>
</ol>

<h2 id="preliminaries">Preliminaries</h2>

<h3 id="notation">Notation</h3>

<p>We use <strong>additive notation</strong> for group operations: scalar multiplication is denoted $a \cdot G$ (rather than $G^a$), group addition is $A + B$ (rather than $A \cdot B$), and the identity element is $\mathcal{O}$ (rather than $1$).
We denote the generators of $\mathbb{G}_1$ and $\mathbb{G}_2$ as $\Gone$ and $\Gtwo$, respectively.</p>

<p>The Groth21 scheme does <strong>not</strong> need pairing-friendly groups (it can be that $\Gr_1 = \Gr_2$), but we describe it as if it used pairing-friendly groups.
This is more general and in line with DFINITY’s implementation over BLS12-381 which has Type-III pairings.</p>

<ul>
  <li>$\term{\lambda}$ - the security parameter (should be 128)
    <ul>
      <li>We work over groups of size $\sizeof{\Gr_1} \equiv 2^{2\lambda}$</li>
    </ul>
  </li>
  <li>$\emph{t}$ - threshold (need $t$ shares to reconstruct)</li>
  <li>$\emph{n}$ - number of players / shares</li>
  <li>$\term{B}=2^{\term{b}}$ - each share is split into a chunk $&lt; B$
    <ul>
      <li>But Groth’s <em>approximate</em> range proof will give a worse guarantee; see <a href="#correct-chunking-zkp">below</a>.</li>
      <li>$B=2^{40}$ should be practical for baby-step giant step ($2^{20} \times .5$ $\mu$s = ~0.5 secs)
        <ul>
          <li>However, Groth21’s approximate range proof means we will have to compute DLs much bigger than $B$</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>$\emph{m}$ - the # of chunks we split a field element share into; typically set to $2\lambda/b$</li>
  <li>Groth21-specific parameters:
    <ul>
      <li>$\term{\ell}$ - number of approximate range proof repetitions</li>
      <li>$\term{E}$ - size of a Fiat-Shamir challenge; set as $E = 2^{\lceil{\lambda}/{\ell}\rceil}$
        <ul>
          <li>Repeating $\ell$ times reduces the risk of fraud on entry $(i, j)$ to $\le E^{-\ell}$</li>
        </ul>
      </li>
      <li>$\term{S}$ - the range that the random Groth sums will be in; should be $\emph{S} = nm(B-1)(E-1)$</li>
      <li>$\term{Z}=2\ell S = 2\ell nm (E-1)(B-1)$ - the size of the range of the sum after <strong>blinding</strong> (see <a href="#nizk-provechunk">$\ProveChunk$</a>)
        <ul>
          <li>The decrypted chunk is guaranteed to be in $[1-Z, Z-1]$
            <ul>
              <li><strong>TODO:</strong> What about $\Delta$?</li>
            </ul>
          </li>
          <li>So the max range of a discrete log is $(Z-1)-(1-Z)+1= 2Z-1$
            <ul>
              <li>The verifier needs to compute $E-1$ discrete logs from a range of size $2Z-1$.</li>
              <li>The cost of this computation is $(E-1)\cdot \sqrt{2Z-1}$.
                <ul>
                  <li>This means that we will need to use a much smaller $B$.</li>
                  <li>We can do slightly better by using batched BSGS DL algorithms</li>
                </ul>
              </li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p class="todo">I am not sure if this is right. It’s been a while. See <a href="/chunky#full-benchmarks">Chunky full benchmarks</a> too, for a slightly different note on DLs.</p>

<h3 id="public-key-encryption">Public key encryption</h3>

<p>$\mathsf{PKE.KeyGen}(1^\lambda)\rightarrow(\term{\dk},\term{\ek})$:</p>

<ul>
  <li>$\dk \randget \Zp$</li>
  <li>$\ek\gets \dk \cdot \Gone$</li>
  <li>(Also includes a ZKPoK but ignoring here)</li>
</ul>

<h2 id="groth21-zk-building-blocks">Groth21 ZK building blocks</h2>

<h3 id="correct-sharing-zkp">ZKP for “correct secret sharing”</h3>

<h4 id="overview-1">Overview</h4>

<ul>
  <li>Public inputs:
    <ul>
      <li>$n$ encryption keys $\ek_i$</li>
      <li>$n$ ElGamal ciphertexts $\term{R},(\term{C_i})_{i\in[n]}$ (with the same randomness)</li>
      <li>Feldman commitment $(\term{A_k})_{k\in[0,t)}$ to polynomial coefficients $a_i$’s that define a polynomial $\term{a(X)}$</li>
    </ul>
  </li>
  <li>Private inputs:
    <ul>
      <li>ElGamal shared randomness $\term{r}$</li>
      <li>Encrypted messages $(\term{s_i})_{i\in[n]}$</li>
    </ul>
  </li>
  <li>Relation:
    <ul>
      <li>$A_k = a_k \cdot \Gtwo,\forall k\in[0,t)$, where $a(X) = \sum_{k\in[0,t)} a_k X^k$</li>
      <li>$R = r \cdot \Gone$</li>
      <li>$C_i = r \cdot \ek_i + s_i \cdot \Gone$, $\forall i\in[n]$</li>
      <li>$s_i = a(i)$</li>
    </ul>
  </li>
</ul>

<h4 id="algorithms">Algorithms</h4>

<p>$\ProveSh
\begin{pmatrix}
\ek_1,\ldots,\ek_n,
A_0,\ldots,A_{t-1},
C_1,\ldots,C_n,R;\<br />
s_1,\ldots,s_n,r
\end{pmatrix}\rightarrow \pi_S$:</p>

<p>Let $a_k$ denote the scalar such that $A_k = a_k \cdot \Gtwo$; then, the relation being proved is that:</p>

\[s_i = \sum_{k\in[0,t-1)} a_k i^k\]

<ul>
  <li>Derive <strong>Fiat-Shamir</strong> challenge $x$ from the public statement</li>
  <li>$(\term{\alpha},\term{\rho}) \randget \Zp^2$</li>
  <li>$\term{Y}\gets \rho \cdot \left(\sum_{i=1}^n x^i \cdot \ek_i\right) + \alpha \cdot \Gone$</li>
  <li>// note: we can think of this 4-round protocol as a 3-round one, where $\left(x,\sum_{i=1}^n x^i \cdot \ek_i\right)$ is part of the public statement (and $\sum_{i=1}^n s_i x^i$ is part of the witness?)</li>
  <li>$\term{F}\gets \rho \cdot \Gone$</li>
  <li>
    <p>$\term{A}\gets \alpha \cdot \Gtwo$</p>
  </li>
  <li>Derive <strong>Fiat-Shamir</strong> challenge $x’$ from transcript so far (i.e., public statement and $F,A,Y$)</li>
  <li>$\term{z_r} \gets \rho + x’r$</li>
  <li>$\term{z_a}\gets \alpha + x’\sum_{i=1}^n s_i x^i$</li>
  <li><strong>return</strong> $(F,A,Y,z_r,z_a)$</li>
</ul>

<p>$\VerSh
\begin{pmatrix}
\ek_1,\ldots,\ek_n,
A_0,\ldots,A_{t-1},
C_1,\ldots,C_n,R;\pi_S
\end{pmatrix}\rightarrow \{0,1\}$:</p>

<ul>
  <li>Parse $\pi_S$</li>
  <li><strong>assert</strong> $x’ \cdot R + F \stackrel{?}{=} z_r \cdot \Gone$</li>
  <li><strong>assert</strong> $x’ \cdot \left(\sum_{k=0}^{t-1} \left(\sum_{i\in[n]} i^k x^i\right) \cdot A_k\right) + A \stackrel{?}{=} z_a \cdot \Gtwo$ (over $\mathbb{G}_2$)</li>
</ul>

<p class="smallnote">Correctness holds because:
\begin{align}
x’ \cdot \left(\sum_{k=0}^{t-1} \left(\sum_{i\in[n]} i^k x^i\right) \cdot A_k\right) + A
&amp;= \alpha \cdot \Gtwo + x’ \cdot \sum_{k=0}^{t-1} \left(\sum_{i=1}^n i^k x^i\right) \cdot A_k\\<br />
&amp;= \alpha \cdot \Gtwo + x’ \cdot \sum_{i=1}^n x^i \cdot \left(\sum_{k=0}^{t-1} i^k \cdot A_k\right)\\<br />
&amp;= \alpha \cdot \Gtwo + x’ \cdot \sum_{i=1}^{n} (s_i x^i) \cdot \Gtwo\\<br />
&amp;= \alpha \cdot \Gtwo + x’ \cdot \left(\sum_{i=1}^{n} s_i x^i\right) \cdot \Gtwo\\<br />
&amp;= \left(\alpha + x’\sum_{i=1}^{n}s_i x^i\right) \cdot \Gtwo\\<br />
&amp;= z_a \cdot \Gtwo
\end{align}</p>

<ul>
  <li><strong>assert</strong> $x’ \cdot \left(\sum_{i=1}^{n} x^i \cdot C_i\right) + Y \stackrel{?}{=} z_r \cdot \left(\sum_{i=1}^{n} x^i \cdot \ek_i\right) + z_a \cdot \Gone$ (over $\mathbb{G}_1$)</li>
</ul>

<p class="smallnote">Correctness holds because:
\begin{align}
x’ \cdot \left(\sum_{i=1}^{n} x^i \cdot C_i\right) + Y
&amp;= x’ \cdot \sum_{i=1}^{n} x^i \cdot (r \cdot \ek_i + s_i \cdot \Gone) + \rho \cdot \left(\sum_{i=1}^n x^i \cdot \ek_i\right) + \alpha \cdot \Gone\\<br />
&amp;= x’ \cdot \sum_{i=1}^{n} (r x^i) \cdot \ek_i + x’ \cdot \sum_{i=1}^{n} (s_i x^i) \cdot \Gone + \rho \cdot \left(\sum_{i=1}^n x^i \cdot \ek_i\right) + \alpha \cdot \Gone\\<br />
&amp;= (rx’) \cdot \left(\sum_{i=1}^{n} x^i \cdot \ek_i\right) + \left(x’ \sum_i s_i x^i\right) \cdot \Gone + \rho \cdot \left(\sum_{i=1}^n x^i \cdot \ek_i\right) + \alpha \cdot \Gone\\<br />
&amp;= (\rho+x’r) \cdot \left(\sum_{i=1}^{n} x^i \cdot \ek_i\right) + \left(\alpha + x’\sum_i s_i x^i\right) \cdot \Gone\\<br />
&amp;= z_r \cdot \left(\sum_{i=1}^{n} x^i \cdot \ek_i\right) + z_a \cdot \Gone
\end{align}</p>

<ul>
  <li><strong>return</strong> 1</li>
</ul>

<h4 id="performance">Performance</h4>

<ul>
  <li><strong>Prover time:</strong>
    <ul>
      <li>size-$(n+1)$ $\mathbb{G}_1$ MSM for $Y$</li>
      <li>1 scalar mul in $\mathbb{G}_1$ for $F$</li>
      <li>1 scalar mul in $\mathbb{G}_2$ for $A$</li>
      <li>(ignored: one degree-$n$ polynomial evaluation at a random point)</li>
    </ul>
  </li>
  <li><strong>Verifier time:</strong>
    <ul>
      <li>size-$(2n+4)$ $\mathbb{G}_1$ MSM (combining first check for $F$ with third check for $Y$)</li>
      <li>size-$(t+2)$ $\mathbb{G}_2$ MSM (second check for $A$)</li>
    </ul>
  </li>
  <li><strong>Proof size:</strong>
    <ul>
      <li>2 field elements in $\Zp$ ($z_r, z_a$)</li>
      <li>2 group elements in $\mathbb{G}_1$ ($F,Y$)</li>
      <li>1 group element in $\mathbb{G}_2$ ($A$)</li>
    </ul>
  </li>
</ul>

<h3 id="correct-chunking-zkp">ZKP of “correct chunking”</h3>

<p>Uses <strong>public parameters</strong> $\emph{\ell},\emph{E},\emph{S},\emph{Z}$.</p>

<h4 id="overview-2">Overview</h4>

<ul>
  <li>Public inputs:
    <ul>
      <li>$n$ encryption keys $\ek_i$</li>
      <li>$nm$ ElGamal ciphertexts $(\term{R_j})<em>{j\in[m]},(\term{C</em>{i,j}})_{i\in[n],j\in[m]}$</li>
    </ul>
  </li>
  <li>Private inputs:
    <ul>
      <li>ElGamal shared randomness $(\term{r_j})_{j\in[m]}$</li>
      <li>Encrypted messages $(\term{s_{i,j}})_{i\in[n],j\in[m]}$</li>
    </ul>
  </li>
  <li>Relation:
    <ul>
      <li>$\forall j\in[m], R_j = r_j \cdot \Gone$</li>
      <li>$\forall i\in[n],j\in[m]$, $C_{i,j} = r_j \cdot \ek_i + s_{i,j} \cdot \Gone$</li>
      <li>$\forall i\in[n],j\in[m],\exists \term{\Delta_{i,j}}\in [1,E-1]$, such that $\Delta_{i,j}\cdot s_{i,j}\in [1-Z,Z-1]$</li>
    </ul>
  </li>
</ul>

<h4 id="algorithms-1">Algorithms</h4>

<p id="nizk-provechunk">$\ProveChunk
\begin{pmatrix}
\ek_1,\ldots,\ek_n,
R_1,\ldots,R_m,
C_{1,1}\ldots,C_{n,m};\<br />
r_1,\ldots,r_m,
s_{1,1},\ldots,s_{n,m}
\end{pmatrix}\rightarrow \pi_C$:</p>

<ul>
  <li>$\term{\ek_0} \randget \mathbb{G}_1$</li>
  <li><strong>do</strong>:
    <ul>
      <li>$(\term{\sigma_1},\ldots,\sigma_\ell) \randget [-S,Z-1]^\ell$ (blinders)</li>
      <li>$(\term{\beta_1},\ldots,\beta_\ell) \randget \Zp^\ell$</li>
      <li>$\forall k\in[\ell]$: (encrypting them)
        <ul>
          <li>$B_k \gets \beta_k \cdot \Gone$</li>
          <li>$C_k \gets \beta_k \cdot \ek_0 + \sigma_k \cdot \Gone$</li>
        </ul>
      </li>
      <li>Derive <strong>Fiat-Shamir</strong> challenges $e_{1,1,1},\ldots,e_{n,m,\ell}\in[0,E)$ from transcript so far (i.e., public statement and $\ek_0,B_1,C_1,\ldots,B_\ell, C_\ell$)</li>
      <li>$\forall k \in[\ell],$ $z_{s,k} \gets \sum_{i\in[n],j\in[m]}e_{i,j,k}\cdot s_{i,j} + \sigma_k$ (random subset sum plus blinders)</li>
    </ul>
  </li>
</ul>

<p class="warning"><strong>WARNING:</strong> Groth says <em>“some sums would require slightly faster or slower computation, so we may use constant time algorithms to prevent timing leaks.”</em> This is worrisome and would need to be carefully implemented.</p>

<ul>
  <li><strong>until</strong> $\forall k\in[\ell],$ $z_{s,k} \in [0,Z-1]$ (expected # of iterations is 2; see below)
    <ul>
      <li>Groth says <em>“The risk of landing outside the range in [the $\ell$ runs that compute all $z_{s,k}$’s] is $\le (\ell S)/Z=(\ell S)/(2\ell S)=\frac{1}{2}$”</em></li>
      <li>If the failure probability is $S/Z$, then the success probability is $p=1-\frac{1}{2}=1/2$. To get a success, the expected # of trials is $1/p=2$.</li>
    </ul>
  </li>
</ul>

<p class="todo"><em>“If failure occurs on $\lambda$ tries then abort.”</em> Why should this abort instead of repeating until success?</p>

<ul>
  <li>$(\delta_0,\ldots,\delta_n) \randget \Zp^n$ (begin $\Sigma$ protocol)</li>
  <li>$\forall i\in[0,n], D_i \gets \delta_i \cdot \Gone$</li>
  <li>
    <p>$Y\gets \sum_{i=0}^n \delta_i \cdot \ek_i$</p>
  </li>
  <li>Derive <strong>Fiat-Shamir</strong> challenge $x\in\{0,1\}^\lambda$ from transcript so far (i.e., public statement and $\ek_0,B_1,C_1,\ldots,B_\ell, C_\ell, e_{1,1,1},\ldots,e_{m,n,\ell },D_0,\ldots,D_n,Y$)</li>
  <li>$\forall i\in[n],z_{r,i} \gets \sum_{j\in[m],k\in[\ell]} e_{i,j,k}\cdot r_j \cdot x^k+\delta_i$</li>
  <li>$z_\beta \gets \sum_{k=1}^\ell \beta_k x^k+\delta_0$</li>
  <li>$\pi_C \gets \begin{pmatrix}
\ek_0,
(B_1,\ldots,B_\ell), (C_1,\ldots,C_\ell),
(D_0,\ldots,D_n), 
Y,\<br />
(z_{s,1},\ldots,z_{s,\ell}),
(z_{r,1},\ldots,z_{r,n}),
z_\beta
\end{pmatrix}$</li>
</ul>

<p id="verchunk">$\VerChunk
\begin{pmatrix}
\ek_1,\ldots,\ek_n,
R_1,\ldots,R_m,
C_{1,1}\ldots,C_{n,m}; \pi_C
\end{pmatrix}\rightarrow \{0,1\}$:</p>

<ul>
  <li><strong>parse</strong> $\pi_C$</li>
  <li><strong>assert</strong> $\forall k\in[\ell], z_{s,k} \in [0,Z-1]$</li>
  <li><strong>assert:</strong></li>
</ul>

<p>\begin{align}
&amp;\forall i\in[n],\ \sum_{j=1}^m \left(\sum_{k=1}^\ell e_{i,j,k}\cdot x^k\right) \cdot R_j + D_i \stackrel{?}{=} z_{r,i} \cdot \Gone\\<br />
&amp;\sum_{k=1}^\ell x^k \cdot B_k + D_0 \stackrel{?}{=} z_\beta \cdot \Gone\\<br />
&amp;\sum_{k=1}^\ell x^k \cdot \left( \sum_{i=1}^n \sum_{j=1}^m e_{i,j,k} \cdot C_{i,j}
\right) + \sum_{k=1}^\ell x^k \cdot C_k + Y\\<br />
&amp;\quad\stackrel{?}{=} \sum_{i=1}^n z_{r,i} \cdot \ek_i + z_\beta \cdot \ek_0 + 
\left(\sum_{k=1}^\ell z_{s,k}\cdot x^k\right) \cdot \Gone
\end{align}</p>

<h3 id="reorganized-verifier">Reorganized verifier</h3>

<p>Pick random $\gamma_i$’s:</p>

<p>\begin{align}
&amp;\forall i\in[n],\ \sum_{j=1}^m \left(\sum_{k=1}^\ell e_{i,j,k}\cdot x^k\right) \cdot R_j + D_i \stackrel{?}{=} z_{r,i} \cdot \Gone\Leftrightarrow\\<br />
&amp;\sum_{i\in[n]}\left(\gamma_i \cdot \sum_{j=1}^m \left(\sum_{k=1}^\ell e_{i,j,k}\cdot x^k\right) \cdot R_j + \gamma_i \cdot D_i - (z_{r,i}\gamma_i) \cdot \Gone\right)\stackrel{?}{=} \mathcal{O}\Leftrightarrow\\<br />
&amp;\sum_{j=1}^m \left(\sum_{i\in[n]}\gamma_i\sum_{k=1}^\ell e_{i,j,k}\cdot x^k\right) \cdot R_j + 
\sum_{i\in[n]} \gamma_i \cdot D_i - \left(\sum_{i\in[n]}z_{r,i}\gamma_i\right) \cdot \Gone \stackrel{?}{=} \mathcal{O}
\end{align}</p>

<h4 id="performance-1">Performance</h4>

<ul>
  <li><strong>Prover time:</strong>
    <ul>
      <li>$\ell$ $\mathbb{G}_1$ scalar muls for $B_k$’s</li>
      <li>$\ell$ size-2 $\mathbb{G}_1$ MSMs for $C_k$’s
        <ul>
          <li>To avoid recomputing these, we first check that the $\sigma_k$’s yield in-range $z_{s,k}$’s before computing $B_k$ and $C_k$</li>
        </ul>
      </li>
      <li>$n$ $\mathbb{G}_1$ scalar muls for $D_i$’s</li>
      <li>size-$n$ $\mathbb{G}_1$ MSM for $Y$</li>
    </ul>
  </li>
  <li><strong>Slightly-optimized verifier time:</strong>
    <ul>
      <li>(ignored: $m$ multiplications modulo $p$, for computing $x,\ldots,x^\ell$)</li>
      <li>size-$(m+n+1)$ $\mathbb{G}_1$ MSMs for checking the $D_i$’s ($\pi_C$); via <a href="#reorganized-verifier">reorganized verifier</a></li>
      <li>size-$(\ell+2)$ $\mathbb{G}_1$ MSM for checking $D_0$ ($\pi_C$)
        <ul>
          <li><strong>Note</strong>: batch with previous one, no, since scalars are big?</li>
        </ul>
      </li>
      <li>$\ell=32$ size-$nm$ $\mathbb{G}_1$ MSMs, but with small scalars $e_{i,j,k} \le E = 2^8$ (for each MSM of $C_{i,j}$’s with $e_{i,j,k}$’s)</li>
      <li>a size-$\ell$ $\mathbb{G}_1$ MSM for linear combination of $C_{i,j}$ MSMs with $x^k$</li>
      <li>size-$(\ell+1+n+2)=(\ell+n+3)$ $\mathbb{G}_1$ MSM for the remaining of the last (third) verifier equation: i.e., scalar muls on $C_k$’s, $Y$, $\ek_i$’s, $\ek_0$ and $\Gone$
        <ul>
          <li><strong>Note</strong>: can batch with previous ones, no, since scalars are big?</li>
        </ul>
      </li>
    </ul>
  </li>
  <li><strong>Optimized verifier time:</strong>
    <ul>
      <li>size-$[(m+n+1) + (\ell+2) + (\ell) + (\ell+n+3)]=(m+2n+3\ell+5)$ $\mathbb{G}_1$ MSM for $\pi_C$ (batched checks)</li>
    </ul>
  </li>
</ul>

<p class="info">Three scalars share the same $\Gone$ base, so we could subtract 2 from this formula (but it won’t make a difference in practice).</p>

<ul>
  <li><strong>Proof size</strong> (according to Groth<sup id="fnref:Grot21e:1"><a href="#fn:Grot21e" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>):
    <ul>
      <li>$\ell$ small integers in $[0, Z)$ (the $z_{s,k}$’s)</li>
      <li>$n+1$ field elements in $\Zp$ (the $z_{r,i}$’s and $z_\beta$)</li>
      <li>$2\ell+n+3$ group elements in $\mathbb{G}_1$
        <ul>
          <li>$2\ell$ for $B_k$’s and $C_k$’s</li>
          <li>$n+1$ for $D_i$’s</li>
          <li>$\ek_0$ and $Y$</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="groth21-pvss-without-forward-secure-encryption">Groth21 PVSS without forward-secure encryption</h2>

<h3 id="algorithms-2">Algorithms</h3>

<p>Parameters: (1) $\emph{\lambda}$ is the security parameter, typically set to 128 (in Groth<sup id="fnref:Grot21e:2"><a href="#fn:Grot21e" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, it is the size of a field element and set to 256); (2) the parameter $\emph{\ell}$ should be picked to optimize performance.</p>

<p class="todo">Investigate optimal $\ell$ via benchmarking.</p>

<p>$\pvss.\setup(1^\lambda, \emph{B}=2^{\emph{b}},\ell) \rightarrow (\prk,\vk)$:</p>

<ul>
  <li>$\emph{m}\gets 2\lambda/b$ (the # of chunks we split a field element share into)</li>
  <li>$\emph{E}\gets 2^{\lceil \lambda/\ell\rceil}$ (the probability of ciphertext $C_{i,j}$ falling outside the range $=E^{-\ell} = 2^{-\lambda}$)</li>
  <li>$\emph{S}=nm(B-1)(E-1)$ (a parameter that arises in the <a href="#correct-chunking-zkp">proof of correct chunking</a>)</li>
  <li>$\emph{Z}=2\ell S$ (when decrypting shares, the worst case range to compute DLs in will be $[1-Z, Z-1]$)</li>
</ul>

<p>$\pvss.\deal(t, n, {\ek_1,\ldots,\ek_n}, a_0) \rightarrow \trx$:</p>

<ul>
  <li>$(a_1,\ldots,a_{t-1}) \randget \Zp^{t-1}$</li>
  <li>$(A_0,\ldots,A_{t-1})\gets(a_0 \cdot \Gtwo,\ldots,a_{t-1} \cdot \Gtwo)$</li>
  <li>$s_i \gets p(i), \forall i\in[n]$, where $p(X) \gets \sum_{i=0}^{t-1} a_i X^i$</li>
  <li>$(r_1,\ldots,r_m)\randget\Zp^m$</li>
  <li>$(R_1,\ldots,R_m)\gets(r_1 \cdot \Gone,\ldots,r_m \cdot \Gone)$</li>
  <li>$\forall i\in[n],j\in[m]$:
    <ul>
      <li>Let $s_{i,j}\in[0,B)$ denote the decomposition of $s_i = \sum_{j=1}^{m} s_{i,j}B^{j-1}$</li>
      <li>$C_{i,j}\gets r_j \cdot \ek_i + s_{i,j} \cdot \Gone$</li>
    </ul>
  </li>
  <li>$r\gets \sum_{j=1}^{m} r_{j}B^{j-1}$ (<strong>note:</strong> this will be the shared ElGamal randomness that encrypts all $s_i$’s)</li>
  <li>$R\gets r \cdot \Gone$</li>
  <li>$\forall i\in[n]$:
    <ul>
      <li>$C_i\gets r \cdot \ek_i + s_i \cdot \Gone$</li>
    </ul>
  </li>
</ul>

<p class="todo">Isn’t it faster for the verifier to reconstruct $C_i$’s from $C_{i,j}$’s?</p>

<ul>
  <li>$\pi_S\gets \ProveSh
\begin{pmatrix}
\ek_1,\ldots,\ek_n,
A_0,\ldots,A_{t-1},
C_1,\ldots,C_n,R;\<br />
s_1,\ldots,s_n,r
\end{pmatrix}$</li>
  <li>$\pi_C\gets \ProveChunk
\begin{pmatrix}
\ek_1,\ldots,\ek_n,
R_1,\ldots,R_m,
C_{1,1}\ldots,C_{n,m};\<br />
r_1,\ldots,r_m,
s_{1,1},\ldots,s_{n,m}
\end{pmatrix}$</li>
  <li>$\trx\gets \begin{pmatrix}
(C_{1,1},\ldots,C_{n,m}),
(R_1,\ldots,R_m),
(A_0,\ldots,A_{t-1}),\<br />
\pi_S,\pi_C
\end{pmatrix}$</li>
</ul>

<p>$\pvss.\verify(\trx, t, n, {\ek_1,\ldots,\ek_n}) \rightarrow \{0,1\}$:</p>

<ul>
  <li>Parse $\trx$ (as per $\pvss.\deal$)</li>
  <li>$\forall i\in[n], C_i \gets \sum_{j=1}^m B^{j-1} \cdot C_{i,j}$</li>
  <li>$R\gets \sum_{j=1}^{m} B^{j-1} \cdot R_{j}$</li>
  <li>$\VerSh
\begin{pmatrix}
\ek_1,\ldots,\ek_n,
A_0,\ldots,A_{t-1},
C_1,\ldots,C_n,R;\pi_S
\end{pmatrix}$</li>
  <li>$\VerChunk
\begin{pmatrix}
\ek_1,\ldots,\ek_n,
R_1,\ldots,R_m,
C_{1,1}\ldots,C_{n,m};\pi_C
\end{pmatrix}$</li>
  <li><strong>return</strong> 1</li>
</ul>

<p>$\pvss.\decrypt(\trx, t, n, i,\dk_i) \rightarrow s_i$:</p>

<ul>
  <li>Parse $\trx$ (as per $\pvss.\deal$)</li>
  <li><strong>for</strong> $j\in[m]$:
    <ul>
      <li>$h_{i,j}\gets C_{i,j} - \dk_i \cdot R_j$</li>
      <li><strong>for</strong> $\Delta_{i,j}\in [1,E-1]$:
        <ul>
          <li>try to compute DL on $\Delta_{i,j}^{-1} \cdot h_{i,j}$ (should be equal to $s_{i,j} \cdot \Gone$)</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p class="todo">If $\Delta_{i,j}\cdot s_{i,j}\in [1-Z,Z-1]$ then the range for the DL computation on $\Delta_{i,j}^{-1} \cdot h_{i,j}$ will be getting smaller and smaller as we increase $\Delta_{i,j}$, no?</p>

<h3 id="asymptotic-performance">Asymptotic performance</h3>

<p class="warning"><strong>Note:</strong> These are for the <strong>non-forward-secure</strong> PVSS variant, to fairly compare to our (future) PVSS construction.</p>

<h4 id="transcript-size">Transcript size</h4>

<ul>
  <li>$\ell$ small integers in $[0,Z)$ (from $\pi_C$)</li>
  <li>$2 + (n+1)\ \sizeof{\Zp}$ (from $\pi_S$ and from $\pi_C$)</li>
  <li>$(nm+m)+ 2 + (2\ell+n+3)\ \sizeof{\mathbb{G}_1}$ (from the ElGamal ciphertexts, from $\pi_S$, and from $\pi_C$)</li>
  <li>$t+1$ $\sizeof{\mathbb{G}_2}$ (from Feldman commitment and from $\pi_S$)</li>
</ul>

<p class="info">The ZKP overheads for $\pi_S$ are in the <a href="#correct-sharing-zkp">correct sharing section</a> and for $\pi_C$ in the <a href="#correct-chunking-zkp">correct chunking section</a>.</p>

<p class="todo">Add sizes for $n=1{,}000$ and $m=10$ (assuming right parameterization).</p>

<h4 id="prover-time">Prover time</h4>

<p>In $\Zp$:</p>

<ol>
  <li>degree $m-1$ poly eval on point $B$ (for computing $r$)</li>
  <li>degree-$n$ poly eval at a random $\Zp$ point (for $\pi_S$)</li>
</ol>

<p>In $\mathbb{G}_1$:</p>

<ol>
  <li>$m+1$ <strong>scalar muls</strong> (from chunked ElGamal $R_j$’s and $R$)</li>
  <li>$nm$ size-2 <strong>MSMs</strong> (where 1 scalar is $&lt;B$) (from chunked ElGamal $C_{i,j}$’s)</li>
  <li>$n$ size-2 <strong>MSMs</strong> (from ElGamal $C_i$’s)</li>
  <li>size-$(n+1)$ <strong>MSM</strong> (for $\pi_S$)</li>
  <li>1 <strong>scalar mul</strong> (for $\pi_S$)</li>
  <li>$\ell+n$ <strong>scalar muls</strong> (for $\pi_C$)</li>
  <li>size-$n$ <strong>MSM</strong> (for $\pi_C$)</li>
  <li>$\ell$ size-2 $\mathbb{G}_1$ <strong>MSMs</strong> (for $\pi_C$)</li>
</ol>

<p>In $\mathbb{G}_2$:</p>

<ol>
  <li>$t$ <strong>scalar muls</strong> (from Feldman commitment)</li>
  <li>1 <strong>scalar mul</strong> (for $\pi_S$)</li>
</ol>

<h4 id="verifier-time">Verifier time</h4>

<p><strong>In $\mathbb{G}_1$:</strong></p>

<ul>
  <li>$n+1$ size-$m$ <strong>MSMs</strong> (where scalars are $B^0\ldots B^{m-1}=2^{2\lambda}$, so first ones are small) for recomputing the $n$ ElGamal $C_i$’s and $R$</li>
</ul>

<p><strong>In $\mathbb{G}_2$:</strong></p>

<p>(See the <a href="#correct-sharing-zkp">correct sharing ZKP performance section</a>.)</p>

<h4 id="share-decryption-time">Share decryption time</h4>

<p><strong><em>Chunk</em> decryption</strong> could involve computing <strong>at most</strong> $\emph{E}-1$ DLs in a range of size $2\emph{Z}-1$.</p>

<p><strong>Share decryption</strong> involves $\sizeof{\F}/\log_2{\emph{B}}$ chunk decryptions.</p>

<p><strong>Honest-case $\mathbb{G}_1$:</strong></p>

<ol>
  <li>$\sizeof{\F}/\log_2{B} \times \sqrt{B}$ group ops</li>
</ol>

<p><strong>Malicious case $\mathbb{G}_1$:</strong></p>

<ol>
  <li>$\sizeof{\F}/\log_2{B} \times (E-1) \times \sqrt{2Z-1}$ group ops</li>
</ol>

<p class="todo">It will not get this bad because the range of the DLs will get smaller as $\Delta_{i,j}$ increases. Plus, it’s unclear how many chunks in a share a malicious dealer will be able to “inflate” in size.</p>

<h2 id="implementations">Implementations</h2>

<ul>
  <li>Sourav’s implementation: <a href="https://github.com/sourav1547/e2e-vss">e2e-vss on GitHub</a></li>
  <li>DFINITY’s implementation: <a href="https://github.com/dfinity/ic/tree/c9879cb1ad485accd438d5560748a0d0ddcba83f/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg">ni_dkg on GitHub</a></li>
</ul>

<h3 id="dfinity-parameterization">DFINITY’s PVSS parameterization (${\lambda},{\ell}, {E}, {B}, {S}, {Z}$)</h3>

<ul>
  <li>$\emph{\lambda}$ = <code class="language-plaintext highlighter-rouge">SECURITY_LEVEL</code> = 256 (<a href="https://github.com/dfinity/ic/blob/714c85c6a4245fb5b39e76f5c8003e6d90e49c4d/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_chunking.rs#L18">source</a>)</li>
  <li>$\emph{\ell}$ = <code class="language-plaintext highlighter-rouge">NUM_ZK_REPETITIONS</code> = 32 (<a href="https://github.com/dfinity/ic/blob/714c85c6a4245fb5b39e76f5c8003e6d90e49c4d/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_chunking.rs#L24">source</a>)</li>
  <li>$\log_2{\emph{E}}$ = <code class="language-plaintext highlighter-rouge">CHALLENGE_BITS = SECURITY_LEVEL / NUM_ZK_REPETITIONS</code> $= 8$ (<a href="https://github.com/dfinity/ic/blob/714c85c6a4245fb5b39e76f5c8003e6d90e49c4d/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_chunking.rs#L27">source</a>)
    <ul>
      <li>$E=2^8=256$</li>
      <li><code class="language-plaintext highlighter-rouge">CHALLENGE_MASK</code> $= E-1= 255$ (<a href="https://github.com/dfinity/ic/blob/714c85c6a4245fb5b39e76f5c8003e6d90e49c4d/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_chunking.rs#L34">source</a>)</li>
    </ul>
  </li>
  <li>$\emph{B}$ = <code class="language-plaintext highlighter-rouge">CHUNK_SIZE</code> $= 2^{16}$ (<a href="https://github.com/dfinity/ic/blob/714c85c6a4245fb5b39e76f5c8003e6d90e49c4d/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/chunking.rs#L11">source</a>; i.e., the cardinality of the range)
    <ul>
      <li>number of chunks $\emph{m}=\sizeof{\F} / \log_2{B} = 256/16 = 16$</li>
      <li><code class="language-plaintext highlighter-rouge">CHUNK_BYTES</code> = 2 bytes = 16 bits</li>
      <li><code class="language-plaintext highlighter-rouge">CHUNK_MIN = 0</code></li>
      <li><code class="language-plaintext highlighter-rouge">CHUNK_MAX</code> $= 2^{\mathsf{CHUNK_SIZE}} - 1$</li>
    </ul>
  </li>
  <li>$\emph{S}$ = <code class="language-plaintext highlighter-rouge">ss</code> = <code class="language-plaintext highlighter-rouge">n * m * (CHUNK_SIZE - 1) * CHALLENGE_MASK</code> $= nm(B-1)(E-1)$ (<a href="https://github.com/dfinity/ic/blob/714c85c6a4245fb5b39e76f5c8003e6d90e49c4d/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_chunking.rs#L210">source</a>)
    <ul>
      <li>e.g., for $n=100$ and $m=256/16=16$, we’d get $S=100\cdot 16(2^{16}-1)\cdot 255$ = 26,738,280,000 (a 35-bit number)</li>
    </ul>
  </li>
  <li>$\emph{Z}$ = <code class="language-plaintext highlighter-rouge">zz</code> $= 2\ell S$ (<a href="https://github.com/dfinity/ic/blob/714c85c6a4245fb5b39e76f5c8003e6d90e49c4d/rs/crypto/internal/crypto_lib/threshold_sig/bls12_381/src/ni_dkg/fs_ni_dkg/nizk_chunking.rs#L211">source</a>)
    <ul>
      <li>e.g., for the same $n,m$ as above, this is $Z=$ 1,711,249,920,000 (a 41-bit number)</li>
    </ul>
  </li>
</ul>

<p><strong>Implications on computing DLs:</strong></p>

<p>Due to the approximate guarantees of the <a href="#correct-chunking-zkp">proof of correct chunking</a>, <strong><em>chunk</em> decryption</strong> could involve computing <strong>at most</strong> $E-1$ DLs in a range of size $2Z-1$. So, computing <strong>at most</strong> 255 DLs of 42-bit numbers. Note that <strong>share decryption</strong> involves $256/\log_2(B)=256/16=16$ <em>chunk</em> decryptions.</p>
<ul>
  <li>We’d need to consider <strong>average-case</strong>, <strong>best-case</strong> and <strong>worst-case</strong> share decryption times in our evaluation?
    <ul>
      <li>Even in the <strong>best-case</strong>, will we be better because we’d have fewer chunks than Groth? The answer is <strong>no</strong>!
        <ul>
          <li>Groth uses $B=2^{16}$ and should(?) need to run a 16-bit DL on all 16 chunks: $2^{16/2} \times 0.5$ microsecond $\times$ 16 = 2.05 millisecs</li>
          <li>Say we use a bigger chunk size $B=2^{37}$ and run a 37-bit DL on all 7 chunks: $2^{37/2} \times 0.5$ microsecond $\times$ 7 = 1.30 secs</li>
          <li>So, bigger chunks does <strong>not</strong> give us an advantage (in the <strong>best case</strong>), because the share decryption time when using $B=2^b$ bits per chunk will be $f(b)=2^{b/2} \cdot (256 / b)$.</li>
          <li>We’d clearly want to use <em>smaller</em>, <strong>not</strong> bigger $b$!</li>
          <li>In fact, the optimal chunk size that minimizes the share decryption time $f(b)$ is $b=2/\ln{2}\approx 3$ bits.</li>
          <li>I think this explains why Groth sets his chunk size to 16, which is bigger than the optimal 3: he is accepting higher share decryption time for smaller transcripts and thus faster proving, faster verification.</li>
        </ul>
      </li>
      <li>We’d definitely do better in the worst-case: an adversarial prover could potentially force the share decryption time to be much higher: e.g., 255 runs of 42-bit(?) DL on each of the 16 chunks. (We’d need to understand how bad it can get though… Maybe the prover can only increase some of the encrypted chunks, not all)</li>
    </ul>
  </li>
</ul>

<h3 id="souravs-implementation">Sourav’s implementation</h3>

<p>Sourav used the same parameterization as <a href="#dfinity-parameterization">DFINITY’s above</a>.</p>

<p>These benchmarks were run in 2024, or earlier:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>groth/deal-t=660/n=1024 time: [3.6432 s 3.6925 s 3.7592 s]
groth/verify-t=660/n=1024 time: [2.1861 s 2.2049 s 2.2291 s]
</code></pre></div></div>

<h2 id="appendix">Appendix</h2>

<h3 id="malicious-single-chunk-inflation">Attack: Inflate 1 chunk to $\approx Z/E$</h3>

<p>The soundness of <a href="#correct-chunking-zkp">$\ProveChunk$</a> only guarantees that the extracted chunk $s_{i,j}$ satisfies:</p>

\[\term{\Delta_{i,j}} \cdot s_{i,j} \in [1-Z, Z-1]\,\]

<p>for some $\emph{\Delta_{i,j}}\in[1,E-1]$.</p>

<p>Worst case has $\Delta_{i,j}=1 \Rightarrow$ admits $\sizeof{s_{i,j}}$ up to $Z-1$.</p>

<p>But an efficient adversary cannot reach this worst case.
Below we show a one-shot deterministic attack that installs a single chunk of size $c \approx Z/E$.</p>

<p>The dealer will set $s_{1,1} = \term{c}$, where:</p>

\[\emph{c} = \left\lfloor \frac{Z-1-S}{E-1}\right\rfloor \approx \frac{Z}{E} \approx 2\ell n m B\]

<p>while setting all other $s_{i,j} \in [0, B-1]$. Recall $\emph{S} = nm(B-1)(E-1)$ and $\emph{Z}=2\ell S$.</p>

<h4 id="step-1-pick-polynomial-with-p1--c">Step 1: Pick polynomial with $p(1) = c$</h4>

<p>PVSS dealing can take place in one out of two settings:</p>

<p><strong>Dealer can freely pick the secret $a_0$</strong>:</p>

<ul>
  <li>Sample $a_1, \ldots, a_{t-1}\randget\Zp$</li>
  <li>Solve for $a_0$ such that $c = p(1) \Leftrightarrow c = \sum_{k=0}^{t-1} a_k \cdot 1^k$
    <ul>
      <li>$\Rightarrow a_0 \gets c - \sum_{k=1}^{t-1} a_k$</li>
    </ul>
  </li>
</ul>

<p><strong>Dealer’s secret $a_0$ is fixed; they cannot pick it</strong>:</p>

<ul>
  <li>Requires $t \geq 2$ (so there is at least one free coefficient besides $a_0$).
    <ul>
      <li>$t=1$ is a degenerate threshold.</li>
      <li>In other words, the secret sharing is trivial: everyone has the same share: $a_0$, which makes this case uninteresting.</li>
    </ul>
  </li>
  <li>Sample $a_2, \ldots, a_{t-1}\randget\Zp$</li>
  <li>Solve for $a_1$ such that $c = p(1) \Leftrightarrow c = \sum_{k=0}^{t-1} a_k \cdot 1^k$
    <ul>
      <li>$\Rightarrow a_1 \gets c - \sum_{k\in[0,t-1],\ k\neq 1} a_k$</li>
    </ul>
  </li>
</ul>

<p class="note">$\ProveSh$ only verifies the polynomial-evaluation relation.
It doesn’t care how coefficients were chosen.</p>

<h4 id="step-2-chunk-share-1-such-that-its-1st-chunk-is-oversized">Step 2: Chunk share 1 such that its 1st chunk is oversized</h4>

<p><strong>First</strong>, chunk $s_1 = c$ as $(s_{1,1}, s_{1,2}, \ldots, s_{1,m}) = (c, 0, 0, \ldots, 0)$. 
Mathematically, this is a valid radix-$B$ decomposition:</p>

\[\sum_{j=1}^m s_{1,j} B^{j-1} = c\,\]

<p>except it is not a base-$B$ decomposition, since one of its digits is above $B$.
This is exactly the core of the issue: the approximate range proof cannot enforce $s_{i,j} \in [0, B)$.</p>

<p><strong>Second</strong>, chunk all other $s_i$’s for $i \geq 2$ honestly into $[0, B-1]^m$.</p>

<p class="note">First, the position of the inflated chunk $c$ can also be arbitrary rather than fixed as the 1st chunk.
Second, the zeros in $(c, 0, 0, \ldots, 0)$ are not essential: any values $s_{1,j}\in[0,B-1]$ work for $j\geq 2$.
The bound on $c$ shifts only by $(m-1)(B-1)$, negligible compared to $c\approx Z/E$, so we can replace them with arbitrary random values in $[0, B-1]$ and pin $p(1)$ to the corresponding $s_1 = c + \sum_{j\geq 2} s_{1,j}B^{j-1}$.</p>

<h4 id="step-3-elgamal-encrypt-run-provesh-honestly">Step 3: ElGamal-encrypt, run $\ProveSh$ honestly</h4>

<p>$\ProveSh$ checks only the polynomial-evaluation and scalar-recombination relations, both of which hold by construction.</p>

<h4 id="step-4-run-provechunk-with-sigma_k--0">Step 4: Run $\ProveChunk$ with $\sigma_k = 0$</h4>

<p><a href="#verchunk">$\VerChunk$</a> checks:</p>
<ol>
  <li>$z_{s,k} \in [0, Z-1]$ for all $k$</li>
  <li>three algebraic identities over $\mathbb{G}_1$ relating $B_k, C_k, D_i, z_{r,i}, z_{s,k}, z_\beta, Y$.</li>
</ol>

<p><strong>Critically: there is no check on $\sigma_k$.</strong>
The verifier sees $\sigma_k$ only through the commitment $C_k = \beta_k \cdot ek_0 + \sigma_k \cdot G_1$, and the algebraic identities will continue to hold for any $\sigma_k$ the prover picks.
The honest prover picks $\sigma_k \randget [-S, Z-1]$ for <strong>zero-knowledge</strong> (to statistically hide $s_{i,j}$’s contribution to $z_{s,k}$).
A malicious dealer may have no interest in preserving ZK.</p>

<p><strong>The dealer will pick $\sigma_k = 0$ for all $k$.</strong> With this choice, $z_{s,k} = \sum_{i,j} e_{i,j,k}\, s_{i,j} + 0 = \term{V_k}$, where</p>

\[\emph{V_k} \triangleq e_{1,1,k} \cdot c + \term{T_k},\qquad \emph{T_k} \triangleq \sum_{(i,j)\neq(1,1)} e_{i,j,k}\, s_{i,j} \in [0, S]\]

<p>Upper bound on $V_k$:</p>

\[V_k \leq (E-1)\cdot c + S\]

<p>By our choice of $c$:</p>

\[V_k \leq (E-1)\cdot \frac{Z-1-S}{E-1} + S = Z - 1\]

<p>Lower bound $V_k \geq 0$ holds trivially. So $z_{s,k} = V_k \in [0, Z-1]$ <strong>always</strong>, regardless of FS challenges. The rejection check in $\ProveChunk$ passes on the first try, every time.
No grinding, no probability analysis.</p>

<!-- NOTE: There must be more sophisticated attacks than this that target multiple chunks, rather than 1

#### Can we push $c$ beyond $Z/E$?

No. For **any** prover strategy (including adaptive choice of $\sigma_k$ and arbitrary grinding), fix all FS inputs except $e_{1,1,k}$. The rejection check translates to $e_{1,1,k}\cdot c$ landing in a width-$Z-1$ interval, which contains at most $\lceil Z/c\rceil$ multiples of $c$ (and at most $E$, since $e_{1,1,k}\in[0,E-1]$). So for any strategy:

$$\Pr[\text{run } k \text{ accepts}] \leq \frac{\min(\lceil Z/c\rceil,\ E)}{E}$$

- **$c \leq Z/E$:** $\lceil Z/c\rceil \geq E$, bound $= E/E = 1$. Our $\sigma_k=0$ attack **realizes** it deterministically.
- **$c > Z/E$:** bound $< 1$, and $\ell$ runs compound as $(\lceil Z/c\rceil/E)^\ell$. At $c=Z$ this is $E^{-\ell} = 2^{-\lambda}$ — the soundness floor.

So $c\approx Z/E$ is simultaneously what our attack achieves and the provable PPT ceiling. The two statements are not in tension — they are the same statement viewed from two sides.

-->

<h4 id="impact-on-share-decryption">Impact on share decryption</h4>

<p>For DFINITY’s parameters ($n=40$, $m=16$, $B=2^{16}$, $E=256$, $\ell=32$, $\lambda=256$):</p>

<ul>
  <li>$S \approx 2^{33.3}$, $Z \approx 2^{39.3}$, $Z/E \approx 2^{31.3}$.</li>
  <li>Honest chunk: $B-1 \approx 2^{16}$</li>
  <li>Malicious chunk from this attack: $c\approx 2^{31}$</li>
  <li>See more cases <a href="/chunky#but-does-the-worst-case-ever-materialize">here</a> for different chunk sizes $B$ and # of players $n$</li>
</ul>

<h4 id="implementation">Implementation</h4>

<p>A working Rust implementation of this attack against the DFINITY parameterization is in <a href="https://github.com/alinush/groth21-rs"><code class="language-plaintext highlighter-rouge">alinush/groth21-rs</code></a> under <code class="language-plaintext highlighter-rouge">src/groth21/malicious.rs</code>, with a test <code class="language-plaintext highlighter-rouge">malicious_deal_passes_verification</code> confirming the transcript clears the Groth21 verifier.</p>

<h3 id="malicious-all-chunk-inflation">Attack: Inflate $m$ chunks to $\approx 2\ell nB$</h3>

<p>Same $\sigma_k = 0$ machinery, same pinned-$p(1)$ polynomial trick.
We only spread the chunk-mass budget across all $m$ chunks of share 1 instead of concentrating it in one.</p>

<p><strong>Setup.</strong> Pick a new per-chunk magnitude $c$, to be bounded below
This is <strong>not</strong> the $c \approx Z/E$ from the single-chunk attack above.
Set $(s_{1,1}, \ldots, s_{1,m}) = (c, c, \ldots, c)$.
Chunk the other shares $i\in \{2,\ldots,n\}$ honestly.
Pin $p(1) \equiv c\cdot\frac{B^m-1}{B-1} \pmod p$ by solving for $a_1$, <a href="#step-1-pick-polynomial-with-p1--c">as described before</a>.</p>

<p><strong>Bound on $c$.</strong> With $\sigma_k = 0$, honest chunks on shares $2,\ldots,n$, and all $m$ chunks of share 1 at $c$, redefine</p>

\[\term{V_k} \triangleq \underbrace{\sum_{j=1}^m e_{1,j,k}\cdot c}_{\text{share 1 (all inflated)}} + \underbrace{\sum_{i=2}^n\sum_{j=1}^m e_{i,j,k}\cdot s_{i,j}}_{\text{honest shares}\,\in\, [0,\,(n-1)m(B-1)(E-1)]}\]

<p>Assume worst case FS challenges, then solve for $c$ (recall from the <a href="#notation">preliminaries</a> that $\emph{Z} = 2\ell nm(B-1)(E-1)$):</p>

<p>\begin{align}
V_k \leq (E-1)\bigl[mc + (n-1)m(B-1)\bigr] &amp;\leq Z-1 \Leftrightarrow\\<br />
(E-1)mc + (E-1)(n-1)m(B-1) &amp;\leq Z-1 \Leftrightarrow\\<br />
(E-1)mc &amp;\leq Z-1 - (E-1)(n-1)m(B-1) \Leftrightarrow\\<br />
c &amp;\leq \frac{Z-1 - (E-1)(n-1)m(B-1)}{(E-1)m}\\<br />
&amp;= \frac{Z-1}{m(E-1)} - (n-1)(B-1)\\<br />
&amp;= \frac{2\ell nm(B-1)(E-1) - 1}{m(E-1)} - (n-1)(B-1)\\<br />
&amp;\leq \frac{2\ell nm(B-1)(E-1)}{m(E-1)} - (n-1)(B-1)\\<br />
&amp;= 2\ell n(B-1) - (n-1)(B-1)\\<br />
&amp;= (B-1)\bigl[2\ell n - (n-1)\bigr]\\<br />
&amp;= (B-1)\bigl[n(2\ell-1) + 1\bigr]\\<br />
&amp;\leq 2\ell n B
\end{align}</p>

<p><strong>Cost comparison.</strong> Recall that batched BSGS can solve $k$ DLs each in a range of size $\leq R$ in time $\sqrt{k \cdot R}$. 
Applying this to share 1’s $m$ chunks:</p>

<ul>
  <li><strong>Honest dealer:</strong> $m$ DLs of size $B \rightarrow \sqrt{mB}$ time via batched BSGS</li>
  <li><a href="#malicious-single-chunk-inflation">Single-chunk attack</a>: $m-1$ DLs of size $B$ and 1 DL of size $Z/E\approx 2\ell n m B$
    <ul>
      <li><em>Smartest algorithm:</em> $\sqrt{mB}$ time for a batched BSGS, which actually identifies the inflated chunk. Then, do a BSGS in time $\sqrt{2\ell n m B}$ for the inflated chunk. This last one dominates.</li>
    </ul>
  </li>
  <li><a href="#malicious-all-chunk-inflation">All-$m$-chunks attack</a> (this section): $m$ DLs of size $2\ell nB$
    <ul>
      <li>Cost: $\sqrt{m \cdot 2\ell nB} = \sqrt{2\ell n m B}$ via batched BSGS. Same as the single chunk attack. Slightly cheaper actually.</li>
      <li>The problem is the verifier doesn’t know which attack took place. So he will often incur the worst-case of the single-chunk attack if he is smart and tries to guess the attack.</li>
    </ul>
  </li>
</ul>

<h4 id="implementation-1">Implementation</h4>

<p>This variant is implemented in <a href="https://github.com/alinush/groth21-rs"><code class="language-plaintext highlighter-rouge">alinush/groth21-rs</code></a> as <code class="language-plaintext highlighter-rouge">malicious_deal_full_share</code> in <code class="language-plaintext highlighter-rouge">src/groth21/malicious.rs</code>, with a test <code class="language-plaintext highlighter-rouge">malicious_deal_full_share_passes_verification</code> confirming the transcript clears the Groth21 verifier.</p>

<h2 id="references">References</h2>

<p>For cited works, see below 👇👇</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:Grot21e">
      <p><strong>Non-interactive distributed key generation and key resharing</strong>, by Jens Groth, <em>in Cryptology ePrint Archive, Report 2021/339</em>, 2021, <a href="https://eprint.iacr.org/2021/339">[URL]</a> <a href="#fnref:Grot21e" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:Grot21e:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a> <a href="#fnref:Grot21e:2" class="reversefootnote" role="doc-backlink">&#8617;<sup>3</sup></a></p>
    </li>
  </ol>
</div>]]></content><author><name>Alin Tomescu</name></author><category term="PVSS" /><category term="zero-knowledge proofs (ZKPs)" /><category term="range proofs" /><category term="ElGamal" /><category term="sigma protocols" /><category term="distributed key generation (DKG)" /><summary type="html"><![CDATA[tl;dr: Groth’s non-interactive distributed key generation paper[^Grot21e], which uses a novel approximate ZK range proofs to argue correct chunking, but inadvertantly increases share decryption time.]]></summary></entry><entry><title type="html">Aptos Move</title><link href="https://alinush.github.io//move" rel="alternate" type="text/html" title="Aptos Move" /><published>2026-04-14T00:00:00+00:00</published><updated>2026-04-14T00:00:00+00:00</updated><id>https://alinush.github.io//aptos-move</id><content type="html" xml:base="https://alinush.github.io//move"><![CDATA[<p class="info"><strong>tl;dr:</strong> a few notes on Move (and maybe on Aptos too).
<!--more--></p>

<!-- Here you can define LaTeX macros -->
<div style="display: none;">$
$</div>
<p><!-- $ --></p>

<h2 id="misc">Misc</h2>

<ul>
  <li><a href="https://github.com/gregnazario/aptos-move-tools/tree/main/move1-to-move2">Move 1 to Move 2</a>, by Greg Nazario</li>
</ul>

<h2 id="keyless-on-chain-configs">Keyless on-chain configs</h2>

<p class="note">Learn more about <a href="/keyless">Keyless here</a>.</p>

<h3 id="0x1keyless_accountgroth16verificationkey">0x1::keyless_account::Groth16VerificationKey</h3>

<ul>
  <li><a href="https://fullnode.devnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::keyless_account::Groth16VerificationKey"><code class="language-plaintext highlighter-rouge">devnet</code> VK</a></li>
  <li><a href="https://fullnode.testnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::keyless_account::Groth16VerificationKey"><code class="language-plaintext highlighter-rouge">testnet</code> VK</a></li>
  <li><a href="https://fullnode.mainnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::keyless_account::Groth16VerificationKey"><code class="language-plaintext highlighter-rouge">mainnet</code> VK</a></li>
</ul>

<h3 id="0x1keyless_accountconfiguration">0x1::keyless_account::Configuration</h3>

<ul>
  <li><a href="https://fullnode.devnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::keyless_account::Configuration"><code class="language-plaintext highlighter-rouge">devnet</code> configuration</a></li>
  <li><a href="https://fullnode.testnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::keyless_account::Configuration"><code class="language-plaintext highlighter-rouge">testnet</code> configuration</a></li>
  <li><a href="https://fullnode.mainnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::keyless_account::Configuration"><code class="language-plaintext highlighter-rouge">mainnet</code> configuration</a></li>
</ul>

<h3 id="0x1jwk_consensus_configjwkconsensusconfig">0x1::jwk_consensus_config::JWKConsensusConfig</h3>

<ul>
  <li><a href="https://fullnode.devnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::jwks::SupportedOIDCProviders"><code class="language-plaintext highlighter-rouge">devnet</code> providers</a></li>
  <li><a href="https://fullnode.testnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::jwks::SupportedOIDCProviders"><code class="language-plaintext highlighter-rouge">testnet</code> providers</a></li>
  <li><a href="https://fullnode.mainnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::jwks::SupportedOIDCProviders"><code class="language-plaintext highlighter-rouge">mainnet</code> providers</a></li>
</ul>

<p>You can <code class="language-plaintext highlighter-rouge">echo</code> the hex bytes (without the <code class="language-plaintext highlighter-rouge">0x</code> prefix) through <code class="language-plaintext highlighter-rouge">xxd -r -p</code> to do a best-effort string decoding to see what’s there. e.g.,:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo </span>68747470733a2f2f6163636f756e74732e676f6f676c652e636f6d2f2e77656c6c2d6b6e6f776e2f6f70656e69642d636f6e66696775726174696f6e | xxd <span class="nt">-r</span> <span class="nt">-p</span>
</code></pre></div></div>

<p>This will output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://accounts.google.com/.well-known/openid-configuration
</code></pre></div></div>

<p class="smallnote">In the past, there was an attempt to migrate from <code class="language-plaintext highlighter-rouge">0x1::jwks::SupportedOIDCProviders</code> to <code class="language-plaintext highlighter-rouge">0x1::jwk_consensus_config::JWKConsensusConfig</code> but we nixed it.</p>

<h3 id="0x1jwkspatchedjwks">0x1::jwks::PatchedJWKs</h3>

<ul>
  <li><a href="https://fullnode.devnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::jwks::PatchedJWKs"><code class="language-plaintext highlighter-rouge">devnet</code> JWKs</a></li>
  <li><a href="https://fullnode.testnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::jwks::PatchedJWKs"><code class="language-plaintext highlighter-rouge">testnet</code> JWKs</a></li>
  <li><a href="https://fullnode.mainnet.aptoslabs.com/v1/accounts/0x1/resource/0x1::jwks::PatchedJWKs"><code class="language-plaintext highlighter-rouge">mainnet</code> JWKs</a></li>
</ul>

<h2 id="references">References</h2>

<p>For cited works, see below 👇👇</p>]]></content><author><name>Alin Tomescu</name></author><category term="Move" /><summary type="html"><![CDATA[tl;dr: a few notes on Move (and maybe on Aptos too).]]></summary></entry><entry><title type="html">TIL: Malleable algebraic NIZKs</title><link href="https://alinush.github.io//malleable-nizk" rel="alternate" type="text/html" title="TIL: Malleable algebraic NIZKs" /><published>2026-04-05T00:00:00+00:00</published><updated>2026-04-05T00:00:00+00:00</updated><id>https://alinush.github.io//til-malleable-algebraic-nizks</id><content type="html" xml:base="https://alinush.github.io//malleable-nizk"><![CDATA[<p class="info"><strong>tl;dr:</strong> This is a “note to self” that there’s some interesting work out there on malleable NIZKs<sup id="fnref:CH20"><a href="#fn:CH20" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>$^,$<sup id="fnref:DaEFplus23e"><a href="#fn:DaEFplus23e" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>.</p>

<!--more-->

<!-- Here you can define LaTeX macros -->
<div style="display: none;">$
$</div>
<p><!-- $ --></p>

<h2 id="references">References</h2>

<p>For cited works, see below 👇👇</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:CH20">
      <p><strong>Shorter Non-Interactive Zero-Knowledge Arguments and ZAPs for Algebraic Languages</strong>, by Geoffroy Couteau and Dominik Hartmann, <em>in Cryptology ePrint Archive, Report 2020/286</em>, 2020, <a href="https://eprint.iacr.org/2020/286">[URL]</a> <a href="#fnref:CH20" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:DaEFplus23e">
      <p><strong>Updatable Privacy-Preserving Blueprints</strong>, by Bernardo David and Felix Engelmann and Tore Frederiksen and Markulf Kohlweiss and Elena Pagnin and Mikhail Volkhov, <em>in Cryptology {ePrint} Archive, Paper 2023/1787</em>, 2023, <a href="https://eprint.iacr.org/2023/1787">[URL]</a> <a href="#fnref:DaEFplus23e" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Alin Tomescu</name></author><category term="zero-knowledge proofs (ZKPs)" /><summary type="html"><![CDATA[tl;dr: This is a “note to self” that there’s some interesting work out there on malleable NIZKs[^CH20]$^,$[^DaEFplus23e].]]></summary></entry><entry><title type="html">Witness encryption (WE)</title><link href="https://alinush.github.io//we" rel="alternate" type="text/html" title="Witness encryption (WE)" /><published>2026-04-04T00:00:00+00:00</published><updated>2026-04-04T00:00:00+00:00</updated><id>https://alinush.github.io//witness-encryption-we</id><content type="html" xml:base="https://alinush.github.io//we"><![CDATA[<p class="info"><strong>tl;dr:</strong> Some notes to self on state-of-the-art witness encryption (WE) schemes.</p>

<!-- Here you can define LaTeX macros -->
<div style="display: none;">$
\def\adp{\mathsf{ADP}}
\def\aadp{\mathsf{AADP}}
\def\eval{\mathsf{eval}}
\def\x{\mathbf{x}}
\def\M{\mathsf{M}}
\def\A{\mathbf{A}}
\def\B{\mathbf{B}}
\def\R{\mathbf{R}}
\def\Rvss{\mathcal{R}_\mathsf{vss}}
\def\Radp#1{\mathcal{R}_\mathsf{adp}^{#1}}
\def\span{\mathsf{span}}
\def\Fbar{\bar{\F}}
$</div>
<p><!-- $ --></p>

<!-- Here you can define LaTeX macros -->
<div style="display: none;">$
\def\stmt{\mathbf{x}}
\def\witn{\mathbf{w}}
%
\def\td{\mathsf{td}}
%
\def\zkpSetup{\mathsf{ZKP}.\mathsf{Setup}}
\def\zkpProve{\mathsf{ZKP}.\mathsf{Prove}}
\def\zkpVerify{\mathsf{ZKP}.\mathsf{Verify}}
\def\zkpSim{\mathsf{ZKP}.\mathsf{Sim}}
$</div>
<p><!-- $ --></p>

<!--more-->

<h2 id="related-work">Related work</h2>

<p>WE is implied by <strong>indistinguishability-obfuscation (iO)</strong> but it is weaker than iO<sup id="fnref:GMM17e"><a href="#fn:GMM17e" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>.</p>

<h3 id="affine-determinant-programs-adps">Affine determinant programs (ADPs)</h3>

<p>Bartusek et al.<sup id="fnref:BIJplus20e"><a href="#fn:BIJplus20e" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> introduced <strong>affine determinant programs (ADPs)</strong> as key building block for WE and for iO.</p>

<p>An ADP is a function:</p>

\[\term{\adp} : \{0,1\}^{\term{n}} \to \{0,1\}\]

<p>specified by a tuple $(\term{\A}, \term{\B_1, \ldots, \B_n})$ of $\term{k}\times k$ matrices over $\term{\Fq}$ and a function $\term{\eval}: \Fq \to \{0,1\}$. Specifically:
\begin{align}
\adp(\x) = \eval\left(\det{\left(\A + \sum_{i\in[n]} x_i \B_i\right)}\right)
\end{align}
where $\term{\x} \bydef (\term{x_1},\ldots,\term{x_n})$
An affine function $\M$ can be defined as:
\begin{align}
\M(\x) \bydef \A + \sum_{i\in[n]} x_i \B_i
\end{align}
so that the ADP can be more simply viewed as:
\begin{align}
\adp(\x) = \eval\left(\det{\M(\x)}\right)
\end{align}</p>

<p class="note">$x_i\B_i$ is defined as the all zeros matrix when $x_i = 0$ and as $\B_i$ when $x_i = 1$.</p>

<p>Viewed as an NP relation, ADP satisfiability becomes:
\begin{align}
\term{\Radp{\eval}}\begin{pmatrix}{}
	\A, \B_1,\ldots,\B_n \in \Fq^{k\times k} \textbf{;}\\<br />
	\x
\end{pmatrix} = 1\Leftrightarrow 
	{ \x \in \{0,1\}^n }
    \wedge
    { 1 = \eval\left(\det{\left(\A + \sum_{i\in[n]} x_i \B_i\right)}\right) }
\end{align}</p>

<p class="warning">The $x\in\{0,1\}^n$ check is actually also implemented as an ADP, but I am not showing it there for simplicity.</p>

<p class="note">The ADP relation is <strong>parameterized</strong> by an $\eval$ function! Typically, the $\eval$ function may test whether its input is zero, or whether it has the right parity.
e.g.:
\begin{align}
\term{\eval_0(y)} = \begin{cases}
    1,&amp; y \equals 0\\<br />
    0,&amp; \text{otherwise}
\end{cases}
\end{align}</p>

<h3 id="encoding-a-dimension-d1-vss-as-an-adp">Encoding a dimension $d=1$ VSS as an ADP</h3>

<p>The (dimension-1) vector subset-sum (VSS) problem can be defined as an NP relation:</p>

<p>\begin{align}
\term{\Rvss}\begin{pmatrix}{}
	\mathbf{h} \in \Z^n, \ell \in \Z \textbf{;}\\<br />
	\witn
\end{pmatrix} = 1\Leftrightarrow { \witn \in \{0,1\}^n } \wedge { \mathbf{h}\cdot \witn = \ell }
\end{align}</p>

<p>They show how there exists a <strong>randomized</strong> ADP over $\Fp$ such that if satisfied it implies VSS satisfiability.
Specifically, to encode a VSS instance $(\mathbf{h},\ell)$ as an ADP, pick a prime $q &gt; \max_{\witn \in \{0,1\}^n} |\mathbf{h}\cdot \witn|$, interpret $\mathbf{h}$ and $\ell$ as elements of $\Fq$, sample $\R \xleftarrow{$} \Fq^{k \times k}$, and set $\A = -\ell \R$ and $\B_i = h_i \R$ for $i \in [n]$.</p>

<p class="warning">The requirement on $q$ ensures that the integer inner product $\mathbf{h}\cdot\witn$ does not wrap around modulo $q$, so that $\mathbf{h}\cdot\witn = \ell$ over $\Z$ iff $\mathbf{h}\cdot\witn = \ell$ over $\Fq$.</p>

<p><strong>Correctness:</strong> $\forall \mathbf{h}\in\Fq^n,\ell \in \Fq,\witn\in\{0,1\}^n$ such that $\mathbf{h}\cdot \witn = \ell$, we have: 
\begin{align}
\Pr_{\R \randget \Fq^{k\times k}}\begin{bmatrix}
    \emph{\Radp{\eval_0}}\begin{pmatrix}
        \underbrace{-\ell \R}_{\A}, \underbrace{h_1 \R}_{\B_1},\ldots,\underbrace{h_n \R}_{\B_n}\textbf{;}\\<br />
        \witn
    \end{pmatrix} = 1 
\end{bmatrix} = 1
\end{align}</p>

<p><strong>Soundness:</strong> $\forall \mathbf{h}\in\Fq^n,\ell \in \Fq,\witn\in\{0,1\}^n$ such that $\mathbf{h}\cdot \witn \ne \ell$, we have: 
\begin{align}
\Pr_{\R \randget \Fq^{k\times k}}\begin{bmatrix}
    \Radp{\eval_0}\begin{pmatrix}
        -\ell \R, h_1 \R,\ldots, h_n \R\textbf{;}\\<br />
        \witn
    \end{pmatrix} = 1
\end{bmatrix} \leq k / q
\end{align}</p>

<p><strong>Proof (Soundness):</strong>
We will prove the contrapositive “ADP for $(\mathbf{h},\ell)$ accepts $\witn$ $\Rightarrow \mathbf{h}\cdot\witn = \ell$.”
Let:
\begin{align}
\aadp_\mathsf{vss}(\witn) = \eval_0\left(\det\left(-\ell \R + \sum_{i\in[n]} w_i h_i \R\right)\right)
\end{align}
denote the AADP for the VSS instance $(\mathbf{h},\ell)$.
Suppose, this ADP accepts $\witn$; i.e.:
\(\begin{align}
1 &amp;= \aadp_\mathsf{vss}(\witn) \Leftrightarrow \\
0 &amp;= \det\left(-\ell \R + \sum_{i\in[n]} w_i h_i \R\right) \\
             &amp;= \det\left(\left(\sum_{i=1}^n w_i h_i - \ell\right) \R\right)\\
             &amp;= \det\left(\left(\mathbf{h} \cdot w - \ell\right) \R\right)\\
             &amp;= \left(\mathbf{h} \cdot w - \ell\right)^k \cdot \det\left(\R\right)
\end{align}\)</p>

<!-- because $\det(c \cdot A) = c^k \det(A)$ -->

<p>This is a product of two terms. It equals zero iff at least one factor is zero:</p>

<ol>
  <li>$\det(\R) = 0$</li>
  <li>$s^k = 0 \iff s = 0 \iff \mathbf{h} \cdot \witn = \ell$ over $\Fq$ $\iff \mathbf{h} \cdot \witn = \ell$ over $\Z$ (by our choice of $q$).</li>
</ol>

<p>Since the soundness assumption is $\mathbf{h} \cdot \witn \neq \ell$, case 2 is ruled out. So the ADP can only accept if $\det(\R) = 0$, which happens with probability <a href="#sampling-singular-matrix">at most $k/q$</a>. $\blacksquare$</p>

<h3 id="arithmetic-adps-aadps">Arithmetic ADPs (AADPs)</h3>

<p>Soukhanov et al.<sup id="fnref:SRGK26e"><a href="#fn:SRGK26e" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> generalize ADPs to <strong>arithmetic ADPs (AADPs)</strong>:</p>

\[\term{\aadp} : \Fbar^{\term{n}} \to \{0,1\}\]

<p>defined from the same $k\times k$ matrices as before, except:</p>
<ul>
  <li>restricted to using $\eval_0$ as its evaluation function, slightly redefined to now take an $\Fbar$, rather than an $\F$, input</li>
  <li>it now takes <strong>non-zero</strong> inputs $\x\in\Fbar$
i.e.,:
\begin{align}
\aadp(\x) = \eval_0\left(\det{\left(\A + \sum_{i\in[n]} x_i \B_i\right)}\right),\forall \x \ne \vec{0}
\end{align}
where $\term{\Fbar}$ denotes the <strong>algebraic closure</strong> of $\F$ (i.e., all field extensions $\F_{q^k}$ for all $k$).</li>
</ul>

<p><strong>Observations:</strong></p>
<ol>
  <li>The restriction to $\eval_0$ is artificial: AFAICT, it’s just because it suffices for the WE use-case</li>
  <li>$\F$ is cryptographically large (e.g., 256-bits)</li>
  <li>The determinant defines a polynomial over $\F$, that can be evaluated by the adversary maliciously with inputs from $\Fbar$ in order to break the encryption. This is why the definition requires the input to be from an algebraic closure.</li>
</ol>

<h2 id="sampling-singular-matrix">Appendix: Probability of sampling a singular matrix over $\Fq$</h2>

<p>The proof above relies on the fact that a uniformly-random $k\times k$ matrix $\R$ over $\Fq$ is singular (i.e., square and non-invertible) with negligible probability.
This follows from a classical result on the <strong>general linear group</strong> $\text{GL}(k, \Fq)$, i.e., the group of all invertible $k\times k$ matrices over $\Fq$.</p>

<p><strong>Counting invertible matrices.</strong>
To build an invertible $k\times k$ matrix over $\Fq$, we choose its rows one at a time, each of which must be linearly independent of the previous ones, leading to full-rank invertible matrix:</p>

<ul>
  <li>Row 1 can be any nonzero vector
    <ul>
      <li>There are $q^k$ possible size-$k$ vectors</li>
      <li>Only one of them is the zero vector, which we want to avoid since it would trivially be in the span of any other row $\Rightarrow$ would not be full rank</li>
      <li>$\Rightarrow q^k - 1$ possible vectors for row 1</li>
    </ul>
  </li>
  <li>Row 2 can be any vector outside the span of row 1:
    <ul>
      <li>So we must exclude the span of row 1, denoted by $\mathbf{r}_1$: i.e., exclude $\span(\mathbf{r}_1) = \{ c \cdot \mathbf{r}_1, \forall c \in \Fq\}$
        <ul>
          <li>This excludes the zero-vector, since $c$ can be zero.</li>
        </ul>
      </li>
      <li>The size of $\span(\mathbf{r}_1)$ is $q$, since $c \in \Fq$ and $\mathbf{r}_1$ is fixed.</li>
      <li>$\Rightarrow q^k - q$ possible vectors for row 2</li>
    </ul>
  </li>
  <li>Row 3 can be any vector outside the span of rows 1–2:
    <ul>
      <li>So we must exclude $\span(\mathbf{r}_1, \mathbf{r}_2) = \{ c_1 \cdot \mathbf{r}_1 + c_2 \cdot \mathbf{r}_2, \forall c_1, c_2 \in \Fq\}$</li>
      <li>$\mathbf{r}_1$ and $\mathbf{r}_2$ are linearly independent $\Rightarrow$ every pair $(c_1, c_2)$ gives a distinct vector $\Rightarrow \sizeof{\span(\mathbf{r}_1, \mathbf{r}_2)} = q^2$.</li>
      <li>$\Rightarrow q^k - q^2$ possible vectors for row 3<br />
  $\vdots\hspace{5em}\vdots\hspace{5em}\vdots\hspace{5em}\vdots\hspace{5em}\vdots\hspace{5em}\vdots$</li>
    </ul>
  </li>
  <li>Row $k$ can be any vector outside the span of the first $k-1$ rows: $q^k - q^{k-1}$ choices.</li>
</ul>

<p>So the number of invertible matrices is:</p>

\[|\text{GL}(k, \Fq)| = \prod_{i=0}^{k-1}(q^k - q^i)\]

<p><strong>Probability of non-singularity.</strong>
Since the total number of $k\times k$ matrices over $\Fq$ is $q^{k^2}$, the probability that a uniformly-random matrix is invertible is:</p>

<p>\begin{align}
\Pr[\det(\R) \neq 0] &amp;= \frac{|\text{GL}(k, \Fq)|}{q^{k^2}}\\<br />
  &amp;= \frac{\prod_{i=0}^{k-1}(q^k - q^i)}{q^{k^2}}\\<br />
  &amp;= \frac{\prod_{i=0}^{k-1}(q^k - q^i)}{(q^k)^k}
   = \frac{\prod_{i=0}^{k-1}(q^k - q^i)}{\prod_{i=0}^{k-1} q^k}\\<br />
  &amp;= \prod_{i=0}^{k-1} \frac{q^k - q^i}{q^k}\\<br />
  &amp;= \prod_{i=0}^{k-1} \left(1 - \frac{q^i}{q^k}\right)\\<br />
  &amp;= \prod_{i=0}^{k-1} \left(1 - q^{i-k}\right)
\end{align}</p>

<p><strong>Lower bound.</strong>
For all $i\in[0,k)$, we have $1 - q^{i-k} \geq 1 - q^{-1} = 1 - 1/q$.
(This is equivalent to $-q^{i-k} \geq - q^{-1} \Leftrightarrow q^{i-k} \leq q^{-1} \Leftrightarrow i - k \leq -1 \Leftrightarrow i \leq k - 1$, which is true from the premise that $i\in[0,k)$.)</p>

<p>Thus:</p>

\[\Pr[\det(\R) \neq 0] \geq \left(1 - \frac{1}{q}\right)^k\]

<p>Equivalently, the probability of singularity is:</p>

<p>\begin{align}
\Pr[\det(\R) = 0] &amp;= 1 - \Pr[\det(\R) \neq 0]\\<br />
  &amp;\leq 1 - \left(1 - \frac{1}{q}\right)^k\\<br />
  &amp;\leq 1 - \left(1 - \frac{k}{q}\right)\\<br />
  &amp;= \frac{k}{q}
\end{align}
where the last inequality applies Bernoulli’s inequality $(1-x)^k \geq 1 - kx$ for $x \in [0, 1]$ and $k \geq 1$.</p>

<p class="smallnote">$x$ here is $1/q$.
Put differently, we’re applying this to $\left(1-\frac{1}{q}\right)^k \ge 1 - \frac{k}{q}\Leftrightarrow - \left(1-\frac{1}{q}\right)^k \le -\left(1-\frac{k}{q}\right)$.</p>

<p>For cryptographic $q$ (e.g., $q \approx 2^{256}$) and any reasonable $k$, this is negligibly small.</p>

<h2 id="references">References</h2>

<p>For cited works, see below 👇👇</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:GMM17e">
      <p><strong>Lower Bounds on Obfuscation from All-or-Nothing Encryption Primitives</strong>, by Sanjam Garg and Mohammad Mahmoody and Ameer Mohammed, <em>in Cryptology {ePrint} Archive, Paper 2017/541</em>, 2017, <a href="https://eprint.iacr.org/2017/541">[URL]</a> <a href="#fnref:GMM17e" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:BIJplus20e">
      <p><strong>Affine Determinant Programs: A Framework for Obfuscation and Witness Encryption</strong>, by James Bartusek and Yuval Ishai and Aayush Jain and Fermi Ma and Amit Sahai and Mark Zhandry, <em>in Cryptology {ePrint} Archive, Paper 2020/889</em>, 2020, <a href="https://eprint.iacr.org/2020/889">[URL]</a> <a href="#fnref:BIJplus20e" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:SRGK26e">
      <p><strong>Implementable Witness Encryption from Arithmetic Affine Determinant Programs</strong>, by Lev Soukhanov and Yaroslav Rebenko and Muhammad El Gebali and Mikhail Komarov, <em>in Cryptology {ePrint} Archive, Paper 2026/175</em>, 2026, <a href="https://eprint.iacr.org/2026/175">[URL]</a> <a href="#fnref:SRGK26e" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Alin Tomescu</name></author><category term="witness encryption (WE)" /><category term="encryption" /><summary type="html"><![CDATA[tl;dr: Some notes to self on state-of-the-art witness encryption (WE) schemes. $ \def\adp{\mathsf{ADP}} \def\aadp{\mathsf{AADP}} \def\eval{\mathsf{eval}} \def\x{\mathbf{x}} \def\M{\mathsf{M}} \def\A{\mathbf{A}} \def\B{\mathbf{B}} \def\R{\mathbf{R}} \def\Rvss{\mathcal{R}_\mathsf{vss}} \def\Radp#1{\mathcal{R}_\mathsf{adp}^{#1}} \def\span{\mathsf{span}} \def\Fbar{\bar{\F}} $ $ \def\stmt{\mathbf{x}} \def\witn{\mathbf{w}} % \def\td{\mathsf{td}} % \def\zkpSetup{\mathsf{ZKP}.\mathsf{Setup}} \def\zkpProve{\mathsf{ZKP}.\mathsf{Prove}} \def\zkpVerify{\mathsf{ZKP}.\mathsf{Verify}} \def\zkpSim{\mathsf{ZKP}.\mathsf{Sim}} $]]></summary></entry><entry><title type="html">Quantum computing</title><link href="https://alinush.github.io//quantum" rel="alternate" type="text/html" title="Quantum computing" /><published>2026-04-03T00:00:00+00:00</published><updated>2026-04-03T00:00:00+00:00</updated><id>https://alinush.github.io//quantum-computing</id><content type="html" xml:base="https://alinush.github.io//quantum"><![CDATA[<p class="info"><strong>tl;dr:</strong> What has happened in quantum computing over the years.</p>

<!--more-->

<!-- Here you can define LaTeX macros -->
<div style="display: none;">$
$</div>
<p><!-- $ --></p>

<h2 id="a-history">A history</h2>

<p>In 2001, Vandersypen et al. claimed an “experimental realization” of Shor’s quantum factoring algorithm<sup id="fnref:VSBplus01"><a href="#fn:VSBplus01" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>.
Their results demonstrate feasibility of building very small, highly specialized quantum circuits.
Specifically, they show a quantum circuit tailored for factoring 15, <strong>but</strong> in designing this circuit they leveraged knowledge of its factorizaton (i.e., $3\times5 = 15$).
Naturally, the relevance of these results has been questioned, most notably by Smolin et al.<sup id="fnref:SSV13"><a href="#fn:SSV13" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> who write:</p>

<blockquote>
  <p>While there is no objection to having a classical compiler help design a quantum circuit (indeed, probably all quantum computers will function in this way), it is not legitimate for a compiler to know the answer to the problem being solved. To even call such a procedure compilation is an abuse of language.</p>
</blockquote>

<p>Others, have been less gentle and mocked this kind of “experimental realization”<sup id="fnref:GN25e"><a href="#fn:GN25e" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>.</p>

<p class="todo">There might’ve been full runs that factor 15 <a href="https://x.com/dallairedemers/status/2041565238357848527?s=46">after</a>.</p>

<p class="todo">Google’s results: Willow, error-correction, new ECDLP algorithm.
Oratomic’s result.
This result<sup id="fnref:FFEB26"><a href="#fn:FFEB26" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>.
<a href="https://algassert.com/post/2500">Craig Gidney’s post</a> and his past paper on Shor.</p>

<h2 id="some-interesting-readings">Some interesting readings</h2>

<ul>
  <li><a href="https://scottaaronson.com/democritus/lec14.html">Lecture 14: Skepticism of quantum computing</a>, Scott Aaronson, Fall 2006</li>
  <li><a href="https://scottaaronson.blog/?p=124">Reasons to believe II: quantum edition</a>, Scott Aaronson, September 8th, 2006</li>
  <li><a href="https://scottaaronson.blog/?p=148">Mistake of the Week: “X works on paper, but not in the real world”</a>, Scott Aaronson, October 26th, 2006
    <ul>
      <li>Scott does away with the well-known <em>“in theory, theres no difference between theory and practice; in practice, there is.”</em> quote, saying <em>“In theory, there’s no difference between theory and practice even in practice.”</em> (Otherwise, the theory is wrong and thus no longer a theory.)</li>
    </ul>
  </li>
  <li><a href="https://blog.cr.yp.to/20251004-weakened.html">NSA and IETF: Can an attacker simply purchase standardization of weakened cryptography</a>, Daniel J. Bernstein, October 2025</li>
  <li><a href="https://bas.westerbaan.name/notes/2026/04/02/factoring.html">Factoring is not a good benchmark to track Q-day</a>, Bas Westerbaan, April 2nd, 2026</li>
  <li><a href="https://nehanarula.org/2026/04/03/bitcoin-and-quantum-computing.html">Bitcoin and Quantum Computing</a>, Neha Narula, April 3rd, 2026</li>
</ul>

<h2 id="blockchain-space-responses-chronological">Blockchain space responses (chronological)</h2>

<ul>
  <li>March 2nd, 2022: Algorand <a href="https://medium.com/algorand/algorand-state-proofs-707d64038e35">announced</a> future support for <a href="https://dev.algorand.co/concepts/protocol/state-proofs/">state proofs</a> using <a href="https://github.com/algorand/falcon">Falcon digital signatures</a></li>
  <li>September 7th, 2022: <a href="https://medium.com/algorand/algorand-protocol-upgrade-introduces-state-proofs-for-trustless-cross-chain-communication-51b4cc21a9f3">Algorand Protocol Upgrade Introduces State Proofs for Trustless Cross Chain Communication and 5x Faster Performance</a>, <em>by Algorand</em></li>
  <li>October 15th, 2025: <a href="https://eips.ethereum.org/EIPS/eip-8051">EIP-8051: Precompile for ML-DSA signature verification</a>, <em>by 	Renaud Dubois and Simon Masson</em></li>
  <li>November 3rd, 2025: <a href="https://algorand.co/blog/technical-brief-quantum-resistant-transactions-on-algorand-with-falcon-signatures">Technical Brief: Quantum-resistant transactions on Algorand with Falcon signatures</a>, <em>by Larkin Young</em></li>
  <li>December 9th, 2025: <a href="https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-137-post-quantum-aptos-accounts-via-slh-dsa-sha2-128s.md">AIP-137: Post-quantum Aptos accounts via SLH-DSA-SHA2-128s signatures</a>, <em>by Alin Tomescu</em></li>
  <li>January 14th, 2026: <a href="https://blog.projecteleven.com/posts/announcing-project-elevens-series-a">Announcing Project Eleven’s Series A</a>, <em>by Project 11</em>
    <ul>
      <li>Mentions a <a href="https://x.com/SolanaFndn/status/2000948477568934084?s=20">Solana testnet with ML-DSA support</a></li>
    </ul>
  </li>
  <li>February 1st, 2026: <a href="https://github.com/solana-foundation/solana-improvement-documents/pull/461">PR for “SIMD-0461: enabling falcon signature verification as a precompile”</a>, <em>by <a href="https://github.com/zz-sol">zz-sol</a></em></li>
</ul>

<h2 id="todos">TODOs</h2>

<ul>
  <li><a href="https://en.wikipedia.org/wiki/Threshold_theorem">The threshold theorem</a> and the reasonableness of its assumptions</li>
  <li>What do we know about CRQC’s ability to break hash functions? e.g., BHT, Grover search. Is there a proof that you cannot do better asymptotically?
    <ul>
      <li>What do we know about quantum algorithms that attack the <em>structure</em> of the hash function, rather than the idealized hash function?</li>
    </ul>
  </li>
  <li>There are many different approaches to building QCs. What are the universal metrics that we should be looking for to judge them?(# of “stable” logical qubits is a popular one, but deeply inadequate)</li>
</ul>

<h2 id="references">References</h2>

<p>For cited works, see below 👇👇</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:VSBplus01">
      <p><strong>Experimental realization of Shor’s quantum factoring algorithm using nuclear magnetic resonance</strong>, by Lieven M. K. Vandersypen and Matthias Steffen and Gregory Breyta and Costantino S. Yannoni and Mark H. Sherwood and Isaac L. Chuang, 2001, <a href="https://arxiv.org/abs/quant-ph/0112176">[URL]</a> <a href="#fnref:VSBplus01" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:SSV13">
      <p><strong>Pretending to factor large numbers on a quantum computer</strong>, by John A. Smolin and Graeme Smith and Alex Vargo, 2013, <a href="https://arxiv.org/abs/1301.7007">[URL]</a> <a href="#fnref:SSV13" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:GN25e">
      <p><strong>Replication of Quantum Factorisation Records with an 8-bit Home Computer, an Abacus, and a Dog</strong>, by Peter Gutmann and Stephan Neuhaus, <em>in Cryptology {ePrint} Archive, Paper 2025/1237</em>, 2025, <a href="https://eprint.iacr.org/2025/1237">[URL]</a> <a href="#fnref:GN25e" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:FFEB26">
      <p><strong>High-threshold decoding of non-Pauli codes for 2D universality</strong>, by Julio C. Magdalena de la Fuente and Noa Feldman and Jens Eisert and Andreas Bauer, 2026, <a href="https://arxiv.org/abs/2604.02033">[URL]</a> <a href="#fnref:FFEB26" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Alin Tomescu</name></author><category term="post-quantum" /><category term="Shor" /><summary type="html"><![CDATA[tl;dr: What has happened in quantum computing over the years.]]></summary></entry></feed>