Browse Source

[WIP] contract/consensus: create single proposal proof/call

aggstam 3 years ago
parent
commit
2c6d392c83

+ 156 - 0
src/contract/consensus/proof/consensus_proposal_v1.zk

@@ -0,0 +1,156 @@
+constant "ConsensusProposal_V1" {
+	EcFixedPointShort VALUE_COMMIT_VALUE,
+	EcFixedPoint VALUE_COMMIT_RANDOM,
+	EcFixedPointBase NULLIFIER_K,
+}
+
+witness "ConsensusProposal_V1" {
+	# Burnt coin secret key
+	Base secret_key,
+	# Unique serial number corresponding to the burnt coin
+	Base serial,
+	# The value of the burnt coin
+	Base value,
+	# The epoch the burnt coin was minted on
+	Base epoch,
+	# The reward value
+	Base reward,
+	# Random blinding factor for the value commitment
+	Scalar value_blind,
+	# Random blinding factor for coin
+	Base coin_blind,
+	# Leaf position of the coin in the Merkle tree of coins
+	Uint32 leaf_pos,
+	# Merkle path to the coin
+	MerklePath path,
+	# Random blinding factor for the serial number of the new coin
+	Scalar new_serial_blind,
+	# The epoch the new coin was minted on
+	Base new_epoch,
+	# X coordinate for new coins' public key
+	Base new_pub_x,
+	# Y coordinate for new coins' public key
+	Base new_pub_y,
+	# Random blinding factor for the value commitment of the new coin
+	Scalar new_value_blind,
+	# Random blinding factor for new coin
+	Base new_coin_blind,
+	# Election seed y
+	Base mu_y,
+	# Election seed rho
+	Base mu_rho,
+	# Sigma1
+	Base sigma1,
+	# Sigma2
+	Base sigma2,
+	# Lottery headstart
+	Base headstart,
+}
+
+circuit "ConsensusProposal_V1" {
+	# Constants
+	ZERO = witness_base(0);
+	SERIAL_PREFIX = witness_base(2);
+	SEED_PREFIX = witness_base(3);
+
+	# Poseidon hash of the nullifier
+	nullifier = poseidon_hash(secret_key, serial);
+	constrain_instance(nullifier);
+
+	# Constrain the epoch this coin was minted on
+	constrain_instance(epoch);
+
+	# We derive coins' public key for the signature and
+	# VRF proof and constrain its coordinates:
+	pub = ec_mul_base(secret_key, NULLIFIER_K);
+	pub_x = ec_get_x(pub);
+	pub_y = ec_get_y(pub);
+	constrain_instance(pub_x);
+	constrain_instance(pub_y);
+
+	# Coin hash	
+	C = poseidon_hash(
+		pub_x,
+		pub_y,
+		value,
+		epoch,
+		serial,
+		coin_blind,
+	);
+
+	# Merkle root
+	root = merkle_root(leaf_pos, path, C);
+	constrain_instance(root);
+	
+	# 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:
+	constrain_instance(ec_get_x(value_commit));
+	constrain_instance(ec_get_y(value_commit));
+
+	# Derive new coin serial from burnt one and constrain the pedersen commitment
+	new_serial = poseidon_hash(SERIAL_PREFIX, secret_key, serial, ZERO);
+	scv = ec_mul_base(new_serial, NULLIFIER_K);
+	scr = ec_mul(new_serial_blind, VALUE_COMMIT_RANDOM);
+	serial_commit = ec_add(scv, scr);
+	# Since serial commit is also a curve point, we'll do the same
+	# coordinate dance:
+	constrain_instance(ec_get_x(serial_commit));
+	constrain_instance(ec_get_y(serial_commit));
+
+	# Constrain reward value
+	constrain_instance(reward);
+
+	# Pedersen commitment for new coin's value
+	new_value = base_add(value, reward);
+	nvcv = ec_mul_short(new_value, VALUE_COMMIT_VALUE);
+	nvcr = ec_mul(new_value_blind, VALUE_COMMIT_RANDOM);
+	new_value_commit = ec_add(nvcv, nvcr);
+	# Since the new value commit is also a curve point, we'll do the same
+	# coordinate dance:
+	constrain_instance(ec_get_x(new_value_commit));
+	constrain_instance(ec_get_y(new_value_commit));
+	
+	# Constrain the epoch the new coin was minted on
+	constrain_instance(new_epoch);
+
+	# Poseidon hash of the new coin
+	new_coin = poseidon_hash(
+		new_pub_x,
+		new_pub_y,
+		new_value,
+		new_epoch,
+		new_serial,
+		new_coin_blind,
+	);
+	constrain_instance(new_coin);
+
+	# Coin y:
+	seed = poseidon_hash(SEED_PREFIX, serial, ZERO);
+	y = poseidon_hash(seed, mu_y);
+	constrain_instance(mu_y);
+	constrain_instance(y);
+
+	# Coin rho(seed):
+	rho = poseidon_hash(seed, mu_rho);
+	constrain_instance(mu_rho);
+	constrain_instance(rho);
+
+	# Calculate lottery target
+	term_1 = base_mul(sigma1, value);
+	term_2 = base_mul(sigma2, value);
+	shifted_term_2 = base_mul(term_2, value);
+	target = base_add(term_1, shifted_term_2);
+	shifted_target = base_add(target, headstart);
+	constrain_instance(sigma1);
+	constrain_instance(sigma2);
+	constrain_instance(headstart);
+
+	# Play lottery
+	less_than_loose(y, shifted_target);
+
+	# At this point we've enforced all of our public inputs.
+}

+ 9 - 1
src/contract/consensus/src/client/genesis_stake_v1.rs

@@ -88,7 +88,15 @@ impl ConsensusGenesisStakeCallBuilder {
             create_consensus_mint_proof(&self.mint_zkbin, &self.mint_pk, &output)?;
 
         // Encrypted note
-        let note = ConsensusNote { serial, value: output.value, epoch, coin_blind, value_blind };
+        let note = ConsensusNote {
+            serial,
+            value: output.value,
+            epoch,
+            coin_blind,
+            value_blind,
+            reward: 0,
+            reward_blind: value_blind,
+        };
 
         let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
 

+ 3 - 0
src/contract/consensus/src/client/mod.rs

@@ -39,5 +39,8 @@ pub mod stake_v1;
 /// and `Consensus::ProposalMintV1` contract calls.
 pub mod proposal_v1;
 
+/// Proposal transaction building API.
+pub mod proposal_v1_2;
+
 /// `Consensus::UnstakeV1` API
 pub mod unstake_v1;

+ 349 - 0
src/contract/consensus/src/client/proposal_v1_2.rs

@@ -0,0 +1,349 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2023 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/>.
+ */
+
+//! This API is crufty. Please rework it into something nice to read and nice to use.
+
+use darkfi::{
+    consensus::{constants::EPOCH_LENGTH, SlotCheckpoint},
+    zk::{halo2::Value, Proof, ProvingKey, Witness, ZkCircuit},
+    zkas::ZkBinary,
+    Result,
+};
+use darkfi_money_contract::{
+    client::{ConsensusNote, ConsensusOwnCoin},
+    model::{ConsensusInput, ConsensusOutput, PALLAS_ZERO},
+};
+use darkfi_sdk::{
+    crypto::{
+        ecvrf::VrfProof, note::AeadEncryptedNote, pasta_prelude::*, pedersen_commitment_base,
+        pedersen_commitment_u64, poseidon_hash, Coin, MerkleNode, MerkleTree, Nullifier, PublicKey,
+        SecretKey,
+    },
+    incrementalmerkletree::{Hashable, Tree},
+    pasta::{group::ff::FromUniformBytes, pallas},
+};
+use log::debug;
+use rand::rngs::OsRng;
+
+use crate::{
+    client::common::{ConsensusBurnInputInfo, ConsensusMintOutputInfo},
+    model::{
+        ConsensusProposalParamsV1, HEADSTART, MU_RHO_PREFIX, MU_Y_PREFIX, REWARD, REWARD_PALLAS,
+        SEED_PREFIX, SERIAL_PREFIX,
+    },
+};
+
+pub struct ConsensusProposalCallDebris {
+    pub params: ConsensusProposalParamsV1,
+    pub proofs: Vec<Proof>,
+    pub signature_secret: SecretKey,
+}
+
+pub struct ConsensusProposalRevealed {
+    pub nullifier: Nullifier,
+    pub epoch: u64,
+    pub public_key: PublicKey,
+    pub merkle_root: MerkleNode,
+    pub value_commit: pallas::Point,
+    pub new_serial: pallas::Base,
+    pub new_serial_commit: pallas::Point,
+    pub new_value_commit: pallas::Point,
+    pub new_epoch: u64,
+    pub new_coin: Coin,
+    pub vrf_proof: VrfProof,
+    pub mu_y: pallas::Base,
+    pub y: pallas::Base,
+    pub mu_rho: pallas::Base,
+    pub rho: pallas::Base,
+    pub sigma1: pallas::Base,
+    pub sigma2: pallas::Base,
+}
+
+impl ConsensusProposalRevealed {
+    pub fn to_vec(&self) -> Vec<pallas::Base> {
+        let epoch_palas = pallas::Base::from(self.epoch);
+        let (pub_x, pub_y) = self.public_key.xy();
+        let value_coords = self.value_commit.to_affine().coordinates().unwrap();
+        let new_serial_coords = self.new_serial_commit.to_affine().coordinates().unwrap();
+        let reward_pallas = pallas::Base::from(REWARD);
+        let new_value_coords = self.new_value_commit.to_affine().coordinates().unwrap();
+        let new_epoch_palas = pallas::Base::from(self.epoch);
+
+        // NOTE: It's important to keep these in the same order
+        // as the `constrain_instance` calls in the zkas code.
+        vec![
+            self.nullifier.inner(),
+            epoch_palas,
+            pub_x,
+            pub_y,
+            self.merkle_root.inner(),
+            *value_coords.x(),
+            *value_coords.y(),
+            *new_serial_coords.x(),
+            *new_serial_coords.y(),
+            reward_pallas,
+            *new_value_coords.x(),
+            *new_value_coords.y(),
+            new_epoch_palas,
+            self.new_coin.inner(),
+            self.mu_y,
+            self.y,
+            self.mu_rho,
+            self.rho,
+            self.sigma1,
+            self.sigma2,
+            HEADSTART,
+        ]
+    }
+}
+
+/// Struct holding necessary information to build a proposal transaction.
+pub struct ConsensusProposalCallBuilder {
+    /// `ConsensusOwnCoin` we're given to use in this builder
+    pub coin: ConsensusOwnCoin,
+    /// Rewarded slot checkpoint
+    pub slot_checkpoint: SlotCheckpoint,
+    /// Merkle tree of coins used to create inclusion proofs
+    pub tree: MerkleTree,
+    /// `Proposal_V1` zkas circuit ZkBinary
+    pub proposal_zkbin: ZkBinary,
+    /// Proving key for the `Proposal_V1` zk circuit
+    pub proposal_pk: ProvingKey,
+}
+
+impl ConsensusProposalCallBuilder {
+    pub fn build(&self) -> Result<ConsensusProposalCallDebris> {
+        debug!("Building Consensus::ProposalBurnV1 contract call for proposal");
+        let value = self.coin.note.value;
+        assert!(value != 0);
+        let epoch = self.slot_checkpoint.slot / EPOCH_LENGTH as u64;
+
+        debug!("Building Consensus::ProposalV1 anonymous input");
+        let leaf_position = self.coin.leaf_position;
+        let root = self.tree.root(0).unwrap();
+        let merkle_path = self.tree.authentication_path(leaf_position, &root).unwrap();
+        let input = ConsensusBurnInputInfo {
+            leaf_position,
+            merkle_path,
+            secret: self.coin.secret,
+            note: self.coin.note.clone(),
+            value_blind: pallas::Scalar::random(&mut OsRng),
+        };
+
+        debug!("Building anonymous output");
+        let reward_blind = pallas::Scalar::random(&mut OsRng);
+        let new_value_blind = input.value_blind + reward_blind;
+        let new_coin_blind = pallas::Base::random(&mut OsRng);
+        let output = ConsensusMintOutputInfo {
+            value: self.coin.note.value + REWARD,
+            epoch: self.coin.note.epoch,
+            public_key: PublicKey::from_secret(self.coin.secret),
+            value_blind: new_value_blind,
+            serial: self.coin.note.serial,
+            coin_blind: new_coin_blind,
+        };
+        debug!("Finished building output");
+
+        debug!("Building Consensus::ProposalV1 contract call for proposal");
+        let (proof, public_inputs) = create_proposal_proof(
+            &self.proposal_zkbin,
+            &self.proposal_pk,
+            &input,
+            &output,
+            &self.slot_checkpoint,
+        )?;
+
+        let input = ConsensusInput {
+            epoch: self.coin.note.epoch,
+            value_commit: public_inputs.value_commit,
+            nullifier: public_inputs.nullifier,
+            merkle_root: public_inputs.merkle_root,
+            signature_public: public_inputs.public_key,
+        };
+
+        // Encrypted note
+        let note = ConsensusNote {
+            serial: public_inputs.new_serial,
+            value: output.value,
+            epoch,
+            coin_blind: new_coin_blind,
+            value_blind: new_value_blind,
+            reward: REWARD,
+            reward_blind,
+        };
+
+        let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
+
+        let output = ConsensusOutput {
+            value_commit: public_inputs.new_value_commit,
+            coin: public_inputs.new_coin,
+            note: encrypted_note,
+        };
+
+        // We now fill this with necessary stuff
+        let new_serial_commit = public_inputs.new_serial_commit;
+        let slot = self.slot_checkpoint.slot;
+        let vrf_proof = public_inputs.vrf_proof;
+        let y = public_inputs.y;
+        let rho = public_inputs.rho;
+        let params = ConsensusProposalParamsV1 {
+            input,
+            output,
+            reward: REWARD,
+            reward_blind,
+            new_serial_commit,
+            slot,
+            vrf_proof,
+            y,
+            rho,
+        };
+        let proofs = vec![proof];
+
+        // Now we should have all the params, zk proofs and signature secret.
+        // We return it all and let the caller deal with it.
+        let debris =
+            ConsensusProposalCallDebris { params, proofs, signature_secret: self.coin.secret };
+        Ok(debris)
+    }
+}
+
+pub fn create_proposal_proof(
+    zkbin: &ZkBinary,
+    pk: &ProvingKey,
+    input: &ConsensusBurnInputInfo,
+    output: &ConsensusMintOutputInfo,
+    slot_checkpoint: &SlotCheckpoint,
+) -> Result<(Proof, ConsensusProposalRevealed)> {
+    // Proof parameters
+    let nullifier = Nullifier::from(poseidon_hash([input.secret.inner(), input.note.serial]));
+    let epoch = input.note.epoch;
+    let epoch_pallas = pallas::Base::from(epoch);
+    let value_pallas = pallas::Base::from(input.note.value);
+    let value_commit = pedersen_commitment_u64(input.note.value, input.value_blind);
+    let public_key = PublicKey::from_secret(input.secret);
+    let (pub_x, pub_y) = public_key.xy();
+
+    // Burnt coin and its merkle_root
+    let coin = poseidon_hash([
+        pub_x,
+        pub_y,
+        value_pallas,
+        epoch_pallas,
+        input.note.serial,
+        input.note.coin_blind,
+    ]);
+
+    let merkle_root = {
+        let position: u64 = input.leaf_position.into();
+        let mut current = MerkleNode::from(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
+    };
+
+    // New coin
+    let new_serial =
+        poseidon_hash([SERIAL_PREFIX, input.secret.inner(), input.note.serial, PALLAS_ZERO]);
+    let new_serial_blind = pallas::Scalar::random(&mut OsRng);
+    let new_serial_commit = pedersen_commitment_base(new_serial, new_serial_blind);
+    let new_value_commit = pedersen_commitment_u64(output.value, output.value_blind);
+    let new_epoch_pallas = pallas::Base::from(output.epoch);
+    let new_value_pallas = pallas::Base::from(output.value);
+    let (new_pub_x, new_pub_y) = output.public_key.xy();
+
+    let new_coin = Coin::from(poseidon_hash([
+        new_pub_x,
+        new_pub_y,
+        new_value_pallas,
+        new_epoch_pallas,
+        output.serial,
+        output.coin_blind,
+    ]));
+
+    let slot_pallas = pallas::Base::from(slot_checkpoint.slot);
+    let seed = poseidon_hash([SEED_PREFIX, input.note.serial, PALLAS_ZERO]);
+    // NOTE: slot checkpoint eta to be renamed to previous_eta,
+    //       corresponding to previous block eta.
+    let mut vrf_input = [0u8; 64];
+    vrf_input[..32].copy_from_slice(&slot_checkpoint.eta.to_repr());
+    vrf_input[32..].copy_from_slice(&slot_pallas.to_repr());
+    let vrf_proof = VrfProof::prove(input.secret.into(), &vrf_input, &mut OsRng);
+    let mut eta = [0u8; 64];
+    eta[..blake3::OUT_LEN].copy_from_slice(vrf_proof.hash_output().as_bytes());
+    let eta = pallas::Base::from_uniform_bytes(&eta);
+    let mu_y = poseidon_hash([MU_Y_PREFIX, eta, slot_pallas]);
+    let y = poseidon_hash([seed, mu_y]);
+    let mu_rho = poseidon_hash([MU_RHO_PREFIX, eta, slot_pallas]);
+    let rho = poseidon_hash([seed, mu_rho]);
+    let (sigma1, sigma2) = (slot_checkpoint.sigma1, slot_checkpoint.sigma2);
+
+    // Generate public inputs, witnesses and proof
+    let public_inputs = ConsensusProposalRevealed {
+        nullifier,
+        epoch,
+        public_key,
+        merkle_root,
+        value_commit,
+        new_serial,
+        new_serial_commit,
+        new_value_commit,
+        new_epoch: output.epoch,
+        new_coin,
+        vrf_proof,
+        mu_y,
+        y,
+        mu_rho,
+        rho,
+        sigma1,
+        sigma2,
+    };
+
+    let prover_witnesses = vec![
+        Witness::Base(Value::known(input.secret.inner())),
+        Witness::Base(Value::known(input.note.serial)),
+        Witness::Base(Value::known(pallas::Base::from(input.note.value))),
+        Witness::Base(Value::known(epoch_pallas)),
+        Witness::Base(Value::known(REWARD_PALLAS)),
+        Witness::Scalar(Value::known(input.value_blind)),
+        Witness::Base(Value::known(input.note.coin_blind)),
+        Witness::Uint32(Value::known(u64::from(input.leaf_position).try_into().unwrap())),
+        Witness::MerklePath(Value::known(input.merkle_path.clone().try_into().unwrap())),
+        Witness::Scalar(Value::known(new_serial_blind)),
+        Witness::Base(Value::known(new_epoch_pallas)),
+        Witness::Base(Value::known(new_pub_x)),
+        Witness::Base(Value::known(new_pub_y)),
+        Witness::Scalar(Value::known(output.value_blind)),
+        Witness::Base(Value::known(output.coin_blind)),
+        Witness::Base(Value::known(mu_y)),
+        Witness::Base(Value::known(mu_rho)),
+        Witness::Base(Value::known(sigma1)),
+        Witness::Base(Value::known(sigma2)),
+        Witness::Base(Value::known(HEADSTART)),
+    ];
+
+    let circuit = ZkCircuit::new(prover_witnesses, zkbin.clone());
+    let proof = Proof::create(pk, &[circuit], &public_inputs.to_vec(), &mut OsRng)?;
+
+    Ok((proof, public_inputs))
+}

+ 2 - 0
src/contract/consensus/src/client/stake_v1.rs

@@ -95,6 +95,8 @@ impl ConsensusStakeCallBuilder {
             epoch: self.epoch,
             coin_blind,
             value_blind: self.value_blind,
+            reward: 0,
+            reward_blind: self.value_blind,
         };
 
         let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;

+ 25 - 1
src/contract/consensus/src/entrypoint.rs

@@ -32,7 +32,10 @@ use darkfi_sdk::{
 };
 use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
 
-use crate::{model::ConsensusProposalRewardUpdateV1, ConsensusFunction};
+use crate::{
+    model::{ConsensusProposalRewardUpdateV1, ConsensusProposalUpdateV1},
+    ConsensusFunction,
+};
 
 /// `Consensus::GenesisStake` functions
 mod genesis_stake_v1;
@@ -68,6 +71,13 @@ use proposal_mint_v1::{
     consensus_proposal_mint_process_update_v1,
 };
 
+/// `Consensus::ProposalV1` functions
+mod proposal_v1;
+use proposal_v1::{
+    consensus_proposal_get_metadata_v1, consensus_proposal_process_instruction_v1,
+    consensus_proposal_process_update_v1,
+};
+
 /// `Consensus::Unstake` functions
 mod unstake_v1;
 use unstake_v1::{
@@ -97,6 +107,7 @@ fn init_contract(cid: ContractId, _ix: &[u8]) -> ContractResult {
     let consensus_burn_v1_bincode = include_bytes!("../proof/consensus_burn_v1.zk.bin");
     let proposal_reward_v1_bincode = include_bytes!("../proof/proposal_reward_v1.zk.bin");
     let proposal_mint_v1_bincode = include_bytes!("../proof/proposal_mint_v1.zk.bin");
+    let consensus_proposal_v1_bincode = include_bytes!("../proof/consensus_proposal_v1.zk.bin");
 
     // For that, we use `zkas_db_set` and pass in the bincode.
     zkas_db_set(&money_mint_v1_bincode[..])?;
@@ -105,6 +116,7 @@ fn init_contract(cid: ContractId, _ix: &[u8]) -> ContractResult {
     zkas_db_set(&consensus_burn_v1_bincode[..])?;
     zkas_db_set(&proposal_reward_v1_bincode[..])?;
     zkas_db_set(&proposal_mint_v1_bincode[..])?;
+    zkas_db_set(&consensus_proposal_v1_bincode[..])?;
 
     // Set up a database tree to hold Merkle roots of all coins
     // k=MerkleNode, v=[]
@@ -187,6 +199,10 @@ fn get_metadata(cid: ContractId, ix: &[u8]) -> ContractResult {
             let metadata = consensus_proposal_mint_get_metadata_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&metadata)?)
         }
+        ConsensusFunction::ProposalV1 => {
+            let metadata = consensus_proposal_get_metadata_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&metadata)?)
+        }
         ConsensusFunction::UnstakeV1 => {
             let metadata = consensus_unstake_get_metadata_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&metadata)?)
@@ -231,6 +247,10 @@ fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
             let update_data = consensus_proposal_mint_process_instruction_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&update_data)?)
         }
+        ConsensusFunction::ProposalV1 => {
+            let update_data = consensus_proposal_process_instruction_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&update_data)?)
+        }
         ConsensusFunction::UnstakeV1 => {
             let update_data = consensus_unstake_process_instruction_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&update_data)?)
@@ -265,6 +285,10 @@ fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult {
             let update: ConsensusStakeUpdateV1 = deserialize(&update_data[1..])?;
             Ok(consensus_proposal_mint_process_update_v1(cid, update)?)
         }
+        ConsensusFunction::ProposalV1 => {
+            let update: ConsensusProposalUpdateV1 = deserialize(&update_data[1..])?;
+            Ok(consensus_proposal_process_update_v1(cid, update)?)
+        }
         ConsensusFunction::UnstakeV1 => {
             let update: ConsensusUnstakeUpdateV1 = deserialize(&update_data[1..])?;
             Ok(consensus_unstake_process_update_v1(cid, update)?)

+ 243 - 0
src/contract/consensus/src/entrypoint/proposal_v1.rs

@@ -0,0 +1,243 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2023 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_money_contract::{
+    error::MoneyError, CONSENSUS_CONTRACT_COINS_TREE, CONSENSUS_CONTRACT_COIN_MERKLE_TREE,
+    CONSENSUS_CONTRACT_COIN_ROOTS_TREE, CONSENSUS_CONTRACT_INFO_TREE,
+    CONSENSUS_CONTRACT_NULLIFIERS_TREE, CONSENSUS_CONTRACT_ZKAS_PROPOSAL_NS_V1,
+};
+use darkfi_sdk::{
+    crypto::{pasta_prelude::*, pedersen_commitment_u64, poseidon_hash, ContractId, MerkleNode},
+    db::{db_contains_key, db_lookup, db_set},
+    error::{ContractError, ContractResult},
+    merkle_add, msg,
+    pasta::{group::ff::FromUniformBytes, pallas},
+    util::{get_slot_checkpoint, get_verifying_slot_epoch},
+    ContractCall,
+};
+use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
+
+use crate::{
+    error::ConsensusError,
+    model::{
+        ConsensusProposalParamsV1, ConsensusProposalUpdateV1, SlotCheckpoint, HEADSTART,
+        MU_RHO_PREFIX, MU_Y_PREFIX,
+    },
+    ConsensusFunction,
+};
+
+/// `get_metadata` function for `Consensus::ProposalV1`
+pub(crate) fn consensus_proposal_get_metadata_v1(
+    _cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: ConsensusProposalParamsV1 = deserialize(&self_.data[1..])?;
+
+    // Public inputs for the ZK proofs we have to verify
+    let mut zk_public_inputs: Vec<(String, Vec<pallas::Base>)> = vec![];
+    // Public keys for the transaction signatures we have to verify
+    let signature_pubkeys = vec![params.input.signature_public];
+
+    // Grab the nullifier for the burnt coin
+    let nullifier = &params.input.nullifier;
+
+    // Grab the mint epoch pallas for the burnt coin
+    let epoch_pallas = pallas::Base::from(params.input.epoch);
+
+    // Grab the public key coordinates for the burnt coin
+    let (pub_x, pub_y) = &params.input.signature_public.xy();
+
+    // Grab the burnt coin merkle root
+    let merkle_root = params.input.merkle_root.inner();
+
+    // Grab the pedersen commitment for the burnt value
+    let value_coords = &params.input.value_commit.to_affine().coordinates().unwrap();
+
+    // Grab the pedersen commitment for the minted serial number
+    let new_serial_coords = &params.new_serial_commit.to_affine().coordinates().unwrap();
+
+    // Grab the reward pallas
+    let reward_pallas = pallas::Base::from(params.reward);
+
+    // Grab the pedersen commitment for the minted value
+    let new_value_coords = &params.output.value_commit.to_affine().coordinates().unwrap();
+
+    // Grab the minting epoch of the verifying slot
+    let new_epoch = get_verifying_slot_epoch();
+
+    // Grab the new coin
+    let new_coin = params.output.coin.inner();
+
+    // Grab proposal coin y and rho for lottery
+    let y = &params.y;
+    let rho = &params.rho;
+
+    // Grab the slot checkpoint to validate consensus parameters against
+    let slot = &params.slot;
+    let Some(slot_checkpoint) = get_slot_checkpoint(*slot)? else {
+        msg!("[ConsensusProposalV1] Error: Missing slot checkpoint {} from db", slot);
+        return Err(ConsensusError::ProposalMissingSlotCheckpoint.into())
+    };
+    let slot_checkpoint: SlotCheckpoint = deserialize(&slot_checkpoint)?;
+
+    // Verify eta VRF proof
+    let slot_pallas = pallas::Base::from(slot_checkpoint.slot);
+    // NOTE: slot checkpoint eta to be renamed to previous_eta,
+    //       corresponding to previous block eta.
+    let mut vrf_input = [0u8; 64];
+    vrf_input[..32].copy_from_slice(&slot_checkpoint.eta.to_repr());
+    vrf_input[32..].copy_from_slice(&slot_pallas.to_repr());
+    let vrf_proof = &params.vrf_proof;
+    if !vrf_proof.verify(params.input.signature_public, &vrf_input) {
+        msg!("[ConsensusProposalV1] Error: eta VRF proof couldn't be verified");
+        return Err(ConsensusError::ProposalErroneousVrfProof.into())
+    }
+    let mut eta = [0u8; 64];
+    eta[..blake3::OUT_LEN].copy_from_slice(vrf_proof.hash_output().as_bytes());
+    let eta = pallas::Base::from_uniform_bytes(&eta);
+
+    // Calculate election seeds
+    let mu_y = poseidon_hash([MU_Y_PREFIX, eta, slot_pallas]);
+    let mu_rho = poseidon_hash([MU_RHO_PREFIX, eta, slot_pallas]);
+
+    // Grab sigmas from slot checkpoint
+    let (sigma1, sigma2) = (slot_checkpoint.sigma1, slot_checkpoint.sigma2);
+
+    zk_public_inputs.push((
+        CONSENSUS_CONTRACT_ZKAS_PROPOSAL_NS_V1.to_string(),
+        vec![
+            nullifier.inner(),
+            epoch_pallas,
+            *pub_x,
+            *pub_y,
+            merkle_root,
+            *value_coords.x(),
+            *value_coords.y(),
+            *new_serial_coords.x(),
+            *new_serial_coords.y(),
+            reward_pallas,
+            *new_value_coords.x(),
+            *new_value_coords.y(),
+            new_epoch.into(),
+            new_coin,
+            mu_y,
+            *y,
+            mu_rho,
+            *rho,
+            sigma1,
+            sigma2,
+            HEADSTART,
+        ],
+    ));
+
+    // Serialize everything gathered and return it
+    let mut metadata = vec![];
+    zk_public_inputs.encode(&mut metadata)?;
+    signature_pubkeys.encode(&mut metadata)?;
+
+    Ok(metadata)
+}
+
+/// `process_instruction` function for `Consensus::ProposalV1`
+pub(crate) fn consensus_proposal_process_instruction_v1(
+    cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: ConsensusProposalParamsV1 = deserialize(&self_.data[1..])?;
+
+    // Access the necessary databases where there is information to
+    // validate this state transition.
+    let nullifiers_db = db_lookup(cid, CONSENSUS_CONTRACT_NULLIFIERS_TREE)?;
+    let coins_db = db_lookup(cid, CONSENSUS_CONTRACT_COINS_TREE)?;
+    let coin_roots_db = db_lookup(cid, CONSENSUS_CONTRACT_COIN_ROOTS_TREE)?;
+
+    // ===================================
+    // Perform the actual state transition
+    // ===================================
+
+    msg!("[ConsensusProposalV1] Validating anonymous input");
+    let input = &params.input;
+    let output = &params.output;
+
+    // The Merkle root is used to know whether this is a coin that
+    // existed in a previous state.
+    if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
+        msg!("[ConsensusProposalV1] Error: Merkle root not found in previous state");
+        return Err(MoneyError::TransferMerkleRootNotFound.into())
+    }
+
+    // The nullifier should not already exist. It is the double-spend protection.
+    if db_contains_key(nullifiers_db, &serialize(&input.nullifier))? {
+        msg!("[ConsensusProposalV1] Error: Duplicate nullifier found");
+        return Err(MoneyError::DuplicateNullifier.into())
+    }
+
+    // Verify value commits match between burnt and mint inputs
+    let mut valcom_total = pallas::Point::identity();
+    valcom_total += input.value_commit;
+    valcom_total += pedersen_commitment_u64(params.reward, params.reward_blind);
+    valcom_total -= output.value_commit;
+    if valcom_total != pallas::Point::identity() {
+        msg!("[ConsensusProposalV1] Error: Value commitments do not result in identity");
+        return Err(MoneyError::ValueMismatch.into())
+    }
+
+    // Newly created coin for this call is in the output. Here we gather it,
+    // and we also check that it hasn't existed before.
+    if db_contains_key(coins_db, &serialize(&output.coin))? {
+        msg!("[ConsensusProposalV1] Error: Duplicate coin found in output");
+        return Err(MoneyError::DuplicateCoin.into())
+    }
+
+    // At this point the state transition has passed, so we create a state update
+    let update = ConsensusProposalUpdateV1 { nullifier: input.nullifier, coin: output.coin };
+    let mut update_data = vec![];
+    update_data.write_u8(ConsensusFunction::UnstakeV1 as u8)?;
+    update.encode(&mut update_data)?;
+
+    // and return it
+    Ok(update_data)
+}
+
+/// `process_update` function for `Consensus::ProposalV1`
+pub(crate) fn consensus_proposal_process_update_v1(
+    cid: ContractId,
+    update: ConsensusProposalUpdateV1,
+) -> ContractResult {
+    // Grab all necessary db handles for where we want to write
+    let nullifiers_db = db_lookup(cid, CONSENSUS_CONTRACT_NULLIFIERS_TREE)?;
+    let info_db = db_lookup(cid, CONSENSUS_CONTRACT_INFO_TREE)?;
+    let coins_db = db_lookup(cid, CONSENSUS_CONTRACT_COINS_TREE)?;
+    let coin_roots_db = db_lookup(cid, CONSENSUS_CONTRACT_COIN_ROOTS_TREE)?;
+
+    msg!("[ConsensusProposalV1] Adding new nullifier to the set");
+    db_set(nullifiers_db, &serialize(&update.nullifier), &[])?;
+
+    msg!("[ConsensusProposalV1] Adding new coin to the set");
+    db_set(coins_db, &serialize(&update.coin), &[])?;
+
+    msg!("[ConsensusProposalV1] Adding new coin to the Merkle tree");
+    let coins: Vec<_> = vec![MerkleNode::from(update.coin.inner())];
+    merkle_add(info_db, coin_roots_db, &serialize(&CONSENSUS_CONTRACT_COIN_MERKLE_TREE), &coins)?;
+
+    Ok(())
+}

+ 2 - 0
src/contract/consensus/src/lib.rs

@@ -30,6 +30,7 @@ pub enum ConsensusFunction {
     ProposalRewardV1 = 0x03,
     ProposalMintV1 = 0x04,
     UnstakeV1 = 0x05,
+    ProposalV1 = 0x06,
 }
 
 impl TryFrom<u8> for ConsensusFunction {
@@ -43,6 +44,7 @@ impl TryFrom<u8> for ConsensusFunction {
             0x03 => Ok(Self::ProposalRewardV1),
             0x04 => Ok(Self::ProposalMintV1),
             0x05 => Ok(Self::UnstakeV1),
+            0x06 => Ok(Self::ProposalV1),
             _ => Err(ContractError::InvalidFunction),
         }
     }

+ 33 - 1
src/contract/consensus/src/model.rs

@@ -18,7 +18,7 @@
 
 use darkfi_money_contract::model::{ClearInput, ConsensusInput, ConsensusOutput, Input, Output};
 use darkfi_sdk::{
-    crypto::{ecvrf::VrfProof, PublicKey},
+    crypto::{ecvrf::VrfProof, Coin, Nullifier, PublicKey},
     pasta::pallas,
 };
 use darkfi_serial::{SerialDecodable, SerialEncodable};
@@ -81,6 +81,38 @@ pub struct ConsensusProposalMintParamsV1 {
 #[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
 pub struct ConsensusProposalRewardUpdateV1 {}
 
+/// Parameters for `Consensus::Proposal`
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct ConsensusProposalParamsV1 {
+    /// Anonymous input
+    pub input: ConsensusInput,
+    /// Anonymous output
+    pub output: ConsensusOutput,
+    /// Reward value
+    pub reward: u64,
+    /// Blinding factor for reward value
+    pub reward_blind: pallas::Scalar,
+    /// Pedersen commitment for the output's serial number
+    pub new_serial_commit: pallas::Point,
+    /// Rewarded slot
+    pub slot: u64,
+    /// VRF proof for eta calculation
+    pub vrf_proof: VrfProof,
+    /// Coin y
+    pub y: pallas::Base,
+    /// Lottery rho used
+    pub rho: pallas::Base,
+}
+
+/// State update for `Consensus::Proposal`
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct ConsensusProposalUpdateV1 {
+    /// Revealed nullifier
+    pub nullifier: Nullifier,
+    /// The newly minted coin
+    pub coin: Coin,
+}
+
 // Consensus parameters configuration.
 // Note: Always verify `pallas::Base` are correct, in case of changes,
 // using pallas_constants tool.

+ 36 - 33
src/contract/consensus/tests/genesis_stake_unstake.rs

@@ -110,39 +110,42 @@ async fn consensus_contract_genesis_stake_unstake() -> Result<()> {
 
     // Verify values match
     assert!(ALICE_INITIAL == alice_staked_oc.note.value);
-    /*
-        // We simulate the proposal of genesis slot
-        let slot_checkpoint = th.get_slot_checkpoints_by_slot(current_slot).await?;
-
-        // With alice's current coin value she can become the slot proposer,
-        // so she creates a proposal transaction to burn her staked coin,
-        // reward herself and mint the new coin.
-        info!(target: "consensus", "[Alice] ====================");
-        info!(target: "consensus", "[Alice] Building proposal tx");
-        info!(target: "consensus", "[Alice] ====================");
-        let (proposal_tx, proposal_params) =
-            th.proposal(Holder::Alice, slot_checkpoint, alice_staked_oc.clone())?;
-
-        info!(target: "consensus", "[Faucet] ===========================");
-        info!(target: "consensus", "[Faucet] Executing Alice proposal tx");
-        info!(target: "consensus", "[Faucet] ===========================");
-        th.execute_proposal_tx(Holder::Faucet, proposal_tx.clone(), &proposal_params, current_slot)
-            .await?;
-
-        info!(target: "consensus", "[Alice] ===========================");
-        info!(target: "consensus", "[Alice] Executing Alice proposal tx");
-        info!(target: "consensus", "[Alice] ===========================");
-        th.execute_proposal_tx(Holder::Alice, proposal_tx, &proposal_params, current_slot).await?;
-
-        th.assert_trees();
-
-        // Gather new staked owncoin which includes the reward
-        let alice_rewarded_staked_oc =
-            th.gather_consensus_owncoin(Holder::Alice, proposal_params.output)?;
-
-        // Verify values match
-        assert!((alice_staked_oc.note.value + REWARD) == alice_rewarded_staked_oc.note.value);
-    */
+
+    // We simulate the proposal of genesis slot
+    let slot_checkpoint = th.get_slot_checkpoints_by_slot(current_slot).await?;
+
+    // With alice's current coin value she can become the slot proposer,
+    // so she creates a proposal transaction to burn her staked coin,
+    // reward herself and mint the new coin.
+    info!(target: "consensus", "[Alice] ====================");
+    info!(target: "consensus", "[Alice] Building proposal tx");
+    info!(target: "consensus", "[Alice] ====================");
+    let (proposal_tx, proposal_params, proposal_secret_key) =
+        th.proposal(Holder::Alice, slot_checkpoint, alice_staked_oc.clone())?;
+
+    info!(target: "consensus", "[Faucet] ===========================");
+    info!(target: "consensus", "[Faucet] Executing Alice proposal tx");
+    info!(target: "consensus", "[Faucet] ===========================");
+    th.execute_proposal_tx(Holder::Faucet, proposal_tx.clone(), &proposal_params, current_slot)
+        .await?;
+
+    info!(target: "consensus", "[Alice] ===========================");
+    info!(target: "consensus", "[Alice] Executing Alice proposal tx");
+    info!(target: "consensus", "[Alice] ===========================");
+    th.execute_proposal_tx(Holder::Alice, proposal_tx, &proposal_params, current_slot).await?;
+
+    th.assert_trees();
+
+    // Gather new staked owncoin which includes the reward
+    let alice_rewarded_staked_oc = th.gather_consensus_owncoin(
+        Holder::Alice,
+        proposal_params.output,
+        Some(proposal_secret_key),
+    )?;
+
+    // Verify values match
+    assert!((alice_staked_oc.note.value + REWARD) == alice_rewarded_staked_oc.note.value);
+
     let alice_rewarded_staked_oc = alice_staked_oc;
 
     // Now Alice can unstake her owncoin

+ 28 - 57
src/contract/consensus/tests/harness.rs

@@ -47,10 +47,10 @@ use rand::rngs::OsRng;
 use darkfi_consensus_contract::{
     client::{
         genesis_stake_v1::ConsensusGenesisStakeCallBuilder,
-        proposal_v1::ConsensusProposalCallBuilder, stake_v1::ConsensusStakeCallBuilder,
+        proposal_v1_2::ConsensusProposalCallBuilder, stake_v1::ConsensusStakeCallBuilder,
         unstake_v1::ConsensusUnstakeCallBuilder,
     },
-    model::{ConsensusGenesisStakeParamsV1, ConsensusProposalMintParamsV1},
+    model::{ConsensusGenesisStakeParamsV1, ConsensusProposalParamsV1},
     ConsensusFunction,
 };
 use darkfi_money_contract::{
@@ -63,8 +63,9 @@ use darkfi_money_contract::{
         Output,
     },
     MoneyFunction, CONSENSUS_CONTRACT_ZKAS_BURN_NS_V1, CONSENSUS_CONTRACT_ZKAS_MINT_NS_V1,
-    CONSENSUS_CONTRACT_ZKAS_PROPOSAL_MINT_NS_V1, CONSENSUS_CONTRACT_ZKAS_PROPOSAL_REWARD_NS_V1,
-    MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+    CONSENSUS_CONTRACT_ZKAS_PROPOSAL_MINT_NS_V1, CONSENSUS_CONTRACT_ZKAS_PROPOSAL_NS_V1,
+    CONSENSUS_CONTRACT_ZKAS_PROPOSAL_REWARD_NS_V1, MONEY_CONTRACT_ZKAS_BURN_NS_V1,
+    MONEY_CONTRACT_ZKAS_MINT_NS_V1,
 };
 
 pub fn init_logger() {
@@ -238,6 +239,7 @@ impl ConsensusTestHarness {
         mkpk!(CONSENSUS_CONTRACT_ZKAS_BURN_NS_V1);
         mkpk!(CONSENSUS_CONTRACT_ZKAS_PROPOSAL_REWARD_NS_V1);
         mkpk!(CONSENSUS_CONTRACT_ZKAS_PROPOSAL_MINT_NS_V1);
+        mkpk!(CONSENSUS_CONTRACT_ZKAS_PROPOSAL_NS_V1);
 
         holders.insert(Holder::Alice, alice);
 
@@ -514,87 +516,56 @@ impl ConsensusTestHarness {
         &mut self,
         holder: Holder,
         slot_checkpoint: SlotCheckpoint,
-        staked_oc: OwnCoin,
-    ) -> Result<(Transaction, ConsensusProposalMintParamsV1)> {
+        staked_oc: ConsensusOwnCoin,
+    ) -> Result<(Transaction, ConsensusProposalParamsV1, SecretKey)> {
         let wallet = self.holders.get_mut(&holder).unwrap();
-        let (burn_pk, burn_zkbin) = self.proving_keys.get(&MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
-        let (reward_pk, reward_zkbin) =
-            self.proving_keys.get(&CONSENSUS_CONTRACT_ZKAS_PROPOSAL_REWARD_NS_V1).unwrap();
-        let (mint_pk, mint_zkbin) =
-            self.proving_keys.get(&CONSENSUS_CONTRACT_ZKAS_PROPOSAL_MINT_NS_V1).unwrap();
+        let (proposal_pk, proposal_zkbin) =
+            self.proving_keys.get(&CONSENSUS_CONTRACT_ZKAS_PROPOSAL_NS_V1).unwrap();
         let tx_action_benchmark = self.tx_action_benchmarks.get_mut(&TxAction::Proposal).unwrap();
         let timer = Instant::now();
 
         // Building Consensus::Unstake params
         let proposal_call_debris = ConsensusProposalCallBuilder {
-            coin: staked_oc.clone(),
-            recipient: wallet.keypair.public,
+            coin: staked_oc,
             slot_checkpoint,
             tree: wallet.consensus_merkle_tree.clone(),
-            burn_zkbin: burn_zkbin.clone(),
-            burn_pk: burn_pk.clone(),
-            reward_zkbin: reward_zkbin.clone(),
-            reward_pk: reward_pk.clone(),
-            mint_zkbin: mint_zkbin.clone(),
-            mint_pk: mint_pk.clone(),
+            proposal_zkbin: proposal_zkbin.clone(),
+            proposal_pk: proposal_pk.clone(),
         }
         .build()?;
-        let (
-            burn_params,
-            burn_proofs,
-            reward_params,
-            reward_proofs,
-            mint_params,
-            mint_proofs,
-            proposal_secret_key,
-        ) = (
-            proposal_call_debris.burn_params,
-            proposal_call_debris.burn_proofs,
-            proposal_call_debris.reward_params,
-            proposal_call_debris.reward_proofs,
-            proposal_call_debris.mint_params,
-            proposal_call_debris.mint_proofs,
+        let (params, proofs, secret_key) = (
+            proposal_call_debris.params,
+            proposal_call_debris.proofs,
             proposal_call_debris.signature_secret,
         );
 
-        // Building proposal tx
-        let mut data = vec![ConsensusFunction::ProposalBurnV1 as u8];
-        burn_params.encode(&mut data)?;
-        let burn_call = ContractCall { contract_id: *CONSENSUS_CONTRACT_ID, data };
-
-        let mut data = vec![ConsensusFunction::ProposalRewardV1 as u8];
-        reward_params.encode(&mut data)?;
-        let reward_call = ContractCall { contract_id: *CONSENSUS_CONTRACT_ID, data };
-
-        let mut data = vec![ConsensusFunction::ProposalMintV1 as u8];
-        mint_params.encode(&mut data)?;
-        let mint_call = ContractCall { contract_id: *CONSENSUS_CONTRACT_ID, data };
-
-        let calls = vec![burn_call, reward_call, mint_call];
-        let proofs = vec![burn_proofs, reward_proofs, mint_proofs];
-        let mut proposal_tx = Transaction { calls, proofs, signatures: vec![] };
-        let burn_sigs = proposal_tx.create_sigs(&mut OsRng, &[proposal_secret_key])?;
-        let reward_sigs = proposal_tx.create_sigs(&mut OsRng, &[proposal_secret_key])?;
-        let mint_sigs = proposal_tx.create_sigs(&mut OsRng, &[proposal_secret_key])?;
-        proposal_tx.signatures = vec![burn_sigs, reward_sigs, mint_sigs];
+        let mut data = vec![ConsensusFunction::ProposalV1 as u8];
+        params.encode(&mut data)?;
+        let call = ContractCall { contract_id: *CONSENSUS_CONTRACT_ID, data };
+
+        let calls = vec![call];
+        let proofs = vec![proofs];
+        let mut tx = Transaction { calls, proofs, signatures: vec![] };
+        let sigs = tx.create_sigs(&mut OsRng, &[secret_key])?;
+        tx.signatures = vec![sigs];
         tx_action_benchmark.creation_times.push(timer.elapsed());
 
         // Calculate transaction sizes
-        let encoded: Vec<u8> = serialize(&proposal_tx);
+        let encoded: Vec<u8> = serialize(&tx);
         let size = ::std::mem::size_of_val(&*encoded);
         tx_action_benchmark.sizes.push(size);
         let base58 = bs58::encode(&encoded).into_string();
         let size = ::std::mem::size_of_val(&*base58);
         tx_action_benchmark.broadcasted_sizes.push(size);
 
-        Ok((proposal_tx, mint_params))
+        Ok((tx, params, secret_key))
     }
 
     pub async fn execute_proposal_tx(
         &mut self,
         holder: Holder,
         tx: Transaction,
-        params: &ConsensusProposalMintParamsV1,
+        params: &ConsensusProposalParamsV1,
         slot: u64,
     ) -> Result<()> {
         let wallet = self.holders.get_mut(&holder).unwrap();

+ 4 - 0
src/contract/money/src/client/mod.rs

@@ -147,6 +147,10 @@ pub struct ConsensusNote {
     pub coin_blind: pallas::Base,
     /// Blinding factor for the value pedersen commitment
     pub value_blind: pallas::Scalar,
+    /// Value of the reward
+    pub reward: u64,
+    /// Blinding factor for the reward value pedersen commitment
+    pub reward_blind: pallas::Scalar,
 }
 
 impl From<ConsensusNote> for MoneyNote {

+ 2 - 0
src/contract/money/src/lib.rs

@@ -108,3 +108,5 @@ pub const CONSENSUS_CONTRACT_ZKAS_BURN_NS_V1: &str = "ConsensusBurn_V1";
 pub const CONSENSUS_CONTRACT_ZKAS_PROPOSAL_REWARD_NS_V1: &str = "ProposalReward_V1";
 /// zkas proposal mint circuit namespace
 pub const CONSENSUS_CONTRACT_ZKAS_PROPOSAL_MINT_NS_V1: &str = "ProposalMint_V1";
+/// zkas proposal circuit namespace
+pub const CONSENSUS_CONTRACT_ZKAS_PROPOSAL_NS_V1: &str = "ConsensusProposal_V1";