resolve conflicts merging dao_demo

This commit is contained in:
Dastan-glitch
2022-10-16 02:50:59 +03:00
96 changed files with 7336 additions and 219 deletions

2
.gitignore vendored
View File

@@ -11,7 +11,7 @@
/tmp/*
/cashierd
/dao-cli
/dao
/daod
/darkfid
/darkotc

5
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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

View File

@@ -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<CliDaoSubCommands>,
}
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<Value> {
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::<DrkConfig>::load(config_path)?;
start(args).await
}

1
bin/dao/README.md Normal file
View File

@@ -0,0 +1 @@
# DAO

View File

@@ -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"

41
bin/dao/dao-cli/run_demo.sh Executable file
View File

@@ -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

241
bin/dao/dao-cli/src/main.rs Normal file
View File

@@ -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<CliDaoSubCommands>,
}
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::<DrkConfig>::load(config_path)?;
start(args).await
}

112
bin/dao/dao-cli/src/rpc.rs Normal file
View File

@@ -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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
let req = JsonRequest::new("exec", json!([bulla]));
self.client.request(req).await
}
}

0
bin/dao/dao_config.toml Normal file
View File

View File

@@ -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"

16
bin/dao/daod/TODO Normal file
View File

@@ -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

View File

@@ -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
}

View File

@@ -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<T> = std::result::Result<T, Error>;
#[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<DarkFiError> 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<DrkCircuitField>)> {
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<PublicKey> {
vec![]
}
fn encode_bytes(
&self,
mut writer: &mut dyn std::io::Write,
) -> std::result::Result<usize, std::io::Error> {
self.encode(&mut writer)
}
}
pub fn state_transition(
states: &StateRegistry,
func_call_index: usize,
parent_tx: &Transaction,
) -> Result<Box<dyn UpdateBase + Send>> {
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::<CallData>());
let call_data = call_data.downcast_ref::<CallData>();
// 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::<money_contract::transfer::validate::CallData>();
let money_transfer_call_data = money_transfer_call_data.unwrap();
assert_eq!(
money_transfer_call_data.type_id(),
TypeId::of::<money_contract::transfer::validate::CallData>()
);
// 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::<dao_contract::State>(*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<Self>, states: &mut StateRegistry) {
let state = states
.lookup_mut::<dao_contract::State>(*CONTRACT_ID)
.expect("Return type is not of type State");
state.proposal_votes.remove(&HashableBase(self.proposal)).unwrap();
}
}

View File

@@ -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,
}
}
}

View File

@@ -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<Box<dyn UpdateBase + Send>> {
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::<CallData>());
let call_data = call_data.downcast_ref::<CallData>();
// 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<Self>, states: &mut StateRegistry) {
// Lookup dao_contract state from registry
let state = states.lookup_mut::<State>(*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<T> = std::result::Result<T, Error>;
#[derive(Clone, SerialEncodable, SerialDecodable)]
pub struct CallData {
pub dao_bulla: DaoBulla,
}
impl CallDataBase for CallData {
fn zk_public_values(&self) -> Vec<(String, Vec<DrkCircuitField>)> {
vec![("dao-mint".to_string(), vec![self.dao_bulla.0])]
}
fn as_any(&self) -> &dyn Any {
self
}
fn signature_public_keys(&self) -> Vec<PublicKey> {
vec![]
}
fn encode_bytes(
&self,
mut writer: &mut dyn std::io::Write,
) -> std::result::Result<usize, std::io::Error> {
self.encode(&mut writer)
}
}

View File

@@ -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)]

View File

@@ -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<T> = std::result::Result<T, Error>;
impl From<DarkFiError> 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<Input>,
}
impl CallDataBase for CallData {
fn zk_public_values(&self) -> Vec<(String, Vec<DrkCircuitField>)> {
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<PublicKey> {
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<usize, std::io::Error> {
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<Box<dyn UpdateBase + Send>> {
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::<CallData>());
let call_data = call_data.downcast_ref::<CallData>();
// 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::<MoneyState>(*money_contract::CONTRACT_ID).unwrap();
if !money_state.is_valid_merkle(&input.merkle_root) {
return Err(Error::InvalidInputMerkleRoot)
}
}
let state = states.lookup::<DaoState>(*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<Self>, states: &mut StateRegistry) {
let state = states.lookup_mut::<DaoState>(*dao_contract::CONTRACT_ID).unwrap();
state.add_proposal_bulla(self.proposal_bulla);
}
}

View File

@@ -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<MerkleNode>,
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<BuilderInput>,
pub proposal: Proposal,
pub dao: DaoParams,
pub dao_leaf_position: incrementalmerkletree::Position,
pub dao_merkle_path: Vec<MerkleNode>,
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(), &current, sibling)
} else {
MerkleNode::combine(level.into(), sibling, &current)
};
}
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(&note, &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,
}
}
}

View File

@@ -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<MerkleNode, MERKLE_DEPTH>;
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<Nullifier>,
}
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<DaoBulla>,
pub dao_tree: MerkleTree,
pub dao_roots: Vec<MerkleNode>,
//proposal_bullas: Vec<pallas::Base>,
pub proposal_tree: MerkleTree,
pub proposal_roots: Vec<MerkleNode>,
pub proposal_votes: HashMap<HashableBase, ProposalVotes>,
}
impl State {
pub fn new() -> Box<dyn Any + Send> {
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)
}
}

View File

@@ -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<T> = std::result::Result<T, Error>;
impl From<DarkFiError> 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<Input>,
}
impl CallDataBase for CallData {
fn zk_public_values(&self) -> Vec<(String, Vec<DrkCircuitField>)> {
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<PublicKey> {
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<usize, std::io::Error> {
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<Box<dyn UpdateBase + Send>> {
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::<CallData>());
let call_data = call_data.downcast_ref::<CallData>();
// This will be inside wasm so unwrap is fine.
let call_data = call_data.unwrap();
let dao_state = states.lookup::<DaoState>(*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::<MoneyState>(*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<Nullifier>,
pub yes_vote_commit: pallas::Point,
pub all_vote_commit: pallas::Point,
}
impl UpdateBase for Update {
fn apply(mut self: Box<Self>, states: &mut StateRegistry) {
let state = states.lookup_mut::<DaoState>(*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);
}
}

View File

@@ -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<MerkleNode>,
pub signature_secret: SecretKey,
}
// TODO: should be token locking voting?
// Inside ZKproof, check proposal is correct.
pub struct Builder {
pub inputs: Vec<BuilderInput>,
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(), &current, sibling)
} else {
MerkleNode::combine(level.into(), sibling, &current)
};
}
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(&note, &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,
}
}
}

View File

@@ -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<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, thiserror::Error)]
pub enum Error {
// #[error("ValueExists")]
// ValueExists,
#[error("DarkFi error: {0}")]
DarkFiError(String),
}
impl From<DarkFiError> 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<DrkCircuitField>)> {
vec![("example-foo".to_string(), vec![self.public_value])]
}
fn as_any(&self) -> &dyn Any {
self
}
fn signature_public_keys(&self) -> Vec<PublicKey> {
vec![self.signature_public]
}
fn encode_bytes(
&self,
mut writer: &mut dyn std::io::Write,
) -> std::result::Result<usize, std::io::Error> {
self.encode(&mut writer)
}
}
// pub fn state_transition(
// states: &StateRegistry,
// func_call_index: usize,
// parent_tx: &Transaction,
// ) -> Result<Box<dyn UpdateBase + Send>> {
// 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::<CallData>());
// let call_data = call_data.downcast_ref::<CallData>();
// // This will be inside wasm so unwrap is fine.
// let call_data = call_data.unwrap();
// let example_state = states.lookup::<State>(*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<Self>, states: &mut StateRegistry) {
let example_state = states.lookup_mut::<State>(*CONTRACT_ID).unwrap();
example_state.add_public_value(self.public_value);
}
}

View File

@@ -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,
// }
// }
// }

View File

@@ -0,0 +1,21 @@
// use std::any::Any;
use pasta_curves::pallas;
pub struct State {
pub public_values: Vec<pallas::Base>,
}
impl State {
// pub fn new() -> Box<dyn Any> {
// 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)
// }
}

View File

@@ -0,0 +1,3 @@
pub mod dao_contract;
pub mod example_contract;
pub mod money_contract;

View File

@@ -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<MerkleNode, MERKLE_DEPTH>;
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<OwnCoin>)>,
}
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<OwnCoin> {
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<MerkleNode>,
/// Nullifiers prevent double spending
pub nullifiers: Vec<Nullifier>,
/// 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<Self> {
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)
}
}

View File

@@ -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<Nullifier>,
/// All coins in a transaction
pub coins: Vec<Coin>,
/// All encrypted notes in a transaction
pub enc_notes: Vec<EncryptedNote2>,
}
impl UpdateBase for Update {
fn apply(mut self: Box<Self>, states: &mut StateRegistry) {
let state = states.lookup_mut::<State>(*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<Box<dyn UpdateBase + Send>> {
// 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::<CallData>());
let call_data = call_data.downcast_ref::<CallData>();
// This will be inside wasm so unwrap is fine.
let call_data = call_data.unwrap();
let state = states.lookup::<State>(*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<ClearInput>,
/// Anonymous inputs
pub inputs: Vec<Input>,
/// Anonymous outputs
pub outputs: Vec<Output>,
}
impl CallDataBase for CallData {
fn zk_public_values(&self) -> Vec<(String, Vec<DrkCircuitField>)> {
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<PublicKey> {
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<usize, std::io::Error> {
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<T> = std::result::Result<T, Error>;
impl From<Error> for VerifyFailed {
fn from(err: Error) -> Self {
Self::InternalError(err.to_string())
}
}
impl From<DarkFiError> for VerifyFailed {
fn from(err: DarkFiError) -> Self {
Self::DarkFiError(err.to_string())
}
}
impl From<DarkFiError> for Error {
fn from(err: DarkFiError) -> Self {
Self::DarkFiError(err.to_string())
}
}
/// Result type used in transaction verifications
pub type VerifyResult<T> = std::result::Result<T, VerifyFailed>;

View File

@@ -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<BuilderClearInputInfo>,
pub inputs: Vec<BuilderInputInfo>,
pub outputs: Vec<BuilderOutputInfo>,
}
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<MerkleNode>,
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<FuncCall> {
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(&note, &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,
})
}
}

56
bin/dao/daod/src/error.rs Normal file
View File

@@ -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<T> = std::result::Result<T, DaoError>;
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()
}

1156
bin/dao/daod/src/main.rs Normal file

File diff suppressed because it is too large Load Diff

98
bin/dao/daod/src/note.rs Normal file
View File

@@ -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<T: Encodable>(note: &T, public: &PublicKey) -> Result<EncryptedNote2> {
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<u8>,
ephem_public: PublicKey,
}
impl EncryptedNote2 {
pub fn decrypt<T: Decodable>(&self, secret: &SecretKey) -> Result<T> {
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<u8>,
}
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(&note, &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);
}
}

402
bin/dao/daod/src/rpc.rs Normal file
View File

@@ -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<Mutex<Client>>,
}
#[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)
}
}
}
}

View File

@@ -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,
},
],

265
bin/dao/daod/src/util.rs Normal file
View File

@@ -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<pallas::Base, darkfi::Error> {
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<H: Hasher>(&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<String, ZkContractInfo>,
}
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<FuncCall>,
pub signatures: Vec<Signature>,
}
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<SecretKey>, func_calls: &Vec<FuncCall>) -> Vec<Signature> {
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<dyn CallDataBase + Send + Sync>,
pub proofs: Vec<Proof>,
}
impl Encodable for FuncCall {
fn encode<W: std::io::Write>(&self, mut w: W) -> std::result::Result<usize, std::io::Error> {
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<DrkCircuitField>)>;
// 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<PublicKey>;
fn encode_bytes(
&self,
writer: &mut dyn std::io::Write,
) -> std::result::Result<usize, std::io::Error>;
}
type GenericContractState = Box<dyn Any + Send>;
pub struct StateRegistry {
pub states: HashMap<HashableBase, GenericContractState>,
}
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<Self>, states: &mut StateRegistry);
}

View File

@@ -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(())
}

View File

@@ -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);
}

View File

@@ -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<T> = std::result::Result<T, Error>;
@@ -111,7 +109,7 @@ pub fn state_transition(
states: &StateRegistry,
func_call_index: usize,
parent_tx: &Transaction,
) -> Result<Box<dyn UpdateBase>> {
) -> Result<Box<dyn UpdateBase + Send>> {
let func_call = &parent_tx.func_calls[func_call_index];
let call_data = func_call.call_data.as_any();

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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<Box<dyn UpdateBase>> {
) -> Result<Box<dyn UpdateBase + Send>> {
let func_call = &parent_tx.func_calls[func_call_index];
let call_data = func_call.call_data.as_any();

View File

@@ -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],
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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<Box<dyn UpdateBase>> {
) -> Result<Box<dyn UpdateBase + Send>> {
let func_call = &parent_tx.func_calls[func_call_index];
let call_data = func_call.call_data.as_any();

View File

@@ -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)]

View File

@@ -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<dyn Any> {
pub fn new() -> Box<dyn Any + Send> {
Box::new(Self {
dao_bullas: Vec::new(),
dao_tree: MerkleTree::new(100),

View File

@@ -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);
}

View File

@@ -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<Box<dyn UpdateBase>> {
) -> Result<Box<dyn UpdateBase + Send>> {
let func_call = &parent_tx.func_calls[func_call_index];
let call_data = func_call.call_data.as_any();

View File

@@ -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)]

View File

@@ -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);
}

View File

@@ -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<T> = std::result::Result<T, Error>;
@@ -61,7 +61,7 @@ pub fn state_transition(
states: &StateRegistry,
func_call_index: usize,
parent_tx: &Transaction,
) -> Result<Box<dyn UpdateBase>> {
) -> Result<Box<dyn UpdateBase + Send>> {
let func_call = &parent_tx.func_calls[func_call_index];
let call_data = func_call.call_data.as_any();

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -0,0 +1,3 @@
pub mod dao_contract;
pub mod example_contract;
pub mod money_contract;

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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<Box<dyn UpdateBase>> {
) -> Result<Box<dyn UpdateBase + Send>> {
// 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");

View File

@@ -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)]

1327
example/dao/dao.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}

View File

@@ -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.
}

View File

@@ -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);
}

View File

@@ -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.
}

View File

@@ -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);
}

14
example/dao/proof/foo.zk Normal file
View File

@@ -0,0 +1,14 @@
constant "DaoMint" {
}
contract "DaoMint" {
Base a,
Base b,
}
circuit "DaoMint" {
c = base_add(a, b);
constrain_instance(c);
}

234
example/dao/util.rs Normal file
View File

@@ -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<H: Hasher>(&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<String, ZkContractInfo>,
}
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<FuncCall>,
pub signatures: Vec<Signature>,
}
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<SecretKey>, func_calls: &Vec<FuncCall>) -> Vec<Signature> {
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<dyn CallDataBase + Send + Sync>,
pub proofs: Vec<Proof>,
}
impl Encodable for FuncCall {
fn encode<W: std::io::Write>(&self, mut w: W) -> std::result::Result<usize, darkfi::Error> {
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<DrkCircuitField>)>;
// 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<PublicKey>;
fn encode_bytes(
&self,
writer: &mut dyn std::io::Write,
) -> std::result::Result<usize, darkfi::Error>;
}
type GenericContractState = Box<dyn Any + Send>;
pub struct StateRegistry {
pub states: HashMap<HashableBase, GenericContractState>,
}
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<Self>, states: &mut StateRegistry);
}

View File

@@ -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);
}

View File

@@ -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<H: Hasher>(&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<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
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<E>(self, value: &str) -> core::result::Result<SecretKey, E>
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<D>(deserializer: D) -> core::result::Result<SecretKey, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes = deserializer.deserialize_str(SecretKeyVisitor).unwrap();
Ok(bytes)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for PublicKey {
fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
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<E>(self, value: &str) -> core::result::Result<PublicKey, E>
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<D>(deserializer: D) -> core::result::Result<PublicKey, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes = deserializer.deserialize_str(PublicKeyVisitor).unwrap();
Ok(bytes)
}
}
impl TryFrom<Address> for PublicKey {
type Error = Error;
fn try_from(address: Address) -> Result<Self> {

View File

@@ -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<Self> {