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