Proofs from Signatures

Generating Proofs via NextJS + Injected Wallet.

When HollowDB is used together with a dApp, we have the perfect opportunity to use zero-knowledge proofs by making use of the user wallet. In particular, we can derive a secret at client-side using the user wallet, and then generate our key from that derived secret. Here is an example flow for such a use-case:

  • Secret: A signature on a pre-determined a constant string, signed by users for your dApp

  • Preimage: the secret hashed to a group element (i.e. a circuit-friendly value)

  • Key: A key derived from the preimage using Poseidon hash (i.e. a zk-friendly hash function)

To try this flow yourself, you can quickly get started with a dApp boilerplate and follow this guide. We think Rainbowkit provides a really simple boilerplate dApp where you can connect your wallet, so start by setting up the scaffold code described at Rainbowkit quickstart.

npm init @rainbow-me/rainbowkit@latest    # npm
pnpm create @rainbow-me/rainbowkit@latest # pnpm
yarn create @rainbow-me/rainbowkit        # yarn

We don't really need anything other than the wallet connection button here, so go ahead to index.tsx and change the component to the following:

return (
  <div className={styles.container}>
    <main className={styles.main}>
      <ConnectButton />
    </main>
  </div>
);

We will make use of our prover utility class, so let us install it too:

yarn add hollowdb-prover # or npm, or pnpm

Then, make sure you configure Webpack options, describe at the section: Proving In NextJS.

Getting the Signature

The first thing we have to do is obtain the user signatures for some string. To do so, add the following to within the component:

const [signature, setSignature] = useState<string>();
const { signMessage } = useSignMessage({
  message: "your-message-for-this-dapp",
  onSuccess: (data) => setSignature(data),
  onError: (err) => alert(err.message),
});

useSignMessage is a hook exported by Wagmi, which Rainbowkit wraps around. When we call signMessage, it will cause for instance MetaMask to pop-up and ask for user to sign a message.

Let's add a tiny button to get that signature, just below <ConnectButton /> we will add:

<div>
  <button
    className={styles.button}
    onClick={() => {
      signMessage();
    }}
  >
    Sign
  </button>
</div>

We have added a tiny style to our button as well, if you would like to add within your Home.module.css:

.button {
  background-color: white;
  color: black;
  border: 2px solid #e7e7e7;
  font-size: larger;
  padding: 5px;
  border-radius: 5px;
  margin: 5px;
}

.button:hover {
  background-color: #e7e7e7;
}

Hashing the Signature

We can hash this signature to a circuit-friendly value; or in a bit more technical term: we can do a hash-to-group operation where the result of hash must conform to some rules. In the simplest case, the resulting digest must be smaller than some value (order of the group).

Our HollowDB Prover utility class provides a hash-to-group function:

const { hashToGroup } = require("hollowdb-prover");

Then we add a useMemo to calculate this hash whenever signature changes.

const preimage = useMemo(() => signature && hashToGroup(signature), [signature]);

We will use this preimage when we are creating zero-knowledge proofs for HollowDB.

You are free to use any other method for the hashing-to-group part, all you have to do is make sure that the resulting digest corresponds to a number that is less than:

21888242871839275222246405745257275088548364400416034343698204186575808495617

For the curious, that is the order of the scalar field of BN254 curve, which is the finite field that our circuit operates on.

Generating the Proof

Our utility package also exports a prover class: Prover. It is really straightforward to use, you just have to provide paths to the WASM circuit and prover key files. These files can be stored under public directory. You can download them from HollowDB repository.

Import our Prover above:

const { Prover } = require("hollowdb-prover");

Let's add our button that will generate the proofs, right inside the same div with the previous button:

<button
  className={styles.button}
  onClick={() => {
    if (!preimage) return alert("Please sign first!");

    // change these based on your application
    const currentValue = { foo: 234 };
    const nextValue = { foo: 456 };

    new Prover("/circuits/hollow-authz.wasm", "/circuits/prover_key.zkey")
      .prove(preimage, currentValue, nextValue)
      .then(({ proof }: { proof: unknown }) => {
        // e.g. make an api call with the proof
        console.log(proof);
        alert("Proof created!");
      });
  }}
>
  Prove
</button>

You can see how the Prover object is created by providing the paths to WASM circuit and the prover key. Then, we simply call the prove function with the preimage, current value and the next value.

The value inputs are "hashed-to-group" and embed within the proof itself, this logic is handled within the function.

Computing the Key

Can we compute the key without generating a proof? Yes, we have the computeKey function for that!

const { computeKey } = require("hollowdb-prover");

We can add another useMemo to calculate this final hash whenever the previous hash changes.

const key = useMemo(() => preimage && computeKey(preimage), [preimage]);

The client can use this key to read values from HollowDB, without needing to generate proofs.

Putting it All Together

Here is how index.tsx looks like in the end:

import { ConnectButton } from "@rainbow-me/rainbowkit";
import type { NextPage } from "next";
import styles from "../styles/Home.module.css";
import { useSignMessage } from "wagmi";
import { useMemo, useState } from "react";
const { hashToGroup, Prover, computeKey } = require("hollowdb-prover");

const Home: NextPage = () => {
  const [signature, setSignature] = useState<string>();
  const { signMessage } = useSignMessage({
    message: "your-message-for-this-dapp",
    onSuccess: (data) => setSignature(data),
    onError: (err) => alert(err.message),
  });
  const preimage = useMemo(() => signature && hashToGroup(signature), [signature]);
  const key = useMemo(() => preimage && computeKey(preimage), [preimage]);

  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <ConnectButton />

        <div>
          <button
            className={styles.button}
            onClick={() => {
              signMessage();
            }}
          >
            Sign
          </button>

          <button
            className={styles.button}
            onClick={() => {
              if (!preimage) return alert("Please sign first!");

              // change these based on your dApp!
              const currentValue = { foo: 234 };
              const nextValue = { foo: 456 };

              new Prover("/circuits/hollow-authz.wasm", "/circuits/prover_key.zkey")
                .prove(preimage, currentValue, nextValue)
                .then(({ proof }: { proof: unknown }) => {
                  // e.g. make an api call with the proof
                  console.log(proof);
                  alert("Proof created!");
                });
            }}
          >
            Prove
          </button>
        </div>
      </main>
    </div>
  );
};

export default Home;

You should see something like this in the middle of the screen when you connect your wallet:

Last updated