Custom Smart Contracts
Users can deploy their own zero-knowledge contracts, written for the DarkFi zkVM, becoming anonymous engineers themselves!
More information about the smart contracts architecture can be found here.
Hello World
For the purposes of this guide we are going to use the smart contract template found here. It’s a very simple smart contract simulating a registration ledger, where users can create their membership keys and register or remove themselves from it. Each user is represented as the poseidon hash of their membership public key.
First, open another terminal, clone the template repository and enter its directory:
$ git clone https://codeberg.org/darkrenaissance/smart-contract
$ cd smart-contract
Here, generate the contract WASM bincode and its client by executing:
$ make
../darkfi/zkas proof/membership_proof.zk -o proof/membership_proof.zk.bin
Wrote output to proof/membership_proof.zk.bin
cargo build --target=wasm32-unknown-unknown --release --lib
...
Compiling membership v0.0.1 (/home/anon/smart-contract)
Finished `release` profile [optimized] target(s) in 14.45s
cp -f target/wasm32-unknown-unknown/release/membership.wasm membership.wasm
cargo build --release --features=client --bin membership
...
Compiling membership v0.0.1 (/home/anon/smart-contract)
Finished `release` profile [optimized] target(s) in 30.78s
cp -f target/release/membership membership
Now both the contract and its client are ready to use. Leave this
terminal open, as we will come back to it later in the guide, and
return back to your drk interactive shell.
Creating contracts
Each contract is controlled by a secret key, from which its Contract ID derives. To deploy a smart contract, we first need to generate an authority keypair. The Contract ID shown in the outputs is a placeholder for the one that will be generated from you. In rest of the guide, use the one you generated by replacing the corresponding placeholder. We can create our own contract authority by executing the following command:
drk> contract generate-deploy
Generating a new keypair
Created new contract deploy authority
Contract ID: {CONTRACT_ID}
You can list your mint authorities with:
drk> contract list
Contract ID | Secret Key | Locked | Lock Height
---------------+-----------------------+--------+-------------
{CONTRACT_ID} | {CONTRACT_SECRET_KEY} | false | -
Deploy transaction
Now that we have a contract authority, we can deploy the example contract we compiled earlier using it:
drk> contract deploy {CONTRACT_ID} ../smart-contract/membership.wasm | broadcast
[mark_tx_spend] Processing transaction: d0824bb0ecb9b12af69579c01c570c0275e399b80ef10f0a9c645af65bdd0415
[mark_tx_spend] Found Money contract in call 1
Broadcasting transaction...
Transaction ID: d0824bb0ecb9b12af69579c01c570c0275e399b80ef10f0a9c645af65bdd0415
Now the transaction should be published to the network. When the transaction is confirmed, the contract history will show its record:
drk> contract list {CONTRACT_ID}
Transaction Hash | Type | Block Height
------------------+------------+--------------
{TX_HASH} | DEPLOYMENT | 34
We can redeploy the contract as many times as we want, as long as it’s not locked. Each redeployment will show a new record in the contract history. We can also export the deployed data by executing:
drk> contract export-data {TX_HASH} > membership.dat
The exported files contains the WASM bincode and instruction data
deployed by that transaction, encoded in base64 as a tuple.
Lock transaction
After we finished deploying our contract and don’t require further code changes, we can lock it. This will not allow further deployment transactions, effectively locking the smart contract on-chain code. To lock down the contract, execute:
drk> contract lock {CONTRACT_ID} | broadcast
[mark_tx_spend] Processing transaction: 9eee9799d77d0ef1dd115738982296c9c481b4412c75a0a0955fd67d87bfe6a0
[mark_tx_spend] Found Money contract in call 1
Broadcasting transaction...
Transaction ID: 9eee9799d77d0ef1dd115738982296c9c481b4412c75a0a0955fd67d87bfe6a0
After the transaction has been confirmed, we will see our contract
Locked status set to true, along with the block height it was
locked on:
drk> contract list
Contract ID | Secret Key | Locked | Lock Height
---------------+-----------------------+--------+-------------
{CONTRACT_ID} | {CONTRACT_SECRET_KEY} | true | 36
We will also see the lock transaction in its history:
drk> contract list {CONTRACT_ID}
Transaction Hash | Type | Block Height
------------------+------------+--------------
{TX_HASH} | DEPLOYMENT | 34
{LOCK_TX_HASH} | LOCK | 36
Interacting with the smart contract
Now that the contract code is set on-chain and cannot be modified further, let’s interact with it using its client!
Registration
Let’s go to the contract client terminal, and create our membership keys:
$ ./membership generate
Secret key: {IDENTITY_SECRET_KEY}
Public key: {IDENTITY_PUBLIC_KEY}
NOTE: This is a very basic example client so secrets keys are used as plainext for simplicity. Do not run this in a machine with commands history or in a hostile environment where your secret key can be exposed.
Now we can can create our register call using our membership secret
key:
$ ./membership register {CONTRACT_ID} {IDENTITY_SECRET_KEY} > register.call
Now we need to go back to our drk interactive shell, to generate the
actual registration transaction, attach a fee to it and broadcast it to
the network:
drk> tx-from-calls < ../smart-contract/register.call | attach-fee | broadcast
[mark_tx_spend] Processing transaction: 23ea7d01ae16389e71d73fa27748ce1633d39c6b55a4aa31d8f5ba1017a4f840
[mark_tx_spend] Found Money contract in call 1
Broadcasting transaction...
Transaction ID: 23ea7d01ae16389e71d73fa27748ce1633d39c6b55a4aa31d8f5ba1017a4f840
After the transaction has been confirmed, our membership commitment will exist in our contract registry.
Deregistration
To remove ourselves from the registry, we create a deregister call
with the contract client:
$ ./membership deregister {CONTRACT_ID} {IDENTITY_SECRET_KEY} > deregister.call
Then, we build the actual deregistration transaction again, attach its fee and broadcast it to the network:
drk> tx-from-calls < ../smart-contract/deregister.call | attach-fee | broadcast
[mark_tx_spend] Processing transaction: f3304e6f5673d9ece211af6dd85c70ec8c8e85e91439b8cffbcf5387b11de1d0
[mark_tx_spend] Found Money contract in call 1
Broadcasting transaction...
Transaction ID: f3304e6f5673d9ece211af6dd85c70ec8c8e85e91439b8cffbcf5387b11de1d0
When the transaction gets confirmed, our membership commitment will will not exist in our contract registry.
Extending the smart contract client
The template client is barebones and doesn’t provide us with a way to
view the on chain records of our registry. For that purpose we can
create a new small program, or extend the client to support this
functionality. Following you will find example code for retrieving
a smart contract’s on-chain records from the JSON-RPC server in
darkfid which we can use to list our registry records:
// Parse Contract ID
let contract_id = match ContractId::from_str(&args.contract_id)?;
// Initialize an rpc client
let rpc_client = RpcClient::new(args.endpoint, executor.clone()).await?;
// Create the request params
let params = JsonValue::Array(vec![
JsonValue::String(contract_id.to_string()),
JsonValue::String(String::from("smart-contract-members")),
]);
// Execute the request
let req = JsonRequest::new("blockchain.get_contract_state", params);
let rep = rpc_client.request(req).await?;
// Parse response
let bytes = base64::decode(rep.get::<String>().unwrap()).unwrap();
let members: BTreeMap<Vec<u8>, Vec<u8>> = deserialize(&bytes)?;
// Print records
println!("{contract_id} members:");
if members.is_empty() {
println!("No members found");
} else {
let mut index = 1;
for member in members.keys() {
let member: pallas::Base = deserialize(member)?;
println!("{index}. {member:?}");
index += 1;
}
}