diff --git a/Cargo.lock b/Cargo.lock index 7522b940ede0..87ae9be27ba5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1538,6 +1538,7 @@ dependencies = [ "darkfi-money-contract", "darkfi-sdk", "darkfi-serial", + "darkfi-timelock-contract", "log", "rand 0.8.5", "simplelog", @@ -1664,6 +1665,27 @@ dependencies = [ "url", ] +[[package]] +name = "darkfi-timelock-contract" +version = "0.4.1" +dependencies = [ + "bs58", + "chacha20poly1305", + "darkfi", + "darkfi-contract-test-harness", + "darkfi-money-contract", + "darkfi-sdk", + "darkfi-serial", + "getrandom 0.2.10", + "halo2_proofs", + "log", + "rand 0.8.5", + "simplelog", + "sled", + "smol", + "thiserror", +] + [[package]] name = "darkfid" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 98dc0e3f3367..785f7badb18d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ members = [ "src/contract/dao", "src/contract/consensus", "src/contract/deployooor", + "src/contract/timelock", #"example/dchat", ] diff --git a/Makefile b/Makefile index 3f2413c921ee..8f9fd947d277 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/src/consensus/validator.rs b/src/consensus/validator.rs index f49b38cdbf0e..622ff1b43144 100644 --- a/src/consensus/validator.rs +++ b/src/consensus/validator.rs @@ -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, }, @@ -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![ ( @@ -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"); diff --git a/src/contract/test-harness/Cargo.toml b/src/contract/test-harness/Cargo.toml index 842b1dc98780..858004a01792 100644 --- a/src/contract/test-harness/Cargo.toml +++ b/src/contract/test-harness/Cargo.toml @@ -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" diff --git a/src/contract/test-harness/src/lib.rs b/src/contract/test-harness/src/lib.rs index 32992a9425f9..2e1e21062da1 100644 --- a/src/contract/test-harness/src/lib.rs +++ b/src/contract/test-harness/src/lib.rs @@ -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, diff --git a/src/contract/test-harness/src/vks.rs b/src/contract/test-harness/src/vks.rs index 454adcf84c96..242083c643fa 100644 --- a/src/contract/test-harness/src/vks.rs +++ b/src/contract/test-harness/src/vks.rs @@ -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 { let output = Command::new("git").arg("rev-parse").arg("--show-toplevel").output()?.stdout; @@ -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![]; @@ -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 @@ -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), } } diff --git a/src/contract/timelock/.gitignore b/src/contract/timelock/.gitignore new file mode 100644 index 000000000000..fb1d460c974d --- /dev/null +++ b/src/contract/timelock/.gitignore @@ -0,0 +1,2 @@ +timelock_contract.wasm +proof/*.zk.bin diff --git a/src/contract/timelock/Cargo.toml b/src/contract/timelock/Cargo.toml new file mode 100644 index 000000000000..b460fd79005a --- /dev/null +++ b/src/contract/timelock/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "darkfi-timelock-contract" +version = "0.4.1" +authors = ["Dyne.org foundation "] +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", +] diff --git a/src/contract/timelock/Makefile b/src/contract/timelock/Makefile new file mode 100644 index 000000000000..237f24305a4b --- /dev/null +++ b/src/contract/timelock/Makefile @@ -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 diff --git a/src/contract/timelock/README.md b/src/contract/timelock/README.md new file mode 100644 index 000000000000..58e8475bb0b2 --- /dev/null +++ b/src/contract/timelock/README.md @@ -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. + +
+ How can we timelock this coin? + + * `spend_hook` can point to the `timelock` smart contract + * `user_data` can contain the wanted block height +
+ +## 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. diff --git a/src/contract/timelock/proof/unlock.zk b/src/contract/timelock/proof/unlock.zk new file mode 100644 index 000000000000..510af171cc94 --- /dev/null +++ b/src/contract/timelock/proof/unlock.zk @@ -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); +} diff --git a/src/contract/timelock/src/entrypoint.rs b/src/contract/timelock/src/entrypoint.rs new file mode 100644 index 000000000000..13be7749260f --- /dev/null +++ b/src/contract/timelock/src/entrypoint.rs @@ -0,0 +1,104 @@ +use darkfi_money_contract::model::MoneyTransferParamsV1; +use darkfi_sdk::{ + crypto::{ContractId, PublicKey}, + db::zkas_db_set, + error::{ContractError, ContractResult}, + msg, + pasta::pallas, + util::{get_verifying_slot, set_return_data}, + ContractCall, +}; +use darkfi_serial::{deserialize, Encodable}; + +use crate::TimelockFunction; + +darkfi_sdk::define_contract!( + init: init_contract, + exec: process_instruction, + apply: process_update, + metadata: get_metadata +); + +fn init_contract(_cid: ContractId, _ix: &[u8]) -> ContractResult { + // We deploy the compiled ZK circuit on-chain by embedding the code in WASM + // and then place it into the database like this: + let unlock_bincode = include_bytes!("../proof/unlock.zk.bin"); + zkas_db_set(&unlock_bincode[..])?; + + Ok(()) +} + +fn get_metadata(_cid: ContractId, ix: &[u8]) -> ContractResult { + let (call_idx, calls): (u32, Vec) = deserialize(ix)?; + if call_idx >= calls.len() as u32 { + msg!("Error: call_idx >= calls.len()"); + return Err(ContractError::Internal) + } + + match TimelockFunction::try_from(calls[call_idx as usize].data[0])? { + TimelockFunction::Unlock => { + // Here we expect that this call_idx is 1, and the previous call_idx is 0. + // We'll take the params from the previous call and fetch `user_data_enc` + // from it. For demo purposes we'll hardcode a single input, but this can + // easily be expanded into a for loop iterating over all inputs in the + // `Money::Transfer` contract call. + assert!(call_idx == 1); + + // Deserialize the parameters from the previous contract call + let money_params: MoneyTransferParamsV1 = deserialize(&calls[0].data[1..])?; + + // Grab `user_data_enc` from the input + let user_data_enc = money_params.inputs[0].user_data_enc; + + // Grab the current block height (get_verifying_slot returns u64) + let block_height = pallas::Base::from(get_verifying_slot()); + + // Now construct the public inputs for the ZK proof + let zk_public_inputs: Vec<(String, Vec)> = + vec![("Unlock".to_string(), vec![block_height, user_data_enc])]; + + // In this contract call, we don't have to verify any signatures, + // so we leave them empty + let signature_pubkeys: Vec = vec![]; + + // Now we serialize everything gathered and return it. + // This data will be fed into the ZK verification process. + let mut metadata = vec![]; + zk_public_inputs.encode(&mut metadata)?; + signature_pubkeys.encode(&mut metadata)?; + + // Export the data + Ok(set_return_data(&metadata)?) + } + } +} + +fn process_instruction(_cid: ContractId, ix: &[u8]) -> ContractResult { + let (call_idx, calls): (u32, Vec) = deserialize(ix)?; + if call_idx >= calls.len() as u32 { + msg!("Error: call_idx >= calls.len()"); + return Err(ContractError::Internal) + } + + match TimelockFunction::try_from(calls[call_idx as usize].data[0])? { + TimelockFunction::Unlock => { + // In here we could perform anything that we require to do + // any kind of verification. It is not needed for the timelock + // since everything that has to be done is enforce the ZK proof. + // + // But for example, `::Unlock` could have its own `Params`, and + // then here we'd assert that the `user_data_enc` from these + // params is the same as `user_data_enc` in the previous call. + + Ok(set_return_data(&[TimelockFunction::Unlock as u8])?) + } + } +} + +fn process_update(_cid: ContractId, update_data: &[u8]) -> ContractResult { + match TimelockFunction::try_from(update_data[0])? { + // The timelock does not have to perform any state update, so we + // just return Ok() here. + TimelockFunction::Unlock => Ok(()), + } +} diff --git a/src/contract/timelock/src/lib.rs b/src/contract/timelock/src/lib.rs new file mode 100644 index 000000000000..7824c04d78c8 --- /dev/null +++ b/src/contract/timelock/src/lib.rs @@ -0,0 +1,21 @@ +use darkfi_sdk::error::ContractError; + +#[repr(u8)] +pub enum TimelockFunction { + Unlock = 0x01, +} + +impl TryFrom for TimelockFunction { + type Error = ContractError; + + fn try_from(b: u8) -> Result { + match b { + 0x01 => Ok(Self::Unlock), + _ => Err(ContractError::InvalidFunction), + } + } +} + +#[cfg(not(feature = "no-entrypoint"))] +/// WASM entrypoint functions +pub mod entrypoint; diff --git a/src/contract/timelock/tests/integration.rs b/src/contract/timelock/tests/integration.rs new file mode 100644 index 000000000000..5867555beb0a --- /dev/null +++ b/src/contract/timelock/tests/integration.rs @@ -0,0 +1,209 @@ +use darkfi::{ + tx::Transaction, + zk::{ + halo2::{Field, Value}, + Proof, Witness, ZkCircuit, + }, + Result, +}; +use darkfi_contract_test_harness::{init_logger, Holder, TestHarness}; +use darkfi_money_contract::{ + client::transfer_v1::TransferCallBuilder, MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, + MONEY_CONTRACT_ZKAS_MINT_NS_V1, +}; +use darkfi_sdk::{ + crypto::{contract_id::TIMELOCK_CONTRACT_ID, poseidon_hash, MONEY_CONTRACT_ID}, + pasta::pallas, + ContractCall, +}; +use darkfi_serial::Encodable; +use darkfi_timelock_contract::TimelockFunction; +use log::info; +use rand::rngs::OsRng; + +#[test] +fn timelock_integration() -> Result<()> { + smol::block_on(async { + init_logger(); + + // Current block height we're verifying on + let mut current_slot = 0; + + // Holders this test will use + const HOLDERS: [Holder; 2] = [Holder::Alice, Holder::Bob]; + + // Initialize harness + let mut th = TestHarness::new(&["money".to_string(), "timelock".to_string()]).await?; + + // Reference Alice & Bob's keypairs + let alice_wallet_keypair = th.holders.get(&Holder::Alice).unwrap().keypair; + let bob_wallet_keypair = th.holders.get(&Holder::Bob).unwrap().keypair; + + // Alice mints some arbitrary tokens to herself. + info!("[Alice] Building ALICE token mint tx"); + let (token_mint_tx, token_mint_params) = + th.token_mint(10000, &Holder::Alice, &Holder::Alice, None, None)?; + + // Execute the transaction for every participant + for holder in &HOLDERS { + info!("[{holder:?}] Executing ALICE token mint tx"); + th.execute_token_mint_tx(holder, &token_mint_tx, &token_mint_params, current_slot) + .await?; + } + + // Alice gathers the minted coin + th.gather_owncoin(&Holder::Alice, &token_mint_params.output, None)?; + // We take note of the token ID to use later + let token_id = th.holders.get(&Holder::Alice).unwrap().unspent_money_coins[0].note.token_id; + + // Now Alice sends those tokens to Bob and applies a timelock to them. + // This means Bob will receive the tokens, but will be unable to spend + // them until a certain block height is reached. + + // We will build the transaction. First, we build `Money::Transfer`. + let (mint_pk, mint_zkbin) = + th.proving_keys.get(&MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string()).unwrap().clone(); + let (burn_pk, burn_zkbin) = + th.proving_keys.get(&MONEY_CONTRACT_ZKAS_BURN_NS_V1.to_string()).unwrap().clone(); + + // Our spend hook will point to the timelock contract, + // and the user_data will specify a certain block height. + // user_data_blind is random and used to obfuscate the height. + let spend_hook = *TIMELOCK_CONTRACT_ID; + let user_data = pallas::Base::from(5); + let user_data_blind = pallas::Base::random(&mut OsRng); + + // Build the Money::Transfer call + let alice_transfer_builder = TransferCallBuilder { + keypair: alice_wallet_keypair, + recipient: bob_wallet_keypair.public, + value: 10000, + token_id, + rcpt_spend_hook: spend_hook.inner(), + rcpt_user_data: user_data, + rcpt_user_data_blind: user_data_blind, + change_spend_hook: pallas::Base::ZERO, + change_user_data: pallas::Base::ZERO, + change_user_data_blind: pallas::Base::ZERO, + coins: th.holders.get(&Holder::Alice).unwrap().unspent_money_coins.clone(), + tree: th.holders.get(&Holder::Alice).unwrap().money_merkle_tree.clone(), + mint_zkbin: mint_zkbin.clone(), + mint_pk: mint_pk.clone(), + burn_zkbin: burn_zkbin.clone(), + burn_pk: burn_pk.clone(), + clear_input: false, + }; + + let alice_transfer_debris = alice_transfer_builder.build()?; + + // We build the transfer transaction + let mut data = vec![MoneyFunction::TransferV1 as u8]; + alice_transfer_debris.params.encode(&mut data)?; + let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }]; + let proofs = vec![alice_transfer_debris.proofs]; + let mut tx = Transaction { calls, proofs, signatures: vec![] }; + let sigs = tx.create_sigs(&mut OsRng, &alice_transfer_debris.signature_secrets)?; + tx.signatures = vec![sigs]; + + // Both Alice and Bob's blockchains execute the tx + th.execute_transfer_tx( + &Holder::Alice, + &tx, + &alice_transfer_debris.params, + current_slot, + true, + ) + .await?; + th.execute_transfer_tx( + &Holder::Bob, + &tx, + &alice_transfer_debris.params, + current_slot, + true, + ) + .await?; + + // Bob gathers the received coin + th.gather_owncoin(&Holder::Bob, &alice_transfer_debris.params.outputs[0], None)?; + + // Bob will transfer the tokens back to Alice, so he creates a transfer tx + let bob_user_data_blind = pallas::Base::random(&mut OsRng); + + let bob_transfer_builder = TransferCallBuilder { + keypair: bob_wallet_keypair, + recipient: alice_wallet_keypair.public, + value: 10000, + token_id, + rcpt_spend_hook: pallas::Base::ZERO, + rcpt_user_data: pallas::Base::ZERO, + rcpt_user_data_blind: pallas::Base::ZERO, + change_spend_hook: pallas::Base::ZERO, + change_user_data: pallas::Base::ZERO, + change_user_data_blind: bob_user_data_blind, + coins: th.holders.get(&Holder::Bob).unwrap().unspent_money_coins.clone(), + tree: th.holders.get(&Holder::Bob).unwrap().money_merkle_tree.clone(), + mint_zkbin: mint_zkbin.clone(), + mint_pk: mint_pk.clone(), + burn_zkbin: burn_zkbin.clone(), + burn_pk: burn_pk.clone(), + clear_input: false, + }; + + let bob_transfer_debris = bob_transfer_builder.build()?; + + // Now since in `rcpt_spend_hook` and `rcpt_user_data` we have + // enforced the timelock, we also have to build the Timelock::Unlock + // call. This one is a bit simpler and we build it directly since there + // is not much data to handle. It will use the params from the transfer + // call and none of its own. However we must build a zk proof. + let (unlock_pk, unlock_zkbin) = th.proving_keys.get(&"Unlock".to_string()).unwrap(); + + // Bob will claim he's unlocking at block height 7 + current_slot = 7; + let unlock_claim = pallas::Base::from(current_slot); + + // Construct the ZK public inputs + let public_inputs = + vec![unlock_claim, poseidon_hash([pallas::Base::from(5), bob_user_data_blind])]; + + // Construct the ZK proof witnesses + let prover_witnesses = vec![ + Witness::Base(Value::known(unlock_claim)), + Witness::Base(Value::known(pallas::Base::from(5))), + Witness::Base(Value::known(bob_user_data_blind)), + ]; + + // Create the ZK proof + let circuit = ZkCircuit::new(prover_witnesses, unlock_zkbin); + let proof = Proof::create(unlock_pk, &[circuit], &public_inputs, &mut OsRng)?; + + // We build the transfer transaction + let mut calls = vec![]; + let mut proofs = vec![]; + + let mut data = vec![MoneyFunction::TransferV1 as u8]; + bob_transfer_debris.params.encode(&mut data)?; + calls.push(ContractCall { contract_id: *MONEY_CONTRACT_ID, data }); + proofs.push(bob_transfer_debris.proofs); + + let data = vec![TimelockFunction::Unlock as u8]; + calls.push(ContractCall { contract_id: *TIMELOCK_CONTRACT_ID, data }); + proofs.push(vec![proof]); + + let mut tx = Transaction { calls, proofs, signatures: vec![] }; + let sigs = tx.create_sigs(&mut OsRng, &bob_transfer_debris.signature_secrets)?; + tx.signatures = vec![sigs, vec![]]; + + // Alice executes the transaction + th.execute_transfer_tx( + &Holder::Alice, + &tx, + &alice_transfer_debris.params, + current_slot, + true, + ) + .await?; + + Ok(()) + }) +} diff --git a/src/sdk/src/crypto/contract_id.rs b/src/sdk/src/crypto/contract_id.rs index 5cf2652d70b9..4d509d3bca35 100644 --- a/src/sdk/src/crypto/contract_id.rs +++ b/src/sdk/src/crypto/contract_id.rs @@ -49,6 +49,10 @@ lazy_static! { /// Contract ID for the native Deployooor contract pub static ref DEPLOYOOOR_CONTRACT_ID: ContractId = ContractId::from(poseidon_hash([*CONTRACT_ID_PREFIX, pallas::Base::zero(), pallas::Base::from(3)])); + + /// Contract ID for the native Timelock contract + pub static ref TIMELOCK_CONTRACT_ID: ContractId = + ContractId::from(poseidon_hash([*CONTRACT_ID_PREFIX, pallas::Base::zero(), pallas::Base::from(4)])); } /// ContractId represents an on-chain identifier for a certain smart contract. diff --git a/src/validator/utils.rs b/src/validator/utils.rs index a7360c0fb04d..331d34aaba6c 100644 --- a/src/validator/utils.rs +++ b/src/validator/utils.rs @@ -16,7 +16,10 @@ * along with this program. If not, see . */ -use darkfi_sdk::crypto::{PublicKey, CONSENSUS_CONTRACT_ID, DAO_CONTRACT_ID, MONEY_CONTRACT_ID}; +use darkfi_sdk::crypto::{ + contract_id::TIMELOCK_CONTRACT_ID, PublicKey, CONSENSUS_CONTRACT_ID, DAO_CONTRACT_ID, + MONEY_CONTRACT_ID, +}; use darkfi_serial::serialize; use log::info; @@ -51,6 +54,9 @@ pub fn deploy_native_contracts( // The Consensus contract uses an empty payload to deploy itself. let consensus_contract_deploy_payload = vec![]; + // The Timelock contract uses an empty payload to deploy itself. + let timelock_contract_deploy_payload = vec![]; + let native_contracts = vec![ ( "Money Contract", @@ -70,6 +76,12 @@ pub fn deploy_native_contracts( 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, + ), ]; for nc in native_contracts {