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.
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
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
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
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
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"));
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
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
Then recompile and run the modified node:
# make
# ./target/release/rusk \
--http-listen-addr 0.0.0.0:4322 \
| grep rusk::http::prover
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
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)