Skip to content

Commit

Permalink
contract/timelock: Add basic contract impl and test
Browse files Browse the repository at this point in the history
  • Loading branch information
parazyd committed Sep 28, 2023
1 parent 343c319 commit 14c9f96
Show file tree
Hide file tree
Showing 17 changed files with 588 additions and 7 deletions.
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ members = [
"src/contract/dao",
"src/contract/consensus",
"src/contract/deployooor",
"src/contract/timelock",

#"example/dchat",
]
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ contracts: zkas
$(MAKE) -C src/contract/dao
$(MAKE) -C src/contract/consensus
$(MAKE) -C src/contract/deployooor
$(MAKE) -C src/contract/timelock

darkfid: $(PROOFS_BIN) contracts
$(MAKE) -C bin/darkfid
Expand Down
11 changes: 10 additions & 1 deletion src/consensus/validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ use std::{collections::HashMap, io::Cursor, sync::Arc};
use darkfi_sdk::{
blockchain::Slot,
crypto::{
contract_id::{CONSENSUS_CONTRACT_ID, DAO_CONTRACT_ID, MONEY_CONTRACT_ID},
contract_id::{
CONSENSUS_CONTRACT_ID, DAO_CONTRACT_ID, MONEY_CONTRACT_ID, TIMELOCK_CONTRACT_ID,
},
schnorr::{SchnorrPublic, SchnorrSecret},
MerkleNode, MerkleTree, PublicKey, SecretKey,
},
Expand Down Expand Up @@ -148,6 +150,7 @@ impl ValidatorState {
let money_contract_deploy_payload = serialize(&faucet_pubkeys);
let dao_contract_deploy_payload = vec![];
let consensus_contract_deploy_payload = vec![];
let timelock_contract_deploy_payload = vec![];

let native_contracts = vec![
(
Expand All @@ -168,6 +171,12 @@ impl ValidatorState {
include_bytes!("../contract/consensus/consensus_contract.wasm").to_vec(),
consensus_contract_deploy_payload,
),
(
"Timelock Contract",
*TIMELOCK_CONTRACT_ID,
include_bytes!("../contract/timelock/timelock_contract.wasm").to_vec(),
timelock_contract_deploy_payload,
),
];

info!(target: "consensus::validator", "Deploying native wasm contracts");
Expand Down
1 change: 1 addition & 0 deletions src/contract/test-harness/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ darkfi-dao-contract = {path = "../dao", features = ["client", "no-entrypoint"]}
darkfi-money-contract = {path = "../money", features = ["client", "no-entrypoint"]}
darkfi-consensus-contract = {path = "../consensus", features = ["client", "no-entrypoint"]}
darkfi-deployooor-contract = {path = "../deployooor", features = ["client", "no-entrypoint"]}
darkfi-timelock-contract = {path = "../timelock", features = ["client", "no-entrypoint"]}

blake3 = "1.4.1"
bs58 = "0.5.0"
Expand Down
4 changes: 2 additions & 2 deletions src/contract/test-harness/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ pub fn init_logger() {
// We check this error so we can execute same file tests in parallel,
// otherwise second one fails to init logger here.
if simplelog::TermLogger::init(
simplelog::LevelFilter::Info,
//simplelog::LevelFilter::Debug,
//simplelog::LevelFilter::Info,
simplelog::LevelFilter::Debug,
//simplelog::LevelFilter::Trace,
cfg.build(),
simplelog::TerminalMode::Mixed,
Expand Down
19 changes: 16 additions & 3 deletions src/contract/test-harness/src/vks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@ use darkfi_money_contract::{
MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1,
};
use darkfi_sdk::crypto::{
contract_id::DEPLOYOOOR_CONTRACT_ID, CONSENSUS_CONTRACT_ID, DAO_CONTRACT_ID, MONEY_CONTRACT_ID,
contract_id::{DEPLOYOOOR_CONTRACT_ID, TIMELOCK_CONTRACT_ID},
CONSENSUS_CONTRACT_ID, DAO_CONTRACT_ID, MONEY_CONTRACT_ID,
};
use darkfi_serial::{deserialize, serialize};
use log::debug;

/// Update this if any circuits are changed
const VKS_HASH: &str = "f6b536bd601d6f0b709800da4cb46f026e3292be05eea1b407fab3146738c80e";
const PKS_HASH: &str = "d0297ba167bae9d74a05f0a57b3e3985591d092d096939f71d3fea9ab11bc96f";
const VKS_HASH: &str = "d40f9b1515fccbc42ec1494d8da6a164942ad0f1f0f6c7ee1645b62d281ffea5";
const PKS_HASH: &str = "98c9f9b1478a5f858d4fee92b30ce63755392de439e2c92bf99e6e4000d37fd1";

fn pks_path(typ: &str) -> Result<PathBuf> {
let output = Command::new("git").arg("rev-parse").arg("--show-toplevel").output()?.stdout;
Expand Down Expand Up @@ -133,6 +134,8 @@ pub fn read_or_gen_vks_and_pks() -> Result<(Pks, Vks)> {
&include_bytes!("../../consensus/proof/consensus_proposal_v1.zk.bin")[..],
// Deployooor
&include_bytes!("../../deployooor/proof/derive_contract_id.zk.bin")[..],
// Timelock
&include_bytes!("../../timelock/proof/unlock.zk.bin")[..],
];

let mut vks = vec![];
Expand Down Expand Up @@ -186,6 +189,9 @@ pub fn inject(sled_db: &sled::Db, vks: &Vks) -> Result<()> {
DEPLOYOOOR_CONTRACT_ID.hash_state_id(SMART_CONTRACT_ZKAS_DB_NAME);
let deployooor_zkas_tree = sled_db.open_tree(deployooor_zkas_tree_ptr)?;

let timelock_zkas_tree_ptr = TIMELOCK_CONTRACT_ID.hash_state_id(SMART_CONTRACT_ZKAS_DB_NAME);
let timelock_zkas_tree = sled_db.open_tree(timelock_zkas_tree_ptr)?;

for (bincode, namespace, vk) in vks.iter() {
match namespace.as_str() {
// Money circuits
Expand Down Expand Up @@ -226,6 +232,13 @@ pub fn inject(sled_db: &sled::Db, vks: &Vks) -> Result<()> {
consensus_zkas_tree.insert(key, value)?;
}

// Timelock circuits
"Unlock" => {
let key = serialize(&"Unlock");
let value = serialize(&(bincode.clone(), vk.clone()));
timelock_zkas_tree.insert(key, value)?;
}

x => panic!("Found unhandled zkas namespace {}", x),
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/contract/timelock/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
timelock_contract.wasm
proof/*.zk.bin
53 changes: 53 additions & 0 deletions src/contract/timelock/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[package]
name = "darkfi-timelock-contract"
version = "0.4.1"
authors = ["Dyne.org foundation <foundation@dyne.org>"]
license = "AGPL-3.0-only"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
bs58 = "0.5.0"
darkfi-sdk = { path = "../../sdk" }
darkfi-serial = { path = "../../serial", features = ["derive", "crypto"] }
darkfi-money-contract = { path = "../money", features = ["no-entrypoint"] }
thiserror = "1.0.47"

# The following dependencies are used for the client API and
# probably shouldn't be in WASM
chacha20poly1305 = { version = "0.10.1", optional = true }
darkfi = { path = "../../../", features = ["zk", "rpc", "blockchain"], optional = true }
halo2_proofs = { version = "0.3.0", optional = true }
log = { version = "0.4.20", optional = true }
rand = { version = "0.8.5", optional = true }

# These are used just for the integration tests
[dev-dependencies]
smol = "1.3.0"
darkfi = {path = "../../../", features = ["tx", "blockchain"]}
darkfi-money-contract = {path = "../money", features = ["client", "no-entrypoint"]}
simplelog = "0.12.1"
sled = "0.34.7"
darkfi-contract-test-harness = {path = "../test-harness"}

# We need to disable random using "custom" which makes the crate a noop
# so the wasm32-unknown-unknown target is enabled.
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2.8", features = ["custom"] }

[features]
default = []
no-entrypoint = []
client = [
"darkfi",
"darkfi-serial/async",
"darkfi-money-contract/client",
"darkfi-money-contract/no-entrypoint",

"rand",
"chacha20poly1305",
"log",
"halo2_proofs",
]
41 changes: 41 additions & 0 deletions src/contract/timelock/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.POSIX:

# Cargo binary
CARGO = cargo +nightly

# zkas compiler binary
ZKAS = ../../../zkas

# zkas circuits
PROOFS_SRC = $(shell find proof -type f -name '*.zk')
PROOFS_BIN = $(PROOFS_SRC:=.bin)

# wasm source files
WASM_SRC = \
$(shell find src -type f) \
$(shell find ../../sdk -type f -name '*.rs') \
$(shell find ../../serial -type f -name '*.rs')

# wasm contract binary
WASM_BIN = timelock_contract.wasm

all: $(WASM_BIN)

$(WASM_BIN): $(WASM_SRC) $(PROOFS_BIN)
$(CARGO) build --release --package darkfi-timelock-contract --target wasm32-unknown-unknown
cp -f ../../../target/wasm32-unknown-unknown/release/darkfi_timelock_contract.wasm $@

$(PROOFS_BIN): $(ZKAS) $(PROOFS_SRC)
$(ZKAS) $(basename $@) -o $@

test-integration: all
$(CARGO) test --release --features=no-entrypoint,client \
--package darkfi-timelock-contract \
--test integration $(ARGS)

test: test-integration

clean:
rm -f $(PROOFS_BIN) $(WASM_BIN)

.PHONY: all test test-integration clean
66 changes: 66 additions & 0 deletions src/contract/timelock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Anonymously timelocking a coin

1. We have a coin `C` which we want to lock from being spent
up until a certain block height.

A coin `C` consists of:

```
poseidon_hash(
pub_x, # X coordinate of the owner's pubkey
pub_y, # Y coordinate of the owner's pubkey
value, # The value of the coin
token, # The token ID of the coin
serial, # The unique serial number corresponding to the coin
spend_hook, # A hook enforcing another contract call once spent
user_data, # Arbitrary data appended to the coin
);
```

By committing to the fields using the hash, we _hide_ the inner
values and only make the commitment/coin `C` public. Data inside
it cannot be retrieved this way since the hash function is one-way.

<details>
<summary>How can we timelock this coin?</summary>

* `spend_hook` can point to the `timelock` smart contract
* `user_data` can contain the wanted block height
</details>

## Spending a coin

To spend a coin, we have to _burn_ it. At this point we also enforce
any `spend_hook` that is set in the coin. If the `spend_hook` is not
zero, the smart contract runtime will enforce that the next contract
in line is the contract pointed by `spend_hook`. In our case this is
going to be the `timelock` contract.

The `user_data` from the coin will be blinded with some random value
in order not to reveal it. This will result in `user_data_enc`, which
can then be used by `timelock` to enforce that same data (block height).

# Transaction format

We want to send a timelocked coin to someone.

## Contract calls

1. `Money::Transfer`
2. `Timelock::Unlock`

# The Timelock contract

* The contract will expect that the previous contract call is
`Money::Transfer`.
* The contract will take `user_data_enc` from that previous call
and enforce it in the public inputs of the ZK proofs used for
the timelock.
* Additionally, the contract will fetch the current block height
in order to plug it into the ZK proof and check for that validity.
* The ZK proof needs to enforce that the current block height (at the
time of contract execution) is less than `user_data`, which
represents the timelock height.

Now, if both `Money::Transfer` and `Timelock::Unlock` pass, the
transaction will pass and the coin is able to be spent.
22 changes: 22 additions & 0 deletions src/contract/timelock/proof/unlock.zk
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
k = 13;
field = "pallas";

constant "Unlock" {}

witness "Unlock" {
Base block_height,
Base user_data,
Base user_data_blind,
}

circuit "Unlock" {
range_check(64, user_data);
range_check(64, block_height);
constrain_instance(block_height);

# Enforce that the current block height is higher than the timelock
less_than_strict(user_data, block_height);

user_data_enc = poseidon_hash(user_data, user_data_blind);
constrain_instance(user_data_enc);
}
Loading

0 comments on commit 14c9f96

Please sign in to comment.