<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://sovrandy.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://sovrandy.com/" rel="alternate" type="text/html" /><updated>2026-09-28T16:55:45+00:00</updated><id>https://sovrandy.com/feed.xml</id><title type="html">Sovrandy</title><subtitle>A blog about Cryptography, Bitcoin, and whatever else I find interesting</subtitle><author><name>Randy Naar</name></author><entry><title type="html">Dog Pack: A Proposal for Private In-Round Bark Transfers</title><link href="https://sovrandy.com/2026/09/27/dog-park-proposal-for-private-in-round-bark-transfers.html" rel="alternate" type="text/html" title="Dog Pack: A Proposal for Private In-Round Bark Transfers" /><published>2026-09-27T00:00:00+00:00</published><updated>2026-09-27T00:00:00+00:00</updated><id>https://sovrandy.com/2026/09/27/dog-park-proposal-for-private-in-round-bark-transfers</id><content type="html" xml:base="https://sovrandy.com/2026/09/27/dog-park-proposal-for-private-in-round-bark-transfers.html"><![CDATA[<video controls="" preload="metadata" style="width: 100%; height: auto;">
  <source src="/video/Dog-Pack-Private-In-Round-Bark-Transfers-1080p.mp4" type="video/mp4" />
  <a href="/video/Dog-Pack-Private-In-Round-Bark-Transfers-1080p.mp4">Watch the video</a>
</video>

<p>This post describes a proposed algorithm to unlink a forfeited vtxo from its in-round output.</p>

<p>The Bark SDK currently only supports in-round transfers between addresses of the same wallet as part of vtxo refreshes. The process is modeled in the following diagram:</p>

<pre><code class="language-text">user -&gt; server: Attests to input and declares desired destination pubkey
server -&gt; user: Gives hash and proposed round
user -&gt; server: Cosigns branches of vtxo tree for proposed round
server: Broadcasts transaction
server -&gt; user: Gives finalized round
user -&gt; server: Gives forfeit signatures spending old vtxo exit to ASP hashlock
server -&gt; user: Gives secret and new vtxo
user: Can now exit new vtxo with secret, or spend vtxo
</code></pre>

<h1 id="overview-of-in-round-privacy-issues">Overview of in-round privacy issues</h1>

<p>When senders refresh their vtxos, they can’t establish privacy across rounds from on-chain observers or the ASP.</p>

<p>On-chain, a stubborn ASP refusing to give the secret for a round output can force a user to exit their old vtxo, and if the ASP refuses to honor the spend of the new vtxo, they can cause that one to exit as well. This non-cooperation creates an on-chain link between the 2 transactions through their shared hash.</p>

<p>Off-chain, the ASP always sees the linkage from the hashlock binding the forfeit transaction to the new round outputs.</p>

<h1 id="addressing-the-privacy-issues">Addressing the privacy issues</h1>

<p>To formalize the problem, we would like to avoid the ASP learning the relationships between input vtxos and round outputs, and find a way to maintain that privacy even when an ASP claims forfeited funds.</p>

<p>The first step I see that violates our 2 goals of on-chain privacy and privacy against ASP round linking is the attestation and output declaration step.</p>

<p>This step involves a user initiating an in-round transfer/refresh by submitting an attestation for an input and declaring an output public key. Since both pieces of information are given in one message, it trivially leaks the input/output relationship. Thus, our first goal is to separate these steps into 2.</p>

<p>The first step will take care of vtxo attestation, and the second step will take care of output declaration.</p>

<h2 id="vtxo-attestation">Vtxo attestation</h2>

<p>In the vtxo attestation step, the user will no longer submit an output public key. A slot is an authorization to receive one output of a specific denomination in the round. The user chooses a fresh random <code>slot_secret</code> and gives the ASP <code>slot_pubkey = slot_secret * G</code> for a requested denomination of their input vtxo, where <code>G</code> is the curve’s generator.</p>

<p>Multiple denominations can be requested by providing different <code>slot_pubkey[i]</code> values, with each public key acting as an authorization bound to its registered denomination. The vtxo owner must sign these slot public keys and their denominations as part of their attestation to create a stronger binding between the two that will be helpful later on.</p>

<p>In terms of validation for this step, the ASP checks that the input is valid, that it hasn’t already been registered, and that the sum of all token denominations equals the input’s value after the agreed fees.</p>

<p>At this step, the ASP also establishes a MuSig2 signing session for the forfeit claim transaction and commits to the partial signature it will use. This is the transaction that lets the ASP collect from the forfeit output, not the transaction that creates that output. We call the partial signature scalar <code>claim_secret</code> and its public commitment <code>claim_point = claim_secret * G</code>. This scalar is specific to the fixed claim signing session. It is not the ASP’s private key. The claim transaction, keys, and both parties’ nonces are fixed here, so the signing challenge is fixed too. The participant calculates <code>claim_point</code> from the ASP’s public nonces and public key contribution using that challenge, then checks the point the ASP provides. We’ll put this point to use in the output declaration step.</p>

<h2 id="output-declaration">Output declaration</h2>

<p>For the output declaration step, we want to prioritize 2 things:</p>

<ol>
  <li>
    <p>The ability for an ASP to verify that an output can only be spent with knowledge of a forfeit claim secret</p>
  </li>
  <li>
    <p>A way for us to hide the forfeit secret used in our output construction</p>
  </li>
</ol>

<p>The existing implementation already meets the first requirement since the output is locked by the same hashlock needed to spend the forfeit output.</p>

<p>The second condition was a bit harder to meet. The challenge, under the existing implementation, is to create a construction such that the condition requiring the preimage of a hash could be obfuscated.</p>

<p>To simplify the task of obfuscation, we’ll first drop the hashlock from the forfeit output and introduce a new secret to lock our round output. Instead of the hashlock, we can use the ASP’s partial signature for the forfeit claim transaction. Going forward, we’ll call this secret <code>claim_secret[i]</code>, where <code>i</code> represents the ith vtxo submitted for the round.</p>

<p>With this, we can bind the reveal of secret information to the action of the ASP’s forfeit claim. The invariant upheld is that when an ASP claims a forfeit, the participant learns a secret allowing them to spend the round output.</p>

<p>Now how can we use this secret to lock our output?</p>

<h3 id="locking-our-output">Locking our output</h3>

<p>Similar to the original model, the participant creates a new keypair to use in their round output. Let’s call the private key in this keypair <code>offset</code> and the public key <code>offset_pubkey = offset * G</code>.</p>

<p>Instead of making <code>offset_pubkey</code> the final output public key we attach to our round output, we define the output public key as <code>output_pubkey = claim_point[i] + offset * G</code>, where <code>claim_point[i]</code> is the partial signature commitment associated with the participant’s input.</p>

<p>Once the participant learns <code>claim_secret[i]</code>, they can calculate the private key <code>output_secret = claim_secret[i] + offset</code> needed to spend that output.</p>

<p>The participant also chooses a fresh, separate keypair to sign their branches of the vtxo tree. They already know this private key, unlike <code>output_secret</code>, which they can’t calculate until they learn <code>claim_secret[i]</code>. The tree-signing pubkey is submitted with the anonymous output request, and the participant checks that the proposed tree uses it for their output.</p>

<h3 id="hiding-the-claim-point-used">Hiding the claim point used</h3>

<p>Hiding which claim point is used is not a straightforward task, so I leveraged one of the few cryptographic primitives known to me: ring signatures.</p>

<p>To hide which <code>claim_point[i]</code> is used when using <code>slot_pubkey[i]</code>, the participant constructs a ring containing every branch pubkey registered for the requested denomination. For each candidate slot <code>j</code> in that denomination ring, we include 2 public keys <code>slot_pubkey[j]</code> and <code>output_pubkey[i] - claim_point[j]</code>. This turns the ring signature into a multi-key ring proof, reminiscent of Monero’s MLSAG (Multi-layered Linkable Spontaneous Anonymous Group).</p>

<p>Suppose Alice, Bob, and Carol each register a slot for the same denomination. Alice’s output proof includes all 3 slots, but only Alice knows the <code>slot_secret</code> and offset for the slot she uses. The ASP can verify the proof without learning which slot that was.</p>

<p>Implicit in the requirements for this mechanism is the ASP publishing each denomination pubkey collected to all participants. Each denomination pubkey will also have been signed by its vtxo owner in the vtxo attestation step, so it can’t be replaced by an ASP-derived value in this step.</p>

<p>In the ring, for one hidden slot <code>i</code>, the combined proof establishes knowledge of <code>slot_secret</code> and <code>offset</code> (since <code>offset_pubkey = output_pubkey[i] - claim_point[i]</code>). Slots from the same input share a <code>claim_point</code>, so the same offset works for those slots, but we still need the <code>slot_secret</code> for whichever slot we use. This approach is similar to the one used in the confidential transaction scheme described in <a href="/2025/12/21/confidential-transactions-for-dummies-pt-2.html">Confidential Transactions For Dummies, Part 2 - Borromean Range Proofs</a>.</p>

<p>The message we’ll sign in the ring signature is:</p>

<pre><code>  "Bark private output declaration v1" ||
  round_id ||
  denomination
</code></pre>

<p><code>"Bark private output declaration v1"</code> acts as a label for domain separation, the round ID marks it for that round and attempt, and the denomination makes it particular to that ring. Here <code>round_id</code> includes both Bark’s <code>round_seq</code> and <code>attempt_seq</code>, since a round can have more than one attempt.</p>

<p>We also include a nullifier, akin to a ‘key image’, in this construction to ensure the same real branch isn’t used twice. The ASP rejects an output registration if its nullifier has already been used for an accepted output in that round.</p>

<p>The combined proof thus contains:</p>

<pre><code class="language-text">slot_pubkey[i] = slot_secret * G
nullifier = slot_secret * nullifier_base
output_pubkey - claim_point[i] = offset * G
</code></pre>

<p><code>nullifier_base</code> is a public curve point calculated with hash-to-curve from a domain-specific label, the round identifier, and the denomination (<code>"Bark slot nullifier v1" || round_id || denomination</code>).</p>

<p><code>nullifier_base</code> can be shared by every candidate in a denomination ring. The proof contains a single <code>nullifier = slot_secret * nullifier_base</code>, which is used in every branch of the ring signature. Only in the real branch does the signer know the <code>slot_secret</code> relating both the slot public key and the nullifier to their respective bases. The other branches are simulated.</p>

<p>The challenge hashes the message, the ring’s full ordered list of public keys, the nullifier, and the branch’s computed nonce points for the slot, nullifier, and offset checks. The public keys are separate inputs to the challenge hash, not part of the message. Since they include <code>output_pubkey - claim_point[j]</code>, this also binds the proof to the requested output public key.</p>

<p>The participant submits <code>output_pubkey[i]</code> (by itself), the tree-signing pubkey, the denomination, and the combined proof.</p>

<p>This request should also come back anonymously, so it must avoid reusing connections or identifiers that link it to input registration.</p>

<p>Once all participants have their exit information and the new round’s funding transaction has the required confirmations, the participant forfeits their old vtxo to the ASP in exchange for the <code>claim_secret</code> that will allow them to calculate <code>output_secret</code>. These confirmations are for the anchor funding the new round outputs, not the already-confirmed anchor behind the old input vtxo.</p>

<p>The ASP knows which input was forfeited and which <code>claim_secret</code> it released. The earlier anonymous output declaration does not tell it which output used that secret.</p>

<p>If the ASP withholds <code>claim_secret[i]</code>, the participant will be forced to exit if they don’t want to lose money. If the ASP still wants to move forward, it will claim the forfeited exit, allowing the participant to extract <code>claim_secret[i]</code> from the completed claim signature and calculate <code>output_secret</code>.</p>

<p>This works because the combined MuSig2 signature scalar is the sum of the participant’s and ASP’s respective partial signature scalars, along with the tweak-derived value made from the script tree. Since the participant knows their own partial signature, they can calculate:</p>

<pre><code>claim_secret = completed_signature_scalar - user_partial_signature - challenge*derive_sign(tweaked_agg_pubkey_parity_bit)*tweak
</code></pre>

<p>The participant also checks that the extracted <code>claim_secret</code> gives the registered <code>claim_point</code>.</p>

<p>If the exited vtxo is never claimed with a forfeit exit, the user is then able to claim their exit as described in the original model.</p>

<h2 id="privacy-after-a-claim">Privacy after a claim</h2>

<p>The ASP already knows every <code>claim_secret</code>, but it doesn’t know which offset the participant chose. Spending an output reveals a signature, not <code>output_secret</code>.</p>

<p>The participant never reveals their <code>slot_secret</code> or offset, including during an exit. The combined proof must hide which slot they used.</p>

<h2 id="drawbacks">Drawbacks</h2>

<p>As with coinjoins, outputs can be linked to vtxo inputs when there is insufficient liquidity. If there’s one large-value vtxo and many small ones, heuristics may be able to link outputs.</p>

<p>The ASP can also impact privacy by providing fake liquidity with its own funds, reducing the anonymity set and making it easier to link real participant inputs and outputs.</p>

<p>The privacy afforded by this scheme also depends on other participants requesting the same denominations. A denomination with only one remaining input provides no input/output privacy. The choice of denominations therefore matters, even if every proof works as intended.</p>

<p>On the topic of denominations, I’ll also mention the problem of bundle fingerprinting. Although I ask that each participant submit their outputs over a fresh connection, timing analysis might bundle outputs submitted around the same time. This is bad because the sum of this subset may uniquely identify an input vtxo. Batching, randomized declaration timing, and avoiding uniquely identifying denomination decompositions can help with this, but the details of doing these things are out of scope for this post.</p>

<p>Another drawback is that the ring proof grows with the number of candidate slots, and splitting an input into smaller denominations creates more outputs and proofs. If those outputs later need to be exited, having more outputs can also mean more on-chain fees.</p>

<p>The ASP can also use its control over secret release to try to learn the relationship through timing analysis. For example, it could release one input’s <code>claim_secret</code> and watch for an output that immediately becomes active or is spent.</p>]]></content><author><name>Randy Naar</name></author><category term="Other" /><summary type="html"><![CDATA[Watch the video]]></summary></entry><entry><title type="html">Inner Product Arguments, Part 1</title><link href="https://sovrandy.com/2026/07/12/inner-product-arguments.html" rel="alternate" type="text/html" title="Inner Product Arguments, Part 1" /><published>2026-07-12T00:00:00+00:00</published><updated>2026-07-12T00:00:00+00:00</updated><id>https://sovrandy.com/2026/07/12/inner-product-arguments</id><content type="html" xml:base="https://sovrandy.com/2026/07/12/inner-product-arguments.html"><![CDATA[<p>Since the focus of my last post was on the mechanics of Hadamard product arguments, I was originally going to use them as a starting point for explaining inner product arguments. In the end I decided to take a different pedagogical approach rather than simply adapting the Hadamard argument to the inner product argument. Instead I’ll take another first principles approach. This will allow me to break down and re-contextualize the proof mechanisms we previously established and hopefully allow us to easier identify components for optimization.</p>

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

<p>The notation I’ll use for single value commitment functions will be a little different from my other posts. In my previous posts I tried to abstract away as much information as I could and used <code>commit(value)</code> to represent commitments to ‘value’ (where ‘value’ is a scalar). Now because it will be relevant to the stuff I’ll show later, I’ll parametrize the commitment function to include the generator as its second argument. Thus we write <code>commit(val, G) = val*G</code> to denote a commitment to the value <code>val</code> with the generator <code>G</code>. I’d also like the reader to assume the commitment function is an elliptic curve multiplication between the scalar value being committed and a generator point on the curve.</p>

<p>The commitment scheme for a vector <code>a</code> is <code>commit(a) = sum(commit(a[i], G_a[i]) for i..len(a))</code> where <code>G_a</code> is a public vector of different generators with length equal to <code>a</code>. We’ll use different generator vectors <code>G_vec</code>, where <code>vec</code> is the vector name, for each vector we commit to.</p>

<h1 id="stages-of-a-zk-argument">Stages of a zk argument</h1>

<p>When talking about zk arguments that prove a relation between 2 hidden values, I think it’s helpful to break the process down into 5 stages. The statement, transformation, consistency check, operation, and verification stage. I’ll expand on these stages below.</p>

<h2 id="statement">Statement</h2>

<p>The statement is the assertion being made about the “hidden” values. Hidden meaning we can only see commitments to the values we are making assertions about.</p>

<h2 id="transformation">Transformation</h2>

<p>The transformation hides the values in the statement such that the output is algebraically linked to the hidden values in a different way than the commitments are. The difference between transformed values and commitments is that transformed values can be used in operations that would be undefined on commitments. For example, multiplication is not defined between 2 commitments whereas it <em>is</em> defined between 2 transformed values.</p>

<h2 id="consistency-check">Consistency Check</h2>

<p>This step is where the verifier checks the transformed values are algebraically linked to the hidden values. This check is defined in a way that makes it nearly impossible to forge and is performed by applying a similar transformation on the statement’s commitment and checking that it equals the transformed value’s commitment.</p>

<h2 id="operation">Operation</h2>

<p>The operation is where the transformed values are run through the operation in the assertion.</p>

<h2 id="verification">Verification</h2>

<p>This step verifies the algebraic link between the operation’s result on the transformed values and the public result of the original values included in the statement.</p>

<h1 id="the-naive-approach">The naive approach</h1>

<p>In this section I’d like to describe what a naive inner product argument looks like when broken down into the stages listed above.</p>

<h2 id="statement-1">Statement</h2>

<p>The statement for the inner product argument informally says “I would like to prove that 2 committed vectors, <code>a</code> and <code>b</code>, have an inner-product equal to a public scalar <code>c</code>.”</p>

<p>As part of this statement the prover needs to give the verifier:</p>
<ul>
  <li>The commitment scheme</li>
  <li><code>A</code> where <code>A = commit(a, G_a)</code></li>
  <li><code>B</code> where <code>B = commit(b, G_b)</code></li>
  <li><code>c</code> where <code>c</code> is the inner-product of <code>a</code> and <code>b</code></li>
  <li>Any other pieces of information required for the proof that can’t be used to leak <code>a</code> and <code>b</code>
    <ul>
      <li>This will includes vectors <code>R_a</code>, <code>R_b</code>, and <code>R_c</code></li>
    </ul>
  </li>
</ul>

<h2 id="transformation-1">Transformation</h2>

<p>We now have to find a transformation that hides vectors <code>a</code> and <code>b</code>. One requirement of this transformation is that it must be some function of an arbitrary value <code>x</code> supplied by the verifier. Another requirement is that it must be possible for the commitment of the transformation to be recreated using an expression of the commitment values provided in the statement.</p>

<p>The simplest transformation that allows for this adds a different random number to each value in the vector like so:</p>

<ul>
  <li><code>a' = a*x + r1</code> where <code>r1</code> is a vector of random numbers</li>
  <li><code>b' = b*x + r2</code> where <code>r2</code> is a vector of random numbers</li>
</ul>

<p>This transformation against <code>a</code> and <code>b</code> results in:</p>
<pre><code>a' = [
  x*a[1] + r_a[1],
  x*a[2] + r_a[2]
]

b' = [
  x*b[1] + r_b[1],
  x*b[2] + r_b[2]
]
</code></pre>

<h2 id="consistency-check-1">Consistency Check</h2>

<p>For the verifier to acknowledge that these modified values are indeed derived from <code>a</code> and <code>b</code>, we must commit the result of our transformations, <code>a'</code> and <code>b'</code>, and reconstruct each vector commitment with the commitments of the original vectors <code>a</code> and <code>b</code>. Let’s see how this process works for <code>a</code>.</p>

<p>Our original commitment <code>A</code> is defined as:</p>
<pre><code>A = commit(a) = a[1]*G_1[1] + a[2]*G_1[2]
</code></pre>

<p>We must now use this commitment to re-create the commitment of <code>a'</code> which is defined as:</p>
<pre><code>A' = commit(a') = (x*a[1] + r_a[1])*G_1[1] + (x*a[2] + r_a[2])*G_1[2]
</code></pre>

<p>With some algebraic manipulation, we can see that the terms that define <code>A</code>, <code>a[1]*G_1[1] + a[2]*G_1[2]</code>, already exist within <code>A'</code>:</p>
<pre><code>A' =
(x*a[1] + r_a[1])*G_1[1] + (x*a[2] + r_a[2])*G_1[2] =
x*a[1]*G_1[1] + r_a[1]*G_1[1] + x*a[2]*G_1[2] + r_a[2]*G_1[2] =
x*a[1]*G_1[1] + x*a[2]*G_1[2] + r_a[1]*G_1[1] + r_a[2]*G_1[2] =
x*(a[1]*G_1[1] + a[2]*G_1[2]) + r_a[1]*G_1[1] + r_a[2]*G_1[2] =
x*(A) + R_a

where R_a = (r_a[1]*G_1[1] + r_a[2]*G_1[2])
</code></pre>

<p>We can see this transformation allows us to check the relation between the original commitments by multiplying our old commitment <code>A</code> by <code>x</code> and adding the commitment <code>R_a</code> onto it. The same process can be performed to verify <code>b'</code>.</p>

<h1 id="operation-1">Operation</h1>

<p>Let’s look at what happens when we perform the inner product operation on transformed values <code>a'</code> and <code>b'</code>, and see if the result is something we can construct with the commitment for the public product <code>c</code>:</p>

<pre><code>a' = [
  x*a[1] + r_a[1],
  x*a[2] + r_a[2]
]

b' = [
  x*b[1] + r_b[1],
  x*b[2] + r_b[2]
]

c' = inner_product(a', b') =  (
  (x*a[1] + r_a[1]) * (x*b[1] + r_b[1]) + # product of first row
  (x*a[2] + r_a[2]) * (x*b[2] + r_b[2]) # product of second row
)

=  (
  x^2(a[1]*b[1]) + x*(a[1]*r_b[1]) + x(r_a[1]*b[1]) + (r_a[1] * r_b[1]) + # Polynomial expansion of first row product
  x^2(a[2]*b[2]) + x*(a[2]*r_b[2]) + x(r_a[2]*b[2]) + (r_a[2] * r_b[2]) # Polynomial expansion of second row product
)

 =  (
  x^2*(a[1]*b[1] + a[2]*b[2]) +
  x*(a[1]*r_b[1] + a[2]*r_b[2] + r_a[1]*b[1] + r_a[2]*b[2]) +
  (r_a[1] * r_b[1] + r_a[2] * r_b[2])
)
</code></pre>

<p>This result can also be interpreted as a sum of dot products between the various components of <code>a'</code> and <code>b'</code>:</p>
<pre><code>c' = inner_product(a', b') = (
  x^2*(a dot b)
  + x*(a dot r_b + r_a dot b)
  + (r_a dot r_b)
)
</code></pre>

<h1 id="verification-1">Verification</h1>

<p>Our goal for this stage is to identify how we can re-create the commitment of <code>c'</code> using the commitment of our public product <code>c</code>. Algebraically we can see the terms needed to create the original inner product <code>a[1]*b[1] + a[2]*b[2]</code> in <code>c</code> as the coefficient for <code>x^2</code>.</p>

<p>The other terms <code>(a[1]*r_b[1] + a[2]*r_b[2] + r_a[1]*b[1] + r_a[2]*b[2])</code> and <code>(r_a[1] * r_b[1] + r_a[2] * r_b[2])</code>, with coefficients <code>x^1</code> and <code>x^0</code> respectively, are also attached to this value. This means we’ll need commitments for those as well.</p>

<p>Since none of these coefficients rely on the verifier-supplied <code>x</code>, we can calculate them before <code>x</code> is ever given and are what we define to be <code>R_c[1]</code> and <code>R_c[2]</code> in the statement.</p>

<p>With these additional terms <code>R_c[1]</code> and <code>R_c[2]</code>, we can verify the inner product by checking the following equality:</p>
<ul>
  <li><code>commit(c', G_c) = x^2*(commit(c, G_c)) + x*(R_c[1]) + R_c[2]</code></li>
</ul>

<h1 id="optimizations">Optimizations</h1>

<p>The rest of this post will detail optimizations for the inner-product argument presented by the paper ‘Efficient Zero-Knowledge Arguments for Arithmetic Circuits in the Discrete Log Setting’ (aka ‘BCC+16’). I took the time to break down the structure of this zk argument into stages because I wanted to frame each optimization as targeting a specific stage. Once the core optimization is understood, the rest of the actions taken can be thought of as compensating for the core change in the other stages.</p>

<h1 id="optimization-1---transformation-step">Optimization 1 - Transformation step</h1>

<p>The first step of the scheme we wish to optimize targets the transformation step. To rehash, the transformation step takes in vectors <code>a</code> and <code>b</code> and spits out <em>equal size</em> transformed vectors <code>a'</code> and <code>b'</code>. Since researchers in this field generally don’t want proving steps to involve returning data proportional in size to the input, a scheme was devised to reduce <code>a'</code> and <code>b'</code> into a singular scalar each. As we’ll see later on, using this optimization alone won’t allow us to make a correct inner product argument, but when mixed with optimization 2, it works.</p>

<p>Focusing back on this optimization, we intuitively know that this mysterious new transformation needs to involve all the terms of the input vector. Otherwise, we wouldn’t be able to verify the inner-product since it’s a function of every element. So now we ask, “What’s the simplest way to consolidate the elements of a vector into a single value?”</p>

<p>Hopefully your mind went to the same operation mine did. It’s one of the simplest operations math has to offer – addition. Now let’s see the impact of making the transformation the sum of all elements in a vector.</p>

<p>One thing we get for free when we do this is the elimination of the per-element random term. If the first 2 elements are summed, we can also think of the random elements associated with each term as being summed. Thus, all random elements can be consolidated into one element.</p>

<p>Also, because it will make things easier later on, let’s add the random value for each vector as the 1st element of the vector. Since our transformation will sum it up whether it’s attached to one element or if it’s another element itself, we take the route that allows us to write the simplest notation.</p>

<p>Here’s what the output of this new transformation looks like:</p>

<pre><code>a' = [
  a[1] + x*a[2] + x*a[3]
]

b' = [
  b[1] + x*b[2] + x*b[3]
]
</code></pre>

<p>If the above looks weird to you, you might be onto something. Simply transforming the terms like this with a simple addition is <em>not</em> secure.</p>

<p>If the verifier only checks the aggregate, then different underlying vectors that lead to the same aggregate can be trivially forged. For example, if the check only depends on <code>a[2] + a[3]</code>, then replacing <code>a[2]</code> with <code>a[2] + 1</code> and <code>a[3]</code> with <code>a[3] - 1</code> gives the same transformed value. If values were allowed to be forged this way, the verifier would no longer have probabilistic confidence that the original vector was committed, since it could now be a different vector that was trivially assembled to have the same sum.</p>

<p>Fortunately we have a fix for this. We just make sure every row has a different power of <code>x</code> applied to it before they are all summed together:</p>

<pre><code>a' = [
  a[1] + x*a[2] + x^2*a[3]
]

b' = [
  b[1] + x*b[2] + x^2*b[3]
]
</code></pre>

<h2 id="consistency-check-2">Consistency check</h2>

<p>Now that we’ve transformed our hidden input, it’s time to check the output of the transformation for algebraic consistency. To do this, we must commit <code>a'</code>. But how do we apply <code>G_a[1]</code> to the <code>a1</code> part, <code>G_a[2]</code> to the <code>a2</code> part, and <code>G_a[3]</code> to the <code>a3</code> part if all three values have been merged into <code>a'</code> and can no longer be separated?</p>

<p>With some thought, someone eventually came to the realization of trying to mirror the operation performed on the elements onto the generators that correspond to those elements. Let’s see what happens when we use <code>G_a' = G_a[1] + G_a[2] + G_a[3]</code> as our new generator.</p>

<p>I’m going to leave the new generator for <code>a'</code>, <code>G_a'</code>, broken down into a sum with the individual generator terms from the statement to allow us to see what happens to all the individual terms in the result <code>a'*G_a'</code> algebraically:</p>

<pre><code>a' = [
  a[1] + x*a[2] + x^2*a[3]
]

commit(a') = (a[1] + x*a[2] + x^2*a[3]) * (G_1[1] + G_1[2] + G_1[3])
= a[1]*G_1[1] + a[1]*G_1[2] + a[1]*G_1[3] + x*a[2]*G_1[1] + x*a[2]*G_1[2] + x*a[2]*G_1[3] + x^2*a[3]*G_1[1] + x^2*a[3]*G_1[2] + x^2*a[3]*G_1[3]

</code></pre>

<p>Remember our goal to prove the expression for  <code>commit(a')</code> contains the commitment to the original vector <code>commit(a) = a[1]*G_a[1] + a[2]*G_a[2] + a[3]*G_a[3]</code>. To work towards this goal, let’s try to at least group the relevant terms together and keep it at the front of the expression:</p>
<pre><code>commit(a') = a[1]*G_a[1] + a[1]*G_a[2] + a[1]*G_a[3] + x*a[2]*G_a[1] + x*a[2]*G_a[2] + x*a[2]*G_a[3] + x^2*a[3]*G_a[1] + x^2*a[3]*G_a[2] + x^2*a[3]*G_a[3]
= (a[1]*G_a[1] + x*a[2]*G_a[2] + x^2*a[3]*G_a[3]) + a[1]*G_a[2] + a[1]*G_a[3] + x*a[2]*G_a[1]  + x*a[2]*G_a[3] + x^2*a[3]*G_a[1] + x^2*a[3]*G_a[2]
</code></pre>

<p>The problem with this expression is that even though the first grouping, <code>(a[1]*G_1[1] + x*a[2]*G_1[2] + x^2*a[3]*G_1[3])</code>, has the same terms of our commitment, it <em>also</em> adds a different power of <code>x</code> to each term. This is a problem because we don’t have individual elements needed to re-create that expression. The whole point of this new transformation is so we could eliminate the need for per-element commitments! This leaves us at an impasse.</p>

<p>The brilliance of the paper ‘Efficient Zero-Knowledge Arguments for Arithmetic Circuits in the Discrete Log Setting’ is that it found a way to tackle this problem. Their insight is that even though we don’t know the individual commitments of the original <code>a</code> vector, there might be modifications they could make to the <strong>individual</strong> elements in the <code>G</code> vector to force the expression into a checkable form.</p>

<p>Their answer to this was to have the verifier pre-multiply each G_i[i] with the inverse of the <code>x</code> that would be attached to the corresponding element in the first grouping to make the <code>x</code>s cancel out. Concretely, they would modify <code>G_1 = [G_1[1] + G_1[2] + G_1[3]]</code> to <code>G_1' = G_1[1] + (x^-1)*G_1[2] + (x^-2)*G_1[3]</code>.</p>

<p>With <code>G_1'</code>, the first grouping would turn into the original commitment and allow the verifier to re-construct <code>commit(a')</code> using <code>commit(a)</code>. The other terms impacted are cross-terms anyway so we can just give the commitments of the <code>x</code> co-efficients at the beginning and have the verifier adapt it with the <code>x</code> they choose during the transformation step for verification.</p>

<p>Let’s see how this works in practice:</p>

<pre><code>a' = [
  a[1] + x*a[2] + x^2*a[3]
]
commit(a') = (a[1] + x*a[2] + x^2*a[3]) * (G_1'[1] + G_1'[2] + G_1'[3])

= (a[1] + x*a[2] + x^2*a[3]) * (G_1[1]*(x^0) + G_1[2]*(x^-1) + G_1[3]*(x^-2))

= a[1]*G_1[1] + a[1]*G_1[2]*(x^-1) + a[1]*G_1[3]*(x^-2) + x*a[2]*G_1[1] + x*a[2]*G_1[2]*(x^-1) + x*a[2]*G_1[3]*(x^-2) + x^2*a[3]*G_1[1] + x^2*a[3]*G_1[2]*(x^-1) + x^2*a[3]*G_1[3]*(x^-2)

= (a[1]*G_1[1] +  x*a[2]*G_1[2]*(x^-1) + x^2*a[3]*G_1[3]*(x^-2)) + a[1]*G_1[2]*(x^-1) + a[1]*G_1[3]*(x^-2) + x*a[2]*G_1[1] + x*a[2]*G_1[3]*(x^-2) + x^2*a[3]*G_1[1] + x^2*a[3]*G_1[2]*(x^-1)

# Let's simplify the first grouping
= (a[1]*G_1[1] +  a[2]*G_1[2] + a[3]*G_1[3]) + (x^-1)*a[1]*G_1[2] + (x^-2)*a[1]*G_1[3] + x*a[2]*G_1[1] + x^-1*a[2]*G_1[3] + x^2*a[3]*G_1[1] + x*a[3]*G_1[2]

# Now let's group the cross terms by their `x` coefficient
= (a[1]*G_1[1] +  a[2]*G_1[2] + a[3]*G_1[3]) + (x^-2)*a[1]*G_1[3] + (x^-1)*(a[1]*G_1[2] + a[2]*G_1[3]) + x*(a[2]*G_1[1] + a[3]*G_1[2]) + x^2*a[3]*G_1[1]

</code></pre>

<p>As you can see from the calculation above, the first grouping is the commitment to the original vector <code>a</code>.</p>

<h2 id="operation-2">Operation</h2>

<pre><code>a' = [
  a[1] + x*a[2] + x^2*a[3]
]

b' = [
  b[1] + x*b[2] + x^2*b[3]
]

a'*b' = (a[1] + x*a[2] + x^2*a[3]) * (b[1] + x*b[2] + x^2*b[3])
= a[1]*b[1] + a[1]*x*b[2] + a[1]*x^2*b[3] + x*a[2]*b[1] + x*a[2]*x*b[2] + x*a[2]*x^2*b[3] + x^2*a[3]*b[1] + x^2*a[3]*x*b[2]  + x^2*a[3]*x^2*b[3]
= x^4*(a[3]*b[3]) + x^3*(a[2]*b[3] + a[3]*b[2]) + x^2(a[1]*b[3] + a[2]*b[2] + a[3]*b[1])+ x*(a[1]*b[2] + a[2]*b[1]) + a[1]*b[1]
</code></pre>

<p>One may think a similar verification operation to the consistency check where the verifier replaces <code>G</code> with <code>G'</code> can now be performed to eliminate the <code>x</code> coefficients that prevent the original dot product (<code>a*b = a[1]*b[1] + a[2]*b[2] + a[3]*b[3]</code>) from forming, but they’d be understandably incorrect. Using a new generator <code>G_c'</code> for the commitment of the public product is not sufficient. Let’s see why.</p>

<h2 id="the-issue">The issue</h2>

<p>Now that we have <code>= x^4*(a[3]*b[3]) + x^3*(a[2]*b[3] + a[3]*b[2]) + x^2(a[1]*b[3] + a[2]*b[2] + a[3]*b[1])+ x*(a[1]*b[2] + a[2]*b[1]) + a[1]*b[1]</code>, you’d think we could eliminate the coefficients of the <code>x^2</code> on <code>a[2]*b[2]</code> and <code>x^4</code> on <code>a[3]*b[3]</code>, and in fact we can! All we have to do is define <code>G_c'</code> to be <code>G_c' = G_c + x^-2*G_c + x^-4*G_c</code>. Each generator term will generate a copy of all terms in <code>c'</code> scaled by the coefficient on that generator term. Each term in the resultant expression of <code>c'*G_c'</code> that <em>isn’t</em> a term of the dot product of <code>a</code> and <code>b</code> should therefore be treated as a cross-term.</p>

<p>The problem with this is that this will generate 2 additional terms <strong>without</strong> an <code>x</code> coefficient that we’d have to commit to. To better visualize this process, let’s work it out:</p>
<pre><code>c' * G_c' =
  (x^4*(a[3]*b[3]) +
  x^3*(a[2]*b[3] + a[3]*b[2]) +
  x^2(a[1]*b[3] + a[2]*b[2] + a[3]*b[1]) +
  x*(a[1]*b[2] + a[2]*b[1])
  + a[1]*b[1]) * (
    G_c + x^-2*G_c + x^-4*G_c
  )

= ... honestly an expression that's too long for me to want to work out

Let's just see which terms would be left over without an `x` coefficient:

(a[1]*b[1])*G_c +  x^2(a[1]*b[3] + a[2]*b[2] + a[3]*b[1])*x^-2*G_c  + (x^4*(a[3]*b[3]))*x^-4*G_c =

a[1]*b[1]*G_c +  a[1]*b[3]*G_c + a[2]*b[2]*G_c + a[3]*b[1]*G_c + a[3]*b[3]*G_c =
(a[1]*b[1]*G_c + a[2]*b[2]*G_c + a[3]*b[3]*G_c) + a[1]*b[3]*G_c + a[3]*b[1]*G_c  =
</code></pre>

<p>Since we have additional terms <code>a[1]*b[3]*G_c</code> and <code>a[3]*b[1]*G_c</code> not scaled by <code>x</code>, this introduces a security concern. If the statement claims the dot product is 5, a prover could supply malicious vector values whose actual dot product is 3, while the additional unscaled cross-terms contribute the missing 2. The verifier would only see the combined value 5 and could not distinguish it from the claimed dot product.</p>

<h1 id="optimization-2---negative-exponents-on-b">Optimization 2 - Negative exponents on <code>b</code></h1>

<p>The next optimization is small but powerful. It allows us to fix the problem preventing us from running with the previous optimization. The insight for this optimization is using <strong>negative</strong> <code>x</code> exponents for the transformation step of <code>b</code>, and only <code>b</code>.</p>

<p>Instead of <code>b'</code> being defined as:</p>

<pre><code>b' = [
  b[1] + x*b[2] + x^2*b[3]
]
</code></pre>

<p>We make it:</p>
<pre><code>b' = [
  b[1] + (x^-1)*b[2] + (x^-2)*b[3]
]
</code></pre>

<p>To accommodate this change we also need to change how we modify the generator for the consistency check of <code>b'</code>. Instead of <code>G_b' = G_b[1] + (x^-1)*G_b[2] + (x^-2)*G_b[3]</code>, we’ll define <code>G_b'</code> as <code>G_b' = G_b[1] + x*G_b[2] + x^2*G_b[3]</code>. Now, because the <code>x</code>s of <code>a'</code> and <code>b'</code> will cancel out, we’ll get the following as the dot product:</p>
<pre><code>a' = [
  a[1] + x*a[2] + x^2*a[3]
]

b' = [
  b[1] + (x^-1)*b[2] + (x^-2)*b[3]
]

a'*b' = (a[1] + x*a[2] + x^2*a[3]) * (b[1] + (x^-1)*b[2] + (x^-2)*b[3])
= a[1]*b[1] + a[1]*(x^-1)*b[2] + a[1]*(x^-2)*b[3] + x*a[2]*b[1] + x*a[2]*(x^-1)*b[2] + x*a[2]*(x^-2)*b[3] + x^2*a[3]*b[1] + x^2*a[3]*(x^-1)*b[2]  + x^2*a[3]*(x^-2)*b[3]
# Now let's group the inner-product terms in the front of the expression
= (a[1]*b[1] + x*a[2]*(x^-1)*b[2] + x^2*a[3]*(x^-2)*b[3] ) + a[1]*(x^-1)*b[2] + a[1]*(x^-2)*b[3] + x*a[2]*b[1] + x*a[2]*(x^-2)*b[3] + x^2*a[3]*b[1] + x^2*a[3]*(x^-1)*b[2]
= (a[1]*b[1] + a[2]*b[2] + a[3]*b[3] ) + (x^-1)*(a[1]*b[2]) + (x^-2)*(a[1]*b[3]) + x*(a[2]*b[1]) + (x^-1)*(a[2]*b[3]) + x^2*(a[3]*b[1]) + x*a[3]*b[2]
= (a[1]*b[1] + a[2]*b[2] + a[3]*b[3]) + (x^-2)*(a[1]*b[3]) + (x^-1)*(a[1]*b[2] + a[2]*b[3]) + x*(a[2]*b[1] + a[3]*b[2]) + x^2*(a[3]*b[1])
</code></pre>

<p>In our verification step we just need to provide 4 cross terms (the coefficients on <code>x^-2</code>, <code>x^-1</code>, <code>x</code>, and <code>x^2</code>) and no modification of the committed dot product is needed!</p>

<h1 id="optimization-3---recursion">Optimization 3 - Recursion</h1>

<p>This optimization provides the biggest reduction in communication cost and it’s only possible with a slight modification of optimization 1. In this modified version, we don’t smush all the elements into 1 expression, we smush them into groups instead.
So this:</p>
<pre><code>a' = [
  a[1] + x*a[2] + x^2*a[3]
]

</code></pre>

<p>Becomes this:</p>
<pre><code>a' = [
  a[1] + x*a[2],
  a[3] + x*0
]
</code></pre>

<p>Keeping that in mind, the main idea of this recursion-focused optimization is having the prover replace giving <code>a'</code> and <code>b'</code>, and instead ask the verifier to <strong>create</strong> the commitments of <code>A' = commit(a')</code> and <code>B' = commit(b')</code> like they would in the consistency check. The prover then withholds the values of <code>a'</code> and <code>b'</code> and <em>just</em> gives the verifier <code>c'</code>. The prover also commits to c’ as C’. Using C, the cross-term commitments fixed before x, and the verifier’s challenge x, the verifier checks that C’ is the correct transformed inner-product commitment.</p>

<p>The assertion of whether the inner-product of <code>a'</code> and <code>b'</code> equals the prover-supplied <code>c'</code> is still unproven. The prover then asks the verifier to perform <em>another</em> inner-product argument using <code>A'</code>, <code>B'</code>, and <code>c'</code> to prove this assertion instead of having the verifier receive and perform the operation on <code>a'</code> and <code>b'</code> directly.</p>

<h2 id="why-skip-the-operation-step">Why skip the operation step?</h2>

<p>Because we group each cross-term by their <code>x</code> co-efficient, and the number of different cross-terms with different <code>x</code> coefficients scales with the size of <code>a'</code> and <code>b'</code>, this step aims to reduce the number of cross-terms by reducing <code>a'</code> and <code>b'</code>.</p>

<p>In our 3 element example, since we only send the cross-terms needed to make <code>A'</code> instead of <code>a'</code> directly, we don’t have to worry about the increase in the number of elements of <code>a'</code> with the grouping scheme. The idea is we can defer the inner-product operation of <code>a'</code> and <code>b'</code> and prove it with the operation of smaller <code>a''</code> and <code>b''</code> vectors instead.</p>

<p>It may be easier to reason why this works using an inductive framework. If we can assert the product of preimages for <code>A</code> and <code>B</code> with knowledge of <code>c = inner_product(a, b)</code>, we should also be able to assert the product of preimages for <code>A'</code> and <code>B'</code> with knowledge of <code>c' = inner_product(a', b')</code>. The only part we need to be careful about is properly linking A, B, and C to A’, B’, and C’.</p>

<h1 id="conclusion">Conclusion</h1>

<p>I’m over explaining the inner-product argument from the cited paper here so I’m going to wrap it up here. In a future post I’ll probably try to make a part 2 where I explain the bulletproofs version of the inner-product argument.</p>]]></content><author><name>Randy Naar</name></author><category term="Other" /><summary type="html"><![CDATA[Since the focus of my last post was on the mechanics of Hadamard product arguments, I was originally going to use them as a starting point for explaining inner product arguments. In the end I decided to take a different pedagogical approach rather than simply adapting the Hadamard argument to the inner product argument. Instead I’ll take another first principles approach. This will allow me to break down and re-contextualize the proof mechanisms we previously established and hopefully allow us to easier identify components for optimization.]]></summary></entry><entry><title type="html">Vectorized Product Arguments</title><link href="https://sovrandy.com/2026/06/19/vectorized-product-arguments.html" rel="alternate" type="text/html" title="Vectorized Product Arguments" /><published>2026-06-19T00:00:00+00:00</published><updated>2026-06-19T00:00:00+00:00</updated><id>https://sovrandy.com/2026/06/19/vectorized-product-arguments</id><content type="html" xml:base="https://sovrandy.com/2026/06/19/vectorized-product-arguments.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>In this post I’ll delve into what the vectorized product argument is and how it works. I was going back and forth trying to decide whether to group in the inner product argument here, but I now think the two concepts are different enough to warrant independent posts. Also, I suggest reading my last two posts on product arguments before reading this one since I assume knowledge of those explanations to simplify what’s written here. This argument is thus framed as an extension of sorts.</p>

<h2 id="some-things-to-know">Some things to know</h2>

<p>Maybe I should have brought this up in my previous post, but I am using <code>1</code>-based indexing when using array notation on sets or vectors. I also tend to implicitly assume the role of prover sometimes so know if you’re reading a snippet written in first person, I’m probably discussing a prover action.</p>

<p>Given that we’re working with vectors now and matrices going forward, I’d also like to mention that I refer to the <code>i</code>th <strong>column</strong> of matrix <code>A</code> as <code>a_i</code> and the <code>i</code>th <strong>row</strong> as a[i]. I’ll always use array notation when indexing into those rows or columns so the first element of the first column will be denoted as <code>a_1[1]</code>, and the first element of the first row will use <code>a[1][1]</code>.</p>

<p>Finally, as usual, I avoid using Pedersen commitments in my writing in exchange for a simpler mental model but Pedersen commitments <em>are</em> what’s used in practice. I believe once you understand the core of my post, extending to Pedersen commitments isn’t too big of a leap which is why I omit it. I may use them in my explanations in the future, but I feel like they’ll impede understanding in these explanatory posts more than they’ll help.</p>

<h2 id="vectorized-product-argument-hadamard-argument">Vectorized product argument (Hadamard argument)</h2>

<p>The difference between proving a Hadamard product as opposed to a simple product between integers is that instead of proving a product of committed values, we’re now proving <strong>multiple different</strong> products corresponding to <strong>different sets</strong> of committed values.</p>

<p>To ease into our understanding of the vectorized product argument, let’s try to create a product argument using vectors, where all vectors only have <strong>one</strong> element. If in the regular product argument we’re proving <code>product(a[i]) for i=1..n</code> is equal to <code>b</code>, then for the vectorized approach we can make each preimage <code>a[i]</code> its own one-dimensional vector <code>[a[i]]</code>. Instead of proving <code>b</code> is the product of secret preimages <code>a[1]</code> and <code>a[2]</code> like in the original product argument, the prover will now have 2 secret preimage <strong>vectors</strong> <code>[a[1]]</code> and <code>[a[2]]</code>. The verifier is then given 2 vectorized commitments <code>[A_1]</code> and <code>[A_2]</code>. Everything else in the proving process for the product argument is the same.</p>

<h2 id="how-does-this-approach-extend-to-vectors-with-multiple-elements">How does this approach extend to vectors with multiple elements?</h2>

<p>First, let’s add a restriction and say each vector has to be the same size. If the vectorized product argument can be seen as proving multiple product arguments at once, this restriction will force every product argument to have the same number of committed factors.</p>

<p>Our next step is to group terms of the same position across product arguments. So if <code>a[1][1]</code> and <code>a[1][2]</code> are the secret terms for one product argument, and <code>a[2][1]</code> and <code>a[2][2]</code> are the secret terms for another product argument, then the prover would respectively group the first and second terms into secret vectors like so:</p>

<p><code>v_first_terms = [a[1][1], a[2][1]]</code></p>

<p><code>v_second_terms = [a[1][2], a[2][2]]</code></p>

<p><code>product_terms = [b[1], b[2]]</code></p>

<p>If you think of them as column-vectors and assembled them into a matrix <code>[v_first_terms, v_second_terms, product_terms]</code>, each <code>row</code> would have the terms for its own product argument. Later in this post, whenever I refer to the <code>row</code>, I’m referring to the individual product arguments.</p>

<p>For each product argument, <code>a[1][1] * a[1][2] = b[1]</code> and <code>a[2][1] * a[2][2] = b[2]</code>, we run the same product argument described in the last post.</p>

<p>To recap, in this naive construction of a Hadamard argument, the prover’s public statement to the verifier now asserts that we know preimages for commitment vectors <code>v_1 = [A[1][1], A[2][1]]</code> and <code>v_2 = [A[1][2], A[2][2]]</code> that have a public Hadamard product of <code>product_terms = [b[1], b[2]]</code>.</p>

<h2 id="why-is-the-hadamard-product-important">Why is the Hadamard product important</h2>

<p>Why bother with introducing vectors and calling it a ‘Hadamard’ product argument if we can accomplish the same thing without the whole notion of a vector? The answer lies in the fact that there is a major speedup we can leverage by aggregating terms.</p>

<h3 id="the-optimization">The optimization</h3>

<p>Let’s review the single value argument to help us discover how we can optimize the vectorized product argument through aggregation.</p>

<p>The product argument shown in <code>Grokking Product Arguments Part 2</code> goes as follows:</p>
<ul>
  <li>Prover makes statement which includes commitments to preimages <code>a</code> and public intermediate products <code>b</code></li>
  <li>Prover also gives commitments to <strong>cross-terms</strong></li>
  <li>Verifier gives prover an integer <code>x</code></li>
  <li>Prover aggregates all <code>a</code>s into one term AND all <code>b</code>s  into another term using verifier’s <code>x</code></li>
  <li>Prover gives aggregate terms <code>a'</code> and <code>b'</code> to verifier</li>
  <li>Verifier computes the product of <code>a'</code> and <code>b'</code> which should contain <code>a*b</code> as a term</li>
  <li>Verifier re-creates the commitment of <code>a'*b'</code> using the commitment of the public product and cross term commitments</li>
</ul>

<p>In our naive vectorized approach, the statement includes commitments to <strong>each</strong> preimage <code>a</code>, but what if there was a way to aggregate commitments for each column vector into a single element – reducing the cost of sending <code>n</code> commitments in a vector to just sending one commitment?</p>

<p>The answer to that ended up being “yes,” which leads to another follow-up: whether a product argument can be constructed on the vector level using these single value vector commitments. As you can imagine, the answer to this is also “yes.” We’ll now focus on <em>how</em> the product argument can be done on the vector-level with single value commitments.</p>

<h2 id="the-new-commitment-function-for-vectors">The new commitment function for vectors</h2>

<p>We define <code>vector_commit(v)</code> for a vector <code>v</code> to be the sum of all elements in <code>v</code> <strong>after</strong> being run through the single value commitment scheme, with each value commitment in the vector using a <strong>different</strong> generator. In other words, a vector commitment can be thought of as the sum of its committed elements, where each element gets its own generator.</p>

<p>The commitment scheme that works for our purposes looks like this (where the generator used depends on <code>i</code>):
<code>commit(a_vec) = sum(commit(a[i], i)) for i=1..len(a_vec)</code></p>

<p>So now we get to figure out how all the other math follows from this new commitment scheme. Like usual, let’s work through an example of vectorizing, and then proving, <code>2</code> product arguments.</p>

<h2 id="example">Example</h2>

<p>Since we’re proving <code>2</code> product arguments, each vector will have <code>2</code> elements. We’ll also define each product argument to have <code>2</code> factors. Since we always set the number of vectors equal to the terms, you might <em>think</em> we only have <code>2</code> vectors. In actuality, it’ll lead to <strong><code>3</code></strong> vectors since there’s a vector for each term <strong>and</strong> the term we add in for randomness (referenced in part 2).</p>

<p>Let’s take a look at what each party has to perform at each step and make notice of the changes that come with the vectorized approach.</p>

<h3 id="statement">Statement</h3>

<p>First, assuming the role of the prover, let’s define our 3 secret column vectors <code>a_1 = [a_1[1], a_1[2]]</code>, <code>a_2 = [a_2[1], a_2[2]]</code>, and <code>a_3 = [a_3[1], a_3[2]]</code>. We also assert that the product of all elements at row index <code>i</code> across the <code>a</code> vectors equals the <code>i</code>th element of the public product vector, <code>b_n</code>. If all column vectors were appended into a matrix, we can conceptualize elements at the same index across column vectors as <code>rows</code>.</p>

<p>As part of our statement to the verifier, we’ll make the commitments for these column vectors <code>A_1 = A_1[1] + A_1[2]</code>, <code>A_2 = A_2[1] + A_2[2]</code>, and <code>A_3 = A_3[1] + A_3[2]</code>.</p>

<h3 id="cross-terms">Cross terms</h3>

<p>Since we’re assuming everything else works like the original product argument, we also give cross-term commitments for each <code>row</code> as if they were being proven independently.</p>

<h3 id="verifier-x">Verifier <code>x</code></h3>

<p>Now comes the verifier’s turn to choose <code>x</code>. When they do, instead of just scaling every <strong>term</strong> with a power of <code>x</code>, the prover now returns a vector sum where they scale every <strong>column vector</strong> with a power of <code>x</code> and get a resulting sum vector <code>a'</code> like so:</p>

<p><code>a' = a_1 + x*a_2 + x^2*a_3</code></p>

<p>This process is also done with the rolling product vectors <code>b_i</code>:</p>

<p><code>b' = b_1*x + b_2</code></p>

<p>To mirror the paper, I’ll put the random masking vector at the end of the <code>b</code> list. So in this example, <code>b_2</code> is the random vector and <code>b_1</code> is the intermediate rolling product vector. We have no <code>b_3</code> because that would just be the public product in the statement.</p>

<h3 id="verifying-the-vector">Verifying the vector</h3>

<p>The tweaked sum vector <code>a'</code> still needs to be checked, just like the tweaked single values did. To do this check, we can re-create the commitment of sum vector <code>a'</code>, <code>A'</code>, using the commitments of individual column vectors <code>A_i</code>. This looks like <code>A' = A_1 + x*A_2 + x^2*A_3</code>. A similar process is then done for the <code>b</code>s in <code>b'</code>, <code>B = x*B_1 + B_2</code>.</p>

<p>Like that, we’re still able to make this commitment scheme work for verifying derived vectors <code>a'</code> and <code>b'</code>!</p>

<h3 id="verifier-computes-product">Verifier computes product</h3>

<p>The process continues with the similarities in that we compute the Hadamard product between <code>a'</code> and <code>b'</code>. The big difference is the last step where we have to re-create the commitment of the resultant vector <code>a'*b'</code> with previously committed values. As a refresher, the committed values we use for this are the column vector commitments that were in the statement, and the cross-term commitments given at the beginning of the exchange.</p>

<p>Also, we will be using the <strong>zero argument</strong> optimization introduced in <code>Grokking Product Arguments Pt 2</code> since it still improves this version of the argument (which is why the exponent on the <code>x</code>s for <code>b_i</code> counted down above).</p>

<p>To solidify this product operation in our head, let’s work out the first row of this product vector below in terms of column vector elements:</p>

<ul>
  <li><code>a'[1]*b'[1]</code> =</li>
  <li><code>(a_1[1] + x*a_2[1] + x^2*a_3[1]) * (x*b_1[1] + b_2[1])</code> =</li>
  <li><code>x*a_1[1]*b_1[1] + x^2*a_2[1]*b_1[1] + x^3*a_3[1]*b_1[1] + a_1[1]*b_2[1] + x*a_2[1]*b_2[1] + x^2*a_3[1]*b_2[1]</code></li>
  <li><code>x^3*a_3[1]*b_1[1] + x^2*(a_2[1]*b_1[1] + a_3[1]*b_2[1]) + x*(a_1[1]*b_1[1] + a_2[1]*b_2[1]) + a_1[1]*b_2[1]</code></li>
</ul>

<p>To verify this product contains the public product in the old single value approach, we would just re-create the commitment of <code>a'*b'</code> using the commitments we were given for each row. This would work, but there IS a more efficient way to do this. Let’s transition into our last optimization.</p>

<h3 id="the-cross-terms">The cross terms</h3>

<p>If we look at the breakdown of each row <code>i</code> in <code>a'*b'</code>, we’ll see they’re ALL of the form:</p>
<ul>
  <li><code>x^3*a_3[i]*b_1[i] + x^2*(a_2[i]*b_1[i] + a_3[i]*b_2[i]) + x*(a_1[i]*b_1[i] + a_2[i]*b_2[i]) + a_1[i]*b_2[i]</code></li>
</ul>

<p>Every row has terms with the same powers of x, <code>x^0</code>, <code>x^1</code>, <code>x^2</code>, and <code>x^3</code>. If we re-defined our protocol to skip doing a <strong>per-row</strong> reconstruction of <code>a'*b'</code>, and instead summed every row, we can group cross terms with the same <code>x</code> factor which means the prover could also aggregate the grouped cross terms sent at the beginning of the argument. This would lessen the number of cross terms the prover has to send, therefore reducing the <strong>communication cost</strong>.</p>

<h3 id="attacking-our-aggregate-scheme">Attacking our aggregate scheme</h3>

<p>One problem that we’ve run into before is the vector for a prover to cheat if they only know the aggregate and not the individual terms. In our example, there is a way to only know the sum of rows for <code>a'*b'</code> and not know the individual Hadamard products for each individual row. By forgoing individual row checks, we are skipping validation of the individual product arguments and a malicious prover could forge a proof.</p>

<p>For example, the prover could assert that the product of row <code>a[1]</code> is <code>b[1]</code> and the product of row <code>a[2]</code> is <code>b[2]</code> but actually make the product of <code>a[1]</code> be <code>b[1] - 1</code> and the product of <code>a[2]</code> be <code>b[2] + 1</code>. Since we’re only checking the sum of the product arguments in the naive approach, this would validate even though we lied about what the individual products were!</p>

<p>To harden our scheme against this attack, we ask the verifier to choose a new value <code>y</code>. We’ll then incorporate <code>y</code> into our sum by doing <code>(a'*b')[i] *y^i</code>. Once we weight the rows with <code>y</code>, the fake aggregate becomes something like <code>(b[1] - 1)*y + (b[2] + 1)*y^2</code>, which no longer matches <code>b[1]*y + b[2]*y^2</code> except for unlucky choices of y.</p>

<p>The big question we have to ask now is <strong>where</strong> in our protocol should the verifier give us this <code>y</code> value?</p>

<p>If we asked the verifier to give the new <code>y</code> at the same time they gave <code>x</code>, this would stop us from aggregating the cross-terms since these coefficients to the <code>x</code> terms in the row sum now include a <code>y</code> term.</p>

<p>The verifier must now give <code>y</code> <strong>before</strong> the prover gives the cross term commitments for the aggregated row check. That way the prover can incorporate <code>y</code> into the cross term commitments and still have the verifier use <code>x</code> to scale what the product terms in the final sum <code>(a'*b')[i] * y^i</code> should be.</p>

<h2 id="link-to-inner-product-argument">Link to inner product argument</h2>

<p>If you look closely at the sum we performed in the step to reduce the number of final cross terms, you’ll see that it was an addition of all the elements in a vector that already represented an element-wise multiplication. Meanwhile, an inner-product is also the sum of elements in a vector resulting from an element-wise product. To prove we know the product of committed vectors <code>a_i</code>, we have computed the <code>y</code>-weighted <strong>inner product</strong> of <code>a'</code> and <code>b'</code>. We’ll leverage this fact in our next post.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The main idea behind the Hadamard argument is that it lets us take a bunch of product arguments with the same structure and prove them together. Each row can be thought of as its own product argument, but instead of sending every commitment and cross-term for every row separately, we stack matching terms into vectors and use vector commitments to keep communication small.</p>

<p>From there, the argument starts to look very similar to the single-value product argument. We still use <code>x</code> to aggregate terms inside each product argument, but after vectorizing, we now have multiple row-level derived products to account for. If we just added those row results together directly, a prover could potentially fake the split across rows while keeping the final aggregate looking correct. This is where <code>y</code> comes in. By weighting each row-derived product with a different power of <code>y</code>, the verifier makes it much harder for fake row values to cancel out in the aggregate.</p>

<p>This is also where the connection to inner products starts to show up. Once we multiply matching entries, scale them by powers of <code>y</code>, and add everything together, we are effectively doing a weighted inner product. So while the Hadamard argument starts as a way to prove element-wise products, the final aggregation step naturally leads us toward inner-product arguments.</p>

<p>In the next post, I’ll dig more directly into that inner-product view and show how the same aggregation ideas can be used as their own standalone argument.</p>]]></content><author><name>Randy Naar</name></author><category term="Other" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Grokking Product Arguments, Part 2</title><link href="https://sovrandy.com/2026/06/06/grokking-product-arguments-pt-2.html" rel="alternate" type="text/html" title="Grokking Product Arguments, Part 2" /><published>2026-06-06T00:00:00+00:00</published><updated>2026-06-06T00:00:00+00:00</updated><id>https://sovrandy.com/2026/06/06/grokking-product-arguments-pt-2</id><content type="html" xml:base="https://sovrandy.com/2026/06/06/grokking-product-arguments-pt-2.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>After starting to draft a post about vectorized product arguments, I realized one of the concepts I was introducing would better be explained in the scalar context first. This concept is another way to make a product argument, something I wrote about in my last post. Both the method in my last post, and the method in this post, are featured in the paper I’m using as reference, ‘Efficient Zero-Knowledge Argument for Correctness of a Shuffle’.</p>

<p>I decided to give this product argument special treatment because it’s used as part of the vectorized Hadamard product argument, but it’s never explained in the simpler case of scalars. Understanding this will prove crucial for understanding the vectorized argument without banging your head against the wall for hours like I did. Know that I do take some liberties with my construction in an effort to make everything easier to follow, but the final result should lead to a very similar construction to the argument presented for Hadamard products in the reference paper.</p>

<h2 id="some-things-to-know">Some things to know</h2>

<p>Just like in my last post, I abstract away Pedersen commitments to focus on the meat of the ideas. If you’re having trouble generalizing the scheme to work with Pedersen commitments, feel free to leave an issue on this blog’s Github repo.</p>

<p>Also, in this post I use the word <strong>tweak</strong> a lot, so I thought I’d define what I mean when I use it before we get into the nitty-gritty. To quote Michael Folkson, who <a href="https://bitcoin.stackexchange.com/questions/113232/what-does-it-mean-to-tweak-a-public-key">answers what it means in the context of public keys</a>, and who in turn quoted <a href="https://github.com/bitcoinops/taproot-workshop/blob/master/2.2-taptweak.ipynb">Bitcoin Optech</a>, “Tweaking a public key means to alter it with a value (the tweak) such that it remains spendable with knowledge of the original private key and tweak.” I use it more to mean performing an operation on a value to get another related value. It may be an abuse of the term, but I use it in this way throughout my post.</p>

<h2 id="alternative-proof-approach-to-single-product-argument">Alternative Proof Approach to single product argument</h2>

<p>The single product argument involves proving that the product of numbers <code>a[i]</code> for <code>i=1..n</code> equals <code>b</code> without revealing any of the <code>a[i]</code>s. I covered the mechanics for this in my post ‘Grokking Product Arguments’, but now, I’d like to show an alternative way we can prove the same thing.</p>

<p>If you recall, the general approach for the product argument presented in part 1 was:</p>
<ul>
  <li>have prover give a commitment to “cross-terms”</li>
  <li>have the verifier give the prover <code>x</code></li>
  <li>have the prover make derived values <code>a[i]'</code> using <code>a[i]</code>s and <code>x</code></li>
  <li>have the verifier multiply tweaked terms to get derived product <code>b'</code></li>
  <li>have verifier re-create derived product using commitments</li>
</ul>

<p>In the alternative approach to this argument, the prover proposes a new tweaking mechanism. Instead of tweaking each term <code>a[i]</code> into <code>a[i]'</code> and returning each <code>a[i]'</code> separately, we can <strong>aggregate</strong> our tweaked terms <code>a[i]'</code> <em>before</em> sending them to the verifier. Doing this would lower the number of distinct elements we have to give the verifier and turns <code>n</code> prover-given elements into <code>1</code>. This aggregation is done by summing the tweaked terms <code>a[i]'</code> together like so: <code>a[1]' + a[2]' + ...</code>. Believe it or not, this aggregation also opens the door for <em>another</em> optimization – removing the <code>r[i]</code> terms.</p>

<p>Because summing the terms together proves an effective way to hide the individual values, you can think of <code>a[i+1]</code> serving as the new <code>r[i]</code> for <code>a[i]</code>. One thing to note is that despite the prover’s ability to effectively hide the individual values, we would still like to add a random element <code>r</code> to not leak the <code>x</code>-scaled sum. Adding the random term <code>r</code> also helps adhere to our definition of, what the reference paper describes as, a ‘special honest zero knowledge argument’. I won’t go into the definition for what that is here, but it provides some guidelines on how to structure our protocol to be a special kind of zero knowledge argument.</p>

<p>So is that it? Optimization complete? Well, no. Some readers may notice that <em>just</em> replacing the <code>r[i]</code>s with a single <code>r</code> and summing the tweaked terms together will break one of the checks performed by the verifier. The verifier now can’t verify whether the prover knows every individual term that was committed to, or just the sum.</p>

<p>Usually the verifier would check each tweaked term <code>x*a[i] + r[i]</code> by using the commitments <code>A[i]=commit(a[i])</code> and <code>R[i]=commit(r[i])</code> returned by the prover like so <code>x*A[i] + R[i]</code>, but now, if the verifier is just given <code>a'</code>, the prover can fake knowing the individual preimages <code>a[i]</code>. So how do we fix this?</p>

<p>One way is to have the verifier give <strong>each term</strong> a unique coefficient <code>x[i]</code>. Doing this prevents the raw sum of <code>a[i]</code>s from being forged since the prover would have to find <code>a'</code> such that <code>a'*G = R + x[1]*A[1] + x[1]*A[2] + ... + x[n]*A[n]</code>, <strong>without</strong> knowing the individual <code>a[i]</code>s and only knowing the aggregate sum. To solve this we’d need to solve the elliptic curve discrete log problem for which all modern EC crypto-systems are based. So pretty unlikely to happen.</p>

<p>Well there’s one more challenge we’d like to overcome. Requiring the verifier to choose <code>n</code> <code>x</code>s increases the number of elements the verifier will have to send the prover. In the literature this metric is known as the <strong>communication cost</strong>. Our next challenge is to try to reduce this communication cost, and lucky for us.</p>

<p>But before I get into that, I’d like to modify the protocol a little bit to hopefully simplify the next explanation. Including the <code>r</code> in the derivation can be a bit tedious and generally serves as another piece of information we have to keep track of. What the reference paper does is prepend <code>r</code> into the list of preimages <code>a[i]</code>, turning <code>r</code> to <code>a[1]</code>. This would make the existing protocol stay mostly the same, except now, we can implicitly <strong>re-define</strong> <code>n</code> to be the number of elements in <code>a</code> <em>including</em> the <code>r</code> element. Alright, now let’s get back to how we’re going to tackle reducing the communication cost of the <code>x</code>s.</p>

<p>Instead of having the verifier pick <code>n</code> <code>x</code>s where <code>n</code> is the number of <code>a</code>s, the verifier can give the prover one verifier-chosen <code>x</code> and have the prover derive the other coefficients from it. More concretely, the verifier asks the prover to define <code>x[i]</code> as <code>x[i]=x^(i - 1)</code> for <code>i=1..n</code>. That way the verifier only needs to give one value to the prover, <code>x</code>. Problem solved!</p>

<p>Ok, one last thing. In our previous post we had the distinction of the simple case and the case of rolling products. This product argument has no such distinction given the <code>2</code> element case balloons the number of <code>a</code>s to <code>3</code> because of the implicit <code>r</code>. Since every case turns into the rolling case of <code>len(a[i]) &gt; 2</code>, we’ll be using the same concept of intermediate products <code>b[i]</code> introduced in the previous post. <code>b[i]</code> being defined as <code>b[i]=a[1]*...*a[i]</code>.</p>

<p>Also similar to our previous post, we will need to tweak these intermediate products to keep them secret. To do this, we’ll tweak them the same way we tweaked our list of preimages <code>a</code>. We’ll include a random element for <code>b[1]</code> and create a sum of intermediate products scaled by different powers of <code>x</code>.</p>

<p>Now, let’s run our explanation of the product argument back with this new and improved protocol.</p>

<h2 id="what-is-the-product-argument">What is the product argument?</h2>

<p>To reiterate from my first post, a <strong>product argument</strong> is a protocol between a prover and a verifier where the prover aims to convince a verifier that the preimages of <code>n</code> committed values, <code>a[i]</code> for <code>i&lt;=n</code>, have a public product <code>b</code>. Just like in my previous post, I’ll be focusing on a simple case to better cement the idea with an example. This simple case will be when <code>n=3</code> since we’ll need a random term to be included even when we’re just interested in the product of 2 committed values.</p>

<p>I’ll also add that as part of the statement being made, for which we need to make this argument for, the prover gives the verifier commitments for every <code>a[i]</code> and commitments to the rolling intermediate products <code>b[i]</code>.</p>

<p>If we’re focusing on the case where <code>n=3</code>, we’ll have 2 <code>b</code>s, <code>b[1]</code> and <code>b[2]</code>.</p>

<h2 id="proof-approach">Proof Approach</h2>

<p>The idea for this new scheme is that the prover will give the verifier commitments to constant values, create derived values <code>a'</code> and <code>b'</code> using verifier-sourced randomness <code>x</code>, ask the verifier to compute the product of the derived value <code>a'</code> with <code>b'</code>, and have the verifier use <code>x</code> in an expression with the pre-committed constants to re-create the product of the derived values. Let’s start the explanation of this argument by diving into what the constants are.</p>

<h3 id="what-are-the-constants-given-at-the-beginning-of-the-protocol">What are the constants given at the beginning of the protocol?</h3>

<p>The constants given to the verifier at the start of the argument are commitments to the same <strong>cross terms</strong> <code>d[i]</code> we described in part 1. These constants are given to the verifier <strong>before</strong> the verifier sends the prover their randomness <code>x</code>.</p>

<p>To make a product argument, we’ll eventually have to compute the product of tweaked values <code>a'</code> and <code>b'</code>. Since these values are tweaked, the tweaked product won’t only contain the public product we’re trying to prove as a term with an <code>x</code> coefficient, it’ll also include a bunch of extra terms. The coefficients of these extra terms can be defined before the verifier sends the prover <code>x</code>, and then later scaled by powers of <code>x</code> when the verifier re-creates the derived product.</p>

<p>If the committed coefficients do not describe the right polynomial structure, the prover is reduced to hoping the verifier’s random <code>x</code> still fits with their fake values. This is very unlikely, which makes this construct effective in preventing the prover from cheating. Just like in the previous product argument, allowing these constants to be chosen freely after seeing <code>x</code> can lead to a forged proof, so it’s imperative that the prover coughs up these values at the very start of the argument.</p>

<h3 id="what-are-the-derived-values">What are the derived values?</h3>

<p>Once the cross term commitments have been sent by the prover, the verifier can now send them a random <code>x</code> term the prover can use to make derived values <code>a'</code> and <code>b'</code>. These derived values can be made using the following expressions:
<code>a' = sum(x^(i - 1) * a[i])</code> for <code>i=1..n</code>
and <code>b'=sum(x^(i - 1) * b[i])</code> for <code>i=1..n</code></p>

<h3 id="proving-the-derivation-uses-a">Proving the derivation uses <code>a</code></h3>

<p>As the verifier, the only term we know from the expression of the derived <code>a' = sum(x^[i]*a[i])</code> is <code>x</code>. We don’t know any of the <code>a[i]</code> prover secrets. Since the verifier has given commitments to <code>a[i]</code> as part of the original statement, those commitments can now be used to link the secret preimages <code>a[i]</code>s to the public derived <code>a'</code>.</p>

<p>We do this by acknowledging that the commitment <code>A'</code> is computable using the commitments given to us at the start of the protocol, <code>A[1]</code>, <code>A[2]</code>, and <code>A[3]</code>. We then compute <code>A[1] + x*A[2] + x^2*A[3]</code> and do an equality check with <code>A'</code> to finish proving the link between the <code>a[i]</code>s and <code>a'</code>.</p>

<h2 id="proving-we-know-the-product-of-a1-a2-and-a3">Proving we know the product of a[1], a[2], and a[3]</h2>

<p>After proving the derived value <code>a'</code> is indeed derived from <code>a[i]</code>, we need to show how the public product <code>b</code> can be derived from <code>a'</code> given <code>a[1]</code> and <code>a[2]</code> are both contained within the expression. In the previous product argument, we had a derivation for each term and had a way to link the product of the derived <code>a[i]'</code>s to the public product via reconstructing the commitment of the product between the derived <code>a'</code>s. In this model, we’re just given a single sum of tweaks and have yet to find an operation on this sum that will yield the product of <code>a[1]</code>, <code>a[2]</code>, and <code>a[3]</code>. So the question is “what operation can we make on <code>a'</code> that will yield an expression with the product of <code>a[1]</code>, <code>a[2]</code>, and <code>a[3]</code> as a term?”</p>

<p>This is where the derived value <code>b'</code> comes in. We can multiply the derived sum <code>a'</code> with the sum of derived intermediate products <code>b'</code> to get an expression that suits our needs. If we think about the breakdown of the product from these derived sums, we see:</p>
<ul>
  <li><code>a'*b'</code> =</li>
  <li><code>(a[1] + x*a[2] + x^2*a[3]) * (b[1] + x*b[2])</code> =</li>
  <li><code>(a[1] + x*a[2] + x^2*a[3]) * (a[1] + x*a[1]*a[2])</code> =</li>
  <li><code>a[1]*b[1] + x*a[1]*b[2] + x*a[2]*b[1] + x^2*a[2]*b[2] + x^2*a[3]*b[1] + x^3*a[3]*b[2]</code></li>
</ul>

<p>We can see the original product <code>a[1]*a[2]*a[3]</code> sitting right there as the coefficient to <code>x^3</code>, since <code>a[3]*b[2] = a[3]*a[1]*a[2]</code>.</p>

<p>To verify the expression above, we then take the <strong>commitment</strong> of <code>a'*b'</code> and recreate it using the cross-term commitments sent at the beginning of the argument and other verifier-known values. For the case of <code>n=2</code> (with the random term <code>a[1]</code>), the verifier wants to recreate commitments to the following terms that were coefficients to <code>x^i</code> in the expression above:</p>
<ul>
  <li><code>a[1]*b[1]</code></li>
  <li><code>x*(a[1]*b[2] + a[2]*b[1])</code></li>
  <li><code>x^2*(a[2]*b[2] + a[3]*b[1])</code></li>
  <li><code>x^3*b</code></li>
</ul>

<p>The constant value commitments mentioned earlier represent commitments to the extra coefficient terms in this derived product. By “extra coefficient terms”, I mean everything that’s not directly the final public product. Meanwhile, the final <code>x^3*b</code> term is computable by the verifier because <code>b</code> is public.</p>

<h2 id="running-through-the-protocol-end-to-end">Running through the protocol end to end</h2>

<p>Let’s recap this new protocol again to summarize the steps.</p>

<p>The prover starts by sending over the commitments to all possible cross terms between the secret intermediate values <code>b[i]</code> and the secret preimages <code>a[i]</code>. If <code>n=3</code>, the following represents all cross term products between <code>a'</code> and <code>b'</code>:</p>

<ul>
  <li><code>a[1]*b[1]</code></li>
  <li><code>(a[1]*b[2] + a[2]*b[1])</code></li>
  <li><code>(a[2]*b[2] + a[3]*b[1])</code></li>
</ul>

<p>You might notice that <strong>included</strong> in these cross terms are some the intermediate products <code>b[i]</code>. For example, the <code>a[2]*b[1]</code> in the second term actually evaluates to <code>b[2]</code> This is because of the relationship between <code>a[i]</code> and <code>b[i]</code>:
<code>b[i]=b[i-1]*a[i]</code> for <code>i &gt; 1</code></p>

<p>Now that the verifier has these values, they pick a random value <code>x</code> to give to the prover.</p>

<p>The prover responds with the derived values <code>a'</code> and <code>b'</code>:</p>
<ul>
  <li><code>a'</code> will be defined as <code>a' = a[1] + x*a[2] + x^2*a[3]</code>.</li>
  <li><code>b'</code> will be defined as <code>b' = b[1] + x*b[2]</code>.</li>
</ul>

<p>It’s now the verifier’s job to multiply these values together and verify the constants we got at the beginning can be used to make the derived product <code>a'*b'</code> when plugged into our expected algebraic structure. Here, our expected algebraic structure is:</p>
<ul>
  <li><code>a'*b'</code></li>
  <li><code>(a[1] + x*a[2] + x^2*a[3]) * (b[1] + x*b[2])</code></li>
  <li><code>a[1]*b[1] + x*a[1]*b[2] + x*a[2]*b[1] + x^2*a[2]*b[2] + x^2*a[3]*b[1] + x^3*a[3]*b[2]</code></li>
  <li><code>x^3*a[3]*b[2] + x^2*(a[2]*b[2] + a[3]*b[1]) + x*(a[1]*b[2] + a[2]*b[1]) + a[1]*b[1]</code></li>
</ul>

<p>So we need to multiply:</p>
<ul>
  <li><code>x^3</code> by the commitment for the final product <code>b</code>, since <code>a[3]*b[2] = a[1]*a[2]*a[3]</code></li>
  <li><code>x^2</code> by the commitment for <code>a[2]*b[2] + a[3]*b[1]</code></li>
  <li><code>x</code> by the commitment for <code>a[1]*b[2] + a[2]*b[1]</code></li>
</ul>

<p>Then sum all of those values together with <code>a[1]*b[1]</code> to make sure it’s equal to <code>a'*b'</code>. And that’s it!</p>

<h1 id="optimizing-away-a-term">Optimizing away a term</h1>

<p>There is one more optimization on the protocol we can make by changing the definition of <code>b'</code>. This alternate expression for <code>b'</code> uses the derivation <code>b'=sum(x^(m - i - 1) * b[i])</code>, with the exponent on the <code>x</code> counting <strong>down</strong> against the increasing indices of <code>b</code>. For our <code>n=3</code> example, where we only have <code>b[1]</code> and <code>b[2]</code> as intermediate products, this means:</p>

<p><code>b' = x*b[1] + b[2]</code></p>

<p>So how does this help us?</p>

<p>In the paper I’m using as reference, ‘Efficient Zero-Knowledge Argument for Correctness of a Shuffle’, something called the <strong>zero argument</strong> is introduced. In the scalar version we’re using here, the zero argument can be understood as a cancellation trick. Aside from making the powers of <code>x</code> in <code>b'</code> count down instead of up, we also add one extra value to <code>b</code> side that equals the negative sum of the rest of the terms in <code>b</code>. This will cause one of the terms in the product <code>a'*b'</code> to equal the sum of intermediate products scaled by a power of <code>x</code>. Since the extra <code>b</code> value was chosen as the negative of the others, the coefficient on that <code>x</code> term now cancels to 0. This lets the verifier replace one reconstructed coefficient with a zero check.</p>

<p>This is easier to appreciate in the full vector argument because the zero argument is doing real work across many values at once. But even in this scalar walkthrough, the key idea is changing the direction of the powers lines terms up so that one part of the derived product can be compared to 0 instead of being reconstructed as another committed coefficient.</p>]]></content><author><name>Randy Naar</name></author><category term="Other" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Grokking Product Arguments, Part 1</title><link href="https://sovrandy.com/2026/05/23/grokking-product-arguments.html" rel="alternate" type="text/html" title="Grokking Product Arguments, Part 1" /><published>2026-05-23T00:00:00+00:00</published><updated>2026-05-23T00:00:00+00:00</updated><id>https://sovrandy.com/2026/05/23/grokking-product-arguments</id><content type="html" xml:base="https://sovrandy.com/2026/05/23/grokking-product-arguments.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>This is a follow-up post to the series I’m making about zero knowledge arguments. In this post I’ll be describing a way to prove the product of n hidden values using something called the ‘product argument’. I’ll focus on the case with just 2 hidden values at first, then inch my way closer to the reference method described in the Bayer–Groth single-value product argument.</p>

<p>I think this will serve as an interesting primitive to any zk-practitioner’s toolbox and is integral in understanding the Bulletproofs confidential transaction scheme that I’ll describe in a future post.</p>

<p>Before reading this, you should know that I take the liberty of making a <strong>commitment</strong> of a value mean the value is being multiplied by a generator <code>G</code> on the <code>secp256k1</code> curve (e.g., <code>commit(a) = a*G</code>). In practice, we’d actually need something like a Pedersen commitment to make these schemes secure. Pedersen commitments add another randomness factor, and although I opted for simple non-Pedersen commitments, know that the high-level algebra doesn’t change much when this randomness is introduced.</p>

<h2 id="what-is-the-product-argument">What is the product argument?</h2>

<p>The <strong>product argument</strong> is a protocol between a prover and a verifier where the prover aims to convince a verifier that the preimages of <code>n</code> committed values have a public product <code>b</code>. I’ll be focusing on the case where <code>n=2</code> to introduce the subject.</p>

<h2 id="proof-approach">Proof Approach</h2>

<p>The product argument works by taking a similar proof approach to the sum argument. The idea is that the prover will give the verifier constant values, create derived values <code>a1'</code> and <code>a2'</code> using verifier-sourced randomness <code>x</code>, ask the verifier to compute the product of the derived values <code>a1'</code> and <code>a2'</code>, and use <code>x</code> in an expression with our committed constants to re-create the product of derived values.</p>

<h3 id="what-are-the-derived-values">What are the derived values?</h3>

<p>The formulas for the derived values are <code>a1' = a1*x + d1</code> and <code>a2' = a2*x + d2</code>, where <code>x</code> is the public verifier-sourced randomness, and <code>d1</code> and <code>d2</code> are secret random values chosen by the prover.</p>

<p>The commitments to the values <code>d1</code> and <code>d2</code> (<code>D1</code> and <code>D2</code>) are the constants given to the verifier <strong>before</strong> the verifier sends us their randomness <code>x</code>. This is to prevent the prover from cheating since letting the prover arbitrarily choose <code>d1</code> and <code>d2</code> after <code>x</code> has been revealed will allow them to forge a proof.</p>

<p>The operations performed on the original preimages to create the derived values can all be audited by the verifier, even without knowing <code>d1</code> and <code>d2</code>. Let’s take a look at how this can be done by assuming the role of the verifier.</p>

<h3 id="proving-the-derivation-uses-a">Proving the derivation uses <code>a</code></h3>

<p>As the verifier, the only term we know from the expression of the derived value <code>a1' = a1*x + d1</code> is <code>x</code>. We don’t know <code>a1</code> or <code>d1</code>, since those are prover-generated secrets, but we’re determined to check that <code>a1'</code> was actually derived from <code>a1</code>.</p>

<p>The trick to this is linking the commitment of <code>a1</code> (<code>A1 = a1*G</code>) to the commitment of <code>a1'</code> (<code>A1' = (x*a1 + d1)*G = x*A1 + D1</code>).</p>

<p>Because of the algebraic properties of commitments, the commitment <code>A1'</code> is computable using <code>A1</code>, which comes from the prover’s original statement, and <code>D1</code>, which is one of the commitments given by the prover at the start of the interaction:
<code>commit(a1') == x*A1 + D1</code></p>

<p>The same process can be done for <code>a2'</code> using one of the other commitment constants, <code>D2</code>.</p>

<h2 id="proving-we-know-the-product-of-a1-and-a2">Proving we know the product of a1 and a2</h2>

<p>After proving the derived values are indeed derived from <code>a1</code> and <code>a2</code>, we need to show how the derived product <code>a1'*a2'</code> relates back to the original product of <code>a1</code> and <code>a2</code>.</p>

<p>We can do this by showing the original product of <code>a1</code> and <code>a2</code> exists as a term in the derived product <code>a1'*a2'</code>. Let’s take a look at what the derived product of <code>a1'</code> and <code>a2'</code> look like when expressed in terms of <code>a1</code> and <code>a2</code> (liberally grouping the terms by their power of <code>x</code>):
<code>(a1*x + d1) * (a2*x + d2) = x^2*(a1*a2) + x*(a1*d2 + a2*d1) + (d1*d2)</code></p>

<p>We can see the original product <code>a1*a2</code> sitting right there as the coefficient to <code>x^2</code>. To re-create this term, the verifier just needs to multiply the public product <code>b</code> with <code>x^2</code>.</p>

<p>The problem is the other two terms (the cross-terms). We can’t create the coefficient of the second term without <code>(a1*d2 + a2*d1)</code>, or the 3rd term without <code>(d1*d2)</code>. Since the <code>a</code>s and <code>d</code>s are all secrets held by the prover, the prover can’t give these raw values to the verifier to check with.</p>

<p>Instead, the prover gives the <strong>commitments</strong> to these cross-terms, let’s call them <code>E1 = commit(a1*d2 + a2*d1)</code> and <code>E0 = commit(d1*d2)</code>, in the beginning of the interaction as ‘constants’ to the verifier (along with <code>D1</code> and <code>D2</code>). Verifying the equality by using the commitments of each term now becomes feasible:
<code>commit(a1'*a2') == x^2*commit(a1*a2) + x*E1 + E0</code></p>

<p>Why does the prover have to give these <code>E1</code> and <code>E0</code> commitments <strong>before</strong> <code>x</code> is chosen? Well, if the prover were allowed to wait until after they received <code>x</code>, they could just lie about the product. The problem would reduce to replacing the product commitment <code>commit(a1*a2)</code>in the following equation with an arbitrary value <code>fake_product</code> and solving for <code>E0</code> like so:</p>

<p><strong>Original equality</strong>:</p>

<p><code>commit(a1'*a2') == x^2*commit(a1*a2) + x*E1 + E0</code></p>

<p><strong>Swap out <code>a1*a2</code> with <code>fake_product</code></strong>:</p>

<p><code>commit(a1'*a2') == x^2*commit(fake_product) + x*E1 + E0</code></p>

<p><strong>Set <code>E1</code> to any random value and solve for <code>E0</code></strong>:</p>

<p><code>E0 = commit(a1'*a2') - x^2*commit(fake_product) - x*E1</code></p>

<h2 id="rolling-products">Rolling products</h2>

<p>Now let’s extend this idea to a rolling product. This means I want to prove <code>b = a1 * a2 * a3</code> without revealing <code>a1</code>, <code>a2</code>, <code>a3</code>, OR the intermediate product of any chosen subset of those elements.</p>

<p>If I wanted to extend our concept to this, I don’t just need the commitments of <code>a1</code>, <code>a2</code>, and <code>a3</code>. I also need the commitment to the intermediate product, which we’ll call <code>b_12 = a1*a2</code> (assuming I’m multiplying from left to right).</p>

<p>Then I could just perform the product argument twice! One argument proving <code>b_12 = a1*a2</code> while keeping <code>a1</code> and <code>a2</code> hidden, and a second argument proving <code>b = b_12*a3</code> while keeping <code>a3</code> hidden. The only changes we’d have to make to our original product argument is having the prover give the verifier the commitment <code>B_12 = commit(b_12)</code> instead of a public scalar product <code>b</code>, and giving a commitment to <code>b_12</code>, <code>b_12' = x*b_12 + d_b</code> (where <code>d_b</code> is a new constant).</p>

<p>Taking this approach is <em>much</em> easier than doing a direct proof where we multiply all the derived values in one go, because then we’d be dealing with a higher degree polynomial and a nightmare of terms:</p>
<pre><code>a1'*a2'*a3' = (a1*x + d1) * (a2*x + d2) * (a3*x + d3) =
x^3*(a1*a2*a3) + x^2*(a1*a3*d2 + a2*a3*d1 + a1*a2*d3) + x*(a3*d1*d2 + a1*d2*d3 + a2*d1*d3) + d1*d2*d3
</code></pre>
<p>I had trouble just typing that. Proving intermediate product commitments and keeping each argument to just 2 terms for the sake of simplicity just makes sense.</p>

<h2 id="rolling-product-optimizations">Rolling product optimizations</h2>

<p>Let’s review the current rolling product process. As mentioned above, the prover creates a derived value for the intermediate product using a new  blinder <code>d_b</code>:
<code>b_12' = b_12*x + d_b</code></p>

<p>With the straightforward product argument, the prover has to send the cross-term commitments (<code>E1</code> and <code>E0</code>), the derived intermediate product <code>b_12'</code>, AND <code>B_12</code>. I’d like to pose the question: can we get away with the prover <em>never</em> transmitting <code>B_12</code>? This would allow us to reduce the amount of data the prover sends the verifier.</p>

<p>Normally, if the prover provides the cross-terms but withholds <code>B_12</code>, the verifier would be stuck stuck. When the verifier would calculate <code>commit(a1'*a2')</code>, they’d have no way to check it, because the <code>x^2 * B_12</code> could not be calculated with <code>B_12</code>.</p>

<p>But if the derived intermediate value is <code>b_12' = b_12*x + d_b</code>, that means its commitment is:
<code>commit(b_12') = x*B_12 + D_b</code></p>

<p>Notice that this expression <strong>contains</strong> <code>B_12</code>, and this can be used to our advantage to eliminate <code>B_12</code> from the verifier’s check. If we multiply <code>commit(b_12')</code> by <code>x</code>, we get:
<code>x * commit(b_12') = x^2*B_12 + x*D_b</code></p>

<p>Now, let’s look at the verifier’s main equation for the multiplied derived values:
<code>commit(a1'*a2') = x^2*B_12 + x*E1 + E0</code></p>

<p>If we subtract the first equation (the original derived product expression) from the second equation (the expression with the intermediate value), the <code>x^2 * B_12</code> terms completely cancel each other out:
<code>commit(a1'*a2') - x*commit(b_12')</code>
<code>= (x^2*B_12 + x*E1 + E0) - (x^2*B_12 + x*D_b)</code>
<code>= x*(E1 - D_b) + E0</code></p>

<p>The prover can simply combine their commitments and send <code>(E1 - D_b)</code> as a single cross-term, along with <code>E0</code>. The verifier computes <code>commit(a1'*a2') - x*commit(b_12')</code>, and checks if it equals <code>x*(E1 - D_b) + E0</code>.</p>

<p>Why does this work as a replacement to giving <code>B_12</code> and checking to see if it’s in the original product and the intermediate value? Well let’s think about it from the angle of trying to cheat the verifier as the prover. If the prover lied about <code>b_12'</code> actually containing <code>a1*a2</code>, then the <code>x^2</code> terms in the subtraction of the 2 equations would not match and could never be eliminated. Doing so would require the prover to commit to a value, that when multiplied by <code>x</code>, can magically get rid of the <code>x^2</code> term and equal an expression made with prover-provided pre-committed values.</p>

<p>By verifying this way, the prover never has to send <code>B_12</code> over the network, saving precious bytes in our proof size.</p>

<h1 id="conclusion">Conclusion</h1>

<p>After going through the mechanics of the product argument, hopefully you see that it’s not too different from the sum argument. It’s a little bit more involved since the derived values <code>x*a1 + d1</code> and <code>x*a2 + d2</code> are polynomials, forcing us to do polynomial multiplication, but I believe the principles used in the argument are a natural extension to the principles used to build sum arguments. My next post will somewhat build on the concepts here to explain product arguments in the vectorized setting and get into how we can prove the inner-product of vectors in zero knowledge. Stay tuned!</p>]]></content><author><name>Randy Naar</name></author><category term="Other" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Proving Sums from Commitments</title><link href="https://sovrandy.com/2026/05/03/proving-sums-from-commitments.html" rel="alternate" type="text/html" title="Proving Sums from Commitments" /><published>2026-05-03T00:00:00+00:00</published><updated>2026-05-03T00:00:00+00:00</updated><id>https://sovrandy.com/2026/05/03/proving-sums-from-commitments</id><content type="html" xml:base="https://sovrandy.com/2026/05/03/proving-sums-from-commitments.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>This post builds intuition for the zero-knowledge arguments needed to understand modern range proof systems like Bulletproofs. It does this by introducing the sum argument, a simple technique for proving that hidden values add up to a known total without revealing said hidden values.</p>

<p>I decided to write this because, when I first encountered these schemes, I could follow the mechanics but struggled to convince myself they were actually secure. This post is my attempt to build that missing intuition.</p>

<p>The paper that finally helped things click for me was Efficient Zero-Knowledge Argument for Correctness of a Shuffle. I spent hours working through the same sections and trying to understand the ideas from different angles. The sum argument I’ll present here is not described explicitly in that paper, but it follows the some of the same underlying logic. The rest of this post focuses on explaining that argument as simply as possible.</p>

<h2 id="what-is-the-sum-argument">What is the sum argument?</h2>

<p>Let’s start with a definition. The <strong>sum argument</strong> is a protocol between a <strong>prover</strong> and a <strong>verifier</strong> where the prover shows that two committed values, <code>A = commit(a)</code> and <code>B = commit(b)</code>, correspond to hidden values that add up to some known value <code>c</code>, without revealing what <code>a</code> and <code>b</code> are.</p>

<p>As a refresher, a commitment is the output of a function that is both hiding and binding. Hiding means it is hard to recover the input from the output. Binding means it is hard to find two different inputs that produce the same output. The concrete commitment function I will use is defined on the secp256k1 curve. It takes a scalar and maps it to a point on the curve, which we refer to as a <strong>commitment</strong>.</p>

<h2 id="proof-approach">Proof approach</h2>

<p>The goal is to show that <code>c</code>, or its commitment <code>C = commit(c)</code>, can be constructed in two different ways using verifier-known values. These constructions need to be algebraically linked to the hidden values <code>a</code> and <code>b</code> in a way that would be very difficult to fake. At the same time, they cannot leak any information about <code>a</code> or <code>b</code>.</p>

<p>So the question becomes: how do we build expressions that are linked to <code>a</code> and <code>b</code>, but still keep them hidden?</p>

<p>A natural approach is to work with expressions derived from <code>a</code> and <code>b</code>. With that in mind, we want to:</p>

<ul>
  <li>Create an expression <code>a'</code> that hides <code>a</code>, but still allows the verifier to check that it was actually used</li>
  <li>Create an expression <code>b'</code> that does the same for <code>b</code></li>
  <li>Show the verifier how to combine <code>a'</code> and <code>b'</code> into a new expression <code>c'</code> that is linked to <code>c</code></li>
  <li>Show a second way to construct <code>c'</code> using values that can be tied back to <code>A</code> and <code>B</code></li>
</ul>

<h1 id="proving-we-know-a-and-b">Proving we know <code>a</code> and <code>b</code></h1>

<p>To build expressions that hide <code>a</code> and <code>b</code>, we also need a way to convince the verifier that these expressions were actually derived from them. Since the verifier does not know <code>a</code> or <code>b</code>, they need to check this indirectly using the commitments <code>A</code> and <code>B</code>.</p>

<p>This is similar to proving knowledge of a private key when the verifier only knows the corresponding public key. A private-public key pair can be viewed as a preimage-commitment pair, so we can reuse that intuition here. In particular, we can use a signature as a derived expression.</p>

<p>This gives us a way to link expressions back to <code>a</code> and <code>b</code> through their commitments <code>A</code> and <code>B</code>, using well-understood techniques. To make this concrete, we’ll use the Schnorr signature scheme.</p>

<h2 id="schnorr-signatures">Schnorr signatures</h2>

<p>A <strong>Schnorr</strong> signature consists of two components, <code>R</code> and <code>s</code>. Suppose <code>a</code> is the hidden value we want to build a derived expression from. The value <code>s</code> is defined as:</p>

<p><code>s = e*a + r</code></p>

<p>Here, <code>e</code> is a challenge chosen by the verifier (or derived from a hash), <code>a</code> is the secret, and <code>r</code> is a random value. The value <code>R</code> is the commitment to <code>r</code>, computed as <code>R = r*G</code>.</p>

<p>For our purposes, we will treat <code>e</code> as a verifier-chosen value so we can describe this in an interactive setting.</p>

<p>Using this construction, we can convince the verifier that we know the value <code>a</code> corresponding to the commitment <code>A</code>. The verifier checks this by computing <code>S = s*G</code> in two ways:</p>

<ul>
  <li>Directly from <code>s</code>, by computing <code>s*G</code></li>
  <li>From known values, by computing <code>R + e*A</code></li>
</ul>

<p>If both results match, then:</p>

<p><code>s*G = R + e*A</code></p>

<p>This works because <code>s</code> contains <code>a</code> inside it. When expanded, the equation becomes:</p>

<p><code>s*G = (e*a + r)*G = e*A + R</code></p>

<p>So any valid <code>s</code> must be consistent in being able to re-create an expression with the committed value <code>A</code>.</p>

<p>Now consider what happens if the prover does not know <code>a</code> and only knows <code>A</code>. Could they still produce values <code>R</code> and <code>s</code> that satisfy this equation?</p>

<p>If the prover could choose both <code>R</code> and <code>s</code> <strong>after</strong> seeing <code>e</code>, they could try to fake a valid pair by picking a random <code>s</code> and solving:</p>

<p><code>R = s*G - e*A</code></p>

<p>But in the protocol, the prover must send <code>R</code> <strong>before</strong> receiving <code>e</code>. This means <code>R</code> cannot depend on <code>e</code>. Once <code>e</code> is revealed, the prover must produce an <code>s</code> such that:</p>

<p><code>s*G = R + e*A</code></p>

<p>Finding such an <code>s</code> without knowing <code>a</code> would require solving a discrete logarithm problem, which is assumed to be hard. This is what gives the scheme its soundness.</p>

<h2 id="proving-we-know-the-sum-of-a-and-b">Proving we know the sum of <code>a</code> and <code>b</code></h2>

<p>We now extend this idea to prove a relation between two hidden values.</p>

<p>To show that <code>a + b = c</code> without revealing <code>a</code> or <code>b</code>, we construct derived expressions for each value:</p>

<ul>
  <li><code>a' = e*a + r1</code></li>
  <li><code>b' = e*b + r2</code></li>
</ul>

<p>These follow the same structure as the Schnorr construction. As before, the verifier must choose <code>e</code> after receiving a commitment to the randomness. In this case, we commit to the sum of the random values:</p>

<p><code>R = (r1 + r2)*G</code></p>

<p>The prover sends <code>R</code> first, then receives <code>e</code>, and then sends <code>a'</code> and <code>b'</code>.</p>

<p>The verifier can now compute:</p>

<p><code>c' = a' + b'</code></p>

<p>Expanding this gives:</p>

<p><code>c' = e*a + r1 + e*b + r2</code></p>

<p>Grouping terms:</p>

<p><code>c' = e*(a + b) + (r1 + r2)</code></p>

<p>Since <code>a + b = c</code>, we can rewrite this as:</p>

<p><code>c' = e*c + r1 + r2</code></p>

<p>Now consider the commitment to <code>c'</code>, which we call <code>C'</code>. The verifier can compute this as:</p>

<p><code>c'*G = e*C + R</code></p>

<p>Here, <code>C = commit(c)</code> and <code>R = (r1 + r2)*G</code>.</p>

<p>This gives the verifier two ways to create the derived value <code>c'</code> with known commitments. And since <code>c'</code> can be directly linked to <code>c</code> using the prover’s commitments, the verifier can be confident the expressions <code>a'</code> and <code>b'</code> truly contained the values <code>a</code> and <code>b</code> to make <code>c</code>.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Hopefully this post builds some intuition for how this proof approach can be generalized to other operations on committed values. In the next post, I’ll show how similar ideas can be used to prove products of committed scalars and even touch on inner-products with vector commitments.</p>

<p>It may seem abstract now, but these ideas are useful on their own and will come together when we get to Bulletproofs.</p>]]></content><author><name>Randy Naar</name></author><category term="Other" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Dog Park - Privacy in Bark</title><link href="https://sovrandy.com/2026/04/19/dog-park-privacy-in-bark.html" rel="alternate" type="text/html" title="Dog Park - Privacy in Bark" /><published>2026-04-19T00:00:00+00:00</published><updated>2026-04-19T00:00:00+00:00</updated><id>https://sovrandy.com/2026/04/19/dog-park-privacy-in-bark</id><content type="html" xml:base="https://sovrandy.com/2026/04/19/dog-park-privacy-in-bark.html"><![CDATA[<p>Coming off the heels of ‘<a href="https://uncensoredtech.substack.com/p/park">Park - Privacy in Ark</a>,’ I wanted to introduce a more comprehensive review of the privacy in Ark. For those wondering what ‘more comprehensive’ means, I’ll be expanding the privacy analysis to protocol primitives and exploring what a privacy-conscious ASP (Ark Service Provider) implementation <em>has</em> to reveal vs. what they choose to reveal. To spice things up, I decided to use the Second implementation, Bark, as a reference for this analysis, but will include occasional notes on what generalizes to other Ark implementations.</p>

<h2 id="ark">Ark</h2>

<p>Before I get into the nitty gritty of the privacy properties of each flow, I’d like to give a basic overview of what the flows are. The 5 flows I’ll be covering are:</p>

<ul>
  <li>onboards</li>
  <li>transfers</li>
  <li>balance checking</li>
  <li>offboards</li>
  <li>unilateral exits</li>
</ul>

<p>As we go through the flows, an easy way to build intuition about what’s public or not is to note the distinction of on-chain vs. off-chain activity. When something is broadcast on-chain, everyone can see it. When something is computed off-chain, that information is known only to the involved counterparties until explicitly revealed to others. Now, let’s get started.</p>

<h2 id="onboards">Onboards</h2>

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

<p>Like all good layer 2 protocols, onboarding in Ark starts with a multisig transaction between the user and some counterparty. This multisig transaction acts like an escrow for which the ASP can claim a user’s funds once they’ve made a payment on the user’s behalf. In the general case, a user will send to their own Ark address when onboarding so they have layer 2 funds available for whenever they need it.</p>

<p>Looking deeper into this escrow transaction, note that the multisig is accomplished using a taproot output with a musig aggregated internal key gating the key path spend. In addition to this musig path, a script path is also included to allow one of the parties to claim their funds after a timeout. The party that gets to claim the timeout can differ between Ark implementations, but since I’m using Bark as my reference, it’ll be the <strong>server</strong> claiming the funds from this transaction if the time runs out.</p>

<p>To avoid having a bad ASP stealing our money by refusing to honor refunds from already-broadcast onboard transactions, we request a refund transaction from the ASP <strong>before</strong> the onboard transaction goes on-chain. We do this by asking the ASP to cosign a new refund transaction, using the unsigned board transaction’s output as input, that spends to a new script containing the refund path. I say ‘containing’ the refund path, because it also contains a musig and timeout path as well. This signed off-chain transaction will allow us to claw back our funds and is an example of a <strong>vtxo</strong> (virtual unspent transaction output), which is a utxo held off-chain that can be broadcast at any time.</p>

<h3 id="privacy">Privacy</h3>

<p>To start thinking about the privacy of onboards, let’s work through what an observer can learn from the on-chain transaction itself.</p>

<p>Assuming the board transaction is made with the Bark sdk, it’ll be a multi-input transaction with 1-2 outputs depending on whether we have change or not. The first output will always contain the amount we would like to board and is locked using a musig internal key along with a script that allows the ASP to take the funds after a timeout.</p>

<p>Since taproot hides both the internal key and the script attached to the output, an outside observer is none the wiser on whether the transaction is even an Ark transaction or even whether it has a script attached to it. Meanwhile, the ASP knows your inputs to the boarding transaction and the vtxo public key the refund spends to.</p>

<p>Now, let’s look at what a transfer looks like once we’ve onboarded a vtxo.</p>

<h2 id="transfers">Transfers</h2>

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

<p>Transfers can get tricky because there are 2 different types of transfers – in-round and out-of-round. When either kind is made, the sending user has notify the recipient via the ASP so the recipient can verify their new balance. This will be covered more extensively in the balance checking section. In this section, I’ll be covering more of the mechanics of the transfer itself.</p>

<h4 id="in-round-transfers">In-round transfers</h4>

<p>After onboarding and getting an initial vtxo, that vtxo is eligible for a transfer of either kind. When you make an <strong>in-round</strong> transaction, you are performing a swap between your vtxo and a new one that’s anchored to a different on-chain transaction. The way it works is you tell the ASP you want to participate in a round, they create the round transaction alongside another series of transactions that allows you to spend from that round, and assign you an output that’s locked by a hash.</p>

<p>You are then required to forfeit your existing vtxo in an offchain transaction, aptly called the ‘forfeit transaction,’ that’s different from the round you’re registering for. The output on this forfeit transaction is locked by a ‘sweep’ path requiring the ASP’s public key and that hash’s secret, along with a ‘refund’ path a user can use to reclaim the vtxo after a timeout. The idea is that the ASP would give you the preimage to spend your new vtxo and use that same preimage to sweep your <strong>old</strong> vtxo.</p>

<p>This hArk, or hashlock-based, approach is what superseded the connector-based approach featured in the original Ark protocol. It’s simpler to implement than connector chains but it does come with some privacy tradeoffs as we’ll see a little later when I touch on blinded output registrations.</p>

<p>Each round can have multiple senders and recipients, and is funded with the ASP’s own money. The series of transactions mentioned before that allow a recipient to put their vtxos on-chain use this round transaction as input to the first presigned transaction in the chain. Each transaction spends from one another and represent smaller round transactions with subsets of the original recipient group in their outputs. Each group can be broken down until there’s only 1 recipient, and the last transaction in this chain is what’s referred to as an <strong>exit</strong> transaction. Each recipient has access to the series of transactions that lead to their exit, and they can broadcast these at any time. It’s this exit transaction, which is initially held off chain like the rest of the transactions, that is signed over to the aforementioned hash in a forfeit transaction so it can be swapped for a vtxo in a new round.</p>

<h4 id="out-of-round-transfers">Out-of-round transfers</h4>

<p>Out of round transfers are those that don’t need an on-chain transaction to be made. Instead of new vtxos being anchored to on-chain transactions, they become anchored to the sender’s <strong>off-chain</strong> exit transaction. When a sender spends their exit to forfeit their vtxo, a new <strong>checkpoint</strong> transaction is made using the old transaction’s exit as its input. This can be conceptualized as an off-chain ‘round’ of sorts, except with one input and potentially multiple outputs that can be spent to an exit transaction for the associated recipient.</p>

<h3 id="privacy-1">Privacy</h3>

<p>For in-round transfers, the only on-chain/public action that’s produced is the round transaction. This round transaction, like other taproot transactions, has its internal key and locking script obfuscated from the world until spend time. Therefore, outside the participating parties, only the ASP knows about vtxo transfers. Unilateral exits can break that shared output with transactions that progressively dwindle the number of recipient groups, and reveal scripts in the process, but I’ll cover more of that in the unilateral exit section.</p>

<h2 id="balance-checking">Balance checking</h2>

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

<p>Every holder of Bitcoin on Bark needs to have a way to query the ASP for any vtxos sent to their Ark address. To prevent random observers from requesting the vtxos of an arbitrary address, an authorization system was built in the form of the Bark mailbox.</p>

<p>The Bark mailbox is a notification system where the ASP stores all notifications for a Bark wallet under a ‘mailbox identifier’ for that wallet. This mailbox identifier needs to be a public key for which the sdk makes a dedicated keypair for.</p>

<p>As mentioned before, a sender needs to give additional information to the ASP to push the notification to the receiver’s mailbox. This additional information can be found in the Ark address generated by the receiver’s wallet. At first, one might think this could be a huge loss for privacy if different addresses could be linked to the same mailbox public key, but Bark has preventative measures against this.</p>

<p>Instead of the raw mailbox public key being included in the address, a blinded/tweaked public key is provided instead. This blinded public key only needs to be decoded by 2 parties, the wallet and the server. So, the most straightforward way to do this is by tweaking the mailbox public key with a shared secret that can be computed using a Diffie-Hellman exchange. The input to this process for the receiving user is the private key belonging to the Ark address we’re generating the tweak for, and the server’s public key. Then when the sender tells the ASP they sent a vtxo to that address, the server can recompute the secret using its private key and the address the vtxo is assigned to.</p>

<h3 id="privacy-2">Privacy</h3>

<p>Because of this mailbox mechanism, an outside observer cannot learn about a Bark wallet’s balance unless given the proper authorization. There’s also no on-chain transaction that would allow an observer to learn much about the wallet’s mailbox public key (much like an on-chain transaction doesn’t reveal anything about the xpub of an on-chain wallet). The ASP of course knows the vtxos that belong to your wallet and the IP addresses you’re making requests from, similar to other consumer wallets.</p>

<h2 id="offboards">Offboards</h2>

<h3 id="overview-3">Overview</h3>

<p>Offboards represent a collaborative exit path for vtxos in Bark. It involves telling the server which vtxos you want to offboard, getting an unsigned offboard transaction in response, signing it along with providing vtxos forfeit signatures, and receiving and broadcasting the final offboard transaction signed by both parties.</p>

<p>Unlike transfers, the forfeit transactions in offboards don’t use hashlocks to prove a transaction was made on their behalf, they use connectors. <strong>Connectors</strong> are transaction outputs first constructed from unsigned unbroadcast transactions, that are used as inputs to transactions that should only be valid if that unsigned transaction is successfully signed and broadcast. The Ark protocol was designed to use this mechanism for both transfers and offboards, but Bark switched to a hashlock exchange to minimize the on-chain footprint.</p>

<h3 id="privacy-3">Privacy</h3>

<p>Given that an offboard transaction can be funded by any of the ASP’s utxos, there is no direct link between the on-chain offboard transaction and the offboarding user’s vtxos. That means an outside observer does not learn which wallet requested the offboard.</p>

<h2 id="unilateral-exits">Unilateral Exits</h2>

<h3 id="overview-4">Overview</h3>

<p>I already touched upon unilateral exits in the transfer section, but it refers to a way that a user can turn their vtxo into an on-chain utxo they can spend <strong>without</strong> any interaction from the server. When you perform a unilateral exit, a series of transactions are published on chain with the last one being freely spendable to an address of the user’s choosing. There are timelocks to allow an ASP to stop the exit of a vtxo that was forfeited to them, AND there’s a separate timelock that will let an ASP claim <em>any</em> exited vtxo after a period in a sweep, but once the user’s timelock ends (which is guaranteed to be before the sweep timelock ends) the user can spend it to wherever they want.</p>

<h3 id="privacy-4">Privacy</h3>

<p>From an outside perspective, still nothing much can be learned from this process. Ignoring generic transaction-graph leakage, the <em>important</em> privacy-relevant information is the revealed locking script and public key of the user who is exiting. If the unilateral exit was done on a sender’s already-spent vtxo, no information can be learned about the recipient that the vtxo was spent to. If the recipient were to perform a unilateral exit <em>after</em> the sender tried to exit their already-spent vtxo, an observer would then be able to link the sender and recipient by matching the hash revealed on both sides.</p>

<p>In summary, exiting a vtxo will create a new utxo that has an amount equaling the vtxo, but it still wouldn’t leak information on the wallet that created it. The sender of that vtxo can also identify the exit as having belonged to that recipient since it was linked to the hash they needed for their forfeit.</p>

<h2 id="blinded-output-registration">Blinded output registration</h2>

<p>Another idea that has been floating around since Ark was conceptualized is this idea for it to operate like a coinjoin in making the ASP oblivious to which outputs a particular vtxo is spent to. In the connector-based design for Ark transfers, this worked, but in the hArk approach, things aren’t as simple.</p>

<p>When using connectors, a forfeit transaction’s spending condition is dependent on whether a <strong>transaction</strong> with a particular output set is broadcast on-chain. With the hArk approach, the forfeit transaction’s spending condition is dependent on an <strong>off-chain</strong> <strong>output</strong> being included in an <strong>already made</strong> transaction. A hArk forfeit itself is one side of an atomic swap with another vtxo.</p>

<p>This means there’s no ambiguity with respect to the output a forfeit transaction is attached to since both are linked by the hash in the new vtxo’s spending condition.</p>

<h2 id="final-thoughts">Final thoughts</h2>

<p>I think the privacy afforded by Bark maintains a nice balance between privacy and usability. When used within the normal operating model, the ASP has no more insight into your transactions than the average wallet enabling on-chain transactions (so excluding wallets connected to one’s node). It’s also not too different from most consumer lightning wallets that use an LSP as the network gateway, although receivers can still have better privacy in lightning. All in all I think Bark seems to pay attention to privacy while not letting it bog down the user experience and I’m hopeful they’ll continue to uphold this standard.</p>]]></content><author><name>Randy Naar</name></author><category term="Other" /><summary type="html"><![CDATA[Coming off the heels of ‘Park - Privacy in Ark,’ I wanted to introduce a more comprehensive review of the privacy in Ark. For those wondering what ‘more comprehensive’ means, I’ll be expanding the privacy analysis to protocol primitives and exploring what a privacy-conscious ASP (Ark Service Provider) implementation has to reveal vs. what they choose to reveal. To spice things up, I decided to use the Second implementation, Bark, as a reference for this analysis, but will include occasional notes on what generalizes to other Ark implementations.]]></summary></entry><entry><title type="html">Confidential Transactions For Dummies, Part 2 - Borromean Range Proofs</title><link href="https://sovrandy.com/2025/12/21/confidential-transactions-for-dummies-pt-2.html" rel="alternate" type="text/html" title="Confidential Transactions For Dummies, Part 2 - Borromean Range Proofs" /><published>2025-12-21T00:00:00+00:00</published><updated>2025-12-21T00:00:00+00:00</updated><id>https://sovrandy.com/2025/12/21/confidential-transactions-for-dummies-pt-2</id><content type="html" xml:base="https://sovrandy.com/2025/12/21/confidential-transactions-for-dummies-pt-2.html"><![CDATA[<h2 id="why-are-range-proofs-important">Why are range proofs important?</h2>
<p>In the previous post, we discussed pedersen commitments and how they hide transaction amounts while still allowing balances to add up correctly. We then touched on the topic of how one can fake 2 outputs that cancel out to spend more money than they have. Range proofs are the patch for this. They let you prove that committed values stay within sane bounds without revealing the values themselves. Without range proofs, confidential transactions don’t work.</p>

<h2 id="the-high-level-details">The high level details</h2>
<p>The flavor of range proofs we’re using is borromean range proofs. Borromean range proofs bring in a new cryptographic construct called ring signatures. Ring signatures let a signer within a group of public keys sign on behalf of the group without exposing themself as the signer. If you like analogies, you can think of this as a police lineup: the verifier knows the real signer is in the lineup, but can’t tell which individual it was. To motivate its use within a range proof, let’s take a look at a somewhat naive solution.</p>

<h2 id="naive-approach">Naive approach</h2>

<p>Imagine we have a list of pedersen commitments where each commitment is a curve point <code>C</code> generated like so:
<code>C = vG + bH</code></p>

<p>What’s cool about curve points is that any point can be treated as a public key in secp256k1, even <code>C</code>. To sign for <code>C</code> I would need to know its discrete log with respect to G, which means I’d need to find a value <code>c</code> such that <code>C = cG</code>. Unfortunately I don’t have a scalar <code>c</code> in <code>C = cG</code> so I can’t actually sign for <code>C</code>.</p>

<p>I <em>can</em> sign for the public keys derived from the terms of <code>C</code> though since I know the private keys are <code>v</code> and <code>b</code> respectively. We’ll use this to our advantage later on. Now let’s use this concept to make a rudimentary range proof where we’ll prove <code>v</code> is in the <strong>range</strong> of 1 to 2.</p>

<p>One way I can prove that the value <code>v</code> in my pedersen commitment is either ‘1’ or ‘2’ is to make a scheme with ring signatures using public keys where all of the public keys are <em>derived</em> from <code>C</code>.</p>

<p>Each public key is generated by taking <code>C</code> and subtracting a value <code>pG</code> where <code>p</code> is represents a possible value we’re trying to prove our value <code>v</code> might assume. So if I’m trying to prove <code>v</code> in <code>C = v*G + b*H</code> is either <code>1</code> or <code>2</code>, I will make 2 public keys <code>C - 1*G</code> and <code>C - 2*G</code>.</p>

<h2 id="what-does-this-scheme-do-for-us">What does this scheme do for us?</h2>

<p>If I’m not lying about my value being 1 or 2, one of my <em>derived</em> public keys will subtract the correct value, allowing me to sign with the pure H multiple b*H.</p>

<p>For example, if my value <code>v</code> is 1, then the first term in the list of public keys would evaluate to <code>bH</code>. This is because <code>C = 1*G + bH</code> and if we replace <code>C</code> in <code>C - (1*G)</code>, we get <code>(1*G + bH) - 1*G = bH</code>.</p>

<p>Meanwhile I could never sign for the public key <code>X = C - 2*G</code> because I don’t know the discrete log for the resulting point <code>X</code>. <code>2*G</code> won’t cancel out one of the terms I know, and I’d need to solve the elliptic curve discrete log problem to find <code>x</code> in <code>x*G = X</code>.</p>

<p>Since we’re utilizing a ring signature scheme, I can sign for all the public keys as a group and not reveal to an outside observer which public key subtracted the correct value.</p>

<p>Thinking more broadly, we can use this strategy for an arbitrary number of values from <code>1</code> to <code>n</code>, instead of <code>1</code> to <code>2</code> like in our toy example. The downside to this scheme is that it requires additional data for every possible value added. The bigger the range, the bigger the signature size grows. This led researchers to the question of whether the size for this scheme could be shrunk down and, lucky for us, the answer was yes.</p>

<h1 id="binary-decomposition">Binary decomposition</h1>
<p>The path to reducing the data of the naive scheme in the last section is through arithmetic. Researchers figured the difficulty of mathematically identifying the signer in a ring signature doesn’t get reduced if we reduce the number of partipants. So lets do that.</p>

<p>Imagine we have a ring signature for a naive range proof where we’re trying to prove a pedersen commitment <code>C</code> contains a value <code>v</code> (in <code>C = v*G + b*H</code>), where <code>v</code> is either <code>0</code> or <code>1</code>. The public keys for this would be <code>C - 0*G</code> or <code>C - 1*G</code>.</p>

<p>Now imagine we made a <strong>second</strong> ring for a pedersen commitment <code>C2</code> where the possible values are <code>0</code> or <code>2</code>. A cool way to link these 2 pedersen commitments, <code>C</code> and <code>C2</code>, is to think about the range of the <strong>sum</strong> of the <code>v</code>s in these 2 commitments. Since the first pedersen commitment has <code>v = 0 or 1</code>, and the second pedersen commitment has <code>v = 0 or 2</code>, the lowest the sum of these values (aka the <code>v</code> terms in the pedersen commitments) can be is <code>0</code>, and the highest the sum of these values can be is <code>3</code>.</p>

<p>Now think of the case where we can make each ring signature be a range proof for 0 or a power of 2. Then when we can break apart a pedersen commitment with a large value <code>v</code> into new pedersen commitments that have new <code>v</code>s that sum to the old <code>v</code>. Just to reiterate, the additional requirement is that the new commitments can only have <code>v</code> be a power of 2.</p>

<p>Now you can think of each ring signature as bits that can commit to a power of 2 being ‘on’ or ‘off’. For example, say we were signing a range proof proving a value in a pedersen commitment was between 0 and 3. In this case, we sign for the ‘on’ value of ring signatures that contain a <code>v</code> with a power of 2 present in the <strong>binary representation</strong> of <code>3</code>. So instead of one ring proving “v is one of {0,1,2,3}”, we’re using 2 to prove the 2^0 bit is 0 or 1 <em>and</em> the 2^1 bit is 0 or 1.</p>

<p>We will also have to split b, but b doesn’t have to get split into its binary representation. We just need to make sure we split <code>b</code> up into <code>n</code> parts, where <code>n</code> is the number of rings we would like to use (<code>n</code> is log_2(v) because it also represent the number of bits needed to represent <code>v</code>). These <code>b</code>s should actually be chosen somewhat randomly with the method I previously described when choosing blinding coefficients for commitments on a transaction’s outputs (in part 1). We can make all numbers random except the last one, and the last number can be chosen to make the sum equal <code>b</code>.</p>

<p>For intuition about this multi-ring approach, we’re saving space because the number of values we’re able to prove exponentially scales with the number of rings (just like adding a bit to series of bits lets us represent twice the amount of numbers). The key observation is that it’s easier to represent bits than exhaustively represent every number.</p>

<p>With separate independent ring signatures, someone could mix-and-match valid signatures from different signing sessions. Borromean ring signatures bind the rings together so they’re all bound to a single signing event. With separate independent ring signatures, someone could mix-and-match valid signatures from different signing sessions (akin to vulnerabilities to CBC-style encryption).</p>]]></content><author><name>Randy Naar</name></author><category term="Other" /><summary type="html"><![CDATA[Why are range proofs important? In the previous post, we discussed pedersen commitments and how they hide transaction amounts while still allowing balances to add up correctly. We then touched on the topic of how one can fake 2 outputs that cancel out to spend more money than they have. Range proofs are the patch for this. They let you prove that committed values stay within sane bounds without revealing the values themselves. Without range proofs, confidential transactions don’t work.]]></summary></entry><entry><title type="html">Confidential Transactions For Dummies, Part 1 - Pedersen Commitments</title><link href="https://sovrandy.com/2025/12/13/confidential-transactions-for-dummies-pt-1.html" rel="alternate" type="text/html" title="Confidential Transactions For Dummies, Part 1 - Pedersen Commitments" /><published>2025-12-13T00:00:00+00:00</published><updated>2025-12-13T00:00:00+00:00</updated><id>https://sovrandy.com/2025/12/13/confidential-transactions-for-dummies-pt-1</id><content type="html" xml:base="https://sovrandy.com/2025/12/13/confidential-transactions-for-dummies-pt-1.html"><![CDATA[<h2 id="prerequisite-knowledge">Prerequisite knowledge</h2>
<p>The requirements are knowledge of the elliptic curve discrete log problem and elliptic curve field operations.</p>

<h2 id="why-should-you-care-about-privacy-on-the-blockchain">Why should you care about privacy on the blockchain?</h2>
<p>I want you to think about every financial transaction made within the past year. Political donations, pharmacy purchases, food delivery orders, etc. Now imagine all that information made public. Or worse—made available to a blackmailer to abuse. Every piece of information about you in the wrong hands can be used against you. Information about what you’re purchasing, who you’re purchasing from, or your purchasing amount is no exception, yet this is the case we find ourselves in with most blockchains today. So what do we do about this privacy problem?</p>

<h2 id="addressing-the-privacy-problem">Addressing the privacy problem</h2>
<p>Having good privacy hygiene and not reusing addresses does some of the heavy lifting in pseudonymizing recipients, but one fundamental component to privacy is being able to hide transaction amounts. Even if I can’t see what you’re purchasing, knowing that you spent $500 at a pharmacy tells me plenty. At first, hiding amounts on the blockchain may seem like an impossible task, but there <em>is</em> a way! This is what’s accomplished by <strong>confidential transactions</strong>.</p>

<h2 id="what-are-confidential-transactions">What are confidential transactions?</h2>
<p>Confidential transactions are transactions where the output amounts are hidden to everyone but me, my recipient, and anyone we decide to share it with. The scheme takes the amount I’m spending and ‘hides’ it with a function that allows everyone to verify I’m not spending more money than I’ve received. The output of this ‘hiding’ function is what we refer to as a <strong>commitment</strong>, and given a commitment, there is <em>no feasible way</em> to recover the original input amount. The original amount is instead hidden in a different part of the transaction for the recipient to recover so they can verify it corresponds with the commitment. If the amount didn’t correspond with the commitment, no future recipients would accept the transfer.</p>

<p>In this post I’ll be focusing on the function that transforms the amount into a commitment, which in this context is a “Pedersen Commitment.”</p>

<h2 id="what-are-pedersen-commitments">What are Pedersen commitments?</h2>
<p>Let’s break this term down. The first thing I’ll explain is what a commitment is. A commitment is an output value of a function f that takes in a secret, or multiple secrets, as input. What makes it a <em>commitment</em> is the property that the input can’t easily be derived from the output (which we refer to as <strong>hiding</strong>), and that another input can’t easily be found that creates that same output (what we call <strong>binding</strong>).</p>

<p><strong>Pedersen</strong> commitments rely on the assumption that the ‘discrete log problem’ is hard. With this assumption, it’s not computationally feasible to find another input that equals that output. <strong>Opening</strong> a commitment means obtaining the input from the committer that produces the commitment.</p>

<p>The paper that introduced Pedersen commitments, written by Torben Pryds <strong>Pedersen</strong>, used it as part of a larger secret sharing scheme and originally formed the commitment like so:</p>

<p><code>g^a · h^b mod p</code></p>

<p>Where <code>a</code> represents the number we want to ‘commit’ to, and <code>b</code> represents a per-commitment <strong>nonce</strong>, called the <strong>blinding factor</strong>, that makes sure we can make different commitments for the same committed value.</p>

<p>In the context of elliptic curves, the expression becomes:</p>

<p><code>a*G + b*H</code></p>

<p>The original scheme worked in a multiplicative group where <code>g^a</code> and <code>h^b</code> are both integers for which multiplication is defined. For elliptic curves, <code>a*G</code> and <code>b*H</code> are <strong>points</strong> for which point multiplication is <strong>not</strong> defined—we only have point addition. This naturally causes a change in the expression to use point addition instead.</p>

<h2 id="how-do-pedersen-commitments-help-us-hide-transaction-amounts">How do Pedersen commitments help us hide transaction amounts?</h2>
<p>The main goal of designing a confidential transaction scheme is to maintain the integrity of the blockchain so that no money is unjustly created. Pedersen commitments allow us to do that by allowing us to perform <em>arithmetic operations on commitments</em>.</p>

<p>Because I can perform arithmetic operations on commitments, I can subtract the sum of all output commitments from the sum of all input commitments and verify that the result is 0 to verify no money is being created (or destroyed in a way that’s unaccounted for). In short, using <code>f</code> to represent the function that turns values into Pedersen commitments, our check looks like:</p>

<p><code>f(inputs) - f(outputs) == 0</code></p>

<p>Let’s try an example. Imagine we have one input value of 3 represented by the commitment <code>in_0 = (3*G) + (7*H)</code>. With this input we make 2 outputs—one with the value of 2 represented by the commitment <code>out_0 = (2*G) + (4*H)</code>, and another with the value of 1 represented by the commitment <code>out_1 = (1*G) + (3*H)</code>. An observer to our transaction is not able to break our commitments into the additive terms, but they can perform the following comparison to show the inputs are equal to the outputs:</p>

<p><code>in_0 - (out_0 + out_1) == 0</code></p>

<p>Inserting our example values for this, we get:</p>

<p><code>(3*G) + (7*H) - ((2*G) + (4*H)) - ((1*G) + (3*H)) == 0</code></p>

<p>If we move terms so that all the <code>G</code>s and <code>H</code>s are grouped together (since addition is commutative on elliptic curves) we can reframe the expression as:</p>

<p><code>(3*G) - (2*G) - (1*G) + (7*H) - (4*H) - (3*H)</code></p>

<p>Both the amounts (3 = 2 + 1) and the blinding factors (7 = 4 + 3) cancel out independently, which is what makes this work.</p>

<p>Notice that the output blinding factor isn’t completely random since both the input and output coefficients of H need to cancel out to 0. To minimize the disruption of this restriction, we usually pick all the output commitment blinding factors <em>except</em> the last one randomly, and pick whatever value cancels out the sum of the other blinding factors as the last one. So basically:</p>

<p><code>last_output_blind = sum_of_input_blinds - sum_of_other_random_output_blinds</code></p>

<p>Unfortunately we have a big problem with our scheme. Namely, the problem of ‘negative’ valued outputs. I say ‘negative’ in quotes because there are no negative values on elliptic curves in the traditional sense. Instead, we refer to a number <code>a</code> as negative with respect to another number <code>b</code> if it is large enough to make the sum <code>a + b</code> wrap around the modulus and produce a result less than <code>b</code>. See my last blog post for more information about “negative” numbers on elliptic curves. For now, let’s keep conceptualizing these numbers as negative.</p>

<p>So here’s the problem. Imagine I have 1 input value of 4 and 2 output values of 1 and 3 respectively. Let’s also say I make commitments for them:</p>

<pre><code>in_0  = 4*G + 2*H
out_0 = 1*G + 1*H
out_1 = 3*G + 1*H
</code></pre>

<p>Let’s think like an attacker here. What’s stopping me from adding an extra output back to myself for a value of 30? If I did, subtracting output commitments from input commitments would now equal -30. Well how about if I added <em>another</em> output with a value of -30 (where -30 is <code>modulus - 30</code>)? These 2 outputs would cancel out in the input/output equality check and no one would know because the values are hidden.</p>

<p>Addressing this problem necessitates the addition of a new tool in our confidential transaction toolbox which we’ll refer to as <strong>range proofs</strong>. Range proofs are a way to prove that all outputs are within a range of values that won’t cause an overflow, and will be the topic of my next blog post “Confidential Transactions for Dummies Part 2.”</p>]]></content><author><name>Randy Naar</name></author><category term="Other" /><summary type="html"><![CDATA[Prerequisite knowledge The requirements are knowledge of the elliptic curve discrete log problem and elliptic curve field operations.]]></summary></entry><entry><title type="html">The TRUTH About “Negative” Numbers on Finite Elliptic Curves!!</title><link href="https://sovrandy.com/2025/09/13/negative-numbers-on-finite-elliptic-curves.html" rel="alternate" type="text/html" title="The TRUTH About “Negative” Numbers on Finite Elliptic Curves!!" /><published>2025-09-13T00:00:00+00:00</published><updated>2025-09-13T00:00:00+00:00</updated><id>https://sovrandy.com/2025/09/13/negative-numbers-on-finite-elliptic-curves</id><content type="html" xml:base="https://sovrandy.com/2025/09/13/negative-numbers-on-finite-elliptic-curves.html"><![CDATA[<p>I think people often struggle to make cryptography concepts clear. I’m not claiming I can, but I can make a commitment to trying—starting with a click-bait title! I might get some information wrong since I’m not a cryptographer, but hopefully I won’t ;)</p>

<h2 id="prerequisite-knowledge">Prerequisite knowledge</h2>
<p>The only requirements are algebra and modular arithmetic.</p>

<h2 id="elliptic-curves">Elliptic curves</h2>
<p>Elliptic curves are what we call functions of the form y² = x³ + ax + b. These functions can be written in different ways, but the representation I’m using is in “Weierstrass” form. The curve used for Bitcoin-related things, secp256k1, looks like this in Weierstrass form:</p>

<p>y² = x³ + 7</p>

<p>And if we graph it, it looks like someone punched a vertical line from the right:</p>

<p><img src="/img/elliptic-curve.png" alt="elliptic curve graph" /></p>

<p>It has a nice shape to it though—like the Snapchat logo turned on its side:</p>

<p><img src="/img/snapchat-logo.png" alt="snapchat logo" /></p>

<h2 id="elliptic-curves-on-finite-fields">Elliptic curves on finite fields</h2>

<p>Well, it was too nice for cryptographers, so they had to make the x &amp; y values wrap around with a modulus! For secp256k1, this modulus is prime. I’d like to say it’s chosen to be prime because it sounds cool (since mathematically you can now say the underlying field form <strong>A GROUP OF PRIME ORDER!!!</strong>), but there are reasons for the selection that I won’t get into here.</p>

<p>Anyway, this is what it looks like with a modulus applied:</p>

<p><img src="/img/finite-elliptic-curve.png" alt="finite elliptic curve graph" /></p>

<p>With a modulus, we’re able to wrangle an <strong>infinite number of points</strong> into something <strong>finite and countable</strong>. And since mathematicians love to count things, as I’ve learned through the math classes I struggled through years ago, we know <strong>how many</strong> points are in a modded elliptic curve as well. Let’s label the modulus applied to the coordinates of elliptic curve points “<strong>p</strong>”, and the number of points this function can generate “<strong>n</strong>”. In secp256k1, <strong>n</strong> is <strong>slightly less</strong> than <strong>p</strong>.</p>

<p>One REALLY important thing I need to cover before moving forward is that for cryptographic applications, we <strong>always</strong> mod the numbers we pass into finite field elliptic curve functions. You might be thinking “obviously! you told us they’d be modded by <strong>p</strong>,” but we actually don’t use <strong>p</strong> as the modulus for scalar inputs to our function. We use <strong>n</strong>! “Inputs” here refers to the scalar multipliers we use in elliptic curve operations, <strong>not the resulting point coordinates</strong>. I just had to mention that to avoid confusion moving forward.</p>

<h2 id="elliptic-curve-point-operations">Elliptic curve point operations</h2>

<p>There are also operations we can perform on <strong>points</strong>, like adding one point to another, but the algorithm to do that is non-obvious and the details are somewhat irrelevant here. It is important to know it can be done in this special world of elliptic curves.</p>

<h2 id="why-do-we-mod-scalars-by-n-instead-of-p">Why do we mod scalars by ‘n’ instead of ‘p’??</h2>

<p>After <strong>n</strong>, the scalar multipliers we input start to produce the same points again since mathematicians <strong>proved</strong> that the points on the curve make a cyclic group. One of the properties of this cyclic group makes it so we have <strong>n</strong> unique points on the curve! Well, why not just work with scalars mod <strong>p</strong> instead? Because the scalar multiplication operation has a period of <strong>n</strong>, not <strong>p</strong>—meaning after <strong>n</strong> steps, we return to our starting point.</p>

<p>I’ll also mention that after modding the scalar by <strong>n</strong>, all coordinate calculations are done <strong>mod p</strong>. Remember, <strong>n</strong> is the order of the curve (number of points), while <strong>p</strong> is the field size for coordinates.</p>

<h2 id="the-curious-case-of-negative-numbers">The curious case of negative numbers</h2>

<p>Coming back to the title, the <strong>TRUTH</strong> about negative numbers in finite elliptic curves is that they don’t exist in the traditional sense! When I talk about ‘numbers’ here, I’m referring to what cryptographers call <em>scalars</em>—the multipliers used in elliptic curve operations. So <strong>not</strong> point coordinates.</p>

<p>For the elliptic curves we’re discussing, I mentioned scalars are <strong>mod n</strong> (the order of the curve), and they’re defined to be <strong>non-negative</strong> integers from 0 to n-1.</p>

<p>So where do negative numbers come from? Am I duping the readers by definition?? Well, yes and no. I’m using “negative” here to mean “the additive inverse”—the thing you add to get back to zero.</p>

<p>With the set of integers, every element has an additive inverse (i.e., 2 + (-2) = 0). With the set of integers modulo some number <strong>n</strong>, every number still has an additive inverse, but it’s just the number that, when added, gives you zero mod <strong>n</strong> (i.e., 6 + 1 ≡ 0 (mod 7)).</p>

<p>With elliptic curves we work with <strong>prime</strong> <strong>p</strong> as modulus, which also makes <strong>p</strong> <em>odd</em>. What does this have to do with anything? Well, I can say that an even number added to another even number will never equal our odd <strong>p</strong>!</p>

<p>We can then label even numbers as ‘positive’ and odd numbers as ‘negative’ since every element in this set of positive numbers has an inverse in the odd set of numbers. There will never be an inverse of an even number in the even set or vice-versa, making the inverses <strong>mutually exclusive</strong>. It’s this mutual exclusivity that lets us relabel these sets as ‘positive’ and ‘negative’. ‘Negative’ is therefore redefined in this context to mean a set of inverses to another set rather than the traditional concept of negative.</p>]]></content><author><name>Randy Naar</name></author><category term="Other" /><summary type="html"><![CDATA[I think people often struggle to make cryptography concepts clear. I’m not claiming I can, but I can make a commitment to trying—starting with a click-bait title! I might get some information wrong since I’m not a cryptographer, but hopefully I won’t ;)]]></summary></entry></feed>