From 5a1ead936653b265ffbba2385f2ad9f888330537 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 12 Aug 2021 18:03:45 +0200 Subject: [PATCH 1/3] bid-contract: Change bool returns by panics and () --- contracts/bid/src/hosted/transaction.rs | 163 ++++++++++-------------- 1 file changed, 67 insertions(+), 96 deletions(-) diff --git a/contracts/bid/src/hosted/transaction.rs b/contracts/bid/src/hosted/transaction.rs index 92c26cead9..ccf67a00aa 100644 --- a/contracts/bid/src/hosted/transaction.rs +++ b/contracts/bid/src/hosted/transaction.rs @@ -10,7 +10,8 @@ use core::ops::DerefMut; use dusk_abi::Transaction; use dusk_blindbid::Bid; use dusk_bls12_381::BlsScalar; -use dusk_pki::{Ownable, PublicKey, StealthAddress}; +use dusk_jubjub::JubJubAffine; +use dusk_pki::{Ownable, PublicKey, PublicSpendKey, StealthAddress}; use dusk_schnorr::Signature; use microkelvin::Nth; use phoenix_core::{Message, Note}; @@ -36,17 +37,14 @@ impl Contract { stealth_address: StealthAddress, correctness_proof: Vec, spending_proof: Vec, - ) -> bool { - // Setup sucess var to true - let mut success = true; - + ) { // Verify proof of Correctness of the Bid. if !rusk_abi::verify_proof( correctness_proof, crate::BID_CORRECTNESS_VD.to_vec(), alloc::vec![(message.value_commitment()).into()], ) { - return false; + panic!("Failed to verify BidCorrectness proof") } // Obtain the current block_height. @@ -72,25 +70,25 @@ impl Contract { // tree .get(*bid.stealth_address().pk_r()) .unwrap() - .is_none() + .is_some() { - // Append Bid to the tree and obtain the position of it. - if let Ok(idx) = self - .tree_mut() - .push(BidLeaf::new(bid, Expiration(expiration))) - { - // Link the One-time PK to the idx in the Map - // Since we checked on the `get` call that the value was not - // inside, there's no need to check that this - // returns `Ok(None)`. So we just unwrap - // the `Result` and keep the `Option` untouched. - self.key_idx_map_mut() - .insert(*bid.stealth_address().pk_r(), idx as usize) - .unwrap(); - } - } else { - return false; - }; + panic!("Entry already found in the map for this Key. You can't bid twice with the same key") + } + + // Append Bid to the tree and obtain the position of it. + if let Ok(idx) = self + .tree_mut() + .push(BidLeaf::new(bid, Expiration(expiration))) + { + // Link the One-time PK to the idx in the Map + // Since we checked on the `get` call that the value was not + // inside, there's no need to check that this + // returns `Ok(None)`. So we just unwrap + // the `Result` and keep the `Option` untouched. + self.key_idx_map_mut() + .insert(*bid.stealth_address().pk_r(), idx as usize) + .unwrap(); + } // Inter-contract call to lock bidder's funds in the Bid contract. let call = Call::send_to_contract_obfuscated( @@ -103,8 +101,6 @@ impl Contract { let call = Transaction::from_canon(&call); dusk_abi::transact_raw(self, &rusk_abi::transfer_contract(), &call) .expect("Failed to send dusk to Bid contract"); - - true } // TODO: Check wether we allow to extend long-time expired Bids. @@ -114,29 +110,20 @@ impl Contract { /// time for his/her `Bid`. That means, remain longer in the Bidding /// consensus process with the same `Bid` and therefore the same /// One-time identity. - pub fn extend_bid(&mut self, sig: Signature, pk: PublicKey) -> bool { - // Setup success to true - let mut success = true; + pub fn extend_bid(&mut self, sig: Signature, pk: PublicKey) { // Check wether there's an entry on the map for the pk. let idx = match self.key_idx_map().get(pk) { // If no entries are found for this PK it's just an err since there // are no bids related to this PK to be extended. Ok(None) => { - success = false; - usize::MAX + panic!("Key not found on the map"); } Ok(Some(idx)) => *idx as usize, Err(_) => { - success = false; - usize::MAX + panic!("Key not found on the map"); } }; - // In case there was an error, we simply return - if !success && idx == usize::MAX { - return false; - } - // Verify the signature by getting `t_e` from the Bid and calling the // VERIFY_SIG host fn. // Fetch the bid object from the tree getting a &mut to it. @@ -145,7 +132,7 @@ impl Contract { { branch } else { - return false; + panic!("Could not retrieve a mutable branch of the BidTree"); }; let bid: &mut BidLeaf = branch_mut.deref_mut(); @@ -163,14 +150,13 @@ impl Contract { pk, BlsScalar::from(bid_expiration), ) { - return false; + panic!("Failed to verify the Extend Signature"); } // Assuming now that the result of the verification is true, we now // should update the expiration of the Bid by `VALIDITY_PERIOD`. bid.bid_mut().extend_expiration(VALIDITY_PERIOD); bid.expiration_mut().0 += VALIDITY_PERIOD; - success } /// This function allows to the contract caller to withdraw it's `Bid` and @@ -189,77 +175,62 @@ impl Contract { pk: PublicKey, note: Note, spend_proof: Vec, - ) -> bool { - // Setup success to true - let mut success = true; + ) { // Check wether there's an entry on the map for the pk. let idx = match self.key_idx_map().get(pk) { // If no entries are found for this PK it's just an err since there // are no bids related to this PK to be extended. - Ok(None) => { - success = false; - usize::MAX - } Ok(Some(idx)) => *idx as usize, - Err(_) => { - success = false; - usize::MAX + _ => { + panic!("Key not found on the map"); } }; - // In case there was an error, we simply return - if !success && idx == usize::MAX { - return false; - } - // Fetch bid info from the tree. Note that we can safely unwrap here due // to the checks done previously while getting the idx from the map. let bid = if let Ok(Some(bid)) = self.tree().get(idx as u64) { bid } else { - return false; + panic!("Failed to fetch the Bid from the tree at ids: {:?}", idx) }; - if *bid.bid().expiration() < dusk_abi::block_height() { - // If we arrived here, the bid is elegible for withdrawal. - // Now we need to check wether the signature is correct. - // Verify schnorr sig. - if !rusk_abi::verify_schnorr_sign( - sig, - pk, - BlsScalar::from(*bid.bid().expiration()), - ) { - return false; - }; - - // Withdraw from Obfuscated call to retire the funds of the bidder. - let call = Call::withdraw_from_obfuscated( - *bid.bid().message(), - *bid.bid().stealth_address(), - note, - note.value_commitment().into(), - spend_proof, - ); + if *bid.bid().expiration() >= dusk_abi::block_height() { + panic!("Bid::Expiration >= actual block_height") + } + // If we arrived here, the bid is elegible for withdrawal. + // Now we need to check wether the signature is correct. + // Verify schnorr sig. + if !rusk_abi::verify_schnorr_sign( + sig, + pk, + BlsScalar::from(*bid.bid().expiration()), + ) { + panic!("Failed to verify the Withdrawal Signature"); + }; - let call = Transaction::from_canon(&call); - dusk_abi::transact_raw(self, &rusk_abi::transfer_contract(), &call) - .expect("Failed to withdraw dusk from the Bid contract"); + // Withdraw from Obfuscated call to retire the funds of the bidder. + let call = Call::withdraw_from_obfuscated( + *bid.bid().message(), + *bid.bid().stealth_address(), + note, + spend_proof, + ); - // If the inter-contract call succeeds, we need to clean the - // tree & map. Note that if we clean the entry - // corresponding to this `PublicKey` from the - // map there will be no need to do so from the tree. Since the - // rest of the functions rely on the map to gain - // access to the bid that is inside of the tree. - self.key_idx_map_mut() - .remove(pk) - .expect("Canon Store error happened."); - // TODO: Zeroize in the tree the leaf that corresponds to the idx - // linked to `pk` in the map. - // See: https://github.com/dusk-network/rusk/issues/164 - true - } else { - false - } + let call = Transaction::from_canon(&call); + dusk_abi::transact_raw(self, &rusk_abi::transfer_contract(), &call) + .expect("Failed to withdraw dusk from the Bid contract"); + + // If the inter-contract call succeeds, we need to clean the + // tree & map. Note that if we clean the entry + // corresponding to this `PublicKey` from the + // map there will be no need to do so from the tree. Since the + // rest of the functions rely on the map to gain + // access to the bid that is inside of the tree. + self.key_idx_map_mut() + .remove(pk) + .expect("Canon Store error happened."); + // TODO: Zeroize in the tree the leaf that corresponds to the idx + // linked to `pk` in the map. + // See: https://github.com/dusk-network/rusk/issues/164 } } From cb09f9fed437440730e28137d1be3a833d5fba9a Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 12 Aug 2021 18:04:19 +0200 Subject: [PATCH 2/3] bid-contract: Add "persistence" feature of `rusk-vm` --- contracts/bid/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/bid/Cargo.toml b/contracts/bid/Cargo.toml index d918df5ef1..3b97cad493 100644 --- a/contracts/bid/Cargo.toml +++ b/contracts/bid/Cargo.toml @@ -31,6 +31,6 @@ dusk-plonk = "0.8" lazy_static = "1.4" rand = "0.8" rusk-profile = { path = "../../rusk-profile" } -rusk-vm = "0.7.0-rc" +rusk-vm = { version = "0.7.0-rc", features = ["persistence"] } transfer-circuits = { path = "../../circuits/transfer", features = ["builder"] } transfer-wrapper = { path = "../../test-utils/transfer-wrapper" } From 8add218ee5b072eedab501107e09366ad9ea49e8 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 12 Aug 2021 18:04:54 +0200 Subject: [PATCH 3/3] bid-contract: Add EXTEND and WITHDRAW tests Resolves: #310 --- contracts/bid/src/hosted.rs | 13 +- contracts/bid/tests/hosted_fns.rs | 258 +++++++++++++++++++++++++++++- 2 files changed, 261 insertions(+), 10 deletions(-) diff --git a/contracts/bid/src/hosted.rs b/contracts/bid/src/hosted.rs index dff84be39a..2674975790 100644 --- a/contracts/bid/src/hosted.rs +++ b/contracts/bid/src/hosted.rs @@ -16,6 +16,7 @@ use alloc::vec::Vec; use canonical::{Canon, CanonError, Sink, Source}; use dusk_abi::{ContractState, ReturnValue}; use dusk_bls12_381::BlsScalar; +use dusk_jubjub::JubJubAffine; use dusk_pki::{PublicKey, StealthAddress}; use dusk_schnorr::Signature; use phoenix_core::{Message, Note}; @@ -64,7 +65,7 @@ fn transaction(bytes: &mut [u8; PAGE_SIZE]) -> Result<(), CanonError> { let correctness_proof = Vec::::decode(&mut source)?; let spending_proof = Vec::::decode(&mut source)?; // Call bid contract fn - let success = slf.bid( + slf.bid( message, hashed_secret, stealth_addr, @@ -76,7 +77,7 @@ fn transaction(bytes: &mut [u8; PAGE_SIZE]) -> Result<(), CanonError> { // return new state ContractState::from_canon(&slf).encode(&mut sink); // return result - ReturnValue::from_canon(&success).encode(&mut sink); + ReturnValue::from_canon(&true).encode(&mut sink); Ok(()) } ops::WITHDRAW => { @@ -85,26 +86,26 @@ fn transaction(bytes: &mut [u8; PAGE_SIZE]) -> Result<(), CanonError> { let pk = PublicKey::decode(&mut source)?; let note = Note::decode(&mut source)?; let spending_proof = Vec::::decode(&mut source)?; - let exec_res = slf.withdraw(sig, pk, note, spending_proof); + slf.withdraw(sig, pk, note, spending_proof); let mut sink = Sink::new(&mut bytes[..]); // return new state ContractState::from_canon(&slf).encode(&mut sink); // return result - ReturnValue::from_canon(&exec_res).encode(&mut sink); + ReturnValue::from_canon(&true).encode(&mut sink); Ok(()) } ops::EXTEND_BID => { // Read host-sent args let sig = Signature::decode(&mut source)?; let pk = PublicKey::decode(&mut source)?; - let exec_res = slf.extend_bid(sig, pk); + slf.extend_bid(sig, pk); let mut sink = Sink::new(&mut bytes[..]); // return new state ContractState::from_canon(&slf).encode(&mut sink); // return result - ReturnValue::from_canon(&exec_res).encode(&mut sink); + ReturnValue::from_canon(&true).encode(&mut sink); Ok(()) } _ => panic!("Unimplemented OP"), diff --git a/contracts/bid/tests/hosted_fns.rs b/contracts/bid/tests/hosted_fns.rs index 7c21de48b9..94013cb91b 100644 --- a/contracts/bid/tests/hosted_fns.rs +++ b/contracts/bid/tests/hosted_fns.rs @@ -5,22 +5,30 @@ // Copyright (c) DUSK NETWORK. All rights reserved. use bid_circuits::BidCorrectnessCircuit; -use bid_contract::Contract as BidContract; +use bid_contract::{ + contract_constants::{MATURITY_PERIOD, VALIDITY_PERIOD}, + Contract as BidContract, +}; use dusk_abi::{ContractId, Transaction}; use dusk_blindbid::{V_RAW_MAX, V_RAW_MIN}; use dusk_bytes::Serializable; use dusk_jubjub::{ JubJubAffine, JubJubScalar, GENERATOR_EXTENDED, GENERATOR_NUMS_EXTENDED, }; -use dusk_pki::{PublicSpendKey, SecretSpendKey}; +use dusk_pki::{PublicKey, PublicSpendKey, SecretSpendKey, ViewKey}; use dusk_plonk::prelude::*; use dusk_poseidon::sponge; +use dusk_schnorr::Signature; use lazy_static::lazy_static; +use microkelvin::DiskBackend; use phoenix_core::{Crossover, Fee, Message, Note}; use rand::{CryptoRng, Rng, RngCore}; -use rusk_vm::Contract; +use rusk_abi::RuskModule; +use rusk_vm::{Contract, NetworkState}; use std::convert::TryInto; -use transfer_circuits::SendToContractObfuscatedCircuit; +use transfer_circuits::{ + SendToContractObfuscatedCircuit, WithdrawFromObfuscatedCircuit, +}; use transfer_contract::TransferContract; lazy_static! { @@ -92,6 +100,54 @@ fn prove_stco( .expect("Failed to generate proof") } +fn prove_wfo( + message_r: JubJubScalar, + message_ssk: &SecretSpendKey, + message: &Message, + output: &Note, + output_vk: Option<&ViewKey>, +) -> Proof { + let mut circuit = WithdrawFromObfuscatedCircuit::new( + message_r, + message_ssk, + message, + output, + output_vk, + ) + .expect("Failed to generate circuit!"); + + let id = WithdrawFromObfuscatedCircuit::CIRCUIT_ID; + let keys = rusk_profile::keys_for(&id) + .expect("Failed to fetch keys from rusk-profile"); + let pk = keys + .get_prover() + .expect("Failed to extract prover key from rusk-profile"); + let pk = ProverKey::from_slice(pk.as_slice()) + .expect("Failed to parse prover key from rusk-profile"); + + let vd = keys + .get_verifier() + .expect("Failed to extract prover key from rusk-profile"); + let vd = VerifierData::from_slice(vd.as_slice()) + .expect("Failed to parse prover key from rusk-profile"); + + let proof = circuit + .gen_proof(&*PP, &pk, b"dusk-network") + .expect("Failed to generate proof"); + + assert!(dusk_plonk::circuit::verify_proof( + &*PP, + vd.key(), + &proof, + &circuit.public_inputs(), + vd.pi_pos(), + b"dusk-network", + ) + .is_ok()); + + proof +} + #[test] fn bid_contract_workflow_works() { let rng = &mut rand::thread_rng(); @@ -114,6 +170,8 @@ fn bid_contract_workflow_works() { let message = Message::new(rng, &r, &psk, value); let (_, blinder) = message.decrypt(&r, &psk).expect("decryption error"); + // Create signature PublicKey + let sig_key = PublicKey::from(&ssk.sk_r(&stealth)); let proof = prove_bid(value.into(), blinder).to_bytes().to_vec(); // Generate env @@ -167,4 +225,196 @@ fn bid_contract_workflow_works() { Some((bid_contract, tx)), ) .expect("Failed execute tx build"); + + // The Bid is now placed inside of the Bid Tree of the contract. + // If we try to extend the Bid with an un-increased block height, the op should fail. + + // Sign the t_e (expiration) of the Bid. + let bad_signature = Signature::new( + &ssk.sk_r(&stealth), + &mut rand::thread_rng(), + BlsScalar::from(block_height + MATURITY_PERIOD + VALIDITY_PERIOD), + ); + + let extend_fail_tx = Transaction::from_canon(&( + bid_contract::ops::EXTEND_BID, + bad_signature, + sig_key, + )); + + // Get notes owned by genesis_ssk + let note = transfer_wrapper::transfer_notes_owned_by( + &network, + block_height, + &genesis_ssk.view_key(), + ) + .expect("Error fetching genesis note") + .last() + .unwrap() + .clone(); + + assert!(transfer_wrapper::execute( + rng, + &mut network, + [(genesis_ssk, note)].iter(), + &psk, + true, + &vk, + fee, + Some((&vk, crossover)), + Some((bid_contract, extend_fail_tx)), + ) + .is_err()); + + // Update the block_height of the NetworkState and try to extend the Bid again. + let persist_id = network + .persist(|| { + let dir = std::env::temp_dir().join("bid_contract_workflow_works"); + std::fs::create_dir_all(&dir).expect("Error on tmp dir creation"); + DiskBackend::new(dir) + }) + .expect("Error in persistence"); + + let block_height = VALIDITY_PERIOD + 1; + // Generate new NetworkState with blockheight = MATURITY_PERIOD + 1 + let mut network = NetworkState::with_block_height(block_height) + .restore(persist_id) + .expect("Error reconstructing the NetworkState"); + let rusk_mod = RuskModule::new(&*PP); + network.register_host_module(rusk_mod); + + // Sign the t_e (expiration) of the Bid. + let signature = Signature::new( + &ssk.sk_r(&stealth), + &mut rand::thread_rng(), + BlsScalar::from(MATURITY_PERIOD + VALIDITY_PERIOD), + ); + + let extend_tx = Transaction::from_canon(&( + bid_contract::ops::EXTEND_BID, + signature, + sig_key, + )); + + // Get notes owned by genesis_ssk + let note = transfer_wrapper::transfer_notes_owned_by(&network, 0, &vk) + .expect("Error fetching genesis note") + .last() + .unwrap() + .clone(); + + fee.gas_limit = note.value(Some(&vk)).unwrap() - 1000; + transfer_wrapper::execute( + rng, + &mut network, + [(ssk, note)].iter(), + &psk, + true, + &vk, + fee, + None, + Some((bid_contract, extend_tx)), + ) + .expect("Failed to extend the bid"); + + // Remove the persist data to generate a new one updated to the actual state. + std::fs::remove_dir_all( + std::env::temp_dir().join("bid_contract_workflow_works"), + ) + .expect("teardown fn error"); + + let persist_id = network + .persist(|| { + let dir = std::env::temp_dir().join("bid_contract_workflow_works"); + std::fs::create_dir_all(&dir).expect("Error on tmp dir creation"); + DiskBackend::new(dir) + }) + .expect("Error in persistence"); + + // Set a new NetworkState with a block_height that leaves the bid expired so that we can withrdraw it + let block_height = MATURITY_PERIOD + 2 * VALIDITY_PERIOD + 1; + // Generate new NetworkState with blockheight = MATURITY_PERIOD + 1 + let mut network = NetworkState::with_block_height(block_height) + .restore(persist_id) + .expect("Error reconstructing the NetworkState"); + let rusk_mod = RuskModule::new(&*PP); + network.register_host_module(rusk_mod); + + // WITHDRAWAL OF THE BID + // Sign the elegibility and call withdraw bid. + let withdraw_signature = Signature::new( + &ssk.sk_r(&stealth), + &mut rand::thread_rng(), + BlsScalar::from(MATURITY_PERIOD + 2 * VALIDITY_PERIOD), + ); + + // Get notes owned by genesis_ssk + let note = transfer_wrapper::transfer_notes_owned_by( + &network, + VALIDITY_PERIOD + 1, // The block height at which the refund of the EXTEND_BID call was appended to the Note tree. + &vk, + ) + .expect("Error fetching genesis note") + .last() + .unwrap() + .clone(); + + fee.gas_limit = note.value(Some(&vk)).unwrap() - 1000; + fee.gas_price = 1; + + let zero_value_note = + Note::obfuscated(&mut rand::thread_rng(), &psk, 0, JubJubScalar::one()); + // Create new Id for the withdrawal + let withdraw_ssk = SecretSpendKey::random(&mut rand::thread_rng()); + let withdraw_psk = withdraw_ssk.public_spend_key(); + let withdraw_vk = withdraw_ssk.view_key(); + let withdraw_note = Note::obfuscated( + &mut rand::thread_rng(), + &withdraw_psk, + value, + JubJubScalar::random(&mut rand::thread_rng()), + ); + + let wfo_proof = + prove_wfo(r, &ssk, &message, &withdraw_note, Some(&withdraw_vk)); + + let withdraw_tx = Transaction::from_canon(&( + bid_contract::ops::WITHDRAW, + withdraw_signature, + sig_key, + withdraw_note, + wfo_proof.to_bytes().to_vec(), + )); + + transfer_wrapper::execute( + rng, + &mut network, + [(ssk, note)].iter(), + &psk, + true, + &vk, + fee, + None, + Some((bid_contract, withdraw_tx)), + ) + .expect("Failed to withdraw the Bid"); + + // Get notes owned by genesis_ssk + let note = transfer_wrapper::transfer_notes_owned_by( + &network, + block_height, + &withdraw_vk, + ) + .expect("Error fetching withdraw note") + .last() + .unwrap() + .clone(); + + assert_eq!(note.value(Some(&withdraw_vk)).unwrap(), value); + + // Teardown + std::fs::remove_dir_all( + std::env::temp_dir().join("bid_contract_workflow_works"), + ) + .expect("teardown fn error"); }