contract/money: Initial outline in src/contract/money.

This commit is contained in:
parazyd
2022-11-15 17:26:20 +01:00
parent 9c0a0e1c10
commit 16178af767
12 changed files with 664 additions and 0 deletions

9
Cargo.lock generated
View File

@@ -1297,6 +1297,15 @@ dependencies = [
"syn",
]
[[package]]
name = "darkfi-money-contract"
version = "0.3.0"
dependencies = [
"darkfi-sdk",
"darkfi-serial",
"getrandom 0.2.8",
]
[[package]]
name = "darkfi-sdk"
version = "0.3.0"

View File

@@ -44,6 +44,8 @@ members = [
"src/serial/derive",
"src/serial/derive-internal",
"src/contract/money",
"example/dchat",
"example/dao",
]

1
src/contract/README.md Normal file
View File

@@ -0,0 +1 @@
This directory contains native WASM contracts on DarkFi.

2
src/contract/money/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
money_contract.wasm
proof/*.zk.bin

View File

@@ -0,0 +1,18 @@
[package]
name = "darkfi-money-contract"
version = "0.3.0"
authors = ["Dyne.org foundation <foundation@dyne.org>"]
license = "AGPL-3.0-only"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
darkfi-sdk = { path = "../../sdk" }
darkfi-serial = { path = "../../serial", features = ["derive", "crypto"] }
# We need to disable random using "custom" which makes the crate a noop
# so the wasm32-unknown-unknown target is enabled.
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2.8", features = ["custom"] }

View File

@@ -0,0 +1,37 @@
# Cargo binary
CARGO ?= cargo
# Zkas binary
ZKAS ?= ../../../zkas
# zkas circuit source files
ZKAS_SRC = $(shell find proof -type f -name '*.zk')
# wasm source files
WASM_SRC = \
$(shell find src -type f) \
$(shell find ../../sdk/src -type f) \
$(shell find ../../serial/src -type f)
# zkas circuit bin files
ZKAS_BIN = $(ZKAS_SRC:=.bin)
# Contract WASM binaries
WASM_BIN = money_contract.wasm
all: $(ZKAS_BIN) $(WASM_BIN)
$(ZKAS_BIN): $(ZKAS_SRC)
$(ZKAS) $(basename $@) -o $@
money_contract.wasm: $(ZKAS_BIN) $(WASM_SRC)
$(CARGO) build --release --package darkfi-money-contract --target wasm32-unknown-unknown
cp -f ../../../target/wasm32-unknown-unknown/release/darkfi_money_contract.wasm $@
test:
clean:
rm -f $(ZKAS_BIN) $(WASM_BIN)
# We always rebuild the wasm no matter what
.PHONY: $(WASM_BIN) all test clean

View File

@@ -0,0 +1,66 @@
constant "Burn" {
EcFixedPointShort VALUE_COMMIT_VALUE,
EcFixedPoint VALUE_COMMIT_RANDOM,
EcFixedPointBase NULLIFIER_K,
}
contract "Burn" {
Base secret,
Base serial,
Base value,
Base token,
Base coin_blind,
Scalar value_blind,
Scalar token_blind,
Uint32 leaf_pos,
MerklePath path,
Base signature_secret,
}
circuit "Burn" {
# 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);
# Pedersen commitment for coin's token ID
tcv = ec_mul_base(token, NULLIFIER_K);
tcr = ec_mul(token_blind, VALUE_COMMIT_RANDOM);
token_commit = ec_add(tcv, tcr);
# Since token_commit is also a curve point, we'll do the same
# coordinate dance:
token_commit_x = ec_get_x(token_commit);
token_commit_y = ec_get_y(token_commit);
constrain_instance(token_commit_x);
constrain_instance(token_commit_y);
# 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, 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,46 @@
constant "Mint" {
EcFixedPointShort VALUE_COMMIT_VALUE,
EcFixedPoint VALUE_COMMIT_RANDOM,
EcFixedPointBase NULLIFIER_K,
}
contract "Mint" {
Base pub_x,
Base pub_y,
Base value,
Base token,
Base serial,
Base coin_blind,
Scalar value_blind,
Scalar token_blind,
}
circuit "Mint" {
# Poseidon hash of the coin
C = poseidon_hash(pub_x, pub_y, value, token, serial, coin_blind);
constrain_instance(C);
# 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 the 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);
# Pedersen commitment for coin's token ID
tcv = ec_mul_base(token, NULLIFIER_K);
tcr = ec_mul(token_blind, VALUE_COMMIT_RANDOM);
token_commit = ec_add(tcv, tcr);
# Since token_commit is also a curve point, we'll do the same
# coordinate dance:
token_commit_x = ec_get_x(token_commit);
token_commit_y = ec_get_y(token_commit);
constrain_instance(token_commit_x);
constrain_instance(token_commit_y);
# At this point we've enforced all of our public inputs.
}

View File

@@ -0,0 +1,299 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2022 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use darkfi_sdk::{
crypto::{ContractId, MerkleNode, MerkleTree, PublicKey},
db::{db_contains_key, db_get, db_init, db_lookup, db_set},
define_contract,
error::{ContractError, ContractResult},
merkle::merkle_add,
msg,
pasta::{arithmetic::CurveAffine, group::Curve, pallas},
tx::ContractCall,
util::set_return_data,
};
use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
/// Functions we allow in this contract
#[repr(u8)]
pub enum MoneyFunction {
Transfer = 0x00,
}
impl From<u8> for MoneyFunction {
fn from(b: u8) -> Self {
match b {
0x00 => Self::Transfer,
_ => panic!("Invalid function ID: {:#04x?}", b),
}
}
}
/// Structures and object definitions
pub mod state;
use state::{MoneyTransferParams, MoneyTransferUpdate};
define_contract!(
init: init_contract,
exec: process_instruction,
apply: process_update,
metadata: get_metadata
);
// These are the different sled trees that will be created
pub const ZKAS_TREE: &str = "zkas";
pub const COIN_ROOTS_TREE: &str = "coin_roots";
pub const NULLIFIERS_TREE: &str = "nullifiers";
pub const INFO_TREE: &str = "info";
// This is a key inside the info tree
pub const COIN_MERKLE_TREE: &str = "coin_tree";
pub const FAUCET_PUBKEYS: &str = "faucet_pubkeys";
/// zkas mint contract namespace
pub const ZKAS_MINT_NS: &str = "Mint";
/// zkas burn contract namespace
pub const ZKAS_BURN_NS: &str = "Burn";
/// This function runs when the contract is (re)deployed and initialized.
fn init_contract(cid: ContractId, _ix: &[u8]) -> ContractResult {
// The zkas circuits can simply be embedded in the wasm and set up by
// the initialization. Note that the tree should then be called "zkas".
// The lookups can then be done by `contract_id+zkas+namespace`.
let zkas_db = db_init(cid, ZKAS_TREE)?;
let mint_bincode = include_bytes!("../proof/mint.zk.bin");
let burn_bincode = include_bytes!("../proof/burn.zk.bin");
/* TODO: Do I really want to make zkas a dependency? Yeah, in the future.
For now we take anything.
let zkbin = ZkBinary::decode(mint_bincode)?;
let mint_namespace = zkbin.namespace.clone();
assert_eq!(&mint_namespace, ZKAS_MINT_NS);
let zkbin = ZkBinary::decode(burn_bincode)?;
let burn_namespace = zkbin.namespace.clone();
assert_eq!(&burn_namespace, ZKAS_BURN_NS);
db_set(zkas_db, &serialize(&mint_namespace), &mint_bincode[..])?;
db_set(zkas_db, &serialize(&burn_namespace), &burn_bincode[..])?;
*/
db_set(zkas_db, &serialize(&ZKAS_MINT_NS.to_string()), &mint_bincode[..])?;
db_set(zkas_db, &serialize(&ZKAS_BURN_NS.to_string()), &burn_bincode[..])?;
// Set up a database tree to hold Merkle roots
let _ = db_init(cid, COIN_ROOTS_TREE)?;
// Set up a database tree to hold nullifiers
let _ = db_init(cid, NULLIFIERS_TREE)?;
// Set up a database tree for arbitrary data
let info_db = db_init(cid, INFO_TREE)?;
// Add a Merkle tree to the info db:
let coin_tree = MerkleTree::new(100);
let mut coin_tree_data = vec![];
// TODO: FIXME: What is this write_u32 doing here?
coin_tree_data.write_u32(0)?;
coin_tree.encode(&mut coin_tree_data)?;
db_set(info_db, &serialize(&COIN_MERKLE_TREE.to_string()), &coin_tree_data)?;
// Whitelisted faucets
let faucet_pubkeys: Vec<PublicKey> = vec![];
db_set(info_db, &serialize(&FAUCET_PUBKEYS.to_string()), &serialize(&faucet_pubkeys))?;
Ok(())
}
/// This function is used by the VM's host to fetch the necessary metadata for
/// verifying signatures and zk proofs.
fn get_metadata(_cid: ContractId, ix: &[u8]) -> ContractResult {
let (call_idx, call): (u32, Vec<ContractCall>) = deserialize(ix)?;
assert!(call_idx < call.len() as u32);
let self_ = &call[call_idx as usize];
match MoneyFunction::from(self_.data[0]) {
MoneyFunction::Transfer => {
let params: MoneyTransferParams = deserialize(&self_.data[1..])?;
let mut zk_public_values: Vec<(String, Vec<pallas::Base>)> = vec![];
let mut signature_pubkeys: Vec<PublicKey> = vec![];
for input in &params.clear_inputs {
signature_pubkeys.push(input.signature_public);
}
for input in &params.inputs {
let value_coords = input.value_commit.to_affine().coordinates().unwrap();
let token_coords = input.token_commit.to_affine().coordinates().unwrap();
let (sig_x, sig_y) = input.signature_public.xy();
zk_public_values.push((
ZKAS_BURN_NS.to_string(),
vec![
input.nullifier.inner(),
*value_coords.x(),
*value_coords.y(),
*token_coords.x(),
*token_coords.y(),
input.merkle_root.inner(),
input.user_data_enc,
sig_x,
sig_y,
],
));
signature_pubkeys.push(input.signature_public);
}
for output in &params.outputs {
let value_coords = output.value_commit.to_affine().coordinates().unwrap();
let token_coords = output.token_commit.to_affine().coordinates().unwrap();
zk_public_values.push((
ZKAS_MINT_NS.to_string(),
vec![
output.coin.inner(),
*value_coords.x(),
*value_coords.y(),
*token_coords.x(),
*token_coords.y(),
],
));
}
let mut metadata = vec![];
zk_public_values.encode(&mut metadata)?;
signature_pubkeys.encode(&mut metadata)?;
// Using this, we pass the above data to the host.
set_return_data(&metadata)?;
}
};
Ok(())
}
/// This function verifies a state transition and produces an
/// update if everything is successful.
fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
let (call_idx, call): (u32, Vec<ContractCall>) = deserialize(ix)?;
assert!(call_idx < call.len() as u32);
let self_ = &call[call_idx as usize];
match MoneyFunction::from(self_.data[0]) {
MoneyFunction::Transfer => {
let params: MoneyTransferParams = deserialize(&self_.data[1..])?;
let info_db = db_lookup(cid, INFO_TREE)?;
let nullifier_db = db_lookup(cid, NULLIFIERS_TREE)?;
let coin_roots_db = db_lookup(cid, COIN_ROOTS_TREE)?;
let Some(faucet_pubkeys) = db_get(info_db, &serialize(&FAUCET_PUBKEYS.to_string()))? else {
msg!("[Transfer] Error: Missing faucet pubkeys from info db");
return Err(ContractError::Internal);
};
let faucet_pubkeys: Vec<PublicKey> = deserialize(&faucet_pubkeys)?;
// State transition for payments
msg!("[Transfer] Iterating over clear inputs");
for (i, input) in params.clear_inputs.iter().enumerate() {
let pk = input.signature_public;
if !faucet_pubkeys.contains(&pk) {
msg!("[Transfer] Error: Clear input {} has invalid faucet pubkey", i);
return Err(ContractError::Custom(20))
}
}
let mut new_coin_roots = vec![];
let mut new_nullifiers = vec![];
msg!("[Transfer] Iterating over anonymous inputs");
for (i, input) in params.inputs.iter().enumerate() {
// The Merkle root is used to know whether this is a coin that existed
// in a previous state.
if new_coin_roots.contains(&input.merkle_root) ||
db_contains_key(coin_roots_db, &serialize(&input.merkle_root))?
{
msg!("[Transfer] Error: Duplicate Merkle root found in input {}", i);
return Err(ContractError::Custom(21))
}
// The nullifiers should not already exist. It is the double-spend protection.
if new_nullifiers.contains(&input.nullifier) ||
db_contains_key(nullifier_db, &serialize(&input.nullifier))?
{
msg!("[Transfer] Error: Duplicate nullifier found in input {}", i);
return Err(ContractError::Custom(22))
}
new_coin_roots.push(input.merkle_root);
new_nullifiers.push(input.nullifier);
}
// Newly created coins for this transaction are in the outputs.
let new_coins = Vec::with_capacity(params.outputs.len());
for (i, output) in params.outputs.iter().enumerate() {
// TODO: Should we have coins in a sled tree too to check dupes?
if new_coins.contains(&output.coin) {
msg!("[Transfer] Error: Duplicate coin found in output {}", i);
return Err(ContractError::Custom(23))
}
}
// Create a state update
let update = MoneyTransferUpdate { nullifiers: new_nullifiers, coins: new_coins };
let mut update_data = vec![];
update_data.write_u8(MoneyFunction::Transfer as u8)?;
update.encode(&mut update_data)?;
set_return_data(&update_data)?;
msg!("[Transfer] State update set!");
Ok(())
}
}
}
fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult {
match MoneyFunction::from(update_data[0]) {
MoneyFunction::Transfer => {
let update: MoneyTransferUpdate = deserialize(&update_data[1..])?;
let info_db = db_lookup(cid, INFO_TREE)?;
let nullifiers_db = db_lookup(cid, NULLIFIERS_TREE)?;
let coin_roots_db = db_lookup(cid, COIN_ROOTS_TREE)?;
for nullifier in update.nullifiers {
db_set(nullifiers_db, &serialize(&nullifier), &[])?;
}
for coin in update.coins {
// TODO: merkle_add() should take a list of coins and batch add them for efficiency
merkle_add(
info_db,
coin_roots_db,
&serialize(&COIN_MERKLE_TREE.to_string()),
&MerkleNode::from(coin.inner()),
)?;
}
Ok(())
}
}
}

View File

@@ -0,0 +1,95 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2022 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use darkfi_sdk::{
crypto::{
pedersen::{ValueBlind, ValueCommit},
Coin, MerkleNode, Nullifier, PublicKey, TokenId,
},
pasta::pallas,
};
use darkfi_serial::{SerialDecodable, SerialEncodable};
/// Inputs and outputs for a payment
#[derive(Debug, SerialEncodable, SerialDecodable)]
pub struct MoneyTransferParams {
/// Clear inputs
pub clear_inputs: Vec<ClearInput>,
/// Anonymous inputs
pub inputs: Vec<Input>,
/// Anonymous outputs
pub outputs: Vec<Output>,
}
/// State update produced by a payment
#[derive(Debug, SerialEncodable, SerialDecodable)]
pub struct MoneyTransferUpdate {
/// Revealed nullifiers
pub nullifiers: Vec<Nullifier>,
/// Minted coins
pub coins: Vec<Coin>,
}
/// A transaction's clear input
#[derive(Debug, SerialEncodable, SerialDecodable)]
pub struct ClearInput {
/// Input's value (amount)
pub value: u64,
/// Input's token ID
pub token_id: TokenId,
/// Blinding factor for `value`
pub value_blind: ValueBlind,
/// Blinding factor for `token_id`
pub token_blind: ValueBlind,
/// Public key for the signature
pub signature_public: PublicKey,
}
/// A transaction's anonymous input
#[derive(Debug, SerialEncodable, SerialDecodable)]
pub struct Input {
/// Pedersen commitment for the input's value
pub value_commit: ValueCommit,
/// Pedersen commitment for the input's token ID
pub token_commit: ValueCommit,
/// Revealed nullifier
pub nullifier: Nullifier,
/// Revealed Merkle root
pub merkle_root: MerkleNode,
/// spend hook (TODO: document)
pub spend_hook: pallas::Base,
/// user data enc (TODO: document)
pub user_data_enc: pallas::Base,
/// Public key for the signature
pub signature_public: PublicKey,
}
/// A transaction's anonymous output
#[derive(Debug, SerialEncodable, SerialDecodable)]
pub struct Output {
/// Pedersen commitment for the output's value
pub value_commit: ValueCommit,
/// Pedersen commitment for the output's token ID
pub token_commit: ValueCommit,
/// Minted coin
pub coin: Coin,
/// The encrypted note ciphertext
pub ciphertext: Vec<u8>,
/// The ephemeral public key
pub ephem_public: PublicKey,
}

View File

@@ -0,0 +1,85 @@
/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2022 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use core::{fmt, str::FromStr};
use std::io;
use darkfi_serial::{SerialDecodable, SerialEncodable};
use pasta_curves::{group::ff::PrimeField, pallas};
/// The `Coin` is represented as a base field element.
#[repr(C)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, SerialEncodable, SerialDecodable)]
pub struct Coin(pallas::Base);
impl Coin {
/// Reference the raw inner base field element
pub fn inner(&self) -> pallas::Base {
self.0
}
/// Try to create a `Coin` type from the given 32 bytes.
/// Returns `Some` if the bytes fit in the base field, and `None` if not.
pub fn from_bytes(bytes: [u8; 32]) -> Option<Self> {
let n = pallas::Base::from_repr(bytes);
match bool::from(n.is_some()) {
true => Some(Self(n.unwrap())),
false => None,
}
}
/// Convert the `Coin` type into 32 raw bytes
pub fn to_bytes(&self) -> [u8; 32] {
self.0.to_repr()
}
}
impl From<pallas::Base> for Coin {
fn from(x: pallas::Base) -> Self {
Self(x)
}
}
impl fmt::Display for Coin {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", bs58::encode(self.to_bytes()).into_string())
}
}
impl FromStr for Coin {
type Err = io::Error;
/// Tries to decode a base58 string into a `Coin` type.
/// This string is the same string received by calling `Coin::to_string()`.
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = match bs58::decode(s).into_vec() {
Ok(v) => v,
Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)),
};
if bytes.len() != 32 {
return Err(io::Error::new(io::ErrorKind::Other, "Length of decoded bytes is not 32"))
}
if let Some(coin) = Self::from_bytes(bytes.try_into().unwrap()) {
return Ok(coin)
}
Err(io::Error::new(io::ErrorKind::Other, "Invalid bytes for Coin"))
}
}

View File

@@ -42,6 +42,10 @@ pub use keypair::{Keypair, PublicKey, SecretKey};
pub mod address;
pub use address::Address;
/// Coin definitions and methods
pub mod coin;
pub use coin::Coin;
/// Contract ID definitions and methods
pub mod contract_id;
pub use contract_id::ContractId;