DEV Community

smith@mcf.rocks
smith@mcf.rocks

Posted on

Dusk - Standalone Prover

Dusk uses ZKPs (Zero-Knowledge-Proofs) to maintain transaction privacy; for example, when doing a simple transfer, a proof must be constructed by the wallet demonstrating the notes involved belong to that wallet and are nullified, later the proof is verified on-chain by the consensus nodes.

So there are two parts 1) proof creation 2) proof verification. Each contract (in this example, the genesis Transfer contract) contains both prover and verification circuits. Verification is fast with constant time. Proof creation can take longer depending on how many notes are involved, in some cases on slower hardware it can be really quite lengthy (>20s) leading to poor UX in the wallet.

Dusk proof creation times

Fortunately, proof creation is trustless and can therefore be done by a third party. Dusk allows a node to run in "Prover" mode, where it only does proof creation.

Right now the web-wallet is hard coded to use Dusk Foundation provers. In the wallet-core code, an UnprovenTransaction structure is converted and sent as bytes to a prover in ./src/compat/tx.rs unproven_tx_to_bytes(), the prover returns the proof, which is checked in prove_tx() -- however, right now, the web-wallet code is in a private repo, so we can't mess with that.

On the other hand, the CLI Wallet allows specification of two endpoints, one for state and one for prover. In our previous tutorials, both point at the local node, like this:

# ./wallet-cli/target/release/rusk-wallet \
  --state http://127.0.0.1:4321 \
  --prover http://127.0.0.1:4321
Enter fullscreen mode Exit fullscreen mode

However they can be different... We will use the same setup as before with a local node and wallet.

On another server, we will make a Prover node, listening on 4322:

# git clone https://github.com/dusk-network/rusk.git
# cd rusk
# make
# ./target/release/rusk -V
rusk 0.7.0 (16cd2d58 2023-12-31)
# cp examples/consensus.keys ~/.dusk/rusk/consensus.keys
# export DUSK_CONSENSUS_KEYS_PASS=password
# ./target/release/rusk --http-listen-addr 0.0.0.0:4322
Enter fullscreen mode Exit fullscreen mode

NB: remember to allow the machine with the state node and wallet access to port 4322 on this machine.

Run the CLI wallet referencing the prover on the other machine:

# ./wallet-cli/target/release/rusk-wallet \
  --state http://127.0.0.1:4321 \
  --prover http://<myOtherServerIP>:4322
Enter fullscreen mode Exit fullscreen mode

Do a transfer from the wallet:

Send 50 Dusk to 3uVEjrfzdhjN1a5o48SpiVU9AFzF5o9FZ9mFR9GfeV7U3nfDpDGfVrLJveFPokHsQiXUjjrQHYZSWNz4PY2UQ4xU

Observe the log output of our Prover node, you will see:

2024-01-01T15:50:18.664338Z  INFO rusk::http: Received Host("rusk"):prove_execute request
Enter fullscreen mode Exit fullscreen mode

The proof creation has taken place on the Prover node, not the local state node.

The action takes place in ./rusk-prover/src/prover/execute.rs

If you look, you will notice there are 4 different proving circuits:

pub static EXEC_1_2_PROVER: Lazy<PlonkProver> =
    Lazy::new(|| fetch_prover("ExecuteCircuitOneTwo"));

pub static EXEC_2_2_PROVER: Lazy<PlonkProver> =
    Lazy::new(|| fetch_prover("ExecuteCircuitTwoTwo"));

pub static EXEC_3_2_PROVER: Lazy<PlonkProver> =
    Lazy::new(|| fetch_prover("ExecuteCircuitThreeTwo"));

pub static EXEC_4_2_PROVER: Lazy<PlonkProver> =
    Lazy::new(|| fetch_prover("ExecuteCircuitFourTwo"));

Enter fullscreen mode Exit fullscreen mode

and

        match utx.inputs().len() {
            1 => local_prove_exec_1_2(&utx, rng),
            2 => local_prove_exec_2_2(&utx, rng),
            3 => local_prove_exec_3_2(&utx, rng),
            4 => local_prove_exec_4_2(&utx, rng),
            _ => Err
Enter fullscreen mode Exit fullscreen mode

The circuit used depends on the number of inputs (notes from the user's wallet); the output is always 2 notes (a new note owned by someone else & the change). The maximum number of notes that can be used is 4, I have no idea what happens if the wallet needs to use more than 4 input notes.

The log output from the node does not really tell us much. I want to know a) which circuit was used, and b) how long the operation took. Let's make some code changes to add this to the log.

In ./rusk/src/lib/http/prover.rs handle() function I amend as follows:

        // smith 
        let utx = UnprovenTransaction::from_slice(request.event_data()).unwrap();
        info!("LocalProver Start inputs:{:?}",utx.inputs().len());

        let response = match topic {
            "prove_execute" => self.prove_execute(request.event_data())?,
            "prove_stct" => self.prove_stct(request.event_data())?,
            "prove_stco" => self.prove_stco(request.event_data())?,
            "prove_wfct" => self.prove_wfct(request.event_data())?,
            "prove_wfco" => self.prove_wfco(request.event_data())?,
            _ => anyhow::bail!("Unsupported"),
        };

        info!("LocalProver End");  // smith
Enter fullscreen mode Exit fullscreen mode

Then recompile and run the modified node:

# make
# ./target/release/rusk \
  --http-listen-addr 0.0.0.0:4322 \
  | grep rusk::http::prover
Enter fullscreen mode Exit fullscreen mode

In the wallet:

Send 1 Dusk to 3uVEjrfzdhjN1a5o48SpiVU9AFzF5o9FZ9mFR9GfeV7U3nfDpDGfVrLJveFPokHsQiXUjjrQHYZSWNz4PY2UQ4xU

For me, the log reported the following:

2024-01-01T17:26:24.444775Z  INFO rusk::http::prover: LocalProver Start inputs:2
2024-01-01T17:26:30.191910Z  INFO rusk::http::prover: LocalProver End
Enter fullscreen mode Exit fullscreen mode

We can see the proof creation took 6s for a Transfer using 2 input notes.

Although the server I am running the Prover on is decent (AMD EPYC 9374F 32-Core) proof creation cannot be multithreaded, so a single fast core is what matters, something like an Intel Core i9-14900K maybe, or perhaps some kind of FPGA solution, who knows.

Top comments (0)