diff --git a/.gitignore b/.gitignore index 68d86805f..95b020d8f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ /tmp/* /cashierd -/dao-cli +/dao /daod /darkfid /darkotc diff --git a/Cargo.lock b/Cargo.lock index e2095df00..413853cb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1100,7 +1100,7 @@ dependencies = [ ] [[package]] -name = "dao-cli" +name = "dao" version = "0.3.0" dependencies = [ "async-channel", @@ -1112,6 +1112,7 @@ dependencies = [ "futures", "log", "num_cpus", + "prettytable-rs", "serde_json", "simplelog", "smol", @@ -1126,11 +1127,13 @@ dependencies = [ "async-executor", "async-std", "async-trait", + "bs58", "crypto_api_chachapoly", "darkfi", "darkfi-serial", "easy-parallel", "futures", + "fxhash", "group", "halo2_gadgets", "halo2_proofs", diff --git a/Cargo.toml b/Cargo.toml index 649b8b49e..492138288 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,8 @@ members = [ "bin/ircd", "bin/ircd2", "bin/dnetview", - "bin/daod", - "bin/dao-cli", + "bin/dao/daod", + "bin/dao/dao-cli", "bin/tau/taud", "bin/tau/tau-cli", "bin/darkwiki/darkwikid", @@ -363,6 +363,16 @@ name = "zk" path = "example/zk.rs" required-features = ["crypto"] +[[example]] +name = "dao" +path = "example/dao/dao.rs" +required-features = ["crypto"] + +[[example]] +name = "test" +path = "example/derive_macro_example.rs" +required-features = ["serial"] + [[example]] name = "lead" path = "example/lead.rs" diff --git a/Makefile b/Makefile index 0141b3683..94264fe7d 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,7 @@ CARGO = cargo #RUSTFLAGS = -C target-cpu=native # Binaries to be built - -BINS = drk darkfid tau taud ircd dnetview darkotc darkwikid darkwiki +BINS = drk darkfid tau taud ircd dnetview darkotc darkwikid darkwiki dao # Common dependencies which should force the binaries to be rebuilt BINDEPS = \ @@ -24,7 +23,7 @@ BINDEPS = \ # ZK proofs to compile with zkas PROOFS = \ - $(shell find bin/daod/proof -type f -name '*.zk') \ + $(shell find bin/dao/daod/proof -type f -name '*.zk') \ $(shell find proof -type f -name '*.zk') \ example/simple.zk diff --git a/bin/dao-cli/src/main.rs b/bin/dao-cli/src/main.rs deleted file mode 100644 index 1b756810c..000000000 --- a/bin/dao-cli/src/main.rs +++ /dev/null @@ -1,77 +0,0 @@ -use clap::{IntoApp, Parser, Subcommand}; -use serde_json::{json, Value}; -use url::Url; - -use darkfi::{ - rpc::{client::RpcClient, jsonrpc::JsonRequest}, - Result, -}; - -#[derive(Subcommand)] -pub enum CliDaoSubCommands { - /// Say hello to the RPC - Hello {}, -} - -/// DAO cli -#[derive(Parser)] -#[clap(name = "dao")] -#[clap(arg_required_else_help(true))] -pub struct CliDao { - /// Increase verbosity - #[clap(short, parse(from_occurrences))] - pub verbose: u8, - #[clap(subcommand)] - pub command: Option, -} -pub struct Rpc { - client: RpcClient, -} - -impl Rpc { - // --> {"jsonrpc": "2.0", "method": "say_hello", "params": [], "id": 42} - // <-- {"jsonrpc": "2.0", "result": "hello world", "id": 42} - async fn say_hello(&self) -> Result { - let req = JsonRequest::new("say_hello", json!([])); - self.client.request(req).await - } -} - -async fn start(options: CliDao) -> Result<()> { - let rpc_addr = "tcp://127.0.0.1:7777"; - let client = Rpc { client: RpcClient::new(Url::parse(rpc_addr)?).await? }; - - #[allow(clippy::single_match)] - match options.command { - Some(CliDaoSubCommands::Hello {}) => { - let reply = client.say_hello().await?; - println!("Server replied: {}", &reply.to_string()); - return Ok(()) - } - None => {} - } - - Ok(()) -} - -#[async_std::main] -async fn main() -> Result<()> { - let args = CliDao::parse(); - let _matches = CliDao::command().get_matches(); - - //let config_path = if args.config.is_some() { - // expand_path(&args.config.clone().unwrap())? - //} else { - // join_config_path(&PathBuf::from("drk.toml"))? - //}; - - // Spawn config file if it's not in place already. - //spawn_config(&config_path, CONFIG_FILE_CONTENTS)?; - - //let (lvl, conf) = log_config(matches)?; - //TermLogger::init(lvl, conf, TerminalMode::Mixed, ColorChoice::Auto)?; - - //let config = Config::::load(config_path)?; - - start(args).await -} diff --git a/bin/dao/README.md b/bin/dao/README.md new file mode 100644 index 000000000..569722ddf --- /dev/null +++ b/bin/dao/README.md @@ -0,0 +1 @@ +# DAO \ No newline at end of file diff --git a/bin/dao-cli/Cargo.toml b/bin/dao/dao-cli/Cargo.toml similarity index 88% rename from bin/dao-cli/Cargo.toml rename to bin/dao/dao-cli/Cargo.toml index 200ffad25..a1f734e15 100644 --- a/bin/dao-cli/Cargo.toml +++ b/bin/dao/dao-cli/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "dao-cli" +name = "dao" version = "0.3.0" edition = "2021" [dependencies.darkfi] -path = "../../" +path = "../../../" features = ["rpc"] [dependencies] @@ -22,6 +22,7 @@ log = "0.4.17" num_cpus = "1.13.1" simplelog = "0.12.0" url = "2.3.1" +prettytable-rs = "0.9.0" # Encoding and parsing serde_json = "1.0.85" diff --git a/bin/dao/dao-cli/run_demo.sh b/bin/dao/dao-cli/run_demo.sh new file mode 100755 index 000000000..c9004553b --- /dev/null +++ b/bin/dao/dao-cli/run_demo.sh @@ -0,0 +1,41 @@ +#!/bin/bash +cargo run create 110 110 1 2 +addr=$(cargo run addr | cut -d " " -f 4) +addr2=$(echo $addr | cut -c 2-) +addr3=${addr2::-1} +echo $addr3 + +cargo run mint 1000000 $addr3 + +alice=$(cargo run keygen) +alice=$(cargo run keygen | cut -d " " -f 4) +alice2=$(echo $alice | cut -c 2-) +alice3=${alice2::-1} +echo $alice3 + +bob=$(cargo run keygen) +bob=$(cargo run keygen | cut -d " " -f 4) +bob2=$(echo $bob | cut -c 2-) +bob3=${bob2::-1} +echo $bob3 + +charlie=$(cargo run keygen) +charlie=$(cargo run keygen | cut -d " " -f 4) +charlie2=$(echo $charlie | cut -c 2-) +charlie3=${charlie2::-1} +echo $charlie3 + +cargo run airdrop $alice3 10000 +cargo run airdrop $bob3 100000 +cargo run airdrop $charlie3 10000 + +proposal=$(cargo run propose $alice3 $charlie3 10000 | cut -d " " -f 3) +proposal2=$(echo $proposal | cut -c 2-) +proposal3=${proposal2::-1} +echo $proposal3 + +cargo run vote $alice3 yes +cargo run vote $bob3 yes +cargo run vote $charlie3 no + +cargo run exec $proposal3 diff --git a/bin/dao/dao-cli/src/main.rs b/bin/dao/dao-cli/src/main.rs new file mode 100644 index 000000000..70ac212f7 --- /dev/null +++ b/bin/dao/dao-cli/src/main.rs @@ -0,0 +1,241 @@ +use std::process::exit; + +use clap::{IntoApp, Parser, Subcommand}; +use prettytable::{format, row, Table}; +use url::Url; + +use darkfi::{rpc::client::RpcClient, Result}; + +mod rpc; + +#[derive(Subcommand)] +pub enum CliDaoSubCommands { + /// Create DAO + Create { + /// Minium number of governance tokens a user must have to propose a vote. + dao_proposer_limit: u64, + + /// Minimum number of governance tokens staked on a proposal for it to pass. + dao_quorum: u64, + + /// Quotient value of minimum vote ratio of yes:no votes required for a proposal to pass. + dao_approval_ratio_quot: u64, + + /// Base value of minimum vote ratio of yes:no votes required for a proposal to pass. + dao_approval_ratio_base: u64, + }, + /// Mint tokens + Addr {}, + GetVotes {}, + GetProposals {}, + Mint { + /// Number of treasury tokens to mint. + token_supply: u64, + + /// Public key of the DAO treasury. + dao_addr: String, + }, + UserBalance { + nym: String, + }, + DaoBalance {}, + DaoBulla {}, + Keygen {}, + /// Airdrop tokens + Airdrop { + nym: String, + + value: u64, + }, + /// Propose + Propose { + sender: String, + + recipient: String, + + amount: u64, + }, + /// Vote + Vote { + nym: String, + + vote: String, + }, + /// Execute + Exec { + bulla: String, + }, +} + +/// DAO cli +#[derive(Parser)] +#[clap(name = "dao")] +#[clap(arg_required_else_help(true))] +pub struct CliDao { + /// Increase verbosity + #[clap(short, parse(from_occurrences))] + pub verbose: u8, + #[clap(subcommand)] + pub command: Option, +} + +pub struct Rpc { + client: RpcClient, +} + +async fn start(options: CliDao) -> Result<()> { + let rpc_addr = "tcp://127.0.0.1:7777"; + let client = Rpc { client: RpcClient::new(Url::parse(rpc_addr)?).await? }; + match options.command { + Some(CliDaoSubCommands::Create { + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_base, + dao_approval_ratio_quot, + }) => { + let reply = client + .create( + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + ) + .await?; + println!("Created DAO bulla: {}", &reply.to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::Addr {}) => { + let reply = client.addr().await?; + println!("DAO public address: {}", &reply.to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::GetVotes {}) => { + let reply = client.get_votes().await?; + println!("{}", &reply.to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::GetProposals {}) => { + let reply = client.get_proposals().await?; + println!("{}", &reply.to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::Mint { token_supply, dao_addr }) => { + let reply = client.mint(token_supply, dao_addr).await?; + println!("{}", &reply.as_str().unwrap().to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::Keygen {}) => { + let reply = client.keygen().await?; + println!("User public key: {}", &reply.to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::Airdrop { nym, value }) => { + let reply = client.airdrop(nym, value).await?; + println!("{}", &reply.as_str().unwrap().to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::DaoBalance {}) => { + let rep = client.dao_balance().await?; + + if !rep.is_object() { + eprintln!("Invalid balance data received from darkfid RPC endpoint."); + exit(1); + } + + let mut table = Table::new(); + table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.set_titles(row!["Token", "Balance"]); + + for i in rep.as_object().unwrap().keys() { + if let Some(balance) = rep[i].as_u64() { + table.add_row(row![i, balance]); + continue + } + + eprintln!("Found invalid balance data for key \"{}\"", i); + } + + if table.is_empty() { + println!("No balances."); + } else { + println!("{}", table); + } + // println!("DAO balance: {}", &reply.to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::DaoBulla {}) => { + let reply = client.dao_bulla().await?; + println!("DAO bulla: {}", &reply.to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::UserBalance { nym }) => { + let rep = client.user_balance(nym).await?; + + if !rep.is_object() { + eprintln!("Invalid balance data received from darkfid RPC endpoint."); + exit(1); + } + + let mut table = Table::new(); + table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.set_titles(row!["Token", "Balance"]); + + for i in rep.as_object().unwrap().keys() { + if let Some(balance) = rep[i].as_u64() { + table.add_row(row![i, balance]); + continue + } + + eprintln!("Found invalid balance data for key \"{}\"", i); + } + + if table.is_empty() { + println!("No balances."); + } else { + println!("{}", table); + } + // println!("User balance: {}", &reply.to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::Propose { sender, recipient, amount }) => { + let reply = client.propose(sender, recipient, amount).await?; + println!("Proposal bulla: {}", &reply.to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::Vote { nym, vote }) => { + let reply = client.vote(nym, vote).await?; + println!("{}", &reply.to_string()); + return Ok(()) + } + Some(CliDaoSubCommands::Exec { bulla }) => { + let reply = client.exec(bulla).await?; + println!("{}", &reply.to_string()); + return Ok(()) + } + None => {} + } + + Ok(()) +} + +#[async_std::main] +async fn main() -> Result<()> { + let args = CliDao::parse(); + let _matches = CliDao::command().get_matches(); + + //let config_path = if args.config.is_some() { + // expand_path(&args.config.clone().unwrap())? + //} else { + // join_config_path(&PathBuf::from("drk.toml"))? + //}; + + // Spawn config file if it's not in place already. + //spawn_config(&config_path, CONFIG_FILE_CONTENTS)?; + + //let (lvl, conf) = log_config(matches)?; + //TermLogger::init(lvl, conf, TerminalMode::Mixed, ColorChoice::Auto)?; + + //let config = Config::::load(config_path)?; + + start(args).await +} diff --git a/bin/dao/dao-cli/src/rpc.rs b/bin/dao/dao-cli/src/rpc.rs new file mode 100644 index 000000000..a84f234ac --- /dev/null +++ b/bin/dao/dao-cli/src/rpc.rs @@ -0,0 +1,112 @@ +use serde_json::{json, Value}; + +use darkfi::{rpc::jsonrpc::JsonRequest, Result}; + +use crate::Rpc; + +impl Rpc { + // --> {"jsonrpc": "2.0", "method": "create", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "creating dao...", "id": 42} + pub async fn create( + &self, + dao_proposer_limit: u64, + dao_quorum: u64, + dao_approval_ratio_quot: u64, + dao_approval_ratio_base: u64, + ) -> Result { + let req = JsonRequest::new( + "create", + json!([ + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + ]), + ); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "mint", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "minting tokens...", "id": 42} + pub async fn addr(&self) -> Result { + let req = JsonRequest::new("get_dao_addr", json!([])); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "mint", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "minting tokens...", "id": 42} + pub async fn mint(&self, token_supply: u64, dao_addr: String) -> Result { + let req = JsonRequest::new("mint", json!([token_supply, dao_addr])); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "airdrop", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "airdropping tokens...", "id": 42} + pub async fn airdrop(&self, nym: String, value: u64) -> Result { + let req = JsonRequest::new("airdrop", json!([nym, value])); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "airdrop", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "airdropping tokens...", "id": 42} + pub async fn keygen(&self) -> Result { + let req = JsonRequest::new("keygen", json!([])); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "airdrop", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "airdropping tokens...", "id": 42} + pub async fn dao_balance(&self) -> Result { + let req = JsonRequest::new("dao_balance", json!([])); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "airdrop", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "airdropping tokens...", "id": 42} + pub async fn dao_bulla(&self) -> Result { + let req = JsonRequest::new("dao_bulla", json!([])); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "airdrop", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "airdropping tokens...", "id": 42} + pub async fn user_balance(&self, nym: String) -> Result { + let req = JsonRequest::new("user_balance", json!([nym])); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "propose", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "creating proposal...", "id": 42} + pub async fn propose(&self, sender: String, recipient: String, amount: u64) -> Result { + let req = JsonRequest::new("propose", json!([sender, recipient, amount])); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "vote", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "voting...", "id": 42} + pub async fn vote(&self, nym: String, vote: String) -> Result { + let req = JsonRequest::new("vote", json!([nym, vote])); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "exec", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "executing...", "id": 42} + pub async fn get_votes(&self) -> Result { + let req = JsonRequest::new("get_votes", json!([])); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "exec", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "executing...", "id": 42} + pub async fn get_proposals(&self) -> Result { + let req = JsonRequest::new("get_proposals", json!([])); + self.client.request(req).await + } + + // --> {"jsonrpc": "2.0", "method": "exec", "params": [], "id": 42} + // <-- {"jsonrpc": "2.0", "result": "executing...", "id": 42} + pub async fn exec(&self, bulla: String) -> Result { + let req = JsonRequest::new("exec", json!([bulla])); + self.client.request(req).await + } +} diff --git a/bin/dao/dao_config.toml b/bin/dao/dao_config.toml new file mode 100644 index 000000000..e69de29bb diff --git a/bin/daod/Cargo.toml b/bin/dao/daod/Cargo.toml similarity index 80% rename from bin/daod/Cargo.toml rename to bin/dao/daod/Cargo.toml index 029e28629..18d3dea1d 100644 --- a/bin/daod/Cargo.toml +++ b/bin/dao/daod/Cargo.toml @@ -4,8 +4,8 @@ version = "0.3.0" edition = "2021" [dependencies] -darkfi = {path = "../../", features = ["rpc", "crypto", "tx", "node"]} -darkfi-serial = {path = "../../src/serial"} +darkfi = {path = "../../../", features = ["rpc", "crypto", "tx", "node"]} +darkfi-serial = {path = "../../../src/serial"} # Async smol = "1.2.5" @@ -33,6 +33,8 @@ group = "0.12.0" # Encoding and parsing serde_json = "1.0.85" +bs58 = "0.4.0" +fxhash = "0.2.1" # Utilities lazy_static = "1.4.0" diff --git a/bin/daod/Makefile b/bin/dao/daod/Makefile similarity index 100% rename from bin/daod/Makefile rename to bin/dao/daod/Makefile diff --git a/bin/dao/daod/TODO b/bin/dao/daod/TODO new file mode 100644 index 000000000..5060f7b09 --- /dev/null +++ b/bin/dao/daod/TODO @@ -0,0 +1,16 @@ +priority: immediate + +* move schema.rs to darkfi/example + * rename it to dao.rs + * we want to be able to run it like how we run example/tx.rs + * cargo run --example dao + * and it should compile and work + +priority: low + +* the things in util.rs are not all utils + * some of them should be moved to other modules eventually + +* rename [foo]_contract to just foo + * contract/dao_contract/ is redundant + * we can just have contract/dao diff --git a/bin/daod/demo-spec.md b/bin/dao/daod/demo-spec.md similarity index 100% rename from bin/daod/demo-spec.md rename to bin/dao/daod/demo-spec.md diff --git a/bin/dao/daod/proof/dao-exec.zk b/bin/dao/daod/proof/dao-exec.zk new file mode 100644 index 000000000..9fa9c199e --- /dev/null +++ b/bin/dao/daod/proof/dao-exec.zk @@ -0,0 +1,168 @@ +constant "DaoExec" { + EcFixedPointShort VALUE_COMMIT_VALUE, + EcFixedPoint VALUE_COMMIT_RANDOM, +} + +contract "DaoExec" { + # proposal params + Base proposal_dest_x, + Base proposal_dest_y, + Base proposal_amount, + Base proposal_serial, + Base proposal_token_id, + Base proposal_blind, + + # DAO params + Base dao_proposer_limit, + Base dao_quorum, + Base dao_approval_ratio_quot, + Base dao_approval_ratio_base, + Base gov_token_id, + Base dao_public_x, + Base dao_public_y, + Base dao_bulla_blind, + + # votes + Base yes_votes_value, + Base all_votes_value, + Scalar yes_votes_blind, + Scalar all_votes_blind, + + # outputs + inputs + Base user_serial, + Base user_coin_blind, + Base dao_serial, + Base dao_coin_blind, + Base input_value, + Scalar input_value_blind, + + # misc + Base dao_spend_hook, + Base user_spend_hook, + Base user_data, +} + +circuit "DaoExec" { + dao_bulla = poseidon_hash( + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + gov_token_id, + dao_public_x, + dao_public_y, + dao_bulla_blind, + ); + # Proposal bulla is valid means DAO bulla is also valid + # because of dao-propose-main.zk, already checks that when + # we first create the proposal. So it is redundant here. + + proposal_bulla = poseidon_hash( + proposal_dest_x, + proposal_dest_y, + proposal_amount, + proposal_serial, + proposal_token_id, + dao_bulla, + proposal_blind, + # @tmp-workaround + proposal_blind, + ); + constrain_instance(proposal_bulla); + + coin_0 = poseidon_hash( + proposal_dest_x, + proposal_dest_y, + proposal_amount, + proposal_token_id, + proposal_serial, + user_spend_hook, + user_data, + proposal_blind, + ); + constrain_instance(coin_0); + + change = base_sub(input_value, proposal_amount); + + coin_1 = poseidon_hash( + dao_public_x, + dao_public_y, + change, + proposal_token_id, + dao_serial, + dao_spend_hook, + dao_bulla, + dao_coin_blind, + ); + constrain_instance(coin_1); + + # Create pedersen commits for win_votes, and total_votes + # and make public + yes_votes_value_c = ec_mul_short(yes_votes_value, VALUE_COMMIT_VALUE); + yes_votes_blind_c = ec_mul(yes_votes_blind, VALUE_COMMIT_RANDOM); + yes_votes_commit = ec_add(yes_votes_value_c, yes_votes_blind_c); + + # get curve points and constrain + yes_votes_commit_x = ec_get_x(yes_votes_commit); + yes_votes_commit_y = ec_get_y(yes_votes_commit); + constrain_instance(yes_votes_commit_x); + constrain_instance(yes_votes_commit_y); + + all_votes_c = ec_mul_short(all_votes_value, VALUE_COMMIT_VALUE); + all_votes_blind_c = ec_mul(all_votes_blind, VALUE_COMMIT_RANDOM); + all_votes_commit = ec_add(all_votes_c, all_votes_blind_c); + + # get curve points and constrain + all_votes_commit_x = ec_get_x(all_votes_commit); + all_votes_commit_y = ec_get_y(all_votes_commit); + constrain_instance(all_votes_commit_x); + constrain_instance(all_votes_commit_y); + + # Create pedersen commit for input_value and make public + + input_value_v = ec_mul_short(input_value, VALUE_COMMIT_VALUE); + input_value_r = ec_mul(input_value_blind, VALUE_COMMIT_RANDOM); + input_value_commit = ec_add(input_value_v, input_value_r); + + # get curve points and constrain + input_value_x = ec_get_x(input_value_commit); + input_value_y = ec_get_y(input_value_commit); + constrain_instance(input_value_x); + constrain_instance(input_value_y); + + constrain_instance(dao_spend_hook); + constrain_instance(user_spend_hook); + constrain_instance(user_data); + + # Check that dao_quorum is less than or equal to all_votes_value + one = witness_base(1); + all_votes_value_1 = base_add(all_votes_value, one); + less_than(dao_quorum, all_votes_value_1); + + # approval_ratio_quot / approval_ratio_base <= yes_votes / all_votes + # + # The above is also equivalent to this: + # + # all_votes * approval_ratio_quot <= yes_votes * approval_ratio_base + + rhs = base_mul(all_votes_value, dao_approval_ratio_quot); + lhs = base_mul(yes_votes_value, dao_approval_ratio_base); + + lhs_1 = base_add(lhs, one); + less_than(rhs, lhs_1); + + #### + + # Create coin 0 + # Create coin 1 + # Check values of coin 0 + coin 1 == input value + # Check value of coin 0 == proposal_amount + # Check public key matches too + # Create the input value commit + # Create the value commits + + # NOTE: there is a vulnerability here where someone can create the exec + # transaction with a bad note so it cannot be decrypted by the receiver + # TODO: research verifiable encryption inside ZK +} + diff --git a/bin/daod/proof/dao-mint.zk b/bin/dao/daod/proof/dao-mint.zk similarity index 100% rename from bin/daod/proof/dao-mint.zk rename to bin/dao/daod/proof/dao-mint.zk diff --git a/bin/daod/proof/dao-propose-burn.zk b/bin/dao/daod/proof/dao-propose-burn.zk similarity index 100% rename from bin/daod/proof/dao-propose-burn.zk rename to bin/dao/daod/proof/dao-propose-burn.zk diff --git a/bin/daod/proof/dao-propose-main.zk b/bin/dao/daod/proof/dao-propose-main.zk similarity index 100% rename from bin/daod/proof/dao-propose-main.zk rename to bin/dao/daod/proof/dao-propose-main.zk diff --git a/bin/daod/proof/dao-vote-burn.zk b/bin/dao/daod/proof/dao-vote-burn.zk similarity index 100% rename from bin/daod/proof/dao-vote-burn.zk rename to bin/dao/daod/proof/dao-vote-burn.zk diff --git a/bin/daod/proof/dao-vote-main.zk b/bin/dao/daod/proof/dao-vote-main.zk similarity index 100% rename from bin/daod/proof/dao-vote-main.zk rename to bin/dao/daod/proof/dao-vote-main.zk diff --git a/bin/daod/proof/foo.zk b/bin/dao/daod/proof/foo.zk similarity index 100% rename from bin/daod/proof/foo.zk rename to bin/dao/daod/proof/foo.zk diff --git a/bin/daod/src/dao_contract/exec/mod.rs b/bin/dao/daod/src/contract/dao_contract/exec/mod.rs similarity index 100% rename from bin/daod/src/dao_contract/exec/mod.rs rename to bin/dao/daod/src/contract/dao_contract/exec/mod.rs diff --git a/bin/dao/daod/src/contract/dao_contract/exec/validate.rs b/bin/dao/daod/src/contract/dao_contract/exec/validate.rs new file mode 100644 index 000000000..7f15d0248 --- /dev/null +++ b/bin/dao/daod/src/contract/dao_contract/exec/validate.rs @@ -0,0 +1,201 @@ +use std::any::{Any, TypeId}; + +use pasta_curves::{ + arithmetic::CurveAffine, + group::{Curve, Group}, + pallas, +}; + +use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; + +use darkfi::{ + crypto::{coin::Coin, keypair::PublicKey, types::DrkCircuitField}, + Error as DarkFiError, +}; + +use crate::{ + contract::{dao_contract, dao_contract::CONTRACT_ID, money_contract}, + util::{CallDataBase, HashableBase, StateRegistry, Transaction, UpdateBase}, +}; + +type Result = std::result::Result; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error("DarkFi error: {0}")] + DarkFiError(String), + + #[error("InvalidNumberOfFuncCalls")] + InvalidNumberOfFuncCalls, + + #[error("InvalidIndex")] + InvalidIndex, + + #[error("InvalidCallData")] + InvalidCallData, + + #[error("InvalidNumberOfOutputs")] + InvalidNumberOfOutputs, + + #[error("InvalidOutput")] + InvalidOutput, + + #[error("InvalidValueCommit")] + InvalidValueCommit, + + #[error("InvalidVoteCommit")] + InvalidVoteCommit, +} + +impl From for Error { + fn from(err: DarkFiError) -> Self { + Self::DarkFiError(err.to_string()) + } +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct CallData { + pub proposal: pallas::Base, + pub coin_0: pallas::Base, + pub coin_1: pallas::Base, + pub yes_votes_commit: pallas::Point, + pub all_votes_commit: pallas::Point, + pub input_value_commit: pallas::Point, +} + +impl CallDataBase for CallData { + fn zk_public_values(&self) -> Vec<(String, Vec)> { + let yes_votes_commit_coords = self.yes_votes_commit.to_affine().coordinates().unwrap(); + + let all_votes_commit_coords = self.all_votes_commit.to_affine().coordinates().unwrap(); + + let input_value_commit_coords = self.input_value_commit.to_affine().coordinates().unwrap(); + + vec![( + "dao-exec".to_string(), + vec![ + self.proposal, + self.coin_0, + self.coin_1, + *yes_votes_commit_coords.x(), + *yes_votes_commit_coords.y(), + *all_votes_commit_coords.x(), + *all_votes_commit_coords.y(), + *input_value_commit_coords.x(), + *input_value_commit_coords.y(), + *super::FUNC_ID, + pallas::Base::from(0), + pallas::Base::from(0), + ], + )] + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn signature_public_keys(&self) -> Vec { + vec![] + } + + fn encode_bytes( + &self, + mut writer: &mut dyn std::io::Write, + ) -> std::result::Result { + self.encode(&mut writer) + } +} + +pub fn state_transition( + states: &StateRegistry, + func_call_index: usize, + parent_tx: &Transaction, +) -> Result> { + let func_call = &parent_tx.func_calls[func_call_index]; + let call_data = func_call.call_data.as_any(); + + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::(); + + // This will be inside wasm so unwrap is fine. + let call_data = call_data.unwrap(); + + // Enforce tx has correct format: + // 1. There should only be 2 func_call's + if parent_tx.func_calls.len() != 2 { + return Err(Error::InvalidNumberOfFuncCalls) + } + + // 2. func_call_index == 1 + if func_call_index != 1 { + return Err(Error::InvalidIndex) + } + + // 3. First item should be a Money::transfer() calldata + if parent_tx.func_calls[0].func_id != *money_contract::transfer::FUNC_ID { + return Err(Error::InvalidCallData) + } + + let money_transfer_call_data = parent_tx.func_calls[0].call_data.as_any(); + let money_transfer_call_data = + money_transfer_call_data.downcast_ref::(); + let money_transfer_call_data = money_transfer_call_data.unwrap(); + assert_eq!( + money_transfer_call_data.type_id(), + TypeId::of::() + ); + + // 4. Money::transfer() has exactly 2 outputs + if money_transfer_call_data.outputs.len() != 2 { + return Err(Error::InvalidNumberOfOutputs) + } + + // Checks: + // 1. Check both coins in Money::transfer() are equal to our coin_0, coin_1 + if money_transfer_call_data.outputs[0].revealed.coin != Coin(call_data.coin_0) { + return Err(Error::InvalidOutput) + } + if money_transfer_call_data.outputs[1].revealed.coin != Coin(call_data.coin_1) { + return Err(Error::InvalidOutput) + } + + // 2. sum of Money::transfer() calldata input_value_commits == our input value commit + let mut input_value_commits = pallas::Point::identity(); + for input in &money_transfer_call_data.inputs { + input_value_commits += input.revealed.value_commit; + } + if input_value_commits != call_data.input_value_commit { + return Err(Error::InvalidValueCommit) + } + + // 3. get the ProposalVote from DAO::State + let state = states + .lookup::(*CONTRACT_ID) + .expect("Return type is not of type State"); + let proposal_votes = state.proposal_votes.get(&HashableBase(call_data.proposal)).unwrap(); + + // 4. check yes_votes_commit is the same as in ProposalVote + if proposal_votes.yes_votes_commit != call_data.yes_votes_commit { + return Err(Error::InvalidVoteCommit) + } + // 5. also check all_votes_commit + if proposal_votes.all_votes_commit != call_data.all_votes_commit { + return Err(Error::InvalidVoteCommit) + } + + Ok(Box::new(Update { proposal: call_data.proposal })) +} + +#[derive(Clone)] +pub struct Update { + pub proposal: pallas::Base, +} + +impl UpdateBase for Update { + fn apply(self: Box, states: &mut StateRegistry) { + let state = states + .lookup_mut::(*CONTRACT_ID) + .expect("Return type is not of type State"); + state.proposal_votes.remove(&HashableBase(self.proposal)).unwrap(); + } +} diff --git a/bin/dao/daod/src/contract/dao_contract/exec/wallet.rs b/bin/dao/daod/src/contract/dao_contract/exec/wallet.rs new file mode 100644 index 000000000..4bff3b4fa --- /dev/null +++ b/bin/dao/daod/src/contract/dao_contract/exec/wallet.rs @@ -0,0 +1,196 @@ +use halo2_proofs::circuit::Value; +use log::debug; +use pasta_curves::{arithmetic::CurveAffine, group::Curve, pallas}; +use rand::rngs::OsRng; + +use darkfi::{ + crypto::{ + keypair::SecretKey, + util::{pedersen_commitment_u64, poseidon_hash}, + Proof, + }, + zk::vm::{Witness, ZkCircuit}, +}; + +use crate::{ + contract::dao_contract::{ + exec::validate::CallData, mint::wallet::DaoParams, propose::wallet::Proposal, CONTRACT_ID, + }, + util::{FuncCall, ZkContractInfo, ZkContractTable}, +}; + +pub struct Builder { + pub proposal: Proposal, + pub dao: DaoParams, + pub yes_votes_value: u64, + pub all_votes_value: u64, + pub yes_votes_blind: pallas::Scalar, + pub all_votes_blind: pallas::Scalar, + pub user_serial: pallas::Base, + pub user_coin_blind: pallas::Base, + pub dao_serial: pallas::Base, + pub dao_coin_blind: pallas::Base, + pub input_value: u64, + pub input_value_blind: pallas::Scalar, + pub hook_dao_exec: pallas::Base, + pub signature_secret: SecretKey, +} + +impl Builder { + pub fn build(self, zk_bins: &ZkContractTable) -> FuncCall { + debug!(target: "dao_contract::exec::wallet::Builder", "build()"); + let mut proofs = vec![]; + + let proposal_dest_coords = self.proposal.dest.0.to_affine().coordinates().unwrap(); + + let proposal_amount = pallas::Base::from(self.proposal.amount); + + let dao_proposer_limit = pallas::Base::from(self.dao.proposer_limit); + let dao_quorum = pallas::Base::from(self.dao.quorum); + let dao_approval_ratio_quot = pallas::Base::from(self.dao.approval_ratio_quot); + let dao_approval_ratio_base = pallas::Base::from(self.dao.approval_ratio_base); + + let dao_pubkey_coords = self.dao.public_key.0.to_affine().coordinates().unwrap(); + + let user_spend_hook = pallas::Base::from(0); + let user_data = pallas::Base::from(0); + let input_value = pallas::Base::from(self.input_value); + let change = input_value - proposal_amount; + + let dao_bulla = poseidon_hash::<8>([ + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + self.dao.gov_token_id, + *dao_pubkey_coords.x(), + *dao_pubkey_coords.y(), + self.dao.bulla_blind, + ]); + + let proposal_bulla = poseidon_hash::<8>([ + *proposal_dest_coords.x(), + *proposal_dest_coords.y(), + proposal_amount, + self.proposal.serial, + self.proposal.token_id, + dao_bulla, + self.proposal.blind, + // @tmp-workaround + self.proposal.blind, + ]); + + let coin_0 = poseidon_hash::<8>([ + *proposal_dest_coords.x(), + *proposal_dest_coords.y(), + proposal_amount, + self.proposal.token_id, + self.proposal.serial, + user_spend_hook, + user_data, + self.proposal.blind, + ]); + + let coin_1 = poseidon_hash::<8>([ + *dao_pubkey_coords.x(), + *dao_pubkey_coords.y(), + change, + self.proposal.token_id, + self.dao_serial, + self.hook_dao_exec, + dao_bulla, + self.dao_coin_blind, + ]); + + let yes_votes_commit = pedersen_commitment_u64(self.yes_votes_value, self.yes_votes_blind); + let yes_votes_commit_coords = yes_votes_commit.to_affine().coordinates().unwrap(); + + let all_votes_commit = pedersen_commitment_u64(self.all_votes_value, self.all_votes_blind); + let all_votes_commit_coords = all_votes_commit.to_affine().coordinates().unwrap(); + + let input_value_commit = pedersen_commitment_u64(self.input_value, self.input_value_blind); + let input_value_commit_coords = input_value_commit.to_affine().coordinates().unwrap(); + + let zk_info = zk_bins.lookup(&"dao-exec".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Binary(info) = zk_info { + info + } else { + panic!("Not binary info") + }; + + let zk_bin = zk_info.bincode.clone(); + + let prover_witnesses = vec![ + // + // proposal params + Witness::Base(Value::known(*proposal_dest_coords.x())), + Witness::Base(Value::known(*proposal_dest_coords.y())), + Witness::Base(Value::known(proposal_amount)), + Witness::Base(Value::known(self.proposal.serial)), + Witness::Base(Value::known(self.proposal.token_id)), + Witness::Base(Value::known(self.proposal.blind)), + // DAO params + Witness::Base(Value::known(dao_proposer_limit)), + Witness::Base(Value::known(dao_quorum)), + Witness::Base(Value::known(dao_approval_ratio_quot)), + Witness::Base(Value::known(dao_approval_ratio_base)), + Witness::Base(Value::known(self.dao.gov_token_id)), + Witness::Base(Value::known(*dao_pubkey_coords.x())), + Witness::Base(Value::known(*dao_pubkey_coords.y())), + Witness::Base(Value::known(self.dao.bulla_blind)), + // votes + Witness::Base(Value::known(pallas::Base::from(self.yes_votes_value))), + Witness::Base(Value::known(pallas::Base::from(self.all_votes_value))), + Witness::Scalar(Value::known(self.yes_votes_blind)), + Witness::Scalar(Value::known(self.all_votes_blind)), + // outputs + inputs + Witness::Base(Value::known(self.user_serial)), + Witness::Base(Value::known(self.user_coin_blind)), + Witness::Base(Value::known(self.dao_serial)), + Witness::Base(Value::known(self.dao_coin_blind)), + Witness::Base(Value::known(input_value)), + Witness::Scalar(Value::known(self.input_value_blind)), + // misc + Witness::Base(Value::known(self.hook_dao_exec)), + Witness::Base(Value::known(user_spend_hook)), + Witness::Base(Value::known(user_data)), + ]; + + let public_inputs = vec![ + proposal_bulla, + coin_0, + coin_1, + *yes_votes_commit_coords.x(), + *yes_votes_commit_coords.y(), + *all_votes_commit_coords.x(), + *all_votes_commit_coords.y(), + *input_value_commit_coords.x(), + *input_value_commit_coords.y(), + self.hook_dao_exec, + user_spend_hook, + user_data, + ]; + + let circuit = ZkCircuit::new(prover_witnesses, zk_bin); + let proving_key = &zk_info.proving_key; + let input_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) + .expect("DAO::exec() proving error!)"); + proofs.push(input_proof); + + let call_data = CallData { + proposal: proposal_bulla, + coin_0, + coin_1, + yes_votes_commit, + all_votes_commit, + input_value_commit, + }; + + FuncCall { + contract_id: *CONTRACT_ID, + func_id: *super::FUNC_ID, + call_data: Box::new(call_data), + proofs, + } + } +} diff --git a/bin/daod/src/dao_contract/mint/mod.rs b/bin/dao/daod/src/contract/dao_contract/mint/mod.rs similarity index 100% rename from bin/daod/src/dao_contract/mint/mod.rs rename to bin/dao/daod/src/contract/dao_contract/mint/mod.rs diff --git a/bin/dao/daod/src/contract/dao_contract/mint/validate.rs b/bin/dao/daod/src/contract/dao_contract/mint/validate.rs new file mode 100644 index 000000000..5cf074b21 --- /dev/null +++ b/bin/dao/daod/src/contract/dao_contract/mint/validate.rs @@ -0,0 +1,71 @@ +use std::any::{Any, TypeId}; + +use darkfi::crypto::{keypair::PublicKey, types::DrkCircuitField}; +use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; + +use crate::{ + contract::dao_contract::{DaoBulla, State, CONTRACT_ID}, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, +}; + +pub fn state_transition( + _states: &StateRegistry, + func_call_index: usize, + parent_tx: &Transaction, +) -> Result> { + let func_call = &parent_tx.func_calls[func_call_index]; + let call_data = func_call.call_data.as_any(); + + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::(); + + // This will be inside wasm so unwrap is fine. + let call_data = call_data.unwrap(); + + Ok(Box::new(Update { dao_bulla: call_data.dao_bulla.clone() })) +} + +#[derive(Clone)] +pub struct Update { + pub dao_bulla: DaoBulla, +} + +impl UpdateBase for Update { + fn apply(self: Box, states: &mut StateRegistry) { + // Lookup dao_contract state from registry + let state = states.lookup_mut::(*CONTRACT_ID).unwrap(); + // Add dao_bulla to state.dao_bullas + state.add_dao_bulla(self.dao_bulla); + } +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error {} + +type Result = std::result::Result; + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct CallData { + pub dao_bulla: DaoBulla, +} + +impl CallDataBase for CallData { + fn zk_public_values(&self) -> Vec<(String, Vec)> { + vec![("dao-mint".to_string(), vec![self.dao_bulla.0])] + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn signature_public_keys(&self) -> Vec { + vec![] + } + + fn encode_bytes( + &self, + mut writer: &mut dyn std::io::Write, + ) -> std::result::Result { + self.encode(&mut writer) + } +} diff --git a/bin/daod/src/dao_contract/mint/wallet.rs b/bin/dao/daod/src/contract/dao_contract/mint/wallet.rs similarity index 95% rename from bin/daod/src/dao_contract/mint/wallet.rs rename to bin/dao/daod/src/contract/dao_contract/mint/wallet.rs index 700b44c9d..e5d48e4fe 100644 --- a/bin/daod/src/dao_contract/mint/wallet.rs +++ b/bin/dao/daod/src/contract/dao_contract/mint/wallet.rs @@ -1,4 +1,6 @@ -use crate::dao_contract::state::DaoBulla; +use halo2_proofs::circuit::Value; +use pasta_curves::{arithmetic::CurveAffine, group::Curve, pallas}; +use rand::rngs::OsRng; use darkfi::{ crypto::{ @@ -8,13 +10,10 @@ use darkfi::{ }, zk::vm::{Witness, ZkCircuit}, }; -use halo2_proofs::circuit::Value; -use pasta_curves::{arithmetic::CurveAffine, group::Curve, pallas}; -use rand::rngs::OsRng; use crate::{ - dao_contract::{mint::validate::CallData, CONTRACT_ID}, - demo::{FuncCall, ZkContractInfo, ZkContractTable}, + contract::dao_contract::{mint::validate::CallData, state::DaoBulla, CONTRACT_ID}, + util::{FuncCall, ZkContractInfo, ZkContractTable}, }; #[derive(Clone)] diff --git a/bin/daod/src/dao_contract/mod.rs b/bin/dao/daod/src/contract/dao_contract/mod.rs similarity index 100% rename from bin/daod/src/dao_contract/mod.rs rename to bin/dao/daod/src/contract/dao_contract/mod.rs diff --git a/bin/daod/src/dao_contract/propose/mod.rs b/bin/dao/daod/src/contract/dao_contract/propose/mod.rs similarity index 100% rename from bin/daod/src/dao_contract/propose/mod.rs rename to bin/dao/daod/src/contract/dao_contract/propose/mod.rs diff --git a/bin/dao/daod/src/contract/dao_contract/propose/validate.rs b/bin/dao/daod/src/contract/dao_contract/propose/validate.rs new file mode 100644 index 000000000..6a761bde2 --- /dev/null +++ b/bin/dao/daod/src/contract/dao_contract/propose/validate.rs @@ -0,0 +1,173 @@ +use std::any::{Any, TypeId}; + +use log::error; +use pasta_curves::{ + arithmetic::CurveAffine, + group::{Curve, Group}, + pallas, +}; + +use darkfi::{ + crypto::{keypair::PublicKey, merkle_node::MerkleNode, types::DrkCircuitField}, + Error as DarkFiError, +}; +use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; + +use crate::{ + contract::{ + dao_contract, dao_contract::State as DaoState, money_contract, + money_contract::state::State as MoneyState, + }, + note::EncryptedNote2, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, +}; + +// used for debugging +// const TARGET: &str = "dao_contract::propose::validate::state_transition()"; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error("Invalid input merkle root")] + InvalidInputMerkleRoot, + + #[error("Invalid DAO merkle root")] + InvalidDaoMerkleRoot, + + #[error("DarkFi error: {0}")] + DarkFiError(String), +} +type Result = std::result::Result; + +impl From for Error { + fn from(err: DarkFiError) -> Self { + Self::DarkFiError(err.to_string()) + } +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct CallData { + pub header: Header, + pub inputs: Vec, +} + +impl CallDataBase for CallData { + fn zk_public_values(&self) -> Vec<(String, Vec)> { + let mut zk_publics = Vec::new(); + let mut total_funds_commit = pallas::Point::identity(); + + assert!(self.inputs.len() > 0, "inputs length cannot be zero"); + for input in &self.inputs { + total_funds_commit += input.value_commit; + let value_coords = input.value_commit.to_affine().coordinates().unwrap(); + + let sigpub_coords = input.signature_public.0.to_affine().coordinates().unwrap(); + + zk_publics.push(( + "dao-propose-burn".to_string(), + vec![ + *value_coords.x(), + *value_coords.y(), + self.header.token_commit, + input.merkle_root.0, + *sigpub_coords.x(), + *sigpub_coords.y(), + ], + )); + } + + let total_funds_coords = total_funds_commit.to_affine().coordinates().unwrap(); + zk_publics.push(( + "dao-propose-main".to_string(), + vec![ + self.header.token_commit, + self.header.dao_merkle_root.0, + self.header.proposal_bulla, + *total_funds_coords.x(), + *total_funds_coords.y(), + ], + )); + + zk_publics + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn signature_public_keys(&self) -> Vec { + let mut signature_public_keys = vec![]; + for input in self.inputs.clone() { + signature_public_keys.push(input.signature_public); + } + signature_public_keys + } + + fn encode_bytes( + &self, + mut writer: &mut dyn std::io::Write, + ) -> std::result::Result { + self.encode(&mut writer) + } +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct Header { + pub dao_merkle_root: MerkleNode, + pub token_commit: pallas::Base, + pub proposal_bulla: pallas::Base, + pub enc_note: EncryptedNote2, +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct Input { + pub value_commit: pallas::Point, + pub merkle_root: MerkleNode, + pub signature_public: PublicKey, +} + +pub fn state_transition( + states: &StateRegistry, + func_call_index: usize, + parent_tx: &Transaction, +) -> Result> { + let func_call = &parent_tx.func_calls[func_call_index]; + let call_data = func_call.call_data.as_any(); + + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::(); + + // This will be inside wasm so unwrap is fine. + let call_data = call_data.unwrap(); + + // Check the merkle roots for the input coins are valid + for input in &call_data.inputs { + let money_state = states.lookup::(*money_contract::CONTRACT_ID).unwrap(); + if !money_state.is_valid_merkle(&input.merkle_root) { + return Err(Error::InvalidInputMerkleRoot) + } + } + + let state = states.lookup::(*dao_contract::CONTRACT_ID).unwrap(); + + // Is the DAO bulla generated in the ZK proof valid + if !state.is_valid_dao_merkle(&call_data.header.dao_merkle_root) { + return Err(Error::InvalidDaoMerkleRoot) + } + + // TODO: look at gov tokens avoid using already spent ones + // Need to spend original coin and generate 2 nullifiers? + + Ok(Box::new(Update { proposal_bulla: call_data.header.proposal_bulla })) +} + +#[derive(Clone)] +pub struct Update { + pub proposal_bulla: pallas::Base, +} + +impl UpdateBase for Update { + fn apply(self: Box, states: &mut StateRegistry) { + let state = states.lookup_mut::(*dao_contract::CONTRACT_ID).unwrap(); + state.add_proposal_bulla(self.proposal_bulla); + } +} diff --git a/bin/dao/daod/src/contract/dao_contract/propose/wallet.rs b/bin/dao/daod/src/contract/dao_contract/propose/wallet.rs new file mode 100644 index 000000000..e19079718 --- /dev/null +++ b/bin/dao/daod/src/contract/dao_contract/propose/wallet.rs @@ -0,0 +1,272 @@ +use halo2_proofs::circuit::Value; +use incrementalmerkletree::Hashable; +use pasta_curves::{ + arithmetic::CurveAffine, + group::{ff::Field, Curve}, + pallas, +}; +use rand::rngs::OsRng; + +use darkfi::{ + crypto::{ + keypair::{PublicKey, SecretKey}, + merkle_node::MerkleNode, + util::{pedersen_commitment_u64, poseidon_hash}, + Proof, + }, + zk::vm::{Witness, ZkCircuit}, +}; +use darkfi_serial::{SerialDecodable, SerialEncodable}; + +use crate::{ + contract::{ + dao_contract::{ + mint::wallet::DaoParams, + propose::validate::{CallData, Header, Input}, + CONTRACT_ID, + }, + money_contract, + }, + note, + util::{FuncCall, ZkContractInfo, ZkContractTable}, +}; + +#[derive(SerialEncodable, SerialDecodable)] +pub struct Note { + pub proposal: Proposal, +} + +pub struct BuilderInput { + pub secret: SecretKey, + pub note: money_contract::transfer::wallet::Note, + pub leaf_position: incrementalmerkletree::Position, + pub merkle_path: Vec, + pub signature_secret: SecretKey, +} + +#[derive(SerialEncodable, SerialDecodable, Clone)] +pub struct Proposal { + pub dest: PublicKey, + pub amount: u64, + pub serial: pallas::Base, + pub token_id: pallas::Base, + pub blind: pallas::Base, +} + +pub struct Builder { + pub inputs: Vec, + pub proposal: Proposal, + pub dao: DaoParams, + pub dao_leaf_position: incrementalmerkletree::Position, + pub dao_merkle_path: Vec, + pub dao_merkle_root: MerkleNode, +} + +impl Builder { + pub fn build(self, zk_bins: &ZkContractTable) -> FuncCall { + let mut proofs = vec![]; + + let gov_token_blind = pallas::Base::random(&mut OsRng); + + let mut inputs = vec![]; + let mut total_funds = 0; + let mut total_funds_blinds = pallas::Scalar::from(0); + + for input in self.inputs { + let funds_blind = pallas::Scalar::random(&mut OsRng); + total_funds += input.note.value; + total_funds_blinds += funds_blind; + + let signature_public = PublicKey::from_secret(input.signature_secret); + + let zk_info = zk_bins.lookup(&"dao-propose-burn".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Binary(info) = zk_info { + info + } else { + panic!("Not binary info") + }; + let zk_bin = zk_info.bincode.clone(); + + // Note from the previous output + let note = input.note; + let leaf_pos: u64 = input.leaf_position.into(); + + let prover_witnesses = vec![ + Witness::Base(Value::known(input.secret.0)), + Witness::Base(Value::known(note.serial)), + Witness::Base(Value::known(pallas::Base::from(0))), + Witness::Base(Value::known(pallas::Base::from(0))), + Witness::Base(Value::known(pallas::Base::from(note.value))), + Witness::Base(Value::known(note.token_id)), + Witness::Base(Value::known(note.coin_blind)), + Witness::Scalar(Value::known(funds_blind)), + Witness::Base(Value::known(gov_token_blind)), + Witness::Uint32(Value::known(leaf_pos.try_into().unwrap())), + Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())), + Witness::Base(Value::known(input.signature_secret.0)), + ]; + + let public_key = PublicKey::from_secret(input.secret); + let coords = public_key.0.to_affine().coordinates().unwrap(); + + let coin = poseidon_hash::<8>([ + *coords.x(), + *coords.y(), + pallas::Base::from(note.value), + note.token_id, + note.serial, + pallas::Base::from(0), + pallas::Base::from(0), + note.coin_blind, + ]); + + let merkle_root = { + let position: u64 = input.leaf_position.into(); + let mut current = MerkleNode(coin); + for (level, sibling) in input.merkle_path.iter().enumerate() { + let level = level as u8; + current = if position & (1 << level) == 0 { + MerkleNode::combine(level.into(), ¤t, sibling) + } else { + MerkleNode::combine(level.into(), sibling, ¤t) + }; + } + current + }; + + let token_commit = poseidon_hash::<2>([note.token_id, gov_token_blind]); + assert_eq!(self.dao.gov_token_id, note.token_id); + + let value_commit = pedersen_commitment_u64(note.value, funds_blind); + let value_coords = value_commit.to_affine().coordinates().unwrap(); + + let sigpub_coords = signature_public.0.to_affine().coordinates().unwrap(); + + let public_inputs = vec![ + *value_coords.x(), + *value_coords.y(), + token_commit, + merkle_root.0, + *sigpub_coords.x(), + *sigpub_coords.y(), + ]; + let circuit = ZkCircuit::new(prover_witnesses, zk_bin); + + let proving_key = &zk_info.proving_key; + let input_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) + .expect("DAO::propose() proving error!"); + proofs.push(input_proof); + + let input = Input { value_commit, merkle_root, signature_public }; + inputs.push(input); + } + + let total_funds_commit = pedersen_commitment_u64(total_funds, total_funds_blinds); + let total_funds_coords = total_funds_commit.to_affine().coordinates().unwrap(); + let total_funds = pallas::Base::from(total_funds); + + let token_commit = poseidon_hash::<2>([self.dao.gov_token_id, gov_token_blind]); + + let proposal_dest_coords = self.proposal.dest.0.to_affine().coordinates().unwrap(); + let proposal_dest_x = *proposal_dest_coords.x(); + let proposal_dest_y = *proposal_dest_coords.y(); + + let proposal_amount = pallas::Base::from(self.proposal.amount); + + let dao_proposer_limit = pallas::Base::from(self.dao.proposer_limit); + let dao_quorum = pallas::Base::from(self.dao.quorum); + let dao_approval_ratio_quot = pallas::Base::from(self.dao.approval_ratio_quot); + let dao_approval_ratio_base = pallas::Base::from(self.dao.approval_ratio_base); + + let dao_pubkey_coords = self.dao.public_key.0.to_affine().coordinates().unwrap(); + + let dao_bulla = poseidon_hash::<8>([ + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + self.dao.gov_token_id, + *dao_pubkey_coords.x(), + *dao_pubkey_coords.y(), + self.dao.bulla_blind, + ]); + + let dao_leaf_position: u64 = self.dao_leaf_position.into(); + + let proposal_bulla = poseidon_hash::<8>([ + proposal_dest_x, + proposal_dest_y, + proposal_amount, + self.proposal.serial, + self.proposal.token_id, + dao_bulla, + self.proposal.blind, + // @tmp-workaround + self.proposal.blind, + ]); + + let zk_info = zk_bins.lookup(&"dao-propose-main".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Binary(info) = zk_info { + info + } else { + panic!("Not binary info") + }; + let zk_bin = zk_info.bincode.clone(); + let prover_witnesses = vec![ + // Proposers total number of gov tokens + Witness::Base(Value::known(total_funds)), + Witness::Scalar(Value::known(total_funds_blinds)), + // Used for blinding exported gov token ID + Witness::Base(Value::known(gov_token_blind)), + // proposal params + Witness::Base(Value::known(proposal_dest_x)), + Witness::Base(Value::known(proposal_dest_y)), + Witness::Base(Value::known(proposal_amount)), + Witness::Base(Value::known(self.proposal.serial)), + Witness::Base(Value::known(self.proposal.token_id)), + Witness::Base(Value::known(self.proposal.blind)), + // DAO params + Witness::Base(Value::known(dao_proposer_limit)), + Witness::Base(Value::known(dao_quorum)), + Witness::Base(Value::known(dao_approval_ratio_quot)), + Witness::Base(Value::known(dao_approval_ratio_base)), + Witness::Base(Value::known(self.dao.gov_token_id)), + Witness::Base(Value::known(*dao_pubkey_coords.x())), + Witness::Base(Value::known(*dao_pubkey_coords.y())), + Witness::Base(Value::known(self.dao.bulla_blind)), + Witness::Uint32(Value::known(dao_leaf_position.try_into().unwrap())), + Witness::MerklePath(Value::known(self.dao_merkle_path.try_into().unwrap())), + ]; + let public_inputs = vec![ + token_commit, + self.dao_merkle_root.0, + proposal_bulla, + *total_funds_coords.x(), + *total_funds_coords.y(), + ]; + let circuit = ZkCircuit::new(prover_witnesses, zk_bin); + + let proving_key = &zk_info.proving_key; + let main_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) + .expect("DAO::propose() proving error!"); + proofs.push(main_proof); + + let note = Note { proposal: self.proposal }; + let enc_note = note::encrypt(¬e, &self.dao.public_key).unwrap(); + let header = Header { + dao_merkle_root: self.dao_merkle_root, + proposal_bulla, + token_commit, + enc_note, + }; + + let call_data = CallData { header, inputs }; + + FuncCall { + contract_id: *CONTRACT_ID, + func_id: *super::FUNC_ID, + call_data: Box::new(call_data), + proofs, + } + } +} diff --git a/bin/dao/daod/src/contract/dao_contract/state.rs b/bin/dao/daod/src/contract/dao_contract/state.rs new file mode 100644 index 000000000..c61e79fcd --- /dev/null +++ b/bin/dao/daod/src/contract/dao_contract/state.rs @@ -0,0 +1,98 @@ +use std::{any::Any, collections::HashMap}; + +use incrementalmerkletree::{bridgetree::BridgeTree, Tree}; +use pasta_curves::{group::Group, pallas}; + +use darkfi_serial::{SerialDecodable, SerialEncodable}; + +use darkfi::crypto::{constants::MERKLE_DEPTH, merkle_node::MerkleNode, nullifier::Nullifier}; + +use crate::util::HashableBase; + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct DaoBulla(pub pallas::Base); + +type MerkleTree = BridgeTree; + +pub struct ProposalVotes { + // TODO: might be more logical to have 'yes_votes_commit' and 'no_votes_commit' + /// Weighted vote commit + pub yes_votes_commit: pallas::Point, + /// All value staked in the vote + pub all_votes_commit: pallas::Point, + /// Vote nullifiers + pub vote_nulls: Vec, +} + +impl ProposalVotes { + pub fn nullifier_exists(&self, nullifier: &Nullifier) -> bool { + self.vote_nulls.iter().any(|n| n == nullifier) + } +} + +/// This DAO state is for all DAOs on the network. There should only be a single instance. +pub struct State { + dao_bullas: Vec, + pub dao_tree: MerkleTree, + pub dao_roots: Vec, + + //proposal_bullas: Vec, + pub proposal_tree: MerkleTree, + pub proposal_roots: Vec, + pub proposal_votes: HashMap, +} + +impl State { + pub fn new() -> Box { + Box::new(Self { + dao_bullas: Vec::new(), + dao_tree: MerkleTree::new(100), + dao_roots: Vec::new(), + //proposal_bullas: Vec::new(), + proposal_tree: MerkleTree::new(100), + proposal_roots: Vec::new(), + proposal_votes: HashMap::new(), + }) + } + + pub fn add_dao_bulla(&mut self, bulla: DaoBulla) { + let node = MerkleNode(bulla.0); + self.dao_bullas.push(bulla); + self.dao_tree.append(&node); + self.dao_roots.push(self.dao_tree.root(0).unwrap()); + } + + pub fn add_proposal_bulla(&mut self, bulla: pallas::Base) { + let node = MerkleNode(bulla); + //self.proposal_bullas.push(bulla); + self.proposal_tree.append(&node); + self.proposal_roots.push(self.proposal_tree.root(0).unwrap()); + self.proposal_votes.insert( + HashableBase(bulla), + ProposalVotes { + yes_votes_commit: pallas::Point::identity(), + all_votes_commit: pallas::Point::identity(), + vote_nulls: Vec::new(), + }, + ); + } + + pub fn lookup_proposal_votes(&self, proposal_bulla: pallas::Base) -> Option<&ProposalVotes> { + self.proposal_votes.get(&HashableBase(proposal_bulla)) + } + pub fn lookup_proposal_votes_mut( + &mut self, + proposal_bulla: pallas::Base, + ) -> Option<&mut ProposalVotes> { + self.proposal_votes.get_mut(&HashableBase(proposal_bulla)) + } + + pub fn is_valid_dao_merkle(&self, root: &MerkleNode) -> bool { + self.dao_roots.iter().any(|m| m == root) + } + + // TODO: This never gets called. + pub fn _is_valid_proposal_merkle(&self, root: &MerkleNode) -> bool { + self.proposal_roots.iter().any(|m| m == root) + } +} diff --git a/bin/daod/src/dao_contract/vote/mod.rs b/bin/dao/daod/src/contract/dao_contract/vote/mod.rs similarity index 100% rename from bin/daod/src/dao_contract/vote/mod.rs rename to bin/dao/daod/src/contract/dao_contract/vote/mod.rs diff --git a/bin/dao/daod/src/contract/dao_contract/vote/validate.rs b/bin/dao/daod/src/contract/dao_contract/vote/validate.rs new file mode 100644 index 000000000..c02b9993c --- /dev/null +++ b/bin/dao/daod/src/contract/dao_contract/vote/validate.rs @@ -0,0 +1,208 @@ +use std::any::{Any, TypeId}; + +use log::error; +use pasta_curves::{ + arithmetic::CurveAffine, + group::{Curve, Group}, + pallas, +}; + +use darkfi::{ + crypto::{ + keypair::PublicKey, merkle_node::MerkleNode, nullifier::Nullifier, types::DrkCircuitField, + }, + Error as DarkFiError, +}; +use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; + +use crate::{ + contract::{ + dao_contract, dao_contract::State as DaoState, money_contract, + money_contract::state::State as MoneyState, + }, + note::EncryptedNote2, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, +}; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error("Invalid proposal")] + InvalidProposal, + + #[error("Voting with already spent coinage")] + SpentCoin, + + #[error("Double voting")] + DoubleVote, + + #[error("Invalid input merkle root")] + InvalidInputMerkleRoot, + + #[error("DarkFi error: {0}")] + DarkFiError(String), +} +type Result = std::result::Result; + +impl From for Error { + fn from(err: DarkFiError) -> Self { + Self::DarkFiError(err.to_string()) + } +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct CallData { + pub header: Header, + pub inputs: Vec, +} + +impl CallDataBase for CallData { + fn zk_public_values(&self) -> Vec<(String, Vec)> { + let mut zk_publics = Vec::new(); + let mut all_votes_commit = pallas::Point::identity(); + + assert!(self.inputs.len() > 0, "inputs length cannot be zero"); + for input in &self.inputs { + all_votes_commit += input.vote_commit; + let value_coords = input.vote_commit.to_affine().coordinates().unwrap(); + + let sigpub_coords = input.signature_public.0.to_affine().coordinates().unwrap(); + + zk_publics.push(( + "dao-vote-burn".to_string(), + vec![ + input.nullifier.0, + *value_coords.x(), + *value_coords.y(), + self.header.token_commit, + input.merkle_root.0, + *sigpub_coords.x(), + *sigpub_coords.y(), + ], + )); + } + + let yes_vote_commit_coords = self.header.yes_vote_commit.to_affine().coordinates().unwrap(); + + let vote_commit_coords = all_votes_commit.to_affine().coordinates().unwrap(); + + zk_publics.push(( + "dao-vote-main".to_string(), + vec![ + self.header.token_commit, + self.header.proposal_bulla, + *yes_vote_commit_coords.x(), + *yes_vote_commit_coords.y(), + *vote_commit_coords.x(), + *vote_commit_coords.y(), + ], + )); + + zk_publics + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn signature_public_keys(&self) -> Vec { + let mut signature_public_keys = vec![]; + for input in self.inputs.clone() { + signature_public_keys.push(input.signature_public); + } + signature_public_keys + } + + fn encode_bytes( + &self, + mut writer: &mut dyn std::io::Write, + ) -> std::result::Result { + self.encode(&mut writer) + } +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct Header { + pub token_commit: pallas::Base, + pub proposal_bulla: pallas::Base, + pub yes_vote_commit: pallas::Point, + pub enc_note: EncryptedNote2, +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct Input { + pub nullifier: Nullifier, + pub vote_commit: pallas::Point, + pub merkle_root: MerkleNode, + pub signature_public: PublicKey, +} + +pub fn state_transition( + states: &StateRegistry, + func_call_index: usize, + parent_tx: &Transaction, +) -> Result> { + let func_call = &parent_tx.func_calls[func_call_index]; + let call_data = func_call.call_data.as_any(); + + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::(); + + // This will be inside wasm so unwrap is fine. + let call_data = call_data.unwrap(); + + let dao_state = states.lookup::(*dao_contract::CONTRACT_ID).unwrap(); + + // Check proposal_bulla exists + let votes_info = dao_state.lookup_proposal_votes(call_data.header.proposal_bulla); + if votes_info.is_none() { + return Err(Error::InvalidProposal) + } + let votes_info = votes_info.unwrap(); + + // Check the merkle roots for the input coins are valid + let mut vote_nulls = Vec::new(); + let mut all_vote_commit = pallas::Point::identity(); + for input in &call_data.inputs { + let money_state = states.lookup::(*money_contract::CONTRACT_ID).unwrap(); + if !money_state.is_valid_merkle(&input.merkle_root) { + return Err(Error::InvalidInputMerkleRoot) + } + + if money_state.nullifier_exists(&input.nullifier) { + return Err(Error::SpentCoin) + } + + if votes_info.nullifier_exists(&input.nullifier) { + return Err(Error::DoubleVote) + } + + all_vote_commit += input.vote_commit; + + vote_nulls.push(input.nullifier); + } + + Ok(Box::new(Update { + proposal_bulla: call_data.header.proposal_bulla, + vote_nulls, + yes_vote_commit: call_data.header.yes_vote_commit, + all_vote_commit, + })) +} + +#[derive(Clone)] +pub struct Update { + proposal_bulla: pallas::Base, + vote_nulls: Vec, + pub yes_vote_commit: pallas::Point, + pub all_vote_commit: pallas::Point, +} + +impl UpdateBase for Update { + fn apply(mut self: Box, states: &mut StateRegistry) { + let state = states.lookup_mut::(*dao_contract::CONTRACT_ID).unwrap(); + let votes_info = state.lookup_proposal_votes_mut(self.proposal_bulla).unwrap(); + votes_info.yes_votes_commit += self.yes_vote_commit; + votes_info.all_votes_commit += self.all_vote_commit; + votes_info.vote_nulls.append(&mut self.vote_nulls); + } +} diff --git a/bin/dao/daod/src/contract/dao_contract/vote/wallet.rs b/bin/dao/daod/src/contract/dao_contract/vote/wallet.rs new file mode 100644 index 000000000..acacf40a5 --- /dev/null +++ b/bin/dao/daod/src/contract/dao_contract/vote/wallet.rs @@ -0,0 +1,292 @@ +use halo2_proofs::circuit::Value; +use incrementalmerkletree::Hashable; +use log::debug; +use pasta_curves::{ + arithmetic::CurveAffine, + group::{ff::Field, Curve}, + pallas, +}; +use rand::rngs::OsRng; + +use darkfi::{ + crypto::{ + keypair::{Keypair, PublicKey, SecretKey}, + merkle_node::MerkleNode, + nullifier::Nullifier, + util::{pedersen_commitment_u64, poseidon_hash}, + Proof, + }, + zk::vm::{Witness, ZkCircuit}, +}; +use darkfi_serial::{SerialDecodable, SerialEncodable}; + +use crate::{ + contract::{ + dao_contract::{ + mint::wallet::DaoParams, + propose::wallet::Proposal, + vote::validate::{CallData, Header, Input}, + CONTRACT_ID, + }, + money_contract, + }, + note, + util::{FuncCall, ZkContractInfo, ZkContractTable}, +}; + +#[derive(SerialEncodable, SerialDecodable)] +pub struct Note { + pub vote: Vote, + pub vote_value: u64, + pub vote_value_blind: pallas::Scalar, +} + +#[derive(SerialEncodable, SerialDecodable)] +pub struct Vote { + pub vote_option: bool, + pub vote_option_blind: pallas::Scalar, +} + +pub struct BuilderInput { + pub secret: SecretKey, + pub note: money_contract::transfer::wallet::Note, + pub leaf_position: incrementalmerkletree::Position, + pub merkle_path: Vec, + pub signature_secret: SecretKey, +} + +// TODO: should be token locking voting? +// Inside ZKproof, check proposal is correct. +pub struct Builder { + pub inputs: Vec, + pub vote: Vote, + pub vote_keypair: Keypair, + pub proposal: Proposal, + pub dao: DaoParams, +} + +impl Builder { + pub fn build(self, zk_bins: &ZkContractTable) -> FuncCall { + debug!(target: "dao_contract::vote::wallet::Builder", "build()"); + let mut proofs = vec![]; + + let gov_token_blind = pallas::Base::random(&mut OsRng); + + let mut inputs = vec![]; + let mut vote_value = 0; + let mut vote_value_blind = pallas::Scalar::from(0); + + for input in self.inputs { + let value_blind = pallas::Scalar::random(&mut OsRng); + + vote_value += input.note.value; + vote_value_blind += value_blind; + + let signature_public = PublicKey::from_secret(input.signature_secret); + + let zk_info = zk_bins.lookup(&"dao-vote-burn".to_string()).unwrap(); + + let zk_info = if let ZkContractInfo::Binary(info) = zk_info { + info + } else { + panic!("Not binary info") + }; + let zk_bin = zk_info.bincode.clone(); + + // Note from the previous output + let note = input.note; + let leaf_pos: u64 = input.leaf_position.into(); + + let prover_witnesses = vec![ + Witness::Base(Value::known(input.secret.0)), + Witness::Base(Value::known(note.serial)), + Witness::Base(Value::known(pallas::Base::from(0))), + Witness::Base(Value::known(pallas::Base::from(0))), + Witness::Base(Value::known(pallas::Base::from(note.value))), + Witness::Base(Value::known(note.token_id)), + Witness::Base(Value::known(note.coin_blind)), + Witness::Scalar(Value::known(vote_value_blind)), + Witness::Base(Value::known(gov_token_blind)), + Witness::Uint32(Value::known(leaf_pos.try_into().unwrap())), + Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())), + Witness::Base(Value::known(input.signature_secret.0)), + ]; + + let public_key = PublicKey::from_secret(input.secret); + let coords = public_key.0.to_affine().coordinates().unwrap(); + + let coin = poseidon_hash::<8>([ + *coords.x(), + *coords.y(), + pallas::Base::from(note.value), + note.token_id, + note.serial, + pallas::Base::from(0), + pallas::Base::from(0), + note.coin_blind, + ]); + + let merkle_root = { + let position: u64 = input.leaf_position.into(); + let mut current = MerkleNode(coin); + for (level, sibling) in input.merkle_path.iter().enumerate() { + let level = level as u8; + current = if position & (1 << level) == 0 { + MerkleNode::combine(level.into(), ¤t, sibling) + } else { + MerkleNode::combine(level.into(), sibling, ¤t) + }; + } + current + }; + + let token_commit = poseidon_hash::<2>([note.token_id, gov_token_blind]); + assert_eq!(self.dao.gov_token_id, note.token_id); + + let nullifier = poseidon_hash::<2>([input.secret.0, note.serial]); + + let vote_commit = pedersen_commitment_u64(note.value, vote_value_blind); + let vote_commit_coords = vote_commit.to_affine().coordinates().unwrap(); + + let sigpub_coords = signature_public.0.to_affine().coordinates().unwrap(); + + let public_inputs = vec![ + nullifier, + *vote_commit_coords.x(), + *vote_commit_coords.y(), + token_commit, + merkle_root.0, + *sigpub_coords.x(), + *sigpub_coords.y(), + ]; + + let circuit = ZkCircuit::new(prover_witnesses, zk_bin); + let proving_key = &zk_info.proving_key; + debug!(target: "dao_contract::vote::wallet::Builder", "input_proof Proof::create()"); + let input_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) + .expect("DAO::vote() proving error!"); + proofs.push(input_proof); + + let input = Input { + nullifier: Nullifier(nullifier), + vote_commit, + merkle_root, + signature_public, + }; + inputs.push(input); + } + + let token_commit = poseidon_hash::<2>([self.dao.gov_token_id, gov_token_blind]); + + let proposal_dest_coords = self.proposal.dest.0.to_affine().coordinates().unwrap(); + + let proposal_amount = pallas::Base::from(self.proposal.amount); + + let dao_proposer_limit = pallas::Base::from(self.dao.proposer_limit); + let dao_quorum = pallas::Base::from(self.dao.quorum); + let dao_approval_ratio_quot = pallas::Base::from(self.dao.approval_ratio_quot); + let dao_approval_ratio_base = pallas::Base::from(self.dao.approval_ratio_base); + + let dao_pubkey_coords = self.dao.public_key.0.to_affine().coordinates().unwrap(); + + let dao_bulla = poseidon_hash::<8>([ + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + self.dao.gov_token_id, + *dao_pubkey_coords.x(), + *dao_pubkey_coords.y(), + self.dao.bulla_blind, + ]); + + let proposal_bulla = poseidon_hash::<8>([ + *proposal_dest_coords.x(), + *proposal_dest_coords.y(), + proposal_amount, + self.proposal.serial, + self.proposal.token_id, + dao_bulla, + self.proposal.blind, + // @tmp-workaround + self.proposal.blind, + ]); + + let vote_option = self.vote.vote_option as u64; + assert!(vote_option == 0 || vote_option == 1); + + let yes_vote_commit = + pedersen_commitment_u64(vote_option * vote_value, self.vote.vote_option_blind); + let yes_vote_commit_coords = yes_vote_commit.to_affine().coordinates().unwrap(); + + let all_vote_commit = pedersen_commitment_u64(vote_value, vote_value_blind); + let all_vote_commit_coords = all_vote_commit.to_affine().coordinates().unwrap(); + + let zk_info = zk_bins.lookup(&"dao-vote-main".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Binary(info) = zk_info { + info + } else { + panic!("Not binary info") + }; + let zk_bin = zk_info.bincode.clone(); + + let prover_witnesses = vec![ + // proposal params + Witness::Base(Value::known(*proposal_dest_coords.x())), + Witness::Base(Value::known(*proposal_dest_coords.y())), + Witness::Base(Value::known(proposal_amount)), + Witness::Base(Value::known(self.proposal.serial)), + Witness::Base(Value::known(self.proposal.token_id)), + Witness::Base(Value::known(self.proposal.blind)), + // DAO params + Witness::Base(Value::known(dao_proposer_limit)), + Witness::Base(Value::known(dao_quorum)), + Witness::Base(Value::known(dao_approval_ratio_quot)), + Witness::Base(Value::known(dao_approval_ratio_base)), + Witness::Base(Value::known(self.dao.gov_token_id)), + Witness::Base(Value::known(*dao_pubkey_coords.x())), + Witness::Base(Value::known(*dao_pubkey_coords.y())), + Witness::Base(Value::known(self.dao.bulla_blind)), + // Vote + Witness::Base(Value::known(pallas::Base::from(vote_option))), + Witness::Scalar(Value::known(self.vote.vote_option_blind)), + // Total number of gov tokens allocated + Witness::Base(Value::known(pallas::Base::from(vote_value))), + Witness::Scalar(Value::known(vote_value_blind)), + // gov token + Witness::Base(Value::known(gov_token_blind)), + ]; + + let public_inputs = vec![ + token_commit, + proposal_bulla, + // this should be a value commit?? + *yes_vote_commit_coords.x(), + *yes_vote_commit_coords.y(), + *all_vote_commit_coords.x(), + *all_vote_commit_coords.y(), + ]; + + let circuit = ZkCircuit::new(prover_witnesses, zk_bin); + + let proving_key = &zk_info.proving_key; + debug!(target: "dao_contract::vote::wallet::Builder", "main_proof = Proof::create()"); + let main_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) + .expect("DAO::vote() proving error!"); + proofs.push(main_proof); + + let note = Note { vote: self.vote, vote_value, vote_value_blind }; + let enc_note = note::encrypt(¬e, &self.vote_keypair.public).unwrap(); + + let header = Header { token_commit, proposal_bulla, yes_vote_commit, enc_note }; + + let call_data = CallData { header, inputs }; + + FuncCall { + contract_id: *CONTRACT_ID, + func_id: *super::FUNC_ID, + call_data: Box::new(call_data), + proofs, + } + } +} diff --git a/bin/daod/src/example_contract/foo/mod.rs b/bin/dao/daod/src/contract/example_contract/foo/mod.rs similarity index 100% rename from bin/daod/src/example_contract/foo/mod.rs rename to bin/dao/daod/src/contract/example_contract/foo/mod.rs diff --git a/bin/dao/daod/src/contract/example_contract/foo/validate.rs b/bin/dao/daod/src/contract/example_contract/foo/validate.rs new file mode 100644 index 000000000..9aea5855b --- /dev/null +++ b/bin/dao/daod/src/contract/example_contract/foo/validate.rs @@ -0,0 +1,93 @@ +use std::any::Any; + +use pasta_curves::pallas; + +use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; + +use darkfi::{ + crypto::{keypair::PublicKey, types::DrkCircuitField}, + Error as DarkFiError, +}; + +use crate::{ + contract::example_contract::{state::State, CONTRACT_ID}, + util::{CallDataBase, StateRegistry, UpdateBase}, +}; + +// type Result = std::result::Result; + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + // #[error("ValueExists")] + // ValueExists, + #[error("DarkFi error: {0}")] + DarkFiError(String), +} + +impl From for Error { + fn from(err: DarkFiError) -> Self { + Self::DarkFiError(err.to_string()) + } +} + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct CallData { + pub public_value: pallas::Base, + pub signature_public: PublicKey, +} + +impl CallDataBase for CallData { + fn zk_public_values(&self) -> Vec<(String, Vec)> { + vec![("example-foo".to_string(), vec![self.public_value])] + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn signature_public_keys(&self) -> Vec { + vec![self.signature_public] + } + + fn encode_bytes( + &self, + mut writer: &mut dyn std::io::Write, + ) -> std::result::Result { + self.encode(&mut writer) + } +} + +// pub fn state_transition( +// states: &StateRegistry, +// func_call_index: usize, +// parent_tx: &Transaction, +// ) -> Result> { +// let func_call = &parent_tx.func_calls[func_call_index]; +// let call_data = func_call.call_data.as_any(); + +// assert_eq!((&*call_data).type_id(), TypeId::of::()); +// let call_data = call_data.downcast_ref::(); + +// // This will be inside wasm so unwrap is fine. +// let call_data = call_data.unwrap(); + +// let example_state = states.lookup::(*CONTRACT_ID).unwrap(); + +// if example_state.public_exists(&call_data.public_value) { +// return Err(Error::ValueExists) +// } + +// Ok(Box::new(Update { public_value: call_data.public_value })) +// } + +#[derive(Clone)] +pub struct Update { + public_value: pallas::Base, +} + +impl UpdateBase for Update { + fn apply(self: Box, states: &mut StateRegistry) { + let example_state = states.lookup_mut::(*CONTRACT_ID).unwrap(); + example_state.add_public_value(self.public_value); + } +} diff --git a/bin/dao/daod/src/contract/example_contract/foo/wallet.rs b/bin/dao/daod/src/contract/example_contract/foo/wallet.rs new file mode 100644 index 000000000..fec043d73 --- /dev/null +++ b/bin/dao/daod/src/contract/example_contract/foo/wallet.rs @@ -0,0 +1,74 @@ +// use log::debug; +// use rand::rngs::OsRng; + +// use halo2_proofs::circuit::Value; +// use pasta_curves::pallas; + +// use darkfi::{ +// crypto::{ +// keypair::{PublicKey, SecretKey}, +// Proof, +// }, +// zk::vm::{Witness, ZkCircuit}, +// }; + +// use crate::{ +// contract::example_contract::{foo::validate::CallData, CONTRACT_ID}, +// util::{FuncCall, ZkContractInfo, ZkContractTable}, +// }; + +// pub struct Foo { +// pub a: u64, +// pub b: u64, +// } + +// pub struct Builder { +// pub foo: Foo, +// pub signature_secret: SecretKey, +// } + +// impl Builder { +// pub fn build(self, zk_bins: &ZkContractTable) -> FuncCall { +// debug!(target: "example_contract::foo::wallet::Builder", "build()"); +// let mut proofs = vec![]; + +// let zk_info = zk_bins.lookup(&"example-foo".to_string()).unwrap(); +// let zk_info = if let ZkContractInfo::Binary(info) = zk_info { +// info +// } else { +// panic!("Not binary info") +// }; + +// let zk_bin = zk_info.bincode.clone(); + +// let prover_witnesses = vec![ +// Witness::Base(Value::known(pallas::Base::from(self.foo.a))), +// Witness::Base(Value::known(pallas::Base::from(self.foo.b))), +// ]; + +// let a = pallas::Base::from(self.foo.a); +// let b = pallas::Base::from(self.foo.b); + +// let c = a + b; + +// let public_inputs = vec![c]; + +// let circuit = ZkCircuit::new(prover_witnesses, zk_bin); +// debug!(target: "example_contract::foo::wallet::Builder", "input_proof Proof::create()"); +// let proving_key = &zk_info.proving_key; +// let input_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) +// .expect("Example::foo() proving error!)"); +// proofs.push(input_proof); + +// let signature_public = PublicKey::from_secret(self.signature_secret); + +// let call_data = CallData { public_value: c, signature_public }; + +// FuncCall { +// contract_id: *CONTRACT_ID, +// func_id: *super::FUNC_ID, +// call_data: Box::new(call_data), +// proofs, +// } +// } +// } diff --git a/bin/daod/src/example_contract/mod.rs b/bin/dao/daod/src/contract/example_contract/mod.rs similarity index 100% rename from bin/daod/src/example_contract/mod.rs rename to bin/dao/daod/src/contract/example_contract/mod.rs diff --git a/bin/dao/daod/src/contract/example_contract/state.rs b/bin/dao/daod/src/contract/example_contract/state.rs new file mode 100644 index 000000000..3b6328e93 --- /dev/null +++ b/bin/dao/daod/src/contract/example_contract/state.rs @@ -0,0 +1,21 @@ +// use std::any::Any; + +use pasta_curves::pallas; + +pub struct State { + pub public_values: Vec, +} + +impl State { + // pub fn new() -> Box { + // Box::new(Self { public_values: Vec::new() }) + // } + + pub fn add_public_value(&mut self, public_value: pallas::Base) { + self.public_values.push(public_value) + } + + // pub fn public_exists(&self, public_value: &pallas::Base) -> bool { + // self.public_values.iter().any(|v| v == public_value) + // } +} diff --git a/bin/dao/daod/src/contract/mod.rs b/bin/dao/daod/src/contract/mod.rs new file mode 100644 index 000000000..94261f732 --- /dev/null +++ b/bin/dao/daod/src/contract/mod.rs @@ -0,0 +1,3 @@ +pub mod dao_contract; +pub mod example_contract; +pub mod money_contract; diff --git a/bin/daod/src/money_contract/mod.rs b/bin/dao/daod/src/contract/money_contract/mod.rs similarity index 100% rename from bin/daod/src/money_contract/mod.rs rename to bin/dao/daod/src/contract/money_contract/mod.rs diff --git a/bin/dao/daod/src/contract/money_contract/state.rs b/bin/dao/daod/src/contract/money_contract/state.rs new file mode 100644 index 000000000..dbc26a7e4 --- /dev/null +++ b/bin/dao/daod/src/contract/money_contract/state.rs @@ -0,0 +1,116 @@ +use incrementalmerkletree::{bridgetree::BridgeTree, Tree}; + +use darkfi::crypto::{ + coin::Coin, + constants::MERKLE_DEPTH, + keypair::{PublicKey, SecretKey}, + merkle_node::MerkleNode, + nullifier::Nullifier, +}; + +use super::transfer; +use crate::note::EncryptedNote2; + +type MerkleTree = BridgeTree; + +pub struct OwnCoin { + pub coin: Coin, + pub note: transfer::wallet::Note, + pub leaf_position: incrementalmerkletree::Position, +} + +pub struct WalletCache { + // Normally this would be a HashMap, but SecretKey is not Hash-able + // TODO: This can be HashableBase + cache: Vec<(SecretKey, Vec)>, +} + +impl WalletCache { + pub fn new() -> Self { + Self { cache: Vec::new() } + } + + /// Must be called at the start to begin tracking received coins for this secret. + pub fn track(&mut self, secret: SecretKey) { + self.cache.push((secret, Vec::new())); + } + + /// Get all coins received by this secret key + /// track() must be called on this secret before calling this or the function will panic. + pub fn get_received(&mut self, secret: &SecretKey) -> Vec { + for (other_secret, own_coins) in self.cache.iter_mut() { + if *secret == *other_secret { + // clear own_coins vec, and return current contents + return std::mem::replace(own_coins, Vec::new()) + } + } + panic!("you forget to track() this secret!"); + } + + pub fn try_decrypt_note( + &mut self, + coin: Coin, + ciphertext: EncryptedNote2, + tree: &mut MerkleTree, + ) { + // Loop through all our secret keys... + for (secret, own_coins) in self.cache.iter_mut() { + // .. attempt to decrypt the note ... + if let Ok(note) = ciphertext.decrypt(secret) { + let leaf_position = tree.witness().expect("coin should be in tree"); + own_coins.push(OwnCoin { coin, note, leaf_position }); + } + } + } +} + +/// The state machine, held in memory. +pub struct State { + /// The entire Merkle tree state + pub tree: MerkleTree, + /// List of all previous and the current Merkle roots. + /// This is the hashed value of all the children. + pub merkle_roots: Vec, + /// Nullifiers prevent double spending + pub nullifiers: Vec, + + /// Public key of the cashier + pub cashier_signature_public: PublicKey, + + /// Public key of the faucet + pub faucet_signature_public: PublicKey, + + pub wallet_cache: WalletCache, +} + +impl State { + pub fn new( + cashier_signature_public: PublicKey, + faucet_signature_public: PublicKey, + ) -> Box { + Box::new(Self { + tree: MerkleTree::new(100), + merkle_roots: vec![], + nullifiers: vec![], + cashier_signature_public, + faucet_signature_public, + wallet_cache: WalletCache::new(), + }) + } + + pub fn is_valid_cashier_public_key(&self, public: &PublicKey) -> bool { + public == &self.cashier_signature_public + } + + pub fn is_valid_faucet_public_key(&self, public: &PublicKey) -> bool { + public == &self.faucet_signature_public + } + + pub fn is_valid_merkle(&self, merkle_root: &MerkleNode) -> bool { + self.merkle_roots.iter().any(|m| m == merkle_root) + } + + pub fn nullifier_exists(&self, nullifier: &Nullifier) -> bool { + self.nullifiers.iter().any(|n| n == nullifier) + } +} diff --git a/bin/daod/src/money_contract/transfer/mod.rs b/bin/dao/daod/src/contract/money_contract/transfer/mod.rs similarity index 100% rename from bin/daod/src/money_contract/transfer/mod.rs rename to bin/dao/daod/src/contract/money_contract/transfer/mod.rs diff --git a/bin/dao/daod/src/contract/money_contract/transfer/validate.rs b/bin/dao/daod/src/contract/money_contract/transfer/validate.rs new file mode 100644 index 000000000..9db64d9f7 --- /dev/null +++ b/bin/dao/daod/src/contract/money_contract/transfer/validate.rs @@ -0,0 +1,375 @@ +use std::any::{Any, TypeId}; + +use incrementalmerkletree::Tree; +use log::{debug, error}; +use pasta_curves::{group::Group, pallas}; + +use darkfi::{ + crypto::{ + coin::Coin, + keypair::PublicKey, + merkle_node::MerkleNode, + nullifier::Nullifier, + types::{DrkCircuitField, DrkTokenId, DrkValueBlind, DrkValueCommit}, + util::{pedersen_commitment_base, pedersen_commitment_u64}, + BurnRevealedValues, MintRevealedValues, + }, + Error as DarkFiError, +}; +use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; + +use crate::{ + contract::{ + dao_contract, + money_contract::{state::State, CONTRACT_ID}, + }, + note::EncryptedNote2, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, +}; + +const TARGET: &str = "money_contract::transfer::validate::state_transition()"; + +/// A struct representing a state update. +/// This gets applied on top of an existing state. +#[derive(Clone)] +pub struct Update { + /// All nullifiers in a transaction + pub nullifiers: Vec, + /// All coins in a transaction + pub coins: Vec, + /// All encrypted notes in a transaction + pub enc_notes: Vec, +} + +impl UpdateBase for Update { + fn apply(mut self: Box, states: &mut StateRegistry) { + let state = states.lookup_mut::(*CONTRACT_ID).unwrap(); + + // Extend our list of nullifiers with the ones from the update + state.nullifiers.append(&mut self.nullifiers); + + //// Update merkle tree and witnesses + for (coin, enc_note) in self.coins.into_iter().zip(self.enc_notes.into_iter()) { + // Add the new coins to the Merkle tree + let node = MerkleNode(coin.0); + state.tree.append(&node); + + // Keep track of all Merkle roots that have existed + state.merkle_roots.push(state.tree.root(0).unwrap()); + + state.wallet_cache.try_decrypt_note(coin, enc_note, &mut state.tree); + } + } +} + +pub fn state_transition( + states: &StateRegistry, + func_call_index: usize, + parent_tx: &Transaction, +) -> Result> { + // Check the public keys in the clear inputs to see if they're coming + // from a valid cashier or faucet. + debug!(target: TARGET, "Iterate clear_inputs"); + let func_call = &parent_tx.func_calls[func_call_index]; + let call_data = func_call.call_data.as_any(); + + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::(); + + // This will be inside wasm so unwrap is fine. + let call_data = call_data.unwrap(); + + let state = states.lookup::(*CONTRACT_ID).expect("Return type is not of type State"); + + // Code goes here + for (i, input) in call_data.clear_inputs.iter().enumerate() { + let pk = &input.signature_public; + // TODO: this depends on the token ID + if !state.is_valid_cashier_public_key(pk) && !state.is_valid_faucet_public_key(pk) { + error!(target: TARGET, "Invalid pubkey for clear input: {:?}", pk); + return Err(Error::VerifyFailed(VerifyFailed::InvalidCashierOrFaucetKey(i))) + } + } + + // Nullifiers in the transaction + let mut nullifiers = Vec::with_capacity(call_data.inputs.len()); + + debug!(target: TARGET, "Iterate inputs"); + for (i, input) in call_data.inputs.iter().enumerate() { + let merkle = &input.revealed.merkle_root; + + // The Merkle root is used to know whether this is a coin that + // existed in a previous state. + if !state.is_valid_merkle(merkle) { + error!(target: TARGET, "Invalid Merkle root (input {})", i); + debug!(target: TARGET, "root: {:?}", merkle); + return Err(Error::VerifyFailed(VerifyFailed::InvalidMerkle(i))) + } + + // Check the spend_hook is satisfied + // The spend_hook says a coin must invoke another contract function when being spent + // If the value is set, then we check the function call exists + let spend_hook = &input.revealed.spend_hook; + if spend_hook != &pallas::Base::from(0) { + // spend_hook is set so we enforce the rules + let mut is_found = false; + for (i, func_call) in parent_tx.func_calls.iter().enumerate() { + // Skip current func_call + if i == func_call_index { + continue + } + + // TODO: we need to change these to pallas::Base + // temporary workaround for now + // if func_call.func_id == spend_hook ... + if func_call.func_id == *dao_contract::exec::FUNC_ID { + is_found = true; + break + } + } + if !is_found { + return Err(Error::VerifyFailed(VerifyFailed::SpendHookNotSatisfied)) + } + } + + // The nullifiers should not already exist. + // It is the double-spend protection. + let nullifier = &input.revealed.nullifier; + if state.nullifier_exists(nullifier) || + (1..nullifiers.len()).any(|i| nullifiers[i..].contains(&nullifiers[i - 1])) + { + error!(target: TARGET, "Duplicate nullifier found (input {})", i); + debug!(target: TARGET, "nullifier: {:?}", nullifier); + return Err(Error::VerifyFailed(VerifyFailed::NullifierExists(i))) + } + + nullifiers.push(input.revealed.nullifier); + } + + debug!(target: TARGET, "Verifying call data"); + match call_data.verify() { + Ok(()) => { + debug!(target: TARGET, "Verified successfully") + } + Err(e) => { + error!(target: TARGET, "Failed verifying zk proofs: {}", e); + return Err(Error::VerifyFailed(VerifyFailed::ProofVerifyFailed(e.to_string()))) + } + } + + // Newly created coins for this transaction + let mut coins = Vec::with_capacity(call_data.outputs.len()); + let mut enc_notes = Vec::with_capacity(call_data.outputs.len()); + + for output in &call_data.outputs { + // Gather all the coins + coins.push(output.revealed.coin); + enc_notes.push(output.enc_note.clone()); + } + + Ok(Box::new(Update { nullifiers, coins, enc_notes })) +} + +/// A DarkFi transaction +#[derive(Debug, Clone, PartialEq, Eq, SerialEncodable, SerialDecodable)] +pub struct CallData { + /// Clear inputs + pub clear_inputs: Vec, + /// Anonymous inputs + pub inputs: Vec, + /// Anonymous outputs + pub outputs: Vec, +} + +impl CallDataBase for CallData { + fn zk_public_values(&self) -> Vec<(String, Vec)> { + let mut public_values = Vec::new(); + for input in &self.inputs { + public_values.push(("money-transfer-burn".to_string(), input.revealed.make_outputs())); + } + for output in &self.outputs { + public_values.push(("money-transfer-mint".to_string(), output.revealed.make_outputs())); + } + public_values + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn signature_public_keys(&self) -> Vec { + let mut signature_public_keys = Vec::new(); + for input in self.clear_inputs.clone() { + signature_public_keys.push(input.signature_public); + } + signature_public_keys + } + + fn encode_bytes( + &self, + mut writer: &mut dyn std::io::Write, + ) -> std::result::Result { + self.encode(&mut writer) + } +} +impl CallData { + /// Verify the transaction + pub fn verify(&self) -> VerifyResult<()> { + // must have minimum 1 clear or anon input, and 1 output + if self.clear_inputs.len() + self.inputs.len() == 0 { + error!("tx::verify(): Missing inputs"); + return Err(VerifyFailed::LackingInputs) + } + if self.outputs.len() == 0 { + error!("tx::verify(): Missing outputs"); + return Err(VerifyFailed::LackingOutputs) + } + + // Accumulator for the value commitments + let mut valcom_total = DrkValueCommit::identity(); + + // Add values from the clear inputs + for input in &self.clear_inputs { + valcom_total += pedersen_commitment_u64(input.value, input.value_blind); + } + // Add values from the inputs + for input in &self.inputs { + valcom_total += &input.revealed.value_commit; + } + // Subtract values from the outputs + for output in &self.outputs { + valcom_total -= &output.revealed.value_commit; + } + + // If the accumulator is not back in its initial state, + // there's a value mismatch. + if valcom_total != DrkValueCommit::identity() { + error!("tx::verify(): Missing funds"); + return Err(VerifyFailed::MissingFunds) + } + + // Verify that the token commitments match + if !self.verify_token_commitments() { + error!("tx::verify(): Token ID mismatch"); + return Err(VerifyFailed::TokenMismatch) + } + + Ok(()) + } + + fn verify_token_commitments(&self) -> bool { + assert_ne!(self.outputs.len(), 0); + let token_commit_value = self.outputs[0].revealed.token_commit; + + let mut failed = + self.inputs.iter().any(|input| input.revealed.token_commit != token_commit_value); + + failed = failed || + self.outputs.iter().any(|output| output.revealed.token_commit != token_commit_value); + + failed = failed || + self.clear_inputs.iter().any(|input| { + pedersen_commitment_base(input.token_id, input.token_blind) != token_commit_value + }); + !failed + } +} + +/// A transaction's clear input +#[derive(Debug, Clone, PartialEq, Eq, SerialEncodable, SerialDecodable)] +pub struct ClearInput { + /// Input's value (amount) + pub value: u64, + /// Input's token ID + pub token_id: DrkTokenId, + /// Blinding factor for `value` + pub value_blind: DrkValueBlind, + /// Blinding factor for `token_id` + pub token_blind: DrkValueBlind, + /// Public key for the signature + pub signature_public: PublicKey, +} + +/// A transaction's anonymous input +#[derive(Debug, Clone, PartialEq, Eq, SerialEncodable, SerialDecodable)] +pub struct Input { + /// Public inputs for the zero-knowledge proof + pub revealed: BurnRevealedValues, +} + +/// A transaction's anonymous output +#[derive(Debug, Clone, PartialEq, Eq, SerialEncodable, SerialDecodable)] +pub struct Output { + /// Public inputs for the zero-knowledge proof + pub revealed: MintRevealedValues, + /// The encrypted note + pub enc_note: EncryptedNote2, +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error(transparent)] + VerifyFailed(#[from] VerifyFailed), + + #[error("DarkFi error: {0}")] + DarkFiError(String), +} + +/// Transaction verification errors +#[derive(Debug, Clone, thiserror::Error)] +pub enum VerifyFailed { + #[error("Transaction has no inputs")] + LackingInputs, + + #[error("Transaction has no outputs")] + LackingOutputs, + + #[error("Invalid cashier/faucet public key for clear input {0}")] + InvalidCashierOrFaucetKey(usize), + + #[error("Invalid Merkle root for input {0}")] + InvalidMerkle(usize), + + #[error("Spend hook invoking function is not attached")] + SpendHookNotSatisfied, + + #[error("Nullifier already exists for input {0}")] + NullifierExists(usize), + + #[error("Token commitments in inputs or outputs to not match")] + TokenMismatch, + + #[error("Money in does not match money out (value commitments)")] + MissingFunds, + + #[error("Failed verifying zk proofs: {0}")] + ProofVerifyFailed(String), + + #[error("Internal error: {0}")] + InternalError(String), + + #[error("DarkFi error: {0}")] + DarkFiError(String), +} + +type Result = std::result::Result; + +impl From for VerifyFailed { + fn from(err: Error) -> Self { + Self::InternalError(err.to_string()) + } +} + +impl From for VerifyFailed { + fn from(err: DarkFiError) -> Self { + Self::DarkFiError(err.to_string()) + } +} + +impl From for Error { + fn from(err: DarkFiError) -> Self { + Self::DarkFiError(err.to_string()) + } +} +/// Result type used in transaction verifications +pub type VerifyResult = std::result::Result; diff --git a/bin/dao/daod/src/contract/money_contract/transfer/wallet.rs b/bin/dao/daod/src/contract/money_contract/transfer/wallet.rs new file mode 100644 index 000000000..d27fe385d --- /dev/null +++ b/bin/dao/daod/src/contract/money_contract/transfer/wallet.rs @@ -0,0 +1,220 @@ +use pasta_curves::group::ff::Field; +use rand::rngs::OsRng; + +use darkfi_serial::{SerialDecodable, SerialEncodable}; + +use darkfi::{ + crypto::{ + burn_proof::create_burn_proof, + keypair::{PublicKey, SecretKey}, + merkle_node::MerkleNode, + mint_proof::create_mint_proof, + types::{ + DrkCoinBlind, DrkSerial, DrkSpendHook, DrkTokenId, DrkUserData, DrkUserDataBlind, + DrkValueBlind, + }, + }, + Result, +}; + +use crate::{ + contract::money_contract::{ + transfer::validate::{CallData, ClearInput, Input, Output}, + CONTRACT_ID, + }, + note, + util::{FuncCall, ZkContractInfo, ZkContractTable}, +}; + +#[derive(Clone, SerialEncodable, SerialDecodable)] +pub struct Note { + pub serial: DrkSerial, + pub value: u64, + pub token_id: DrkTokenId, + pub spend_hook: DrkSpendHook, + pub user_data: DrkUserData, + pub coin_blind: DrkCoinBlind, + pub value_blind: DrkValueBlind, + pub token_blind: DrkValueBlind, +} + +pub struct Builder { + pub clear_inputs: Vec, + pub inputs: Vec, + pub outputs: Vec, +} + +pub struct BuilderClearInputInfo { + pub value: u64, + pub token_id: DrkTokenId, + pub signature_secret: SecretKey, +} + +pub struct BuilderInputInfo { + pub leaf_position: incrementalmerkletree::Position, + pub merkle_path: Vec, + pub secret: SecretKey, + pub note: Note, + pub user_data_blind: DrkUserDataBlind, + pub value_blind: DrkValueBlind, + pub signature_secret: SecretKey, +} + +pub struct BuilderOutputInfo { + pub value: u64, + pub token_id: DrkTokenId, + pub public: PublicKey, + pub serial: DrkSerial, + pub coin_blind: DrkCoinBlind, + pub spend_hook: DrkSpendHook, + pub user_data: DrkUserData, +} + +impl Builder { + fn compute_remainder_blind( + clear_inputs: &[ClearInput], + input_blinds: &[DrkValueBlind], + output_blinds: &[DrkValueBlind], + ) -> DrkValueBlind { + let mut total = DrkValueBlind::zero(); + + for input in clear_inputs { + total += input.value_blind; + } + + for input_blind in input_blinds { + total += input_blind; + } + + for output_blind in output_blinds { + total -= output_blind; + } + + total + } + + pub fn build(self, zk_bins: &ZkContractTable) -> Result { + assert!(self.clear_inputs.len() + self.inputs.len() > 0); + + let mut clear_inputs = vec![]; + let token_blind = DrkValueBlind::random(&mut OsRng); + for input in &self.clear_inputs { + let signature_public = PublicKey::from_secret(input.signature_secret); + let value_blind = DrkValueBlind::random(&mut OsRng); + + let clear_input = ClearInput { + value: input.value, + token_id: input.token_id, + value_blind, + token_blind, + signature_public, + }; + clear_inputs.push(clear_input); + } + + let mut proofs = vec![]; + let mut inputs = vec![]; + let mut input_blinds = vec![]; + + for input in self.inputs { + let value_blind = input.value_blind; + input_blinds.push(value_blind); + + let zk_info = zk_bins.lookup(&"money-transfer-burn".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Native(info) = zk_info { + info + } else { + panic!("Not native info") + }; + let burn_pk = &zk_info.proving_key; + + // Note from the previous output + let note = input.note.clone(); + + let (burn_proof, revealed) = create_burn_proof( + burn_pk, + note.value, + note.token_id, + value_blind, + token_blind, + note.serial, + note.spend_hook, + note.user_data, + input.user_data_blind, + note.coin_blind, + input.secret, + input.leaf_position, + input.merkle_path.clone(), + input.signature_secret, + )?; + proofs.push(burn_proof); + + let input = Input { revealed }; + inputs.push(input); + } + + let mut outputs = vec![]; + let mut output_blinds = vec![]; + // This value_blind calc assumes there will always be at least a single output + assert!(self.outputs.len() > 0); + + for (i, output) in self.outputs.iter().enumerate() { + let value_blind = if i == self.outputs.len() - 1 { + Self::compute_remainder_blind(&clear_inputs, &input_blinds, &output_blinds) + } else { + DrkValueBlind::random(&mut OsRng) + }; + output_blinds.push(value_blind); + + let serial = output.serial; + let coin_blind = output.coin_blind; + + let zk_info = zk_bins.lookup(&"money-transfer-mint".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Native(info) = zk_info { + info + } else { + panic!("Not native info") + }; + let mint_pk = &zk_info.proving_key; + + let (mint_proof, revealed) = create_mint_proof( + mint_pk, + output.value, + output.token_id, + value_blind, + token_blind, + serial, + output.spend_hook, + output.user_data, + coin_blind, + output.public, + )?; + proofs.push(mint_proof); + + let note = Note { + serial, + value: output.value, + token_id: output.token_id, + spend_hook: output.spend_hook, + user_data: output.user_data, + coin_blind, + value_blind, + token_blind, + }; + + let encrypted_note = note::encrypt(¬e, &output.public)?; + + let output = Output { revealed, enc_note: encrypted_note }; + outputs.push(output); + } + + let call_data = CallData { clear_inputs, inputs, outputs }; + + Ok(FuncCall { + contract_id: *CONTRACT_ID, + func_id: *super::FUNC_ID, + call_data: Box::new(call_data), + proofs, + }) + } +} diff --git a/bin/dao/daod/src/error.rs b/bin/dao/daod/src/error.rs new file mode 100644 index 000000000..d33356221 --- /dev/null +++ b/bin/dao/daod/src/error.rs @@ -0,0 +1,56 @@ +use serde_json::Value; + +use darkfi::rpc::jsonrpc::{ErrorCode::ServerError, JsonError, JsonResult}; + +#[derive(Debug, thiserror::Error)] +pub enum DaoError { + #[error("No Proposals found")] + NoProposals, + #[error("No DAO params found")] + DaoNotConfigured, + #[error("State transition failed: '{0}'")] + StateTransitionFailed(String), + #[error("Wallet does not exist")] + NoWalletFound, + #[error("State not found")] + StateNotFound, + #[error("InternalError")] + Darkfi(#[from] darkfi::error::Error), + #[error("Verify proof failed: '{0}', '{0}'")] + VerifyProofFailed(usize, String), +} + +pub type DaoResult = std::result::Result; + +pub enum RpcError { + Vote = -32101, + Propose = -32102, + Exec = -32103, + Airdrop = -32104, + Mint = -32105, + Keygen = -32106, + Create = -32107, + Parse = -32108, + Balance = -32109, +} + +fn to_tuple(e: RpcError) -> (i64, String) { + let msg = match e { + RpcError::Vote => "Failed to cast a Vote", + RpcError::Propose => "Failed to generate a Proposal", + RpcError::Airdrop => "Failed to transfer an airdrop", + RpcError::Keygen => "Failed to generate keypair", + RpcError::Create => "Failed to create DAO", + RpcError::Exec => "Failed to execute Proposal", + RpcError::Mint => "Failed to mint DAO treasury", + RpcError::Parse => "Generic parsing error", + RpcError::Balance => "Failed to get balance", + }; + + (e as i64, msg.to_string()) +} + +pub fn server_error(e: RpcError, id: Value) -> JsonResult { + let (code, msg) = to_tuple(e); + JsonError::new(ServerError(code), Some(msg), id).into() +} diff --git a/bin/dao/daod/src/main.rs b/bin/dao/daod/src/main.rs new file mode 100644 index 000000000..77d06fd23 --- /dev/null +++ b/bin/dao/daod/src/main.rs @@ -0,0 +1,1156 @@ +use std::{sync::Arc, time::Instant}; + +use fxhash::FxHashMap; +use incrementalmerkletree::{Position, Tree}; +use log::debug; +use pasta_curves::{ + arithmetic::CurveAffine, + group::{ff::Field, Curve, Group}, + pallas, +}; +use rand::rngs::OsRng; +use simplelog::{ColorChoice, LevelFilter, TermLogger, TerminalMode}; +use url::Url; + +use darkfi::{ + crypto::{ + keypair::{Keypair, PublicKey, SecretKey}, + merkle_node::MerkleNode, + proof::{ProvingKey, VerifyingKey}, + types::{DrkSpendHook, DrkUserData, DrkValue}, + util::{pedersen_commitment_u64, poseidon_hash}, + }, + rpc::server::listen_and_serve, + zk::circuit::{BurnContract, MintContract}, + zkas::ZkBinary, + Result, +}; + +mod contract; +mod error; +mod note; +mod rpc; +mod util; + +use crate::{ + contract::{ + dao_contract::{self, mint::wallet::DaoParams, propose::wallet::Proposal, DaoBulla}, + money_contract::{self, state::OwnCoin}, + }, + error::{DaoError, DaoResult}, + rpc::JsonRpcInterface, + util::{sign, StateRegistry, Transaction, ZkContractTable, DRK_ID, GOV_ID}, +}; + +////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////// +//// dao-demo 0.1 +//// +//// This is a very early prototype intended to demonstrate the underlying +//// crypto of fully anonymous DAOs. DAO participants can own and operate +//// a collective treasury according to rules set by the DAO. Communities +//// can coordinate financially in the cover of a protective darkness, +//// free from surveillance and persecution. +//// +//// The following information is completely hidden: +//// +//// * DAO treasury +//// * DAO parameters +//// * DAO participants +//// * Proposals +//// * Votes +//// +//// The DAO enables participants to make proposals, cast votes, and spend +//// money from the DAO treasury if a proposal passes. The basic operation +//// involves transferring money from a treasury to a public key specified +//// in a Proposal. This operation can only happen if several conditions are +//// met. +//// +//// At its basis, the DAO is a treasury that is owned by everyone who holds +//// the DAO governance token. These constraints, also known as DAO parameters, +//// are configured by DAO participants and enforced by ZK cryptography. +//// +//// In this demo, the constraints are: +//// +//// 1. DAO quorum: the number of governance tokens that must be allocated +//// to a proposal in order for a proposal to pass. +//// 2. Proposer limit: the number of governance tokens required to make a +//// proposal. +//// 3. DAO approval ratio: The ratio of yes/ no votes required for a +//// proposal to pass. +//// +//// In addition, DAO participants must prove ownership of governance tokens +//// order to vote. Their vote is weighted according to the number of governance +//// tokens in their wallet. In this current implementation, users do not spend +//// or lock up these coins in order to vote- they simply prove ownership of them. +//// +//// In the current prototype, the following information is exposed: +//// +//// * Encrypted votes are publicly linked to the proposal identifier hash, +//// meaning that it is possible to see that there is voting activity associated +//// with a particular proposal identifier, but the contents of the proposal, +//// how one has voted, and the associated DAO is fully private. +//// * In the burn phase of casting a vote, we reveal a public value called a +//// nullifier. The same public value is revealed when we spend the coins we +//// used to vote, meaning you can link a vote with a user when they spend +//// governance tokens. This is bad but is easily fixable. We will update the +//// code to use different values in the vote (by creating an intermediate Coin +//// used for voting). +//// * Votes are currently encrypted to the DAO public key. This means that +//// any DAO participant can decrypt votes as they come in. In the future, +//// we can delay the decryption so that you cannot read votes until the final +//// tally. +//// +//// Additionally, the dao-demo app shown below is highly limited. Namely, we use +//// a single God daemon to operate all the wallets. In the next version, every user +//// wallet will be a seperate daemon connecting over a network and running on a +//// blockchain. +//// +//// ///////////////////////////////////////////////////////////////////// +//// +//// dao-demo 0.1 TODOs: +//// +//// High priority: +//// +//// 5. vote() should pass a ProposalBulla +//// +//// Less priority: +//// +//// 1. Better document CLI/ CLI help. +//// +//// 2. Token id is hardcoded rn. Change this so users can specify token_id +//// as either xdrk or gdrk. In dao-cli we run a match statement to link to +//// the corresponding static values XDRK_ID and GDRK_ID. Note: xdrk is used +//// only for the DAO treasury. gdrk is the governance token used to operate +//// the DAO. +//// +//// 3. Implement money transfer between MoneyWallets so users can send tokens to +//// eachother. +//// +//// 4. Make CLI usage more interactive. Example: when I cast a vote, output: +//// "You voted {} with value {}." where value is the number of gDRK in a users +//// wallet (and the same for making a proposal etc). +//// +//// 5. Currently, DaoWallet stores DaoParams, DaoBulla's and Proposal's in a +//// Vector. We retrieve values through indexing, meaning that we +//// cannot currently support multiple DAOs and multiple proposals. +//// +//// Instead, dao_wallet.create_dao() should create a struct called Dao +//// which stores dao_info: HashMap and proposals: +//// HashMap. Users pass the DaoBulla and +//// ProposalBulla and we lookup the corresponding data. struct Dao should +//// be owned by DaoWallet. +//// +//// 6. Error handling :) +//// +////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////// + +pub struct Client { + dao_wallet: DaoWallet, + money_wallets: FxHashMap, + cashier_wallet: CashierWallet, + states: StateRegistry, + zk_bins: ZkContractTable, +} + +impl Client { + fn new() -> Self { + // For this early demo we store all wallets in a single Client. + let dao_wallet = DaoWallet::new(); + let money_wallets = FxHashMap::default(); + let cashier_wallet = CashierWallet::new(); + + // Lookup table for smart contract states + let states = StateRegistry::new(); + + // Initialize ZK binary table + let zk_bins = ZkContractTable::new(); + + Self { dao_wallet, money_wallets, cashier_wallet, states, zk_bins } + } + + // Load ZK contracts into the ZkContractTable and initialize the StateRegistry. + fn init(&mut self) -> Result<()> { + //We use these to initialize the money state. + let faucet_signature_secret = SecretKey::random(&mut OsRng); + let faucet_signature_public = PublicKey::from_secret(faucet_signature_secret); + + debug!(target: "demo", "Loading dao-mint.zk"); + let zk_dao_mint_bincode = include_bytes!("../proof/dao-mint.zk.bin"); + let zk_dao_mint_bin = ZkBinary::decode(zk_dao_mint_bincode)?; + self.zk_bins.add_contract("dao-mint".to_string(), zk_dao_mint_bin, 13); + + debug!(target: "demo", "Loading money-transfer contracts"); + let start = Instant::now(); + let mint_pk = ProvingKey::build(11, &MintContract::default()); + debug!("Mint PK: [{:?}]", start.elapsed()); + let start = Instant::now(); + let burn_pk = ProvingKey::build(11, &BurnContract::default()); + debug!("Burn PK: [{:?}]", start.elapsed()); + let start = Instant::now(); + let mint_vk = VerifyingKey::build(11, &MintContract::default()); + debug!("Mint VK: [{:?}]", start.elapsed()); + let start = Instant::now(); + let burn_vk = VerifyingKey::build(11, &BurnContract::default()); + debug!("Burn VK: [{:?}]", start.elapsed()); + + self.zk_bins.add_native("money-transfer-mint".to_string(), mint_pk, mint_vk); + self.zk_bins.add_native("money-transfer-burn".to_string(), burn_pk, burn_vk); + debug!(target: "demo", "Loading dao-propose-main.zk"); + let zk_dao_propose_main_bincode = include_bytes!("../proof/dao-propose-main.zk.bin"); + let zk_dao_propose_main_bin = ZkBinary::decode(zk_dao_propose_main_bincode)?; + self.zk_bins.add_contract("dao-propose-main".to_string(), zk_dao_propose_main_bin, 13); + debug!(target: "demo", "Loading dao-propose-burn.zk"); + let zk_dao_propose_burn_bincode = include_bytes!("../proof/dao-propose-burn.zk.bin"); + let zk_dao_propose_burn_bin = ZkBinary::decode(zk_dao_propose_burn_bincode)?; + self.zk_bins.add_contract("dao-propose-burn".to_string(), zk_dao_propose_burn_bin, 13); + debug!(target: "demo", "Loading dao-vote-main.zk"); + let zk_dao_vote_main_bincode = include_bytes!("../proof/dao-vote-main.zk.bin"); + let zk_dao_vote_main_bin = ZkBinary::decode(zk_dao_vote_main_bincode)?; + self.zk_bins.add_contract("dao-vote-main".to_string(), zk_dao_vote_main_bin, 13); + debug!(target: "demo", "Loading dao-vote-burn.zk"); + let zk_dao_vote_burn_bincode = include_bytes!("../proof/dao-vote-burn.zk.bin"); + let zk_dao_vote_burn_bin = ZkBinary::decode(zk_dao_vote_burn_bincode)?; + self.zk_bins.add_contract("dao-vote-burn".to_string(), zk_dao_vote_burn_bin, 13); + let zk_dao_exec_bincode = include_bytes!("../proof/dao-exec.zk.bin"); + let zk_dao_exec_bin = ZkBinary::decode(zk_dao_exec_bincode)?; + self.zk_bins.add_contract("dao-exec".to_string(), zk_dao_exec_bin, 13); + + let cashier_signature_public = self.cashier_wallet.signature_public(); + + let money_state = + money_contract::state::State::new(cashier_signature_public, faucet_signature_public); + self.states.register(*money_contract::CONTRACT_ID, money_state); + + let dao_state = dao_contract::State::new(); + self.states.register(*dao_contract::CONTRACT_ID, dao_state); + + Ok(()) + } + + fn create_dao( + &mut self, + dao_proposer_limit: u64, + dao_quorum: u64, + dao_approval_ratio_quot: u64, + dao_approval_ratio_base: u64, + token_id: pallas::Base, + ) -> DaoResult { + let tx = self.dao_wallet.mint_tx( + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + token_id, + &self.zk_bins, + ); + + self.validate(&tx)?; + // Only witness the value once the transaction is confirmed. + self.dao_wallet.update_witness(&mut self.states).unwrap(); + + // Retrieve DAO bulla from the state. + let dao_bulla = { + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + let call_data = + call_data.downcast_ref::().unwrap(); + call_data.dao_bulla.clone() + }; + + debug!(target: "demo", "Create DAO bulla: {:?}", dao_bulla.0); + + // We store these values in a vector we can easily retrieve DAO values for the demo. + let dao_params = DaoParams { + proposer_limit: dao_proposer_limit, + quorum: dao_quorum, + approval_ratio_quot: dao_approval_ratio_quot, + approval_ratio_base: dao_approval_ratio_base, + gov_token_id: token_id, + public_key: self.dao_wallet.keypair.public, + bulla_blind: self.dao_wallet.bulla_blind, + }; + + self.dao_wallet.params.push(dao_params); + self.dao_wallet.bullas.push(dao_bulla.clone()); + + Ok(dao_bulla.0) + } + + fn mint_treasury( + &mut self, + token_id: pallas::Base, + token_supply: u64, + recipient: PublicKey, + ) -> DaoResult<()> { + self.dao_wallet.track(&mut self.states)?; + + let tx = self + .cashier_wallet + .mint(token_id, token_supply, self.dao_wallet.bullas[0].0, recipient, &self.zk_bins) + .unwrap(); + + self.validate(&tx)?; + self.update_wallets()?; + + Ok(()) + } + + fn airdrop_user( + &mut self, + value: u64, + token_id: pallas::Base, + addr: PublicKey, + ) -> DaoResult<()> { + // let wallet = self.money_wallets.get(&nym).unwrap(); + // let addr = wallet.get_public_key(); + + let tx = self.cashier_wallet.airdrop(value, token_id, addr, &self.zk_bins).unwrap(); + self.validate(&tx)?; + self.update_wallets()?; + + Ok(()) + } + + // TODO: Change these into errors instead of expects. + fn validate(&mut self, tx: &Transaction) -> DaoResult<()> { + debug!(target: "dao_demo::client::validate()", "commencing validate sequence"); + let mut updates = vec![]; + + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + // So then the verifier will lookup the corresponding state_transition and apply + // functions based off the func_id + + if func_call.func_id == *money_contract::transfer::FUNC_ID { + debug!("money_contract::transfer::state_transition()"); + match money_contract::transfer::validate::state_transition(&self.states, idx, &tx) { + Ok(update) => { + updates.push(update); + } + Err(e) => return Err(DaoError::StateTransitionFailed(e.to_string())), + } + } else if func_call.func_id == *dao_contract::mint::FUNC_ID { + debug!("dao_contract::mint::state_transition()"); + match dao_contract::mint::validate::state_transition(&self.states, idx, &tx) { + Ok(update) => { + updates.push(update); + } + Err(e) => return Err(DaoError::StateTransitionFailed(e.to_string())), + } + } else if func_call.func_id == *dao_contract::propose::FUNC_ID { + debug!(target: "demo", "dao_contract::propose::state_transition()"); + match dao_contract::propose::validate::state_transition(&self.states, idx, &tx) { + Ok(update) => { + updates.push(update); + } + Err(e) => return Err(DaoError::StateTransitionFailed(e.to_string())), + } + } else if func_call.func_id == *dao_contract::vote::FUNC_ID { + debug!(target: "demo", "dao_contract::vote::state_transition()"); + match dao_contract::vote::validate::state_transition(&self.states, idx, &tx) { + Ok(update) => { + updates.push(update); + } + Err(e) => return Err(DaoError::StateTransitionFailed(e.to_string())), + } + } else if func_call.func_id == *dao_contract::exec::FUNC_ID { + debug!("dao_contract::exec::state_transition()"); + match dao_contract::exec::validate::state_transition(&self.states, idx, &tx) { + Ok(update) => { + updates.push(update); + } + Err(e) => return Err(DaoError::StateTransitionFailed(e.to_string())), + } + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut self.states); + } + + tx.zk_verify(&self.zk_bins)?; + tx.verify_sigs(); + + Ok(()) + } + + fn update_wallets(&mut self) -> DaoResult<()> { + let state = self.states.lookup_mut::(*money_contract::CONTRACT_ID); + if state.is_none() { + return Err(DaoError::StateNotFound) + } + let state = state.unwrap(); + + let dao_coins = state.wallet_cache.get_received(&self.dao_wallet.keypair.secret); + for coin in dao_coins { + let note = coin.note.clone(); + let coords = self.dao_wallet.keypair.public.0.to_affine().coordinates().unwrap(); + + let coin_hash = poseidon_hash::<8>([ + *coords.x(), + *coords.y(), + DrkValue::from(note.value), + note.token_id, + note.serial, + note.spend_hook, + note.user_data, + note.coin_blind, + ]); + + assert_eq!(coin_hash, coin.coin.0); + assert_eq!(note.spend_hook, *dao_contract::exec::FUNC_ID); + assert_eq!(note.user_data, self.dao_wallet.bullas[0].0); + + self.dao_wallet.own_coins.push((coin, false)); + debug!("DAO received a coin worth {} xDRK", note.value); + } + + for (_key, wallet) in &mut self.money_wallets { + let coins = state.wallet_cache.get_received(&wallet.keypair.secret); + for coin in coins { + let note = coin.note.clone(); + let coords = wallet.keypair.public.0.to_affine().coordinates().unwrap(); + + let coin_hash = poseidon_hash::<8>([ + *coords.x(), + *coords.y(), + DrkValue::from(note.value), + note.token_id, + note.serial, + note.spend_hook, + note.user_data, + note.coin_blind, + ]); + + assert_eq!(coin_hash, coin.coin.0); + wallet.own_coins.push((coin, false)); + } + } + + Ok(()) + } + + fn propose( + &mut self, + recipient: PublicKey, + token_id: pallas::Base, + amount: u64, + sender: PublicKey, + ) -> DaoResult { + let params = self.dao_wallet.params[0].clone(); + + let dao_leaf_position = self.dao_wallet.leaf_position; + + // To be able to make a proposal, we must prove we have ownership + // of governance tokens, and that the quantity of governance + // tokens is within the accepted proposer limit. + let sender_wallet = self.money_wallets.get_mut(&sender); + if sender_wallet.is_none() { + return Err(DaoError::NoWalletFound) + } + let sender_wallet = sender_wallet.unwrap(); + + let tx = sender_wallet.propose_tx( + params.clone(), + recipient, + token_id, + amount, + dao_leaf_position, + &self.zk_bins, + &mut self.states, + )?; + + self.validate(&tx)?; + self.update_wallets()?; + + let proposal_bulla = self.dao_wallet.store_proposal(&tx)?; + + Ok(proposal_bulla) + } + + fn cast_vote(&mut self, pubkey: PublicKey, vote: bool) -> DaoResult<()> { + let dao_key = self.dao_wallet.keypair; + if self.dao_wallet.proposals.is_empty() { + return Err(DaoError::NoProposals) + } + let proposal = self.dao_wallet.proposals[0].clone(); + + if self.dao_wallet.params.is_empty() { + return Err(DaoError::DaoNotConfigured) + } + let dao_params = self.dao_wallet.params[0].clone(); + let dao_keypair = self.dao_wallet.keypair; + + let voter_wallet = self.money_wallets.get_mut(&pubkey); + if voter_wallet.is_none() { + return Err(DaoError::NoWalletFound) + } + let voter_wallet = voter_wallet.unwrap(); + + let tx = voter_wallet.vote_tx( + vote, + dao_key, + proposal, + dao_params, + dao_keypair, + &self.zk_bins, + &mut self.states, + )?; + + self.validate(&tx)?; + self.update_wallets()?; + + self.dao_wallet.store_vote(&tx).unwrap(); + + Ok(()) + } + + fn exec_proposal(&mut self, bulla: pallas::Base) -> DaoResult<()> { + if self.dao_wallet.proposals.is_empty() { + return Err(DaoError::NoProposals) + } + let proposal = self.dao_wallet.proposals[0].clone(); + + if self.dao_wallet.params.is_empty() { + return Err(DaoError::DaoNotConfigured) + } + let dao_params = self.dao_wallet.params[0].clone(); + + let tx = self + .dao_wallet + .exec_tx(proposal, bulla, dao_params, &self.zk_bins, &mut self.states) + .unwrap(); + + self.validate(&tx)?; + self.update_wallets()?; + + Ok(()) + } +} + +struct DaoWallet { + keypair: Keypair, + signature_secret: SecretKey, + bulla_blind: pallas::Base, + leaf_position: Position, + proposal_bullas: Vec, + bullas: Vec, + params: Vec, + own_coins: Vec<(OwnCoin, bool)>, + proposals: Vec, + vote_notes: Vec, +} +impl DaoWallet { + fn new() -> Self { + let keypair = Keypair::random(&mut OsRng); + let signature_secret = SecretKey::random(&mut OsRng); + let bulla_blind = pallas::Base::random(&mut OsRng); + let leaf_position = Position::zero(); + let proposal_bullas = Vec::new(); + let bullas = Vec::new(); + let params = Vec::new(); + let own_coins: Vec<(OwnCoin, bool)> = Vec::new(); + let proposals: Vec = Vec::new(); + let vote_notes = Vec::new(); + + Self { + keypair, + signature_secret, + bulla_blind, + leaf_position, + proposal_bullas, + bullas, + params, + own_coins, + proposals, + vote_notes, + } + } + + fn get_public_key(&self) -> PublicKey { + self.keypair.public + } + + fn track(&self, states: &mut StateRegistry) -> DaoResult<()> { + let state = states.lookup_mut::(*money_contract::CONTRACT_ID); + if state.is_none() { + return Err(DaoError::StateNotFound) + } + let state = state.unwrap(); + state.wallet_cache.track(self.keypair.secret); + Ok(()) + } + + // Mint the DAO bulla. + fn mint_tx( + &mut self, + dao_proposer_limit: u64, + dao_quorum: u64, + dao_approval_ratio_quot: u64, + dao_approval_ratio_base: u64, + token_id: pallas::Base, + zk_bins: &ZkContractTable, + ) -> Transaction { + debug!(target: "dao-demo::dao::mint_tx()", "START"); + let builder = dao_contract::mint::wallet::Builder { + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + gov_token_id: token_id, + dao_pubkey: self.keypair.public, + dao_bulla_blind: self.bulla_blind, + _signature_secret: self.signature_secret, + }; + let func_call = builder.build(zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign(vec![self.signature_secret], &func_calls); + Transaction { func_calls, signatures } + } + + fn update_witness(&mut self, states: &mut StateRegistry) -> DaoResult<()> { + let state = states.lookup_mut::(*dao_contract::CONTRACT_ID); + if state.is_none() { + return Err(DaoError::StateNotFound) + } + let state = state.unwrap(); + let path = state.dao_tree.witness().unwrap(); + self.leaf_position = path; + Ok(()) + } + + // TODO: Make this a HashMap + // only parse it to a String in the cli. + fn balances(&self) -> Result> { + let mut ret: FxHashMap = FxHashMap::default(); + for (coin, is_spent) in &self.own_coins { + if *is_spent {} + if coin.note.token_id == *DRK_ID { + let id = "DRK".to_owned(); + ret.insert(id, coin.note.value); + } else if coin.note.token_id == *GOV_ID { + let id = "GOV".to_owned(); + ret.insert(id, coin.note.value); + } + } + Ok(ret) + } + + fn store_proposal(&mut self, tx: &Transaction) -> Result { + let (proposal, proposal_bulla) = { + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + let call_data = + call_data.downcast_ref::().unwrap(); + + let header = &call_data.header; + let note: dao_contract::propose::wallet::Note = + header.enc_note.decrypt(&self.keypair.secret).unwrap(); + + // Return the proposal info + (note.proposal, call_data.header.proposal_bulla) + }; + debug!(target: "demo", "Proposal now active!"); + debug!(target: "demo", " destination: {:?}", proposal.dest); + debug!(target: "demo", " amount: {}", proposal.amount); + debug!(target: "demo", " token_id: {:?}", proposal.token_id); + debug!(target: "demo", "Proposal bulla: {:?}", proposal_bulla); + + self.proposals.push(proposal); + self.proposal_bullas.push(proposal_bulla); + + Ok(proposal_bulla) + } + + // We decrypt the votes in a transaction and add it to the wallet. + fn store_vote(&mut self, tx: &Transaction) -> Result<()> { + let vote_note = { + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + let call_data = + call_data.downcast_ref::().unwrap(); + + let header = &call_data.header; + let note: dao_contract::vote::wallet::Note = + header.enc_note.decrypt(&self.keypair.secret).unwrap(); + note + }; + + self.vote_notes.push(vote_note); + + Ok(()) + } + + fn get_proposals(&self) -> &Vec { + &self.proposals + } + + fn get_votes(&self) -> &Vec { + &self.vote_notes + } + + // TODO: Explicit error handling. + fn get_treasury_path( + &self, + own_coin: &OwnCoin, + states: &StateRegistry, + ) -> Result<(Position, Vec)> { + let (money_leaf_position, money_merkle_path) = { + let state = + states.lookup::(*money_contract::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = own_coin.leaf_position.clone(); + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + Ok((money_leaf_position, money_merkle_path)) + } + + fn exec_tx( + &self, + proposal: Proposal, + _proposal_bulla: pallas::Base, + dao_params: DaoParams, + zk_bins: &ZkContractTable, + states: &mut StateRegistry, + ) -> Result { + let dao_bulla = self.bullas[0].clone(); + + let mut inputs = Vec::new(); + let mut total_input_value = 0; + + let tx_signature_secret = SecretKey::random(&mut OsRng); + let exec_signature_secret = SecretKey::random(&mut OsRng); + + let user_serial = pallas::Base::random(&mut OsRng); + let user_coin_blind = pallas::Base::random(&mut OsRng); + let user_data_blind = pallas::Base::random(&mut OsRng); + let input_value_blind = pallas::Scalar::random(&mut OsRng); + let dao_serial = pallas::Base::random(&mut OsRng); + let dao_coin_blind = pallas::Base::random(&mut OsRng); + // disabled + let user_spend_hook = pallas::Base::from(0); + let user_data = pallas::Base::from(0); + + for (coin, is_spent) in &self.own_coins { + let is_spent = is_spent.clone(); + if is_spent { + continue + } + let (treasury_leaf_position, treasury_merkle_path) = + self.get_treasury_path(&coin, states)?; + + let input_value = coin.note.value; + + let input = { + money_contract::transfer::wallet::BuilderInputInfo { + leaf_position: treasury_leaf_position, + merkle_path: treasury_merkle_path, + secret: self.keypair.secret, + note: coin.note.clone(), + user_data_blind, + value_blind: input_value_blind, + signature_secret: tx_signature_secret, + } + }; + total_input_value += input_value; + inputs.push(input); + } + + let builder = { + money_contract::transfer::wallet::Builder { + clear_inputs: vec![], + inputs, + outputs: vec![ + // Sending money + money_contract::transfer::wallet::BuilderOutputInfo { + value: proposal.amount, + token_id: proposal.token_id, + public: proposal.dest, + serial: proposal.serial, + coin_blind: proposal.blind, + spend_hook: user_spend_hook, + user_data, + }, + // Change back to DAO + money_contract::transfer::wallet::BuilderOutputInfo { + value: total_input_value - proposal.amount, + token_id: *DRK_ID, + public: self.keypair.public, + serial: dao_serial, + coin_blind: dao_coin_blind, + spend_hook: *dao_contract::exec::FUNC_ID, + user_data: dao_bulla.0, + }, + ], + } + }; + + let transfer_func_call = builder.build(zk_bins).unwrap(); + + let mut yes_votes_value = 0; + let mut yes_votes_blind = pallas::Scalar::from(0); + let mut yes_votes_commit = pallas::Point::identity(); + + let mut all_votes_value = 0; + let mut all_votes_blind = pallas::Scalar::from(0); + let mut all_votes_commit = pallas::Point::identity(); + + for (i, note) in self.vote_notes.iter().enumerate() { + let vote_commit = pedersen_commitment_u64(note.vote_value, note.vote_value_blind); + + all_votes_commit += vote_commit; + all_votes_blind += note.vote_value_blind; + + let yes_vote_commit = pedersen_commitment_u64( + note.vote.vote_option as u64 * note.vote_value, + note.vote.vote_option_blind, + ); + + yes_votes_commit += yes_vote_commit; + yes_votes_blind += note.vote.vote_option_blind; + + let vote_option = note.vote.vote_option; + + if vote_option { + yes_votes_value += note.vote_value; + } + all_votes_value += note.vote_value; + let vote_result: String = + if vote_option { "yes".to_string() } else { "no".to_string() }; + + debug!("Voter {} voted {}", i, vote_result); + } + + debug!("Outcome = {} / {}", yes_votes_value, all_votes_value); + + assert!(all_votes_commit == pedersen_commitment_u64(all_votes_value, all_votes_blind)); + assert!(yes_votes_commit == pedersen_commitment_u64(yes_votes_value, yes_votes_blind)); + + let builder = { + dao_contract::exec::wallet::Builder { + proposal: proposal.clone(), + dao: dao_params.clone(), + yes_votes_value, + all_votes_value, + yes_votes_blind, + all_votes_blind, + user_serial, + user_coin_blind, + dao_serial, + dao_coin_blind, + input_value: total_input_value, + input_value_blind, + hook_dao_exec: *dao_contract::exec::FUNC_ID, + signature_secret: exec_signature_secret, + } + }; + + let exec_func_call = builder.build(zk_bins); + let func_calls = vec![transfer_func_call, exec_func_call]; + + let signatures = sign(vec![tx_signature_secret, exec_signature_secret], &func_calls); + Ok(Transaction { func_calls, signatures }) + } +} + +// Stores governance tokens and related secret values. +struct MoneyWallet { + keypair: Keypair, + signature_secret: SecretKey, + own_coins: Vec<(OwnCoin, bool)>, +} + +impl MoneyWallet { + // fn signature_public(&self) -> PublicKey { + // PublicKey::from_secret(self.signature_secret) + // } + + // fn get_public_key(&self) -> PublicKey { + // self.keypair.public + // } + + fn track(&self, states: &mut StateRegistry) -> DaoResult<()> { + let state = states.lookup_mut::(*money_contract::CONTRACT_ID); + if state.is_none() { + return Err(DaoError::StateNotFound) + } + let state = state.unwrap(); + state.wallet_cache.track(self.keypair.secret); + Ok(()) + } + + // TODO: Make this a HashMap + // only parse it to a String in the cli. + fn balances(&self) -> Result> { + let mut ret: FxHashMap = FxHashMap::default(); + for (coin, is_spent) in &self.own_coins { + if *is_spent {} + if coin.note.token_id == *DRK_ID { + let id = "DRK".to_owned(); + ret.insert(id, coin.note.value); + } else if coin.note.token_id == *GOV_ID { + let id = "GOV".to_owned(); + ret.insert(id, coin.note.value); + } + } + Ok(ret) + } + + fn propose_tx( + &mut self, + params: DaoParams, + recipient: PublicKey, + token_id: pallas::Base, + amount: u64, + dao_leaf_position: Position, + zk_bins: &ZkContractTable, + states: &mut StateRegistry, + ) -> Result { + let mut inputs = Vec::new(); + + for (coin, is_spent) in &self.own_coins { + let is_spent = is_spent.clone(); + if is_spent { + continue + } + let (money_leaf_position, money_merkle_path) = self.get_path(&states, &coin).unwrap(); + + let input = { + dao_contract::propose::wallet::BuilderInput { + secret: self.keypair.secret, + note: coin.note.clone(), + leaf_position: money_leaf_position, + merkle_path: money_merkle_path, + signature_secret: self.signature_secret, + } + }; + inputs.push(input); + } + + let (dao_merkle_path, dao_merkle_root) = { + let state = states.lookup::(*dao_contract::CONTRACT_ID).unwrap(); + let tree = &state.dao_tree; + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(dao_leaf_position, &root).unwrap(); + (merkle_path, root) + }; + + let proposal = { + dao_contract::propose::wallet::Proposal { + dest: recipient, + amount, + serial: pallas::Base::random(&mut OsRng), + token_id, + blind: pallas::Base::random(&mut OsRng), + } + }; + + let builder = dao_contract::propose::wallet::Builder { + inputs, + proposal, + dao: params.clone(), + dao_leaf_position, + dao_merkle_path, + dao_merkle_root, + }; + + let func_call = builder.build(zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign(vec![self.signature_secret], &func_calls); + Ok(Transaction { func_calls, signatures }) + } + + fn get_path( + &self, + states: &StateRegistry, + own_coin: &OwnCoin, + ) -> Result<(Position, Vec)> { + let (money_leaf_position, money_merkle_path) = { + let state = + states.lookup::(*money_contract::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = own_coin.leaf_position.clone(); + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + Ok((money_leaf_position, money_merkle_path)) + } + + fn vote_tx( + &mut self, + vote_option: bool, + _dao_key: Keypair, + proposal: Proposal, + dao_params: DaoParams, + dao_keypair: Keypair, + zk_bins: &ZkContractTable, + states: &mut StateRegistry, + ) -> Result { + let mut inputs = Vec::new(); + + // We must prove we have sufficient governance tokens in order to vote. + for (coin, _is_spent) in &self.own_coins { + let (money_leaf_position, money_merkle_path) = self.get_path(states, &coin).unwrap(); + + let input = { + dao_contract::vote::wallet::BuilderInput { + secret: self.keypair.secret, + note: coin.note.clone(), + leaf_position: money_leaf_position, + merkle_path: money_merkle_path, + signature_secret: self.signature_secret, + } + }; + inputs.push(input); + } + + let builder = { + dao_contract::vote::wallet::Builder { + inputs, + vote: dao_contract::vote::wallet::Vote { + vote_option, + vote_option_blind: pallas::Scalar::random(&mut OsRng), + }, + // For this demo votes are encrypted for the DAO. + vote_keypair: dao_keypair, + proposal: proposal.clone(), + dao: dao_params.clone(), + } + }; + + let func_call = builder.build(zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign(vec![self.signature_secret], &func_calls); + Ok(Transaction { func_calls, signatures }) + } +} + +async fn start_rpc(client: Client) -> Result<()> { + let rpc_addr = Url::parse("tcp://127.0.0.1:7777").unwrap(); + + let rpc_client = JsonRpcInterface::new(client); + + let rpc_interface = Arc::new(rpc_client); + + listen_and_serve(rpc_addr, rpc_interface).await.unwrap(); + Ok(()) +} + +// Mint authority that mints the DAO treasury and airdrops governance tokens. +#[derive(Clone)] +struct CashierWallet { + // keypair: Keypair, + signature_secret: SecretKey, +} + +impl CashierWallet { + fn new() -> Self { + // let keypair = Keypair::random(&mut OsRng); + let signature_secret = SecretKey::random(&mut OsRng); + // Self { keypair, signature_secret } + Self { signature_secret } + } + + fn signature_public(&self) -> PublicKey { + PublicKey::from_secret(self.signature_secret) + } + + fn mint( + &mut self, + token_id: pallas::Base, + token_supply: u64, + dao_bulla: pallas::Base, + recipient: PublicKey, + zk_bins: &ZkContractTable, + ) -> Result { + let spend_hook = *dao_contract::exec::FUNC_ID; + let user_data = dao_bulla; + let value = token_supply; + + let tx = + self.transfer_tx(value, token_id, spend_hook, user_data, recipient, zk_bins).unwrap(); + + Ok(tx) + } + + fn transfer_tx( + &self, + value: u64, + token_id: pallas::Base, + spend_hook: pallas::Base, + user_data: pallas::Base, + recipient: PublicKey, + zk_bins: &ZkContractTable, + ) -> Result { + let builder = { + money_contract::transfer::wallet::Builder { + clear_inputs: vec![money_contract::transfer::wallet::BuilderClearInputInfo { + value, + token_id, + signature_secret: self.signature_secret, + }], + inputs: vec![], + outputs: vec![money_contract::transfer::wallet::BuilderOutputInfo { + value, + token_id, + public: recipient, + serial: pallas::Base::random(&mut OsRng), + coin_blind: pallas::Base::random(&mut OsRng), + spend_hook, + user_data, + }], + } + }; + let func_call = builder.build(zk_bins).unwrap(); + let func_calls = vec![func_call]; + + let signatures = sign(vec![self.signature_secret], &func_calls); + Ok(Transaction { func_calls, signatures }) + } + + fn airdrop( + &mut self, + value: u64, + token_id: pallas::Base, + recipient: PublicKey, + zk_bins: &ZkContractTable, + ) -> Result { + // Spend hook and user data disabled + let spend_hook = DrkSpendHook::from(0); + let user_data = DrkUserData::from(0); + + let tx = + self.transfer_tx(value, token_id, spend_hook, user_data, recipient, zk_bins).unwrap(); + + Ok(tx) + } +} + +#[async_std::main] +async fn main() -> Result<()> { + TermLogger::init( + LevelFilter::Debug, + simplelog::Config::default(), + TerminalMode::Mixed, + ColorChoice::Auto, + ) + .unwrap(); + + let mut client = Client::new(); + client.init()?; + + start_rpc(client).await.unwrap(); + + Ok(()) +} diff --git a/bin/dao/daod/src/note.rs b/bin/dao/daod/src/note.rs new file mode 100644 index 000000000..b5f72e68f --- /dev/null +++ b/bin/dao/daod/src/note.rs @@ -0,0 +1,98 @@ +use crypto_api_chachapoly::ChachaPolyIetf; +use rand::rngs::OsRng; + +use darkfi::{ + crypto::{ + diffie_hellman::{kdf_sapling, sapling_ka_agree}, + keypair::{PublicKey, SecretKey}, + }, + Error, Result, +}; +use darkfi_serial::{Decodable, Encodable, SerialDecodable, SerialEncodable}; + +pub const AEAD_TAG_SIZE: usize = 16; + +pub fn encrypt(note: &T, public: &PublicKey) -> Result { + let ephem_secret = SecretKey::random(&mut OsRng); + let ephem_public = PublicKey::from_secret(ephem_secret); + let shared_secret = sapling_ka_agree(&ephem_secret, public); + let key = kdf_sapling(&shared_secret, &ephem_public); + + let mut input = Vec::new(); + note.encode(&mut input)?; + + let mut ciphertext = vec![0; input.len() + AEAD_TAG_SIZE]; + assert_eq!( + ChachaPolyIetf::aead_cipher() + .seal_to(&mut ciphertext, &input, &[], key.as_ref(), &[0u8; 12]) + .unwrap(), + input.len() + AEAD_TAG_SIZE + ); + + Ok(EncryptedNote2 { ciphertext, ephem_public }) +} + +#[derive(Debug, Clone, PartialEq, Eq, SerialEncodable, SerialDecodable)] +pub struct EncryptedNote2 { + ciphertext: Vec, + ephem_public: PublicKey, +} + +impl EncryptedNote2 { + pub fn decrypt(&self, secret: &SecretKey) -> Result { + let shared_secret = sapling_ka_agree(secret, &self.ephem_public); + let key = kdf_sapling(&shared_secret, &self.ephem_public); + + let mut plaintext = vec![0; self.ciphertext.len()]; + assert_eq!( + ChachaPolyIetf::aead_cipher() + .open_to(&mut plaintext, &self.ciphertext, &[], key.as_ref(), &[0u8; 12]) + .map_err(|_| Error::NoteDecryptionFailed)?, + self.ciphertext.len() - AEAD_TAG_SIZE + ); + + T::decode(&plaintext[..]).map_err(Error::from) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use darkfi::crypto::{ + keypair::Keypair, + types::{DrkCoinBlind, DrkSerial, DrkTokenId, DrkValueBlind}, + }; + use group::ff::Field; + + #[test] + fn test_note_encdec() { + #[derive(SerialEncodable, SerialDecodable)] + struct MyNote { + serial: DrkSerial, + value: u64, + token_id: DrkTokenId, + coin_blind: DrkCoinBlind, + value_blind: DrkValueBlind, + token_blind: DrkValueBlind, + memo: Vec, + } + let note = MyNote { + serial: DrkSerial::random(&mut OsRng), + value: 110, + token_id: DrkTokenId::random(&mut OsRng), + coin_blind: DrkCoinBlind::random(&mut OsRng), + value_blind: DrkValueBlind::random(&mut OsRng), + token_blind: DrkValueBlind::random(&mut OsRng), + memo: vec![32, 223, 231, 3, 1, 1], + }; + + let keypair = Keypair::random(&mut OsRng); + + let encrypted_note = encrypt(¬e, &keypair.public).unwrap(); + let note2: MyNote = encrypted_note.decrypt(&keypair.secret).unwrap(); + assert_eq!(note.value, note2.value); + assert_eq!(note.token_id, note2.token_id); + assert_eq!(note.token_blind, note2.token_blind); + assert_eq!(note.memo, note2.memo); + } +} diff --git a/bin/dao/daod/src/rpc.rs b/bin/dao/daod/src/rpc.rs new file mode 100644 index 000000000..275f17aba --- /dev/null +++ b/bin/dao/daod/src/rpc.rs @@ -0,0 +1,402 @@ +use async_std::sync::Mutex; +use std::{str::FromStr, sync::Arc}; + +use async_trait::async_trait; +use log::{debug, error}; +use pasta_curves::group::ff::PrimeField; +use rand::rngs::OsRng; +use serde_json::{json, Value}; + +use darkfi::{ + crypto::keypair::{Keypair, PublicKey, SecretKey}, + rpc::{ + jsonrpc::{ErrorCode::*, JsonError, JsonRequest, JsonResponse, JsonResult}, + server::RequestHandler, + }, +}; + +use crate::{ + contract::money_contract::state::OwnCoin, + error::{server_error, RpcError}, + util::{parse_b58, DRK_ID, GOV_ID}, + Client, MoneyWallet, +}; + +pub struct JsonRpcInterface { + client: Arc>, +} + +#[async_trait] +impl RequestHandler for JsonRpcInterface { + async fn handle_request(&self, req: JsonRequest) -> JsonResult { + if !req.params.is_array() { + return JsonError::new(InvalidParams, None, req.id).into() + } + + let params = req.params.as_array().unwrap(); + + debug!(target: "RPC", "--> {}", serde_json::to_string(&req).unwrap()); + + match req.method.as_str() { + Some("create") => return self.create_dao(req.id, params).await, + Some("get_dao_addr") => return self.get_dao_addr(req.id, params).await, + Some("get_votes") => return self.get_votes(req.id, params).await, + Some("get_proposals") => return self.get_proposals(req.id, params).await, + Some("dao_balance") => return self.dao_balance(req.id, params).await, + Some("dao_bulla") => return self.dao_bulla(req.id, params).await, + Some("user_balance") => return self.user_balance(req.id, params).await, + Some("mint") => return self.mint_treasury(req.id, params).await, + Some("keygen") => return self.keygen(req.id, params).await, + Some("airdrop") => return self.airdrop_tokens(req.id, params).await, + Some("propose") => return self.create_proposal(req.id, params).await, + Some("vote") => return self.vote(req.id, params).await, + Some("exec") => return self.execute(req.id, params).await, + Some(_) | None => return JsonError::new(MethodNotFound, None, req.id).into(), + } + } +} + +impl JsonRpcInterface { + pub fn new(client: Client) -> Self { + let client = Arc::new(Mutex::new(client)); + Self { client } + } + + // --> {"method": "create", "params": []} + // <-- {"result": "creating dao..."} + async fn create_dao(&self, id: Value, params: &[Value]) -> JsonResult { + let dao_proposer_limit = params[0].as_u64(); + if dao_proposer_limit.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let dao_proposer_limit = dao_proposer_limit.unwrap(); + + let dao_quorum = params[1].as_u64(); + if dao_quorum.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let dao_quorum = dao_quorum.unwrap(); + + let dao_approval_ratio_quot = params[2].as_u64(); + if dao_approval_ratio_quot.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let dao_approval_ratio_quot = dao_approval_ratio_quot.unwrap(); + + let dao_approval_ratio_base = params[3].as_u64(); + if dao_approval_ratio_base.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let dao_approval_ratio_base = dao_approval_ratio_base.unwrap(); + + let mut client = self.client.lock().await; + + match client.create_dao( + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + *GOV_ID, + ) { + Ok(bulla) => { + let bulla: String = bs58::encode(bulla.to_repr()).into_string(); + JsonResponse::new(json!(bulla), id).into() + } + Err(e) => { + error!("Failed to create DAO: {}", e); + return server_error(RpcError::Create, id) + } + } + } + + // --> {"method": "get_dao_addr", "params": []} + // <-- {"result": "getting dao public addr..."} + async fn get_dao_addr(&self, id: Value, _params: &[Value]) -> JsonResult { + let client = self.client.lock().await; + let pubkey = client.dao_wallet.get_public_key(); + let addr: String = bs58::encode(pubkey.to_bytes()).into_string(); + JsonResponse::new(json!(addr), id).into() + } + + // --> {"method": "get_dao_addr", "params": []} + // <-- {"result": "getting dao public addr..."} + async fn get_votes(&self, id: Value, _params: &[Value]) -> JsonResult { + let client = self.client.lock().await; + let vote_notes = client.dao_wallet.get_votes(); + let mut vote_data = vec![]; + + for note in vote_notes { + let vote_option = note.vote.vote_option; + let vote_value = note.vote_value; + vote_data.push((vote_option, vote_value)); + } + + JsonResponse::new(json!(vote_data), id).into() + } + + // --> {"method": "get_dao_addr", "params": []} + // <-- {"result": "getting dao public addr..."} + async fn get_proposals(&self, id: Value, _params: &[Value]) -> JsonResult { + let client = self.client.lock().await; + let proposals = client.dao_wallet.get_proposals(); + let mut proposal_data = vec![]; + + for proposal in proposals { + let dest = proposal.dest; + let amount = proposal.amount; + let token_id = proposal.token_id; + let token_id: String = bs58::encode(token_id.to_repr()).into_string(); + proposal_data.push((dest, amount, token_id)); + } + + JsonResponse::new(json!(proposal_data), id).into() + } + + async fn dao_balance(&self, id: Value, _params: &[Value]) -> JsonResult { + let client = self.client.lock().await; + let balance = client.dao_wallet.balances().unwrap(); + JsonResponse::new(json!(balance), id).into() + } + + async fn dao_bulla(&self, id: Value, _params: &[Value]) -> JsonResult { + let client = self.client.lock().await; + let dao_bullas = client.dao_wallet.bullas.clone(); + let mut bulla_vec = Vec::new(); + + for bulla in dao_bullas { + let dao_bulla: String = bs58::encode(bulla.0.to_repr()).into_string(); + bulla_vec.push(dao_bulla); + } + + JsonResponse::new(json!(bulla_vec), id).into() + } + + async fn user_balance(&self, id: Value, params: &[Value]) -> JsonResult { + let client = self.client.lock().await; + let nym = params[0].as_str(); + if nym.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let nym = nym.unwrap(); + + match PublicKey::from_str(nym) { + Ok(key) => match client.money_wallets.get(&key) { + Some(wallet) => { + let balance = wallet.balances().unwrap(); + JsonResponse::new(json!(balance), id).into() + } + None => { + error!("No wallet found for provided key"); + return server_error(RpcError::Balance, id) + } + }, + Err(_) => { + error!("Could not parse PublicKey from string"); + return server_error(RpcError::Parse, id) + } + } + } + + // --> {"method": "mint_treasury", "params": []} + // <-- {"result": "minting treasury..."} + async fn mint_treasury(&self, id: Value, params: &[Value]) -> JsonResult { + let mut client = self.client.lock().await; + + let token_supply = params[0].as_u64(); + if token_supply.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let token_supply = token_supply.unwrap(); + + let addr = params[1].as_str(); + if addr.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let addr = addr.unwrap(); + + match PublicKey::from_str(addr) { + Ok(dao_addr) => match client.mint_treasury(*DRK_ID, token_supply, dao_addr) { + Ok(_) => JsonResponse::new(json!("DAO treasury minted successfully."), id).into(), + Err(e) => { + error!("Failed to mint treasury: {}", e); + return server_error(RpcError::Mint, id) + } + }, + Err(_) => { + error!("Failed to parse PublicKey from String"); + return server_error(RpcError::Parse, id) + } + } + } + + // Create a new wallet for governance tokens. + async fn keygen(&self, id: Value, _params: &[Value]) -> JsonResult { + let mut client = self.client.lock().await; + // let nym = params[0].as_str().unwrap().to_string(); + + let keypair = Keypair::random(&mut OsRng); + let signature_secret = SecretKey::random(&mut OsRng); + let own_coins: Vec<(OwnCoin, bool)> = Vec::new(); + let money_wallet = MoneyWallet { keypair, signature_secret, own_coins }; + + match money_wallet.track(&mut client.states) { + Ok(_) => { + client.money_wallets.insert(keypair.public, money_wallet); + let addr: String = bs58::encode(keypair.public.to_bytes()).into_string(); + JsonResponse::new(json!(addr), id).into() + } + Err(e) => { + error!("Failed to airdrop tokens: {}", e); + return server_error(RpcError::Keygen, id) + } + } + } + + // --> {"method": "airdrop_tokens", "params": []} + // <-- {"result": "airdropping tokens..."} + async fn airdrop_tokens(&self, id: Value, params: &[Value]) -> JsonResult { + let mut client = self.client.lock().await; + // let zk_bins = &client.zk_bins; + + let addr = params[0].as_str(); + if addr.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let addr = addr.unwrap(); + + let value = params[1].as_u64(); + if value.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let value = value.unwrap(); + + match PublicKey::from_str(addr) { + Ok(key) => match client.airdrop_user(value, *GOV_ID, key) { + Ok(_) => JsonResponse::new(json!("Tokens airdropped successfully."), id).into(), + Err(e) => { + error!("Failed to airdrop tokens: {}", e); + return server_error(RpcError::Airdrop, id) + } + }, + Err(_) => { + error!("Failed parsing PublicKey from String"); + return server_error(RpcError::Parse, id) + } + } + } + // --> {"method": "create_proposal", "params": []} + // <-- {"result": "creating proposal..."} + async fn create_proposal(&self, id: Value, params: &[Value]) -> JsonResult { + let mut client = self.client.lock().await; + + if params.is_empty() { + return JsonError::new(InvalidParams, None, id).into() + } + + let sender = params[0].as_str(); + if sender.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let sender = sender.unwrap(); + + let recipient = params[1].as_str(); + if recipient.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let recipient = recipient.unwrap(); + + let amount = params[2].as_u64(); + if amount.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let amount = amount.unwrap(); + + let recv_addr = PublicKey::from_str(recipient); + if recv_addr.is_err() { + return JsonError::new(InvalidParams, None, id).into() + } + let recv_addr = recv_addr.unwrap(); + + let sndr_addr = PublicKey::from_str(sender); + if sndr_addr.is_err() { + return JsonError::new(InvalidParams, None, id).into() + } + let sndr_addr = sndr_addr.unwrap(); + + match client.propose(recv_addr, *DRK_ID, amount, sndr_addr) { + Ok(bulla) => { + let bulla: String = bs58::encode(bulla.to_repr()).into_string(); + + JsonResponse::new(json!(bulla), id).into() + } + Err(e) => { + error!("Failed to make Proposal: {}", e); + return server_error(RpcError::Propose, id) + } + } + } + // --> {"method": "vote", "params": []} + // <-- {"result": "voting..."} + async fn vote(&self, id: Value, params: &[Value]) -> JsonResult { + let mut client = self.client.lock().await; + let mut vote_bool = true; + + let addr = params[0].as_str(); + if addr.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let addr = addr.unwrap(); + + let vote_str = params[1].as_str(); + if vote_str.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let vote_str = vote_str.unwrap(); + + match vote_str { + "yes" => {} + "no" => vote_bool = false, + _ => return JsonError::new(InvalidParams, None, id).into(), + } + + match PublicKey::from_str(addr) { + Ok(key) => match client.cast_vote(key, vote_bool) { + Ok(_) => JsonResponse::new(json!("Vote cast successfully."), id).into(), + Err(e) => { + error!("Failed casting vote: {}", e); + return server_error(RpcError::Vote, id) + } + }, + Err(_) => { + error!("Failed parsing PublicKey from String"); + return server_error(RpcError::Parse, id) + } + } + } + // --> {"method": "execute", "params": []} + // <-- {"result": "executing..."} + async fn execute(&self, id: Value, params: &[Value]) -> JsonResult { + let mut client = self.client.lock().await; + + let bulla_str = params[0].as_str(); + if bulla_str.is_none() { + return JsonError::new(InvalidParams, None, id).into() + } + let bulla_str = bulla_str.unwrap(); + + let bulla = parse_b58(bulla_str); + match bulla { + Ok(bulla) => match client.exec_proposal(bulla) { + Ok(_) => JsonResponse::new(json!("Proposal executed successfully."), id).into(), + Err(e) => { + error!("Failed executing proposal: {}", e); + return server_error(RpcError::Exec, id) + } + }, + Err(e) => { + error!("Failed parsing bulla: {}", e); + return server_error(RpcError::Parse, id) + } + } + } +} diff --git a/bin/daod/src/demo.rs b/bin/dao/daod/src/schema.rs similarity index 99% rename from bin/daod/src/demo.rs rename to bin/dao/daod/src/schema.rs index b616c8ae3..7cbe26cbc 100644 --- a/bin/daod/src/demo.rs +++ b/bin/dao/daod/src/schema.rs @@ -36,7 +36,7 @@ use darkfi::{ zkas::decoder::ZkBinary, }; -use crate::{dao_contract, example_contract, money_contract}; +use crate::contract::{dao_contract, example_contract, money_contract}; // TODO: Anonymity leaks in this proof of concept: // @@ -1255,6 +1255,7 @@ pub async fn demo() -> Result<()> { serial: dao_serial, coin_blind: dao_coin_blind, spend_hook: *dao_contract::exec::FUNC_ID, + // TODO: should be DAO bulla user_data: proposal_bulla, }, ], diff --git a/bin/dao/daod/src/util.rs b/bin/dao/daod/src/util.rs new file mode 100644 index 000000000..30780e597 --- /dev/null +++ b/bin/dao/daod/src/util.rs @@ -0,0 +1,265 @@ +use std::{any::Any, collections::HashMap, hash::Hasher}; + +use lazy_static::lazy_static; +use log::debug; +use pasta_curves::{ + group::ff::{Field, PrimeField}, + pallas, +}; +use rand::rngs::OsRng; + +use darkfi::{ + crypto::{ + keypair::{PublicKey, SecretKey}, + proof::{ProvingKey, VerifyingKey}, + schnorr::{SchnorrPublic, SchnorrSecret, Signature}, + types::DrkCircuitField, + Proof, + }, + zk::{vm::ZkCircuit, vm_stack::empty_witnesses}, + zkas::decoder::ZkBinary, + Error, +}; +use darkfi_serial::Encodable; + +use crate::error::{DaoError, DaoResult}; + +/// Parse pallas::Base from a base58-encoded string +pub fn parse_b58(s: &str) -> std::result::Result { + let bytes = bs58::decode(s).into_vec()?; + if bytes.len() != 32 { + return Err(Error::ParseFailed("Failed parsing DrkTokenId from base58 string")) + } + + let ret = pallas::Base::from_repr(bytes.try_into().unwrap()); + if ret.is_some().unwrap_u8() == 1 { + return Ok(ret.unwrap()) + } + + Err(Error::ParseFailed("Failed parsing DrkTokenId from base58 string")) +} + +// The token of the DAO treasury. +lazy_static! { + pub static ref DRK_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} + +// Governance tokens that are airdropped to users to operate the DAO. +lazy_static! { + pub static ref GOV_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} + +#[derive(Eq, PartialEq, Debug)] +pub struct HashableBase(pub pallas::Base); + +impl std::hash::Hash for HashableBase { + fn hash(&self, state: &mut H) { + let bytes = self.0.to_repr(); + bytes.hash(state); + } +} + +#[derive(Clone)] +pub struct ZkBinaryContractInfo { + pub k_param: u32, + pub bincode: ZkBinary, + pub proving_key: ProvingKey, + pub verifying_key: VerifyingKey, +} + +#[derive(Clone)] +pub struct ZkNativeContractInfo { + pub proving_key: ProvingKey, + pub verifying_key: VerifyingKey, +} + +#[derive(Clone)] +pub enum ZkContractInfo { + Binary(ZkBinaryContractInfo), + Native(ZkNativeContractInfo), +} + +#[derive(Clone)] +pub struct ZkContractTable { + // Key will be a hash of zk binary contract on chain + table: HashMap, +} + +impl ZkContractTable { + pub fn new() -> Self { + Self { table: HashMap::new() } + } + + pub fn add_contract(&mut self, key: String, bincode: ZkBinary, k_param: u32) { + let witnesses = empty_witnesses(&bincode); + let circuit = ZkCircuit::new(witnesses, bincode.clone()); + let proving_key = ProvingKey::build(k_param, &circuit); + let verifying_key = VerifyingKey::build(k_param, &circuit); + let info = ZkContractInfo::Binary(ZkBinaryContractInfo { + k_param, + bincode, + proving_key, + verifying_key, + }); + self.table.insert(key, info); + } + + pub fn add_native( + &mut self, + key: String, + proving_key: ProvingKey, + verifying_key: VerifyingKey, + ) { + self.table.insert( + key, + ZkContractInfo::Native(ZkNativeContractInfo { proving_key, verifying_key }), + ); + } + + pub fn lookup(&self, key: &String) -> Option<&ZkContractInfo> { + self.table.get(key) + } +} + +pub struct Transaction { + pub func_calls: Vec, + pub signatures: Vec, +} + +impl Transaction { + /// Verify ZK contracts for the entire tx + /// In real code, we could parallelize this for loop + /// TODO: fix use of unwrap with Result type stuff + pub fn zk_verify(&self, zk_bins: &ZkContractTable) -> DaoResult<()> { + for func_call in &self.func_calls { + let proofs_public_vals = &func_call.call_data.zk_public_values(); + + assert_eq!( + proofs_public_vals.len(), + func_call.proofs.len(), + "proof_public_vals.len()={} and func_call.proofs.len()={} do not match", + proofs_public_vals.len(), + func_call.proofs.len() + ); + for (i, (proof, (key, public_vals))) in + func_call.proofs.iter().zip(proofs_public_vals.iter()).enumerate() + { + match zk_bins.lookup(key).unwrap() { + ZkContractInfo::Binary(info) => { + let verifying_key = &info.verifying_key; + let verify_result = proof.verify(&verifying_key, public_vals); + if verify_result.is_err() { + return Err(DaoError::VerifyProofFailed(i, key.to_string())) + } + //assert!(verify_result.is_ok(), "verify proof[{}]='{}' failed", i, key); + } + ZkContractInfo::Native(info) => { + let verifying_key = &info.verifying_key; + let verify_result = proof.verify(&verifying_key, public_vals); + if verify_result.is_err() { + return Err(DaoError::VerifyProofFailed(i, key.to_string())) + } + //assert!(verify_result.is_ok(), "verify proof[{}]='{}' failed", i, key); + } + }; + debug!(target: "demo", "zk_verify({}) passed [i={}]", key, i); + } + } + Ok(()) + } + + pub fn verify_sigs(&self) { + let mut unsigned_tx_data = vec![]; + for (i, (func_call, signature)) in + self.func_calls.iter().zip(self.signatures.clone()).enumerate() + { + func_call.encode(&mut unsigned_tx_data).expect("failed to encode data"); + let signature_pub_keys = func_call.call_data.signature_public_keys(); + for signature_pub_key in signature_pub_keys { + let verify_result = signature_pub_key.verify(&unsigned_tx_data[..], &signature); + assert!(verify_result, "verify sigs[{}] failed", i); + } + debug!(target: "demo", "verify_sigs({}) passed", i); + } + } +} + +pub fn sign(signature_secrets: Vec, func_calls: &Vec) -> Vec { + let mut signatures = vec![]; + let mut unsigned_tx_data = vec![]; + for (_i, (signature_secret, func_call)) in + signature_secrets.iter().zip(func_calls.iter()).enumerate() + { + func_call.encode(&mut unsigned_tx_data).expect("failed to encode data"); + let signature = signature_secret.sign(&unsigned_tx_data[..]); + signatures.push(signature); + } + signatures +} + +type ContractId = pallas::Base; +type FuncId = pallas::Base; + +pub struct FuncCall { + pub contract_id: ContractId, + pub func_id: FuncId, + pub call_data: Box, + pub proofs: Vec, +} + +impl Encodable for FuncCall { + fn encode(&self, mut w: W) -> std::result::Result { + let mut len = 0; + len += self.contract_id.encode(&mut w)?; + len += self.func_id.encode(&mut w)?; + len += self.proofs.encode(&mut w)?; + len += self.call_data.encode_bytes(&mut w)?; + Ok(len) + } +} + +pub trait CallDataBase { + // Public values for verifying the proofs + // Needed so we can convert internal types so they can be used in Proof::verify() + fn zk_public_values(&self) -> Vec<(String, Vec)>; + + // For upcasting to CallData itself so it can be read in state_transition() + fn as_any(&self) -> &dyn Any; + + // Public keys we will use to verify transaction signatures. + fn signature_public_keys(&self) -> Vec; + + fn encode_bytes( + &self, + writer: &mut dyn std::io::Write, + ) -> std::result::Result; +} + +type GenericContractState = Box; + +pub struct StateRegistry { + pub states: HashMap, +} + +impl StateRegistry { + pub fn new() -> Self { + Self { states: HashMap::new() } + } + + pub fn register(&mut self, contract_id: ContractId, state: GenericContractState) { + debug!(target: "StateRegistry::register()", "contract_id: {:?}", contract_id); + self.states.insert(HashableBase(contract_id), state); + } + + pub fn lookup_mut<'a, S: 'static>(&'a mut self, contract_id: ContractId) -> Option<&'a mut S> { + self.states.get_mut(&HashableBase(contract_id)).and_then(|state| state.downcast_mut()) + } + + pub fn lookup<'a, S: 'static>(&'a self, contract_id: ContractId) -> Option<&'a S> { + self.states.get(&HashableBase(contract_id)).and_then(|state| state.downcast_ref()) + } +} + +pub trait UpdateBase { + fn apply(self: Box, states: &mut StateRegistry); +} diff --git a/bin/daod/src/main.rs b/bin/daod/src/main.rs deleted file mode 100644 index 339ad8f8b..000000000 --- a/bin/daod/src/main.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::sync::Arc; - -use async_trait::async_trait; -use log::debug; -use serde_json::{json, Value}; -use simplelog::{ColorChoice, LevelFilter, TermLogger, TerminalMode}; -use url::Url; - -use darkfi::{ - rpc::{ - jsonrpc::{ErrorCode::*, JsonError, JsonRequest, JsonResponse, JsonResult}, - server::{listen_and_serve, RequestHandler}, - }, - Result, -}; - -mod dao_contract; -mod example_contract; -mod money_contract; - -mod demo; -mod note; - -use crate::demo::demo; - -async fn _start() -> Result<()> { - let rpc_addr = Url::parse("tcp://127.0.0.1:7777")?; - let rpc_interface = Arc::new(JsonRpcInterface {}); - - listen_and_serve(rpc_addr, rpc_interface).await?; - Ok(()) -} - -struct JsonRpcInterface {} - -#[async_trait] -impl RequestHandler for JsonRpcInterface { - async fn handle_request(&self, req: JsonRequest) -> JsonResult { - if req.params.as_array().is_none() { - return JsonError::new(InvalidParams, None, req.id).into() - } - - debug!(target: "RPC", "--> {}", serde_json::to_string(&req).unwrap()); - - match req.method.as_str() { - Some("say_hello") => return self.say_hello(req.id, req.params).await, - Some(_) | None => return JsonError::new(MethodNotFound, None, req.id).into(), - } - } -} - -impl JsonRpcInterface { - // --> {"method": "say_hello", "params": []} - // <-- {"result": "hello world"} - async fn say_hello(&self, id: Value, _params: Value) -> JsonResult { - JsonResponse::new(json!("hello world"), id).into() - } -} - -#[async_std::main] -async fn main() -> Result<()> { - TermLogger::init( - LevelFilter::Debug, - simplelog::Config::default(), - TerminalMode::Mixed, - ColorChoice::Auto, - )?; - - //start().await?; - demo().await.unwrap(); - Ok(()) -} diff --git a/example/dao/contract/dao_contract/exec/mod.rs b/example/dao/contract/dao_contract/exec/mod.rs new file mode 100644 index 000000000..bd241396a --- /dev/null +++ b/example/dao/contract/dao_contract/exec/mod.rs @@ -0,0 +1,10 @@ +use lazy_static::lazy_static; +use pasta_curves::{group::ff::Field, pallas}; +use rand::rngs::OsRng; + +pub mod validate; +pub mod wallet; + +lazy_static! { + pub static ref FUNC_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} diff --git a/bin/daod/src/dao_contract/exec/validate.rs b/example/dao/contract/dao_contract/exec/validate.rs similarity index 97% rename from bin/daod/src/dao_contract/exec/validate.rs rename to example/dao/contract/dao_contract/exec/validate.rs index bef22d72e..047eca12d 100644 --- a/bin/daod/src/dao_contract/exec/validate.rs +++ b/example/dao/contract/dao_contract/exec/validate.rs @@ -13,10 +13,8 @@ use darkfi::{ use std::any::{Any, TypeId}; use crate::{ - dao_contract, - dao_contract::CONTRACT_ID, - demo::{CallDataBase, HashableBase, StateRegistry, Transaction, UpdateBase}, - money_contract, + contract::{dao_contract, dao_contract::CONTRACT_ID, money_contract}, + util::{CallDataBase, HashableBase, StateRegistry, Transaction, UpdateBase}, }; type Result = std::result::Result; @@ -111,7 +109,7 @@ pub fn state_transition( states: &StateRegistry, func_call_index: usize, parent_tx: &Transaction, -) -> Result> { +) -> Result> { let func_call = &parent_tx.func_calls[func_call_index]; let call_data = func_call.call_data.as_any(); diff --git a/bin/daod/src/dao_contract/exec/wallet.rs b/example/dao/contract/dao_contract/exec/wallet.rs similarity index 98% rename from bin/daod/src/dao_contract/exec/wallet.rs rename to example/dao/contract/dao_contract/exec/wallet.rs index b8fcfc97a..4d55cec10 100644 --- a/bin/daod/src/dao_contract/exec/wallet.rs +++ b/example/dao/contract/dao_contract/exec/wallet.rs @@ -14,10 +14,10 @@ use darkfi::{ }; use crate::{ - dao_contract::{ + contract::dao_contract::{ exec::validate::CallData, mint::wallet::DaoParams, propose::wallet::Proposal, CONTRACT_ID, }, - demo::{FuncCall, ZkContractInfo, ZkContractTable}, + util::{FuncCall, ZkContractInfo, ZkContractTable}, }; pub struct Builder { diff --git a/example/dao/contract/dao_contract/mint/mod.rs b/example/dao/contract/dao_contract/mint/mod.rs new file mode 100644 index 000000000..871ab40be --- /dev/null +++ b/example/dao/contract/dao_contract/mint/mod.rs @@ -0,0 +1,44 @@ +use lazy_static::lazy_static; +use pasta_curves::{group::ff::Field, pallas}; +use rand::rngs::OsRng; + +pub mod validate; +/// This is an anonymous contract function that mutates the internal DAO state. +/// +/// Corresponds to `mint(proposer_limit, quorum, approval_ratio, dao_pubkey, dao_blind)` +/// +/// The prover creates a `Builder`, which then constructs the `Tx` that the verifier can +/// check using `state_transition()`. +/// +/// # Arguments +/// +/// * `proposer_limit` - Number of governance tokens that holder must possess in order to +/// propose a new vote. +/// * `quorum` - Number of minimum votes that must be met for a proposal to pass. +/// * `approval_ratio` - Ratio of winning to total votes for a proposal to pass. +/// * `dao_pubkey` - Public key of the DAO for permissioned access. This can also be +/// shared publicly if you want a full decentralized DAO. +/// * `dao_blind` - Blinding factor for the DAO bulla. +/// +/// # Example +/// +/// ```rust +/// let dao_proposer_limit = 110; +/// let dao_quorum = 110; +/// let dao_approval_ratio = 2; +/// +/// let builder = dao_contract::Mint::Builder( +/// dao_proposer_limit, +/// dao_quorum, +/// dao_approval_ratio, +/// gov_token_id, +/// dao_pubkey, +/// dao_blind +/// ); +/// let tx = builder.build(); +/// ``` +pub mod wallet; + +lazy_static! { + pub static ref FUNC_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} diff --git a/bin/daod/src/dao_contract/mint/validate.rs b/example/dao/contract/dao_contract/mint/validate.rs similarity index 91% rename from bin/daod/src/dao_contract/mint/validate.rs rename to example/dao/contract/dao_contract/mint/validate.rs index 02d848d14..35a29c3b4 100644 --- a/bin/daod/src/dao_contract/mint/validate.rs +++ b/example/dao/contract/dao_contract/mint/validate.rs @@ -4,15 +4,15 @@ use darkfi::crypto::{keypair::PublicKey, types::DrkCircuitField}; use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable}; use crate::{ - dao_contract::{DaoBulla, State, CONTRACT_ID}, - demo::{CallDataBase, StateRegistry, Transaction, UpdateBase}, + contract::dao_contract::{DaoBulla, State, CONTRACT_ID}, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, }; pub fn state_transition( _states: &StateRegistry, func_call_index: usize, parent_tx: &Transaction, -) -> Result> { +) -> Result> { let func_call = &parent_tx.func_calls[func_call_index]; let call_data = func_call.call_data.as_any(); diff --git a/example/dao/contract/dao_contract/mint/wallet.rs b/example/dao/contract/dao_contract/mint/wallet.rs new file mode 100644 index 000000000..cf4b4bc4f --- /dev/null +++ b/example/dao/contract/dao_contract/mint/wallet.rs @@ -0,0 +1,96 @@ +use darkfi::{ + crypto::{ + keypair::{PublicKey, SecretKey}, + util::poseidon_hash, + Proof, + }, + zk::vm::{Witness, ZkCircuit}, +}; +use halo2_proofs::circuit::Value; +use pasta_curves::{arithmetic::CurveAffine, group::Curve, pallas}; +use rand::rngs::OsRng; + +use crate::{ + contract::dao_contract::{mint::validate::CallData, state::DaoBulla, CONTRACT_ID}, + util::{FuncCall, ZkContractInfo, ZkContractTable}, +}; + +#[derive(Clone)] +pub struct DaoParams { + pub proposer_limit: u64, + pub quorum: u64, + pub approval_ratio_quot: u64, + pub approval_ratio_base: u64, + pub gov_token_id: pallas::Base, + pub public_key: PublicKey, + pub bulla_blind: pallas::Base, +} + +pub struct Builder { + pub dao_proposer_limit: u64, + pub dao_quorum: u64, + pub dao_approval_ratio_quot: u64, + pub dao_approval_ratio_base: u64, + pub gov_token_id: pallas::Base, + pub dao_pubkey: PublicKey, + pub dao_bulla_blind: pallas::Base, + pub _signature_secret: SecretKey, +} + +impl Builder { + /// Consumes self, and produces the function call + pub fn build(self, zk_bins: &ZkContractTable) -> FuncCall { + // Dao bulla + let dao_proposer_limit = pallas::Base::from(self.dao_proposer_limit); + let dao_quorum = pallas::Base::from(self.dao_quorum); + let dao_approval_ratio_quot = pallas::Base::from(self.dao_approval_ratio_quot); + let dao_approval_ratio_base = pallas::Base::from(self.dao_approval_ratio_base); + + let dao_pubkey_coords = self.dao_pubkey.0.to_affine().coordinates().unwrap(); + + let dao_bulla = poseidon_hash::<8>([ + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + self.gov_token_id, + *dao_pubkey_coords.x(), + *dao_pubkey_coords.y(), + self.dao_bulla_blind, + ]); + let dao_bulla = DaoBulla(dao_bulla); + + // Now create the mint proof + let zk_info = zk_bins.lookup(&"dao-mint".to_string()).unwrap(); + let zk_info = if let ZkContractInfo::Binary(info) = zk_info { + info + } else { + panic!("Not binary info") + }; + let zk_bin = zk_info.bincode.clone(); + let prover_witnesses = vec![ + Witness::Base(Value::known(dao_proposer_limit)), + Witness::Base(Value::known(dao_quorum)), + Witness::Base(Value::known(dao_approval_ratio_quot)), + Witness::Base(Value::known(dao_approval_ratio_base)), + Witness::Base(Value::known(self.gov_token_id)), + Witness::Base(Value::known(*dao_pubkey_coords.x())), + Witness::Base(Value::known(*dao_pubkey_coords.y())), + Witness::Base(Value::known(self.dao_bulla_blind)), + ]; + let public_inputs = vec![dao_bulla.0]; + let circuit = ZkCircuit::new(prover_witnesses, zk_bin); + + let proving_key = &zk_info.proving_key; + let mint_proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng) + .expect("DAO::mint() proving error!"); + + let call_data = CallData { dao_bulla }; + FuncCall { + contract_id: *CONTRACT_ID, + func_id: *super::FUNC_ID, + call_data: Box::new(call_data), + proofs: vec![mint_proof], + } + } +} diff --git a/example/dao/contract/dao_contract/mod.rs b/example/dao/contract/dao_contract/mod.rs new file mode 100644 index 000000000..a795d74be --- /dev/null +++ b/example/dao/contract/dao_contract/mod.rs @@ -0,0 +1,20 @@ +use lazy_static::lazy_static; +use pasta_curves::{group::ff::Field, pallas}; +use rand::rngs::OsRng; + +// mint() +pub mod mint; +// propose() +pub mod propose; +// vote{} +pub mod vote; +// exec{} +pub mod exec; + +pub mod state; + +pub use state::{DaoBulla, State}; + +lazy_static! { + pub static ref CONTRACT_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} diff --git a/example/dao/contract/dao_contract/propose/mod.rs b/example/dao/contract/dao_contract/propose/mod.rs new file mode 100644 index 000000000..bd241396a --- /dev/null +++ b/example/dao/contract/dao_contract/propose/mod.rs @@ -0,0 +1,10 @@ +use lazy_static::lazy_static; +use pasta_curves::{group::ff::Field, pallas}; +use rand::rngs::OsRng; + +pub mod validate; +pub mod wallet; + +lazy_static! { + pub static ref FUNC_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} diff --git a/bin/daod/src/dao_contract/propose/validate.rs b/example/dao/contract/dao_contract/propose/validate.rs similarity index 95% rename from bin/daod/src/dao_contract/propose/validate.rs rename to example/dao/contract/dao_contract/propose/validate.rs index e29d49578..d7f122f11 100644 --- a/bin/daod/src/dao_contract/propose/validate.rs +++ b/example/dao/contract/dao_contract/propose/validate.rs @@ -14,12 +14,12 @@ use darkfi::{ }; use crate::{ - dao_contract, - dao_contract::State as DaoState, - demo::{CallDataBase, StateRegistry, Transaction, UpdateBase}, - money_contract, - money_contract::state::State as MoneyState, + contract::{ + dao_contract, dao_contract::State as DaoState, money_contract, + money_contract::state::State as MoneyState, + }, note::EncryptedNote2, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, }; // used for debugging @@ -129,7 +129,7 @@ pub fn state_transition( states: &StateRegistry, func_call_index: usize, parent_tx: &Transaction, -) -> Result> { +) -> Result> { let func_call = &parent_tx.func_calls[func_call_index]; let call_data = func_call.call_data.as_any(); diff --git a/bin/daod/src/dao_contract/propose/wallet.rs b/example/dao/contract/dao_contract/propose/wallet.rs similarity index 97% rename from bin/daod/src/dao_contract/propose/wallet.rs rename to example/dao/contract/dao_contract/propose/wallet.rs index 89f7b4a87..257cdeef6 100644 --- a/bin/daod/src/dao_contract/propose/wallet.rs +++ b/example/dao/contract/dao_contract/propose/wallet.rs @@ -19,13 +19,16 @@ use darkfi::{ }; use crate::{ - dao_contract::{ - mint::wallet::DaoParams, - propose::validate::{CallData, Header, Input}, - CONTRACT_ID, + contract::{ + dao_contract::{ + mint::wallet::DaoParams, + propose::validate::{CallData, Header, Input}, + CONTRACT_ID, + }, + money_contract, }, - demo::{FuncCall, ZkContractInfo, ZkContractTable}, - money_contract, note, + note, + util::{FuncCall, ZkContractInfo, ZkContractTable}, }; #[derive(SerialEncodable, SerialDecodable)] diff --git a/bin/daod/src/dao_contract/state.rs b/example/dao/contract/dao_contract/state.rs similarity index 97% rename from bin/daod/src/dao_contract/state.rs rename to example/dao/contract/dao_contract/state.rs index f41148c5f..9d08ab128 100644 --- a/bin/daod/src/dao_contract/state.rs +++ b/example/dao/contract/dao_contract/state.rs @@ -6,7 +6,7 @@ use pasta_curves::{group::Group, pallas}; use darkfi::crypto::{constants::MERKLE_DEPTH, merkle_node::MerkleNode, nullifier::Nullifier}; -use crate::demo::HashableBase; +use crate::util::HashableBase; #[derive(Clone, SerialEncodable, SerialDecodable)] pub struct DaoBulla(pub pallas::Base); @@ -42,7 +42,7 @@ pub struct State { } impl State { - pub fn new() -> Box { + pub fn new() -> Box { Box::new(Self { dao_bullas: Vec::new(), dao_tree: MerkleTree::new(100), diff --git a/example/dao/contract/dao_contract/vote/mod.rs b/example/dao/contract/dao_contract/vote/mod.rs new file mode 100644 index 000000000..bd241396a --- /dev/null +++ b/example/dao/contract/dao_contract/vote/mod.rs @@ -0,0 +1,10 @@ +use lazy_static::lazy_static; +use pasta_curves::{group::ff::Field, pallas}; +use rand::rngs::OsRng; + +pub mod validate; +pub mod wallet; + +lazy_static! { + pub static ref FUNC_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} diff --git a/bin/daod/src/dao_contract/vote/validate.rs b/example/dao/contract/dao_contract/vote/validate.rs similarity index 95% rename from bin/daod/src/dao_contract/vote/validate.rs rename to example/dao/contract/dao_contract/vote/validate.rs index 101550d32..3c8a1847e 100644 --- a/bin/daod/src/dao_contract/vote/validate.rs +++ b/example/dao/contract/dao_contract/vote/validate.rs @@ -16,12 +16,12 @@ use darkfi::{ }; use crate::{ - dao_contract, - dao_contract::State as DaoState, - demo::{CallDataBase, StateRegistry, Transaction, UpdateBase}, - money_contract, - money_contract::state::State as MoneyState, + contract::{ + dao_contract, dao_contract::State as DaoState, money_contract, + money_contract::state::State as MoneyState, + }, note::EncryptedNote2, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, }; #[derive(Debug, Clone, thiserror::Error)] @@ -140,7 +140,7 @@ pub fn state_transition( states: &StateRegistry, func_call_index: usize, parent_tx: &Transaction, -) -> Result> { +) -> Result> { let func_call = &parent_tx.func_calls[func_call_index]; let call_data = func_call.call_data.as_any(); diff --git a/bin/daod/src/dao_contract/vote/wallet.rs b/example/dao/contract/dao_contract/vote/wallet.rs similarity index 97% rename from bin/daod/src/dao_contract/vote/wallet.rs rename to example/dao/contract/dao_contract/vote/wallet.rs index a1fc38232..1f8acb569 100644 --- a/bin/daod/src/dao_contract/vote/wallet.rs +++ b/example/dao/contract/dao_contract/vote/wallet.rs @@ -21,14 +21,17 @@ use darkfi::{ }; use crate::{ - dao_contract::{ - mint::wallet::DaoParams, - propose::wallet::Proposal, - vote::validate::{CallData, Header, Input}, - CONTRACT_ID, + contract::{ + dao_contract::{ + mint::wallet::DaoParams, + propose::wallet::Proposal, + vote::validate::{CallData, Header, Input}, + CONTRACT_ID, + }, + money_contract, }, - demo::{FuncCall, ZkContractInfo, ZkContractTable}, - money_contract, note, + note, + util::{FuncCall, ZkContractInfo, ZkContractTable}, }; #[derive(SerialEncodable, SerialDecodable)] diff --git a/example/dao/contract/example_contract/foo/mod.rs b/example/dao/contract/example_contract/foo/mod.rs new file mode 100644 index 000000000..bd241396a --- /dev/null +++ b/example/dao/contract/example_contract/foo/mod.rs @@ -0,0 +1,10 @@ +use lazy_static::lazy_static; +use pasta_curves::{group::ff::Field, pallas}; +use rand::rngs::OsRng; + +pub mod validate; +pub mod wallet; + +lazy_static! { + pub static ref FUNC_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} diff --git a/bin/daod/src/example_contract/foo/validate.rs b/example/dao/contract/example_contract/foo/validate.rs similarity index 93% rename from bin/daod/src/example_contract/foo/validate.rs rename to example/dao/contract/example_contract/foo/validate.rs index 301aeb087..5a21618d0 100644 --- a/bin/daod/src/example_contract/foo/validate.rs +++ b/example/dao/contract/example_contract/foo/validate.rs @@ -9,8 +9,8 @@ use darkfi::{ use std::any::{Any, TypeId}; use crate::{ - demo::{CallDataBase, StateRegistry, Transaction, UpdateBase}, - example_contract::{state::State, CONTRACT_ID}, + contract::example_contract::{state::State, CONTRACT_ID}, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, }; type Result = std::result::Result; @@ -61,7 +61,7 @@ pub fn state_transition( states: &StateRegistry, func_call_index: usize, parent_tx: &Transaction, -) -> Result> { +) -> Result> { let func_call = &parent_tx.func_calls[func_call_index]; let call_data = func_call.call_data.as_any(); diff --git a/bin/daod/src/example_contract/foo/wallet.rs b/example/dao/contract/example_contract/foo/wallet.rs similarity index 93% rename from bin/daod/src/example_contract/foo/wallet.rs rename to example/dao/contract/example_contract/foo/wallet.rs index cc0e08739..c41d9d5fa 100644 --- a/bin/daod/src/example_contract/foo/wallet.rs +++ b/example/dao/contract/example_contract/foo/wallet.rs @@ -13,8 +13,8 @@ use darkfi::{ }; use crate::{ - demo::{FuncCall, ZkContractInfo, ZkContractTable}, - example_contract::{foo::validate::CallData, CONTRACT_ID}, + contract::example_contract::{foo::validate::CallData, CONTRACT_ID}, + util::{FuncCall, ZkContractInfo, ZkContractTable}, }; pub struct Foo { diff --git a/example/dao/contract/example_contract/mod.rs b/example/dao/contract/example_contract/mod.rs new file mode 100644 index 000000000..ff17f70fd --- /dev/null +++ b/example/dao/contract/example_contract/mod.rs @@ -0,0 +1,12 @@ +use lazy_static::lazy_static; +use pasta_curves::{group::ff::Field, pallas}; +use rand::rngs::OsRng; + +// foo() +pub mod foo; + +pub mod state; + +lazy_static! { + pub static ref CONTRACT_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} diff --git a/bin/daod/src/example_contract/state.rs b/example/dao/contract/example_contract/state.rs similarity index 100% rename from bin/daod/src/example_contract/state.rs rename to example/dao/contract/example_contract/state.rs diff --git a/example/dao/contract/mod.rs b/example/dao/contract/mod.rs new file mode 100644 index 000000000..94261f732 --- /dev/null +++ b/example/dao/contract/mod.rs @@ -0,0 +1,3 @@ +pub mod dao_contract; +pub mod example_contract; +pub mod money_contract; diff --git a/example/dao/contract/money_contract/mod.rs b/example/dao/contract/money_contract/mod.rs new file mode 100644 index 000000000..52c888f99 --- /dev/null +++ b/example/dao/contract/money_contract/mod.rs @@ -0,0 +1,13 @@ +use lazy_static::lazy_static; +use pasta_curves::{group::ff::Field, pallas}; +use rand::rngs::OsRng; + +// transfer() +pub mod transfer; + +pub mod state; +pub use state::State; + +lazy_static! { + pub static ref CONTRACT_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} diff --git a/bin/daod/src/money_contract/state.rs b/example/dao/contract/money_contract/state.rs similarity index 100% rename from bin/daod/src/money_contract/state.rs rename to example/dao/contract/money_contract/state.rs diff --git a/example/dao/contract/money_contract/transfer/mod.rs b/example/dao/contract/money_contract/transfer/mod.rs new file mode 100644 index 000000000..8134f0b30 --- /dev/null +++ b/example/dao/contract/money_contract/transfer/mod.rs @@ -0,0 +1,11 @@ +use lazy_static::lazy_static; +use pasta_curves::{group::ff::Field, pallas}; +use rand::rngs::OsRng; + +pub mod validate; +pub mod wallet; +pub use wallet::{Builder, BuilderClearInputInfo, BuilderInputInfo, BuilderOutputInfo, Note}; + +lazy_static! { + pub static ref FUNC_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} diff --git a/bin/daod/src/money_contract/transfer/validate.rs b/example/dao/contract/money_contract/transfer/validate.rs similarity index 98% rename from bin/daod/src/money_contract/transfer/validate.rs rename to example/dao/contract/money_contract/transfer/validate.rs index 92fb1e2c5..3ae481856 100644 --- a/bin/daod/src/money_contract/transfer/validate.rs +++ b/example/dao/contract/money_contract/transfer/validate.rs @@ -19,10 +19,12 @@ use darkfi::{ }; use crate::{ - dao_contract, - demo::{CallDataBase, StateRegistry, Transaction, UpdateBase}, - money_contract::{state::State, CONTRACT_ID}, + contract::{ + dao_contract, + money_contract::{state::State, CONTRACT_ID}, + }, note::EncryptedNote2, + util::{CallDataBase, StateRegistry, Transaction, UpdateBase}, }; const TARGET: &str = "money_contract::transfer::validate::state_transition()"; @@ -64,7 +66,7 @@ pub fn state_transition( states: &StateRegistry, func_call_index: usize, parent_tx: &Transaction, -) -> Result> { +) -> Result> { // Check the public keys in the clear inputs to see if they're coming // from a valid cashier or faucet. debug!(target: TARGET, "Iterate clear_inputs"); diff --git a/bin/daod/src/money_contract/transfer/wallet.rs b/example/dao/contract/money_contract/transfer/wallet.rs similarity index 98% rename from bin/daod/src/money_contract/transfer/wallet.rs rename to example/dao/contract/money_contract/transfer/wallet.rs index 592b7d520..765476aff 100644 --- a/bin/daod/src/money_contract/transfer/wallet.rs +++ b/example/dao/contract/money_contract/transfer/wallet.rs @@ -17,12 +17,12 @@ use darkfi::{ }; use crate::{ - demo::{FuncCall, ZkContractInfo, ZkContractTable}, - money_contract::{ + contract::money_contract::{ transfer::validate::{CallData, ClearInput, Input, Output}, CONTRACT_ID, }, note, + util::{FuncCall, ZkContractInfo, ZkContractTable}, }; #[derive(Clone, SerialEncodable, SerialDecodable)] diff --git a/example/dao/dao.rs b/example/dao/dao.rs new file mode 100644 index 000000000..3c6de49ed --- /dev/null +++ b/example/dao/dao.rs @@ -0,0 +1,1327 @@ +use incrementalmerkletree::Tree; +use log::debug; +use pasta_curves::{ + arithmetic::CurveAffine, + group::{ff::Field, Curve, Group}, + pallas, +}; +use rand::rngs::OsRng; +use std::{ + any::{Any, TypeId}, + time::Instant, +}; + +use darkfi::{ + crypto::{ + keypair::{Keypair, PublicKey, SecretKey}, + proof::{ProvingKey, VerifyingKey}, + types::{DrkSpendHook, DrkUserData, DrkValue}, + util::{pedersen_commitment_u64, poseidon_hash}, + }, + zk::circuit::{BurnContract, MintContract}, + zkas::decoder::ZkBinary, +}; + +mod contract; +mod note; +mod util; + +use crate::{ + contract::{dao_contract, example_contract, money_contract}, + util::{sign, StateRegistry, Transaction, ZkContractTable}, +}; + +// TODO: Anonymity leaks in this proof of concept: +// +// * Vote updates are linked to the proposal_bulla +// * Nullifier of vote will link vote with the coin when it's spent + +// TODO: strategize and cleanup Result/Error usage +// TODO: fix up code doc + +type Result = std::result::Result>; + +// #[derive(Eq, PartialEq)] +// pub struct HashableBase(pub pallas::Base); + +// impl std::hash::Hash for HashableBase { +// fn hash(&self, state: &mut H) { +// let bytes = self.0.to_repr(); +// bytes.hash(state); +// } +// } + +// pub struct ZkBinaryContractInfo { +// pub k_param: u32, +// pub bincode: ZkBinary, +// pub proving_key: ProvingKey, +// pub verifying_key: VerifyingKey, +// } +// pub struct ZkNativeContractInfo { +// pub proving_key: ProvingKey, +// pub verifying_key: VerifyingKey, +// } + +// pub enum ZkContractInfo { +// Binary(ZkBinaryContractInfo), +// Native(ZkNativeContractInfo), +// } + +// pub struct ZkContractTable { +// // Key will be a hash of zk binary contract on chain +// table: HashMap, +// } + +// impl ZkContractTable { +// fn new() -> Self { +// Self { table: HashMap::new() } +// } + +// fn add_contract(&mut self, key: String, bincode: ZkBinary, k_param: u32) { +// let witnesses = empty_witnesses(&bincode); +// let circuit = ZkCircuit::new(witnesses, bincode.clone()); +// let proving_key = ProvingKey::build(k_param, &circuit); +// let verifying_key = VerifyingKey::build(k_param, &circuit); +// let info = ZkContractInfo::Binary(ZkBinaryContractInfo { +// k_param, +// bincode, +// proving_key, +// verifying_key, +// }); +// self.table.insert(key, info); +// } + +// fn add_native(&mut self, key: String, proving_key: ProvingKey, verifying_key: VerifyingKey) { +// self.table.insert( +// key, +// ZkContractInfo::Native(ZkNativeContractInfo { proving_key, verifying_key }), +// ); +// } + +// pub fn lookup(&self, key: &String) -> Option<&ZkContractInfo> { +// self.table.get(key) +// } +// } + +// pub struct Transaction { +// pub func_calls: Vec, +// pub signatures: Vec, +// } + +// impl Transaction { +// /// Verify ZK contracts for the entire tx +// /// In real code, we could parallelize this for loop +// /// TODO: fix use of unwrap with Result type stuff +// fn zk_verify(&self, zk_bins: &ZkContractTable) { +// for func_call in &self.func_calls { +// let proofs_public_vals = &func_call.call_data.zk_public_values(); + +// assert_eq!( +// proofs_public_vals.len(), +// func_call.proofs.len(), +// "proof_public_vals.len()={} and func_call.proofs.len()={} do not match", +// proofs_public_vals.len(), +// func_call.proofs.len() +// ); +// for (i, (proof, (key, public_vals))) in +// func_call.proofs.iter().zip(proofs_public_vals.iter()).enumerate() +// { +// match zk_bins.lookup(key).unwrap() { +// ZkContractInfo::Binary(info) => { +// let verifying_key = &info.verifying_key; +// let verify_result = proof.verify(&verifying_key, public_vals); +// assert!(verify_result.is_ok(), "verify proof[{}]='{}' failed", i, key); +// } +// ZkContractInfo::Native(info) => { +// let verifying_key = &info.verifying_key; +// let verify_result = proof.verify(&verifying_key, public_vals); +// assert!(verify_result.is_ok(), "verify proof[{}]='{}' failed", i, key); +// } +// }; +// debug!(target: "demo", "zk_verify({}) passed [i={}]", key, i); +// } +// } +// } + +// fn verify_sigs(&self) { +// let mut unsigned_tx_data = vec![]; +// for (i, (func_call, signature)) in +// self.func_calls.iter().zip(self.signatures.clone()).enumerate() +// { +// func_call.encode(&mut unsigned_tx_data).expect("failed to encode data"); +// let signature_pub_keys = func_call.call_data.signature_public_keys(); +// for signature_pub_key in signature_pub_keys { +// let verify_result = signature_pub_key.verify(&unsigned_tx_data[..], &signature); +// assert!(verify_result, "verify sigs[{}] failed", i); +// } +// debug!(target: "demo", "verify_sigs({}) passed", i); +// } +// } +// } + +// fn sign(signature_secrets: Vec, func_calls: &Vec) -> Vec { +// let mut signatures = vec![]; +// let mut unsigned_tx_data = vec![]; +// for (_i, (signature_secret, func_call)) in +// signature_secrets.iter().zip(func_calls.iter()).enumerate() +// { +// func_call.encode(&mut unsigned_tx_data).expect("failed to encode data"); +// let signature = signature_secret.sign(&unsigned_tx_data[..]); +// signatures.push(signature); +// } +// signatures +// } + +// type ContractId = pallas::Base; +// type FuncId = pallas::Base; + +// pub struct FuncCall { +// pub contract_id: ContractId, +// pub func_id: FuncId, +// pub call_data: Box, +// pub proofs: Vec, +// } + +// impl Encodable for FuncCall { +// fn encode(&self, mut w: W) -> std::result::Result { +// let mut len = 0; +// len += self.contract_id.encode(&mut w)?; +// len += self.func_id.encode(&mut w)?; +// len += self.proofs.encode(&mut w)?; +// len += self.call_data.encode_bytes(&mut w)?; +// Ok(len) +// } +// } + +// pub trait CallDataBase { +// // Public values for verifying the proofs +// // Needed so we can convert internal types so they can be used in Proof::verify() +// fn zk_public_values(&self) -> Vec<(String, Vec)>; + +// // For upcasting to CallData itself so it can be read in state_transition() +// fn as_any(&self) -> &dyn Any; + +// // Public keys we will use to verify transaction signatures. +// fn signature_public_keys(&self) -> Vec; + +// fn encode_bytes( +// &self, +// writer: &mut dyn std::io::Write, +// ) -> std::result::Result; +// } + +// type GenericContractState = Box; + +// pub struct StateRegistry { +// pub states: HashMap, +// } + +// impl StateRegistry { +// fn new() -> Self { +// Self { states: HashMap::new() } +// } + +// fn register(&mut self, contract_id: ContractId, state: GenericContractState) { +// debug!(target: "StateRegistry::register()", "contract_id: {:?}", contract_id); +// self.states.insert(HashableBase(contract_id), state); +// } + +// pub fn lookup_mut<'a, S: 'static>(&'a mut self, contract_id: ContractId) -> Option<&'a mut S> { +// self.states.get_mut(&HashableBase(contract_id)).and_then(|state| state.downcast_mut()) +// } + +// pub fn lookup<'a, S: 'static>(&'a self, contract_id: ContractId) -> Option<&'a S> { +// self.states.get(&HashableBase(contract_id)).and_then(|state| state.downcast_ref()) +// } +// } + +// pub trait UpdateBase { +// fn apply(self: Box, states: &mut StateRegistry); +// } + +/////////////////////////////////////////////////// +///// Example contract +/////////////////////////////////////////////////// +pub async fn example() -> Result<()> { + debug!(target: "demo", "Stage 0. Example contract"); + // Lookup table for smart contract states + let mut states = StateRegistry::new(); + + // Initialize ZK binary table + let mut zk_bins = ZkContractTable::new(); + + let zk_example_foo_bincode = include_bytes!("proof/foo.zk.bin"); + let zk_example_foo_bin = ZkBinary::decode(zk_example_foo_bincode)?; + zk_bins.add_contract("example-foo".to_string(), zk_example_foo_bin, 13); + + let example_state = example_contract::state::State::new(); + states.register(*example_contract::CONTRACT_ID, example_state); + + //// Wallet + + let foo = example_contract::foo::wallet::Foo { a: 5, b: 10 }; + let signature_secret = SecretKey::random(&mut OsRng); + + let builder = example_contract::foo::wallet::Builder { foo, signature_secret }; + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign(vec![signature_secret], &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *example_contract::foo::FUNC_ID { + debug!("example_contract::foo::state_transition()"); + + let update = example_contract::foo::validate::state_transition(&states, idx, &tx) + .expect("example_contract::foo::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + Ok(()) +} + +#[async_std::main] +async fn main() -> Result<()> { + // Example smart contract + //// TODO: this will be moved to a different file + example().await?; + + // Money parameters + let xdrk_supply = 1_000_000; + let xdrk_token_id = pallas::Base::random(&mut OsRng); + + // Governance token parameters + let gdrk_supply = 1_000_000; + let gdrk_token_id = pallas::Base::random(&mut OsRng); + + // DAO parameters + let dao_proposer_limit = 110; + let dao_quorum = 110; + let dao_approval_ratio_quot = 1; + let dao_approval_ratio_base = 2; + + // Lookup table for smart contract states + let mut states = StateRegistry::new(); + + // Initialize ZK binary table + let mut zk_bins = ZkContractTable::new(); + + debug!(target: "demo", "Loading dao-mint.zk"); + let zk_dao_mint_bincode = include_bytes!("proof/dao-mint.zk.bin"); + let zk_dao_mint_bin = ZkBinary::decode(zk_dao_mint_bincode)?; + zk_bins.add_contract("dao-mint".to_string(), zk_dao_mint_bin, 13); + + debug!(target: "demo", "Loading money-transfer contracts"); + { + let start = Instant::now(); + let mint_pk = ProvingKey::build(11, &MintContract::default()); + debug!("Mint PK: [{:?}]", start.elapsed()); + let start = Instant::now(); + let burn_pk = ProvingKey::build(11, &BurnContract::default()); + debug!("Burn PK: [{:?}]", start.elapsed()); + let start = Instant::now(); + let mint_vk = VerifyingKey::build(11, &MintContract::default()); + debug!("Mint VK: [{:?}]", start.elapsed()); + let start = Instant::now(); + let burn_vk = VerifyingKey::build(11, &BurnContract::default()); + debug!("Burn VK: [{:?}]", start.elapsed()); + + zk_bins.add_native("money-transfer-mint".to_string(), mint_pk, mint_vk); + zk_bins.add_native("money-transfer-burn".to_string(), burn_pk, burn_vk); + } + debug!(target: "demo", "Loading dao-propose-main.zk"); + let zk_dao_propose_main_bincode = include_bytes!("proof/dao-propose-main.zk.bin"); + let zk_dao_propose_main_bin = ZkBinary::decode(zk_dao_propose_main_bincode)?; + zk_bins.add_contract("dao-propose-main".to_string(), zk_dao_propose_main_bin, 13); + debug!(target: "demo", "Loading dao-propose-burn.zk"); + let zk_dao_propose_burn_bincode = include_bytes!("proof/dao-propose-burn.zk.bin"); + let zk_dao_propose_burn_bin = ZkBinary::decode(zk_dao_propose_burn_bincode)?; + zk_bins.add_contract("dao-propose-burn".to_string(), zk_dao_propose_burn_bin, 13); + debug!(target: "demo", "Loading dao-vote-main.zk"); + let zk_dao_vote_main_bincode = include_bytes!("proof/dao-vote-main.zk.bin"); + let zk_dao_vote_main_bin = ZkBinary::decode(zk_dao_vote_main_bincode)?; + zk_bins.add_contract("dao-vote-main".to_string(), zk_dao_vote_main_bin, 13); + debug!(target: "demo", "Loading dao-vote-burn.zk"); + let zk_dao_vote_burn_bincode = include_bytes!("proof/dao-vote-burn.zk.bin"); + let zk_dao_vote_burn_bin = ZkBinary::decode(zk_dao_vote_burn_bincode)?; + zk_bins.add_contract("dao-vote-burn".to_string(), zk_dao_vote_burn_bin, 13); + let zk_dao_exec_bincode = include_bytes!("proof/dao-exec.zk.bin"); + let zk_dao_exec_bin = ZkBinary::decode(zk_dao_exec_bincode)?; + zk_bins.add_contract("dao-exec".to_string(), zk_dao_exec_bin, 13); + + // State for money contracts + let cashier_signature_secret = SecretKey::random(&mut OsRng); + let cashier_signature_public = PublicKey::from_secret(cashier_signature_secret); + let faucet_signature_secret = SecretKey::random(&mut OsRng); + let faucet_signature_public = PublicKey::from_secret(faucet_signature_secret); + + /////////////////////////////////////////////////// + + let money_state = + money_contract::state::State::new(cashier_signature_public, faucet_signature_public); + states.register(*money_contract::CONTRACT_ID, money_state); + + ///////////////////////////////////////////////////// + + let dao_state = dao_contract::State::new(); + states.register(*dao_contract::CONTRACT_ID, dao_state); + + ///////////////////////////////////////////////////// + ////// Create the DAO bulla + ///////////////////////////////////////////////////// + debug!(target: "demo", "Stage 1. Creating DAO bulla"); + + //// Wallet + + //// Setup the DAO + let dao_keypair = Keypair::random(&mut OsRng); + let dao_bulla_blind = pallas::Base::random(&mut OsRng); + + let signature_secret = SecretKey::random(&mut OsRng); + // Create DAO mint tx + let builder = dao_contract::mint::wallet::Builder { + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + gov_token_id: gdrk_token_id, + dao_pubkey: dao_keypair.public, + dao_bulla_blind, + _signature_secret: signature_secret, + }; + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign(vec![signature_secret], &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + // So then the verifier will lookup the corresponding state_transition and apply + // functions based off the func_id + if func_call.func_id == *dao_contract::mint::FUNC_ID { + debug!("dao_contract::mint::state_transition()"); + + let update = dao_contract::mint::validate::state_transition(&states, idx, &tx) + .expect("dao_contract::mint::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + // Wallet stuff + + // In your wallet, wait until you see the tx confirmed before doing anything below + // So for example keep track of tx hash + //assert_eq!(tx.hash(), tx_hash); + + // We need to witness() the value in our local merkle tree + // Must be called as soon as this DAO bulla is added to the state + let dao_leaf_position = { + let state = states.lookup_mut::(*dao_contract::CONTRACT_ID).unwrap(); + state.dao_tree.witness().unwrap() + }; + + // It might just be easier to hash it ourselves from keypair and blind... + let dao_bulla = { + assert_eq!(tx.func_calls.len(), 1); + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::().unwrap(); + call_data.dao_bulla.clone() + }; + debug!(target: "demo", "Create DAO bulla: {:?}", dao_bulla.0); + + /////////////////////////////////////////////////// + //// Mint the initial supply of treasury token + //// and send it all to the DAO directly + /////////////////////////////////////////////////// + debug!(target: "demo", "Stage 2. Minting treasury token"); + + let state = states.lookup_mut::(*money_contract::CONTRACT_ID).unwrap(); + state.wallet_cache.track(dao_keypair.secret); + + //// Wallet + + // Address of deployed contract in our example is dao_contract::exec::FUNC_ID + // This field is public, you can see it's being sent to a DAO + // but nothing else is visible. + // + // In the python code we wrote: + // + // spend_hook = b"0xdao_ruleset" + // + let spend_hook = *dao_contract::exec::FUNC_ID; + // The user_data can be a simple hash of the items passed into the ZK proof + // up to corresponding linked ZK proof to interpret however they need. + // In out case, it's the bulla for the DAO + let user_data = dao_bulla.0; + + let builder = money_contract::transfer::wallet::Builder { + clear_inputs: vec![money_contract::transfer::wallet::BuilderClearInputInfo { + value: xdrk_supply, + token_id: xdrk_token_id, + signature_secret: cashier_signature_secret, + }], + inputs: vec![], + outputs: vec![money_contract::transfer::wallet::BuilderOutputInfo { + value: xdrk_supply, + token_id: xdrk_token_id, + public: dao_keypair.public, + serial: pallas::Base::random(&mut OsRng), + coin_blind: pallas::Base::random(&mut OsRng), + spend_hook, + user_data, + }], + }; + + let func_call = builder.build(&zk_bins)?; + let func_calls = vec![func_call]; + + let signatures = sign(vec![cashier_signature_secret], &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + // So then the verifier will lookup the corresponding state_transition and apply + // functions based off the func_id + if func_call.func_id == *money_contract::transfer::FUNC_ID { + debug!("money_contract::transfer::state_transition()"); + + let update = money_contract::transfer::validate::state_transition(&states, idx, &tx) + .expect("money_contract::transfer::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + // DAO reads the money received from the encrypted note + + let state = states.lookup_mut::(*money_contract::CONTRACT_ID).unwrap(); + let mut recv_coins = state.wallet_cache.get_received(&dao_keypair.secret); + assert_eq!(recv_coins.len(), 1); + let dao_recv_coin = recv_coins.pop().unwrap(); + let treasury_note = dao_recv_coin.note; + + // Check the actual coin received is valid before accepting it + + let coords = dao_keypair.public.0.to_affine().coordinates().unwrap(); + let coin = poseidon_hash::<8>([ + *coords.x(), + *coords.y(), + DrkValue::from(treasury_note.value), + treasury_note.token_id, + treasury_note.serial, + treasury_note.spend_hook, + treasury_note.user_data, + treasury_note.coin_blind, + ]); + assert_eq!(coin, dao_recv_coin.coin.0); + + assert_eq!(treasury_note.spend_hook, *dao_contract::exec::FUNC_ID); + assert_eq!(treasury_note.user_data, dao_bulla.0); + + debug!("DAO received a coin worth {} xDRK", treasury_note.value); + + /////////////////////////////////////////////////// + //// Mint the governance token + //// Send it to three hodlers + /////////////////////////////////////////////////// + debug!(target: "demo", "Stage 3. Minting governance token"); + + //// Wallet + + // Hodler 1 + let gov_keypair_1 = Keypair::random(&mut OsRng); + // Hodler 2 + let gov_keypair_2 = Keypair::random(&mut OsRng); + // Hodler 3: the tiebreaker + let gov_keypair_3 = Keypair::random(&mut OsRng); + + let state = states.lookup_mut::(*money_contract::CONTRACT_ID).unwrap(); + state.wallet_cache.track(gov_keypair_1.secret); + state.wallet_cache.track(gov_keypair_2.secret); + state.wallet_cache.track(gov_keypair_3.secret); + + let gov_keypairs = vec![gov_keypair_1, gov_keypair_2, gov_keypair_3]; + + // Spend hook and user data disabled + let spend_hook = DrkSpendHook::from(0); + let user_data = DrkUserData::from(0); + + let output1 = money_contract::transfer::wallet::BuilderOutputInfo { + value: 400000, + token_id: gdrk_token_id, + public: gov_keypair_1.public, + serial: pallas::Base::random(&mut OsRng), + coin_blind: pallas::Base::random(&mut OsRng), + spend_hook, + user_data, + }; + + let output2 = money_contract::transfer::wallet::BuilderOutputInfo { + value: 400000, + token_id: gdrk_token_id, + public: gov_keypair_2.public, + serial: pallas::Base::random(&mut OsRng), + coin_blind: pallas::Base::random(&mut OsRng), + spend_hook, + user_data, + }; + + let output3 = money_contract::transfer::wallet::BuilderOutputInfo { + value: 200000, + token_id: gdrk_token_id, + public: gov_keypair_3.public, + serial: pallas::Base::random(&mut OsRng), + coin_blind: pallas::Base::random(&mut OsRng), + spend_hook, + user_data, + }; + + assert!(2 * 400000 + 200000 == gdrk_supply); + + let builder = money_contract::transfer::wallet::Builder { + clear_inputs: vec![money_contract::transfer::wallet::BuilderClearInputInfo { + value: gdrk_supply, + token_id: gdrk_token_id, + signature_secret: cashier_signature_secret, + }], + inputs: vec![], + outputs: vec![output1, output2, output3], + }; + + let func_call = builder.build(&zk_bins)?; + let func_calls = vec![func_call]; + + let signatures = sign(vec![cashier_signature_secret], &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + // So then the verifier will lookup the corresponding state_transition and apply + // functions based off the func_id + if func_call.func_id == *money_contract::transfer::FUNC_ID { + debug!("money_contract::transfer::state_transition()"); + + let update = money_contract::transfer::validate::state_transition(&states, idx, &tx) + .expect("money_contract::transfer::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + let mut gov_recv = vec![None, None, None]; + // Check that each person received one coin + for (i, key) in gov_keypairs.iter().enumerate() { + let gov_recv_coin = { + let state = + states.lookup_mut::(*money_contract::CONTRACT_ID).unwrap(); + let mut recv_coins = state.wallet_cache.get_received(&key.secret); + assert_eq!(recv_coins.len(), 1); + let recv_coin = recv_coins.pop().unwrap(); + let note = &recv_coin.note; + + assert_eq!(note.token_id, gdrk_token_id); + // Normal payment + assert_eq!(note.spend_hook, pallas::Base::from(0)); + assert_eq!(note.user_data, pallas::Base::from(0)); + + let coords = key.public.0.to_affine().coordinates().unwrap(); + let coin = poseidon_hash::<8>([ + *coords.x(), + *coords.y(), + DrkValue::from(note.value), + note.token_id, + note.serial, + note.spend_hook, + note.user_data, + note.coin_blind, + ]); + assert_eq!(coin, recv_coin.coin.0); + + debug!("Holder{} received a coin worth {} gDRK", i, note.value); + + recv_coin + }; + gov_recv[i] = Some(gov_recv_coin); + } + // unwrap them for this demo + let gov_recv: Vec<_> = gov_recv.into_iter().map(|r| r.unwrap()).collect(); + + /////////////////////////////////////////////////// + // DAO rules: + // 1. gov token IDs must match on all inputs + // 2. proposals must be submitted by minimum amount + // 3. all votes >= quorum + // 4. outcome > approval_ratio + // 5. structure of outputs + // output 0: value and address + // output 1: change address + /////////////////////////////////////////////////// + + /////////////////////////////////////////////////// + // Propose the vote + // In order to make a valid vote, first the proposer must + // meet a criteria for a minimum number of gov tokens + /////////////////////////////////////////////////// + debug!(target: "demo", "Stage 4. Propose the vote"); + + //// Wallet + + // TODO: look into proposal expiry once time for voting has finished + + let user_keypair = Keypair::random(&mut OsRng); + + let (money_leaf_position, money_merkle_path) = { + let state = states.lookup::(*money_contract::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = gov_recv[0].leaf_position.clone(); + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + // TODO: is it possible for an invalid transfer() to be constructed on exec()? + // need to look into this + let signature_secret = SecretKey::random(&mut OsRng); + let input = dao_contract::propose::wallet::BuilderInput { + secret: gov_keypair_1.secret, + note: gov_recv[0].note.clone(), + leaf_position: money_leaf_position, + merkle_path: money_merkle_path, + signature_secret, + }; + + let (dao_merkle_path, dao_merkle_root) = { + let state = states.lookup::(*dao_contract::CONTRACT_ID).unwrap(); + let tree = &state.dao_tree; + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(dao_leaf_position, &root).unwrap(); + (merkle_path, root) + }; + + let dao_params = dao_contract::mint::wallet::DaoParams { + proposer_limit: dao_proposer_limit, + quorum: dao_quorum, + approval_ratio_base: dao_approval_ratio_base, + approval_ratio_quot: dao_approval_ratio_quot, + gov_token_id: gdrk_token_id, + public_key: dao_keypair.public, + bulla_blind: dao_bulla_blind, + }; + + let proposal = dao_contract::propose::wallet::Proposal { + dest: user_keypair.public, + amount: 1000, + serial: pallas::Base::random(&mut OsRng), + token_id: xdrk_token_id, + blind: pallas::Base::random(&mut OsRng), + }; + + let builder = dao_contract::propose::wallet::Builder { + inputs: vec![input], + proposal: proposal.clone(), + dao: dao_params.clone(), + dao_leaf_position, + dao_merkle_path, + dao_merkle_root, + }; + + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign(vec![signature_secret], &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *dao_contract::propose::FUNC_ID { + debug!(target: "demo", "dao_contract::propose::state_transition()"); + + let update = dao_contract::propose::validate::state_transition(&states, idx, &tx) + .expect("dao_contract::propose::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + // Read received proposal + let (proposal, proposal_bulla) = { + assert_eq!(tx.func_calls.len(), 1); + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + assert_eq!( + (&*call_data).type_id(), + TypeId::of::() + ); + let call_data = + call_data.downcast_ref::().unwrap(); + + let header = &call_data.header; + let note: dao_contract::propose::wallet::Note = + header.enc_note.decrypt(&dao_keypair.secret).unwrap(); + + // TODO: check it belongs to DAO bulla + + // Return the proposal info + (note.proposal, call_data.header.proposal_bulla) + }; + debug!(target: "demo", "Proposal now active!"); + debug!(target: "demo", " destination: {:?}", proposal.dest); + debug!(target: "demo", " amount: {}", proposal.amount); + debug!(target: "demo", " token_id: {:?}", proposal.token_id); + debug!(target: "demo", " dao_bulla: {:?}", dao_bulla.0); + debug!(target: "demo", "Proposal bulla: {:?}", proposal_bulla); + + /////////////////////////////////////////////////// + // Proposal is accepted! + // Start the voting + /////////////////////////////////////////////////// + + // Copying these schizo comments from python code: + // Lets the voting begin + // Voters have access to the proposal and dao data + // vote_state = VoteState() + // We don't need to copy nullifier set because it is checked from gov_state + // in vote_state_transition() anyway + // + // TODO: what happens if voters don't unblind their vote + // Answer: + // 1. there is a time limit + // 2. both the MPC or users can unblind + // + // TODO: bug if I vote then send money, then we can double vote + // TODO: all timestamps missing + // - timelock (future voting starts in 2 days) + // Fix: use nullifiers from money gov state only from + // beginning of gov period + // Cannot use nullifiers from before voting period + + debug!(target: "demo", "Stage 5. Start voting"); + + // We were previously saving updates here for testing + // let mut updates = vec![]; + + // User 1: YES + + let (money_leaf_position, money_merkle_path) = { + let state = states.lookup::(*money_contract::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = gov_recv[0].leaf_position.clone(); + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + let signature_secret = SecretKey::random(&mut OsRng); + let input = dao_contract::vote::wallet::BuilderInput { + secret: gov_keypair_1.secret, + note: gov_recv[0].note.clone(), + leaf_position: money_leaf_position, + merkle_path: money_merkle_path, + signature_secret, + }; + + let vote_option: bool = true; + + assert!(vote_option == true || vote_option == false); + + // We create a new keypair to encrypt the vote. + // For the demo MVP, you can just use the dao_keypair secret + let vote_keypair_1 = Keypair::random(&mut OsRng); + + let builder = dao_contract::vote::wallet::Builder { + inputs: vec![input], + vote: dao_contract::vote::wallet::Vote { + vote_option, + vote_option_blind: pallas::Scalar::random(&mut OsRng), + }, + vote_keypair: vote_keypair_1, + proposal: proposal.clone(), + dao: dao_params.clone(), + }; + debug!(target: "demo", "build()..."); + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign(vec![signature_secret], &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *dao_contract::vote::FUNC_ID { + debug!(target: "demo", "dao_contract::vote::state_transition()"); + + let update = dao_contract::vote::validate::state_transition(&states, idx, &tx) + .expect("dao_contract::vote::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + // Secret vote info. Needs to be revealed at some point. + // TODO: look into verifiable encryption for notes + // TODO: look into timelock puzzle as a possibility + let vote_note_1 = { + assert_eq!(tx.func_calls.len(), 1); + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::().unwrap(); + + let header = &call_data.header; + let note: dao_contract::vote::wallet::Note = + header.enc_note.decrypt(&vote_keypair_1.secret).unwrap(); + note + }; + debug!(target: "demo", "User 1 voted!"); + debug!(target: "demo", " vote_option: {}", vote_note_1.vote.vote_option); + debug!(target: "demo", " value: {}", vote_note_1.vote_value); + + // User 2: NO + + let (money_leaf_position, money_merkle_path) = { + let state = states.lookup::(*money_contract::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = gov_recv[1].leaf_position.clone(); + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + let signature_secret = SecretKey::random(&mut OsRng); + let input = dao_contract::vote::wallet::BuilderInput { + secret: gov_keypair_2.secret, + note: gov_recv[1].note.clone(), + leaf_position: money_leaf_position, + merkle_path: money_merkle_path, + signature_secret, + }; + + let vote_option: bool = false; + + assert!(vote_option == true || vote_option == false); + + // We create a new keypair to encrypt the vote. + let vote_keypair_2 = Keypair::random(&mut OsRng); + + let builder = dao_contract::vote::wallet::Builder { + inputs: vec![input], + vote: dao_contract::vote::wallet::Vote { + vote_option, + vote_option_blind: pallas::Scalar::random(&mut OsRng), + }, + vote_keypair: vote_keypair_2, + proposal: proposal.clone(), + dao: dao_params.clone(), + }; + debug!(target: "demo", "build()..."); + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign(vec![signature_secret], &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *dao_contract::vote::FUNC_ID { + debug!(target: "demo", "dao_contract::vote::state_transition()"); + + let update = dao_contract::vote::validate::state_transition(&states, idx, &tx) + .expect("dao_contract::vote::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + // Secret vote info. Needs to be revealed at some point. + // TODO: look into verifiable encryption for notes + // TODO: look into timelock puzzle as a possibility + let vote_note_2 = { + assert_eq!(tx.func_calls.len(), 1); + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::().unwrap(); + + let header = &call_data.header; + let note: dao_contract::vote::wallet::Note = + header.enc_note.decrypt(&vote_keypair_2.secret).unwrap(); + note + }; + debug!(target: "demo", "User 2 voted!"); + debug!(target: "demo", " vote_option: {}", vote_note_2.vote.vote_option); + debug!(target: "demo", " value: {}", vote_note_2.vote_value); + + // User 3: YES + + let (money_leaf_position, money_merkle_path) = { + let state = states.lookup::(*money_contract::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = gov_recv[2].leaf_position.clone(); + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + let signature_secret = SecretKey::random(&mut OsRng); + let input = dao_contract::vote::wallet::BuilderInput { + secret: gov_keypair_3.secret, + note: gov_recv[2].note.clone(), + leaf_position: money_leaf_position, + merkle_path: money_merkle_path, + signature_secret, + }; + + let vote_option: bool = true; + + assert!(vote_option == true || vote_option == false); + + // We create a new keypair to encrypt the vote. + let vote_keypair_3 = Keypair::random(&mut OsRng); + + let builder = dao_contract::vote::wallet::Builder { + inputs: vec![input], + vote: dao_contract::vote::wallet::Vote { + vote_option, + vote_option_blind: pallas::Scalar::random(&mut OsRng), + }, + vote_keypair: vote_keypair_3, + proposal: proposal.clone(), + dao: dao_params.clone(), + }; + debug!(target: "demo", "build()..."); + let func_call = builder.build(&zk_bins); + let func_calls = vec![func_call]; + + let signatures = sign(vec![signature_secret], &func_calls); + let tx = Transaction { func_calls, signatures }; + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *dao_contract::vote::FUNC_ID { + debug!(target: "demo", "dao_contract::vote::state_transition()"); + + let update = dao_contract::vote::validate::state_transition(&states, idx, &tx) + .expect("dao_contract::vote::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + // Secret vote info. Needs to be revealed at some point. + // TODO: look into verifiable encryption for notes + // TODO: look into timelock puzzle as a possibility + let vote_note_3 = { + assert_eq!(tx.func_calls.len(), 1); + let func_call = &tx.func_calls[0]; + let call_data = func_call.call_data.as_any(); + assert_eq!((&*call_data).type_id(), TypeId::of::()); + let call_data = call_data.downcast_ref::().unwrap(); + + let header = &call_data.header; + let note: dao_contract::vote::wallet::Note = + header.enc_note.decrypt(&vote_keypair_3.secret).unwrap(); + note + }; + debug!(target: "demo", "User 3 voted!"); + debug!(target: "demo", " vote_option: {}", vote_note_3.vote.vote_option); + debug!(target: "demo", " value: {}", vote_note_3.vote_value); + + // Every votes produces a semi-homomorphic encryption of their vote. + // Which is either yes or no + // We copy the state tree for the governance token so coins can be used + // to vote on other proposals at the same time. + // With their vote, they produce a ZK proof + nullifier + // The votes are unblinded by MPC to a selected party at the end of the + // voting period. + // (that's if we want votes to be hidden during voting) + + let mut yes_votes_value = 0; + let mut yes_votes_blind = pallas::Scalar::from(0); + let mut yes_votes_commit = pallas::Point::identity(); + + let mut all_votes_value = 0; + let mut all_votes_blind = pallas::Scalar::from(0); + let mut all_votes_commit = pallas::Point::identity(); + + // We were previously saving votes to a Vec for testing. + // However since Update is now UpdateBase it gets moved into update.apply(). + // So we need to think of another way to run these tests. + //assert!(updates.len() == 3); + + for (i, note /* update*/) in [vote_note_1, vote_note_2, vote_note_3] + .iter() /*.zip(updates)*/ + .enumerate() + { + let vote_commit = pedersen_commitment_u64(note.vote_value, note.vote_value_blind); + //assert!(update.value_commit == all_vote_value_commit); + all_votes_commit += vote_commit; + all_votes_blind += note.vote_value_blind; + + let yes_vote_commit = pedersen_commitment_u64( + note.vote.vote_option as u64 * note.vote_value, + note.vote.vote_option_blind, + ); + //assert!(update.yes_vote_commit == yes_vote_commit); + + yes_votes_commit += yes_vote_commit; + yes_votes_blind += note.vote.vote_option_blind; + + let vote_option = note.vote.vote_option; + + if vote_option { + yes_votes_value += note.vote_value; + } + all_votes_value += note.vote_value; + let vote_result: String = if vote_option { "yes".to_string() } else { "no".to_string() }; + + debug!("Voter {} voted {}", i, vote_result); + } + + debug!("Outcome = {} / {}", yes_votes_value, all_votes_value); + + assert!(all_votes_commit == pedersen_commitment_u64(all_votes_value, all_votes_blind)); + assert!(yes_votes_commit == pedersen_commitment_u64(yes_votes_value, yes_votes_blind)); + + /////////////////////////////////////////////////// + // Execute the vote + /////////////////////////////////////////////////// + + //// Wallet + + // Used to export user_data from this coin so it can be accessed by DAO::exec() + let user_data_blind = pallas::Base::random(&mut OsRng); + + let user_serial = pallas::Base::random(&mut OsRng); + let user_coin_blind = pallas::Base::random(&mut OsRng); + let dao_serial = pallas::Base::random(&mut OsRng); + let dao_coin_blind = pallas::Base::random(&mut OsRng); + let input_value = treasury_note.value; + let input_value_blind = pallas::Scalar::random(&mut OsRng); + let tx_signature_secret = SecretKey::random(&mut OsRng); + let exec_signature_secret = SecretKey::random(&mut OsRng); + + let (treasury_leaf_position, treasury_merkle_path) = { + let state = states.lookup::(*money_contract::CONTRACT_ID).unwrap(); + let tree = &state.tree; + let leaf_position = dao_recv_coin.leaf_position.clone(); + let root = tree.root(0).unwrap(); + let merkle_path = tree.authentication_path(leaf_position, &root).unwrap(); + (leaf_position, merkle_path) + }; + + let input = money_contract::transfer::wallet::BuilderInputInfo { + leaf_position: treasury_leaf_position, + merkle_path: treasury_merkle_path, + secret: dao_keypair.secret, + note: treasury_note, + user_data_blind, + value_blind: input_value_blind, + signature_secret: tx_signature_secret, + }; + + let builder = money_contract::transfer::wallet::Builder { + clear_inputs: vec![], + inputs: vec![input], + outputs: vec![ + // Sending money + money_contract::transfer::wallet::BuilderOutputInfo { + value: 1000, + token_id: xdrk_token_id, + public: user_keypair.public, + serial: proposal.serial, + coin_blind: proposal.blind, + spend_hook: pallas::Base::from(0), + user_data: pallas::Base::from(0), + }, + // Change back to DAO + money_contract::transfer::wallet::BuilderOutputInfo { + value: xdrk_supply - 1000, + token_id: xdrk_token_id, + public: dao_keypair.public, + serial: dao_serial, + coin_blind: dao_coin_blind, + spend_hook: *dao_contract::exec::FUNC_ID, + user_data: proposal_bulla, + }, + ], + }; + + let transfer_func_call = builder.build(&zk_bins)?; + + let builder = dao_contract::exec::wallet::Builder { + proposal, + dao: dao_params, + yes_votes_value, + all_votes_value, + yes_votes_blind, + all_votes_blind, + user_serial, + user_coin_blind, + dao_serial, + dao_coin_blind, + input_value, + input_value_blind, + hook_dao_exec: *dao_contract::exec::FUNC_ID, + signature_secret: exec_signature_secret, + }; + let exec_func_call = builder.build(&zk_bins); + let func_calls = vec![transfer_func_call, exec_func_call]; + + let signatures = sign(vec![tx_signature_secret, exec_signature_secret], &func_calls); + let tx = Transaction { func_calls, signatures }; + + { + // Now the spend_hook field specifies the function DAO::exec() + // so Money::transfer() must also be combined with DAO::exec() + + assert_eq!(tx.func_calls.len(), 2); + let transfer_func_call = &tx.func_calls[0]; + let transfer_call_data = transfer_func_call.call_data.as_any(); + + assert_eq!( + (&*transfer_call_data).type_id(), + TypeId::of::() + ); + let transfer_call_data = + transfer_call_data.downcast_ref::(); + let transfer_call_data = transfer_call_data.unwrap(); + // At least one input has this field value which means DAO::exec() is invoked. + assert_eq!(transfer_call_data.inputs.len(), 1); + let input = &transfer_call_data.inputs[0]; + assert_eq!(input.revealed.spend_hook, *dao_contract::exec::FUNC_ID); + let user_data_enc = poseidon_hash::<2>([dao_bulla.0, user_data_blind]); + assert_eq!(input.revealed.user_data_enc, user_data_enc); + } + + //// Validator + + let mut updates = vec![]; + // Validate all function calls in the tx + for (idx, func_call) in tx.func_calls.iter().enumerate() { + if func_call.func_id == *dao_contract::exec::FUNC_ID { + debug!("dao_contract::exec::state_transition()"); + + let update = dao_contract::exec::validate::state_transition(&states, idx, &tx) + .expect("dao_contract::exec::validate::state_transition() failed!"); + updates.push(update); + } else if func_call.func_id == *money_contract::transfer::FUNC_ID { + debug!("money_contract::transfer::state_transition()"); + + let update = money_contract::transfer::validate::state_transition(&states, idx, &tx) + .expect("money_contract::transfer::validate::state_transition() failed!"); + updates.push(update); + } + } + + // Atomically apply all changes + for update in updates { + update.apply(&mut states); + } + + // Other stuff + tx.zk_verify(&zk_bins); + tx.verify_sigs(); + + //// Wallet + + Ok(()) +} diff --git a/bin/daod/src/note.rs b/example/dao/note.rs similarity index 100% rename from bin/daod/src/note.rs rename to example/dao/note.rs diff --git a/bin/daod/proof/dao-exec.zk b/example/dao/proof/dao-exec.zk similarity index 100% rename from bin/daod/proof/dao-exec.zk rename to example/dao/proof/dao-exec.zk diff --git a/example/dao/proof/dao-mint.zk b/example/dao/proof/dao-mint.zk new file mode 100644 index 000000000..f08955394 --- /dev/null +++ b/example/dao/proof/dao-mint.zk @@ -0,0 +1,32 @@ +constant "DaoMint" { +} + +contract "DaoMint" { + Base dao_proposer_limit, + Base dao_quorum, + Base dao_approval_ratio_quot, + Base dao_approval_ratio_base, + Base gdrk_token_id, + Base dao_public_x, + Base dao_public_y, + Base dao_bulla_blind, +} + +circuit "DaoMint" { + # This circuit is not that interesting. + # It just states the bulla is a hash of 8 values. + + # BullaMint subroutine + bulla = poseidon_hash( + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + gdrk_token_id, + dao_public_x, + dao_public_y, + dao_bulla_blind, + ); + constrain_instance(bulla); +} + diff --git a/example/dao/proof/dao-propose-burn.zk b/example/dao/proof/dao-propose-burn.zk new file mode 100644 index 000000000..41502a212 --- /dev/null +++ b/example/dao/proof/dao-propose-burn.zk @@ -0,0 +1,63 @@ +constant "DaoProposeInput" { + EcFixedPointShort VALUE_COMMIT_VALUE, + EcFixedPoint VALUE_COMMIT_RANDOM, + EcFixedPointBase NULLIFIER_K, +} + +contract "DaoProposeInput" { + Base secret, + Base serial, + Base spend_hook, + Base user_data, + Base value, + Base token, + Base coin_blind, + Scalar value_blind, + Base token_blind, + Uint32 leaf_pos, + MerklePath path, + Base signature_secret, +} + +circuit "DaoProposeInput" { + # Poseidon hash of the nullifier + #nullifier = poseidon_hash(secret, serial); + #constrain_instance(nullifier); + + # Pedersen commitment for coin's value + vcv = ec_mul_short(value, VALUE_COMMIT_VALUE); + vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM); + value_commit = ec_add(vcv, vcr); + # Since value_commit is a curve point, we fetch its coordinates + # and constrain them: + value_commit_x = ec_get_x(value_commit); + value_commit_y = ec_get_y(value_commit); + constrain_instance(value_commit_x); + constrain_instance(value_commit_y); + + # Commitment for coin's token ID + token_commit = poseidon_hash(token, token_blind); + constrain_instance(token_commit); + + # Coin hash + pub = ec_mul_base(secret, NULLIFIER_K); + pub_x = ec_get_x(pub); + pub_y = ec_get_y(pub); + C = poseidon_hash(pub_x, pub_y, value, token, serial, spend_hook, user_data, coin_blind); + + # Merkle root + root = merkle_root(leaf_pos, path, C); + constrain_instance(root); + + # Finally, we derive a public key for the signature and + # constrain its coordinates: + signature_public = ec_mul_base(signature_secret, NULLIFIER_K); + signature_x = ec_get_x(signature_public); + signature_y = ec_get_y(signature_public); + constrain_instance(signature_x); + constrain_instance(signature_y); + + # At this point we've enforced all of our public inputs. +} + + diff --git a/example/dao/proof/dao-propose-main.zk b/example/dao/proof/dao-propose-main.zk new file mode 100644 index 000000000..2d7db4d02 --- /dev/null +++ b/example/dao/proof/dao-propose-main.zk @@ -0,0 +1,88 @@ +constant "DaoProposeMain" { + EcFixedPointShort VALUE_COMMIT_VALUE, + EcFixedPoint VALUE_COMMIT_RANDOM, +} + +contract "DaoProposeMain" { + # Proposers total number of gov tokens + Base total_funds, + Scalar total_funds_blind, + + # Check the inputs and this proof are for the same token + Base gov_token_blind, + + # proposal params + Base proposal_dest_x, + Base proposal_dest_y, + Base proposal_amount, + Base proposal_serial, + Base proposal_token_id, + Base proposal_blind, + + # DAO params + Base dao_proposer_limit, + Base dao_quorum, + Base dao_approval_ratio_quot, + Base dao_approval_ratio_base, + Base gov_token_id, + Base dao_public_x, + Base dao_public_y, + Base dao_bulla_blind, + + Uint32 dao_leaf_pos, + MerklePath dao_path, +} + +circuit "DaoProposeMain" { + token_commit = poseidon_hash(gov_token_id, gov_token_blind); + constrain_instance(token_commit); + + dao_bulla = poseidon_hash( + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + gov_token_id, + dao_public_x, + dao_public_y, + dao_bulla_blind, + ); + dao_root = merkle_root(dao_leaf_pos, dao_path, dao_bulla); + constrain_instance(dao_root); + # Proves this DAO is valid + + proposal_bulla = poseidon_hash( + proposal_dest_x, + proposal_dest_y, + proposal_amount, + proposal_serial, + proposal_token_id, + dao_bulla, + proposal_blind, + # @tmp-workaround + proposal_blind, + ); + constrain_instance(proposal_bulla); + + # Rangeproof check for proposal amount + zero = witness_base(0); + less_than(zero, proposal_amount); + + # This is the main check + # We check that dao_proposer_limit <= total_funds + one = witness_base(1); + total_funds_1 = base_add(total_funds, one); + less_than(dao_proposer_limit, total_funds_1); + + # Pedersen commitment for coin's value + vcv = ec_mul_short(total_funds, VALUE_COMMIT_VALUE); + vcr = ec_mul(total_funds_blind, VALUE_COMMIT_RANDOM); + total_funds_commit = ec_add(vcv, vcr); + # Since total_funds_commit is a curve point, we fetch its coordinates + # and constrain them: + total_funds_commit_x = ec_get_x(total_funds_commit); + total_funds_commit_y = ec_get_y(total_funds_commit); + constrain_instance(total_funds_commit_x); + constrain_instance(total_funds_commit_y); +} + diff --git a/example/dao/proof/dao-vote-burn.zk b/example/dao/proof/dao-vote-burn.zk new file mode 100644 index 000000000..b9b096ef2 --- /dev/null +++ b/example/dao/proof/dao-vote-burn.zk @@ -0,0 +1,64 @@ +constant "DaoVoteInput" { + EcFixedPointShort VALUE_COMMIT_VALUE, + EcFixedPoint VALUE_COMMIT_RANDOM, + EcFixedPointBase NULLIFIER_K, +} + +contract "DaoVoteInput" { + Base secret, + Base serial, + Base spend_hook, + Base user_data, + Base value, + Base gov_token_id, + Base coin_blind, + Scalar value_blind, + Base gov_token_blind, + Uint32 leaf_pos, + MerklePath path, + Base signature_secret, +} + +circuit "DaoVoteInput" { + # Poseidon hash of the nullifier + nullifier = poseidon_hash(secret, serial); + constrain_instance(nullifier); + + # Pedersen commitment for coin's value + vcv = ec_mul_short(value, VALUE_COMMIT_VALUE); + vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM); + value_commit = ec_add(vcv, vcr); + # Since value_commit is a curve point, we fetch its coordinates + # and constrain them: + value_commit_x = ec_get_x(value_commit); + value_commit_y = ec_get_y(value_commit); + constrain_instance(value_commit_x); + constrain_instance(value_commit_y); + + # Commitment for coin's token ID + token_commit = poseidon_hash(gov_token_id, gov_token_blind); + constrain_instance(token_commit); + + # Coin hash + pub = ec_mul_base(secret, NULLIFIER_K); + pub_x = ec_get_x(pub); + pub_y = ec_get_y(pub); + C = poseidon_hash(pub_x, pub_y, value, gov_token_id, serial, spend_hook, user_data, coin_blind); + + # Merkle root + root = merkle_root(leaf_pos, path, C); + constrain_instance(root); + + # Finally, we derive a public key for the signature and + # constrain its coordinates: + signature_public = ec_mul_base(signature_secret, NULLIFIER_K); + signature_x = ec_get_x(signature_public); + signature_y = ec_get_y(signature_public); + constrain_instance(signature_x); + constrain_instance(signature_y); + + # At this point we've enforced all of our public inputs. +} + + + diff --git a/example/dao/proof/dao-vote-main.zk b/example/dao/proof/dao-vote-main.zk new file mode 100644 index 000000000..f7a9b1ba2 --- /dev/null +++ b/example/dao/proof/dao-vote-main.zk @@ -0,0 +1,98 @@ +constant "DaoVoteMain" { + EcFixedPointShort VALUE_COMMIT_VALUE, + EcFixedPoint VALUE_COMMIT_RANDOM, +} + +contract "DaoVoteMain" { + # proposal params + Base proposal_dest_x, + Base proposal_dest_y, + Base proposal_amount, + Base proposal_serial, + Base proposal_token_id, + Base proposal_blind, + + # DAO params + Base dao_proposer_limit, + Base dao_quorum, + Base dao_approval_ratio_quot, + Base dao_approval_ratio_base, + Base gov_token_id, + Base dao_public_x, + Base dao_public_y, + Base dao_bulla_blind, + + # Is the vote yes or no + Base vote_option, + Scalar yes_vote_blind, + + # Total amount of capital allocated to vote + Base all_votes_value, + Scalar all_votes_blind, + + # Check the inputs and this proof are for the same token + Base gov_token_blind, +} + +circuit "DaoVoteMain" { + token_commit = poseidon_hash(gov_token_id, gov_token_blind); + constrain_instance(token_commit); + + dao_bulla = poseidon_hash( + dao_proposer_limit, + dao_quorum, + dao_approval_ratio_quot, + dao_approval_ratio_base, + gov_token_id, + dao_public_x, + dao_public_y, + dao_bulla_blind, + ); + # Proposal bulla is valid means DAO bulla is also valid + # because of dao-propose-main.zk, already checks that when + # we first create the proposal. So it is redundant here. + + proposal_bulla = poseidon_hash( + proposal_dest_x, + proposal_dest_y, + proposal_amount, + proposal_serial, + proposal_token_id, + dao_bulla, + proposal_blind, + # @tmp-workaround + proposal_blind, + ); + constrain_instance(proposal_bulla); + # TODO: we need to check the proposal isn't invalidated + # that is expired or already executed. + + # normally we call this yes vote + # Pedersen commitment for vote option + yes_votes_value = base_mul(vote_option, all_votes_value); + yes_votes_value_c = ec_mul_short(yes_votes_value, VALUE_COMMIT_VALUE); + yes_votes_blind_c = ec_mul(yes_vote_blind, VALUE_COMMIT_RANDOM); + yes_votes_commit = ec_add(yes_votes_value_c, yes_votes_blind_c); + + # get curve points and constrain + yes_votes_commit_x = ec_get_x(yes_votes_commit); + yes_votes_commit_y = ec_get_y(yes_votes_commit); + constrain_instance(yes_votes_commit_x); + constrain_instance(yes_votes_commit_y); + + # Pedersen commitment for vote value + all_votes_c = ec_mul_short(all_votes_value, VALUE_COMMIT_VALUE); + all_votes_blind_c = ec_mul(all_votes_blind, VALUE_COMMIT_RANDOM); + all_votes_commit = ec_add(all_votes_c, all_votes_blind_c); + + # get curve points and constrain + all_votes_commit_x = ec_get_x(all_votes_commit); + all_votes_commit_y = ec_get_y(all_votes_commit); + constrain_instance(all_votes_commit_x); + constrain_instance(all_votes_commit_y); + + # Vote option should be 0 or 1 + bool_check(vote_option); +} + + diff --git a/example/dao/proof/foo.zk b/example/dao/proof/foo.zk new file mode 100644 index 000000000..21c42991d --- /dev/null +++ b/example/dao/proof/foo.zk @@ -0,0 +1,14 @@ +constant "DaoMint" { +} + +contract "DaoMint" { + Base a, + Base b, +} + +circuit "DaoMint" { + c = base_add(a, b); + constrain_instance(c); +} + + diff --git a/bin/daod/demo/classnamespace.py b/example/dao/schema/classnamespace.py similarity index 100% rename from bin/daod/demo/classnamespace.py rename to example/dao/schema/classnamespace.py diff --git a/bin/daod/demo/crypto.py b/example/dao/schema/crypto.py similarity index 100% rename from bin/daod/demo/crypto.py rename to example/dao/schema/crypto.py diff --git a/bin/daod/demo/main.py b/example/dao/schema/main.py similarity index 100% rename from bin/daod/demo/main.py rename to example/dao/schema/main.py diff --git a/bin/daod/demo/money.py b/example/dao/schema/money.py similarity index 100% rename from bin/daod/demo/money.py rename to example/dao/schema/money.py diff --git a/example/dao/util.rs b/example/dao/util.rs new file mode 100644 index 000000000..c319f9ef6 --- /dev/null +++ b/example/dao/util.rs @@ -0,0 +1,234 @@ +use lazy_static::lazy_static; +use log::debug; +use pasta_curves::{ + group::ff::{Field, PrimeField}, + pallas, +}; +use rand::rngs::OsRng; +use std::{any::Any, collections::HashMap, hash::Hasher}; + +use darkfi::{ + crypto::{ + keypair::{PublicKey, SecretKey}, + proof::{ProvingKey, VerifyingKey}, + schnorr::{SchnorrPublic, SchnorrSecret, Signature}, + types::DrkCircuitField, + Proof, + }, + util::serial::Encodable, + zk::{vm::ZkCircuit, vm_stack::empty_witnesses}, + zkas::decoder::ZkBinary, +}; + +// TODO: base58 encoding/ decoding + +lazy_static! { + pub static ref XDRK_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} + +lazy_static! { + pub static ref GDRK_ID: pallas::Base = pallas::Base::random(&mut OsRng); +} + +#[derive(Eq, PartialEq)] +pub struct HashableBase(pub pallas::Base); + +impl std::hash::Hash for HashableBase { + fn hash(&self, state: &mut H) { + let bytes = self.0.to_repr(); + bytes.hash(state); + } +} + +pub struct ZkBinaryContractInfo { + pub k_param: u32, + pub bincode: ZkBinary, + pub proving_key: ProvingKey, + pub verifying_key: VerifyingKey, +} +pub struct ZkNativeContractInfo { + pub proving_key: ProvingKey, + pub verifying_key: VerifyingKey, +} + +pub enum ZkContractInfo { + Binary(ZkBinaryContractInfo), + Native(ZkNativeContractInfo), +} + +pub struct ZkContractTable { + // Key will be a hash of zk binary contract on chain + table: HashMap, +} + +impl ZkContractTable { + pub fn new() -> Self { + Self { table: HashMap::new() } + } + + pub fn add_contract(&mut self, key: String, bincode: ZkBinary, k_param: u32) { + let witnesses = empty_witnesses(&bincode); + let circuit = ZkCircuit::new(witnesses, bincode.clone()); + let proving_key = ProvingKey::build(k_param, &circuit); + let verifying_key = VerifyingKey::build(k_param, &circuit); + let info = ZkContractInfo::Binary(ZkBinaryContractInfo { + k_param, + bincode, + proving_key, + verifying_key, + }); + self.table.insert(key, info); + } + + pub fn add_native( + &mut self, + key: String, + proving_key: ProvingKey, + verifying_key: VerifyingKey, + ) { + self.table.insert( + key, + ZkContractInfo::Native(ZkNativeContractInfo { proving_key, verifying_key }), + ); + } + + pub fn lookup(&self, key: &String) -> Option<&ZkContractInfo> { + self.table.get(key) + } +} + +pub struct Transaction { + pub func_calls: Vec, + pub signatures: Vec, +} + +impl Transaction { + /// Verify ZK contracts for the entire tx + /// In real code, we could parallelize this for loop + /// TODO: fix use of unwrap with Result type stuff + pub fn zk_verify(&self, zk_bins: &ZkContractTable) { + for func_call in &self.func_calls { + let proofs_public_vals = &func_call.call_data.zk_public_values(); + + assert_eq!( + proofs_public_vals.len(), + func_call.proofs.len(), + "proof_public_vals.len()={} and func_call.proofs.len()={} do not match", + proofs_public_vals.len(), + func_call.proofs.len() + ); + for (i, (proof, (key, public_vals))) in + func_call.proofs.iter().zip(proofs_public_vals.iter()).enumerate() + { + match zk_bins.lookup(key).unwrap() { + ZkContractInfo::Binary(info) => { + let verifying_key = &info.verifying_key; + let verify_result = proof.verify(&verifying_key, public_vals); + assert!(verify_result.is_ok(), "verify proof[{}]='{}' failed", i, key); + } + ZkContractInfo::Native(info) => { + let verifying_key = &info.verifying_key; + let verify_result = proof.verify(&verifying_key, public_vals); + assert!(verify_result.is_ok(), "verify proof[{}]='{}' failed", i, key); + } + }; + debug!(target: "demo", "zk_verify({}) passed [i={}]", key, i); + } + } + } + + pub fn verify_sigs(&self) { + let mut unsigned_tx_data = vec![]; + for (i, (func_call, signature)) in + self.func_calls.iter().zip(self.signatures.clone()).enumerate() + { + func_call.encode(&mut unsigned_tx_data).expect("failed to encode data"); + let signature_pub_keys = func_call.call_data.signature_public_keys(); + for signature_pub_key in signature_pub_keys { + let verify_result = signature_pub_key.verify(&unsigned_tx_data[..], &signature); + assert!(verify_result, "verify sigs[{}] failed", i); + } + debug!(target: "demo", "verify_sigs({}) passed", i); + } + } +} + +pub fn sign(signature_secrets: Vec, func_calls: &Vec) -> Vec { + let mut signatures = vec![]; + let mut unsigned_tx_data = vec![]; + for (_i, (signature_secret, func_call)) in + signature_secrets.iter().zip(func_calls.iter()).enumerate() + { + func_call.encode(&mut unsigned_tx_data).expect("failed to encode data"); + let signature = signature_secret.sign(&unsigned_tx_data[..]); + signatures.push(signature); + } + signatures +} + +type ContractId = pallas::Base; +type FuncId = pallas::Base; + +pub struct FuncCall { + pub contract_id: ContractId, + pub func_id: FuncId, + pub call_data: Box, + pub proofs: Vec, +} + +impl Encodable for FuncCall { + fn encode(&self, mut w: W) -> std::result::Result { + let mut len = 0; + len += self.contract_id.encode(&mut w)?; + len += self.func_id.encode(&mut w)?; + len += self.proofs.encode(&mut w)?; + len += self.call_data.encode_bytes(&mut w)?; + Ok(len) + } +} + +pub trait CallDataBase { + // Public values for verifying the proofs + // Needed so we can convert internal types so they can be used in Proof::verify() + fn zk_public_values(&self) -> Vec<(String, Vec)>; + + // For upcasting to CallData itself so it can be read in state_transition() + fn as_any(&self) -> &dyn Any; + + // Public keys we will use to verify transaction signatures. + fn signature_public_keys(&self) -> Vec; + + fn encode_bytes( + &self, + writer: &mut dyn std::io::Write, + ) -> std::result::Result; +} + +type GenericContractState = Box; + +pub struct StateRegistry { + pub states: HashMap, +} + +impl StateRegistry { + pub fn new() -> Self { + Self { states: HashMap::new() } + } + + pub fn register(&mut self, contract_id: ContractId, state: GenericContractState) { + debug!(target: "StateRegistry::register()", "contract_id: {:?}", contract_id); + self.states.insert(HashableBase(contract_id), state); + } + + pub fn lookup_mut<'a, S: 'static>(&'a mut self, contract_id: ContractId) -> Option<&'a mut S> { + self.states.get_mut(&HashableBase(contract_id)).and_then(|state| state.downcast_mut()) + } + + pub fn lookup<'a, S: 'static>(&'a self, contract_id: ContractId) -> Option<&'a S> { + self.states.get(&HashableBase(contract_id)).and_then(|state| state.downcast_ref()) + } +} + +pub trait UpdateBase { + fn apply(self: Box, states: &mut StateRegistry); +} diff --git a/example/derive_macro_example.rs b/example/derive_macro_example.rs new file mode 100644 index 000000000..67ec29ac7 --- /dev/null +++ b/example/derive_macro_example.rs @@ -0,0 +1,12 @@ +use darkfi::serial::SerialEncodable; + +#[derive(Debug, SerialEncodable)] +struct Test { + one: u64, + two: u64, +} + +fn main() { + let test = Test { one: 1, two: 2 }; + println!("Test: {:?}", test); +} diff --git a/src/crypto/keypair.rs b/src/crypto/keypair.rs index 5db16905c..c00197046 100644 --- a/src/crypto/keypair.rs +++ b/src/crypto/keypair.rs @@ -1,6 +1,10 @@ -use std::{convert::TryFrom, str::FromStr}; +use std::{ + convert::TryFrom, + hash::{Hash, Hasher}, + str::FromStr, +}; -use darkfi_serial::{SerialDecodable, SerialEncodable}; +use darkfi_serial::{Decodable, Encodable, SerialDecodable, SerialEncodable}; use halo2_gadgets::ecc::chip::FixedPoint; use pasta_curves::{ arithmetic::CurveAffine, @@ -18,6 +22,8 @@ use crate::{ }; #[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg(feature = "serde")] +#[derive(serde::Deserialize, serde::Serialize)] pub struct Keypair { pub secret: SecretKey, pub public: PublicKey, @@ -36,7 +42,7 @@ impl Keypair { } #[derive(Copy, Clone, PartialEq, Eq, Debug, SerialDecodable, SerialEncodable)] -pub struct SecretKey(pallas::Base); +pub struct SecretKey(pub pallas::Base); impl SecretKey { pub fn random(mut rng: impl RngCore) -> Self { @@ -119,6 +125,13 @@ impl PublicKey { } } +impl Hash for PublicKey { + fn hash(&self, state: &mut H) { + let bytes = self.0.to_affine().to_bytes(); + bytes.hash(state); + } +} + impl FromStr for PublicKey { type Err = crate::Error; @@ -133,6 +146,98 @@ impl FromStr for PublicKey { } } +#[cfg(feature = "serde")] +impl serde::Serialize for SecretKey { + fn serialize(&self, serializer: S) -> core::result::Result + where + S: serde::Serializer, + { + let mut bytes = vec![]; + self.encode(&mut bytes).unwrap(); + let hex_repr = hex::encode(&bytes); + serializer.serialize_str(&hex_repr) + } +} + +#[cfg(feature = "serde")] +struct SecretKeyVisitor; + +#[cfg(feature = "serde")] +impl<'de> serde::de::Visitor<'de> for SecretKeyVisitor { + type Value = SecretKey; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("hex string") + } + + fn visit_str(self, value: &str) -> core::result::Result + where + E: serde::de::Error, + { + let bytes = hex::decode(value).unwrap(); + let mut r = std::io::Cursor::new(bytes); + let decoded: SecretKey = SecretKey::decode(&mut r).unwrap(); + Ok(decoded) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for SecretKey { + fn deserialize(deserializer: D) -> core::result::Result + where + D: serde::Deserializer<'de>, + { + let bytes = deserializer.deserialize_str(SecretKeyVisitor).unwrap(); + Ok(bytes) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for PublicKey { + fn serialize(&self, serializer: S) -> core::result::Result + where + S: serde::Serializer, + { + let mut bytes = vec![]; + self.encode(&mut bytes).unwrap(); + let hex_repr = hex::encode(&bytes); + serializer.serialize_str(&hex_repr) + } +} + +#[cfg(feature = "serde")] +struct PublicKeyVisitor; + +#[cfg(feature = "serde")] +impl<'de> serde::de::Visitor<'de> for PublicKeyVisitor { + type Value = PublicKey; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("hex string") + } + + fn visit_str(self, value: &str) -> core::result::Result + where + E: serde::de::Error, + { + let bytes = hex::decode(value).unwrap(); + let mut r = std::io::Cursor::new(bytes); + let decoded: PublicKey = PublicKey::decode(&mut r).unwrap(); + Ok(decoded) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> core::result::Result + where + D: serde::Deserializer<'de>, + { + let bytes = deserializer.deserialize_str(PublicKeyVisitor).unwrap(); + Ok(bytes) + } +} + impl TryFrom
for PublicKey { type Error = Error; fn try_from(address: Address) -> Result { diff --git a/src/crypto/nullifier.rs b/src/crypto/nullifier.rs index 0c7693123..35773d14a 100644 --- a/src/crypto/nullifier.rs +++ b/src/crypto/nullifier.rs @@ -2,7 +2,7 @@ use darkfi_serial::{SerialDecodable, SerialEncodable}; use pasta_curves::{group::ff::PrimeField, pallas}; #[derive(Clone, Copy, Debug, PartialEq, Eq, SerialEncodable, SerialDecodable)] -pub struct Nullifier(pallas::Base); +pub struct Nullifier(pub pallas::Base); impl Nullifier { pub fn from_bytes(bytes: [u8; 32]) -> Option {