Просмотр исходного кода

contract/money: new PoWReward call added

aggstam 2 лет назад
Родитель
Сommit
bcbc636318

+ 6 - 1
src/contract/money/Makefile

@@ -51,12 +51,17 @@ test-genesis-mint: all
 		--package darkfi-money-contract \
 		--test genesis_mint $(ARGS)
 
+test-pow-reward: all
+	$(CARGO) test --release --features=no-entrypoint,client \
+		--package darkfi-money-contract \
+		--test pow_reward $(ARGS)
+
 bench:
 	$(CARGO) test --release --features=no-entrypoint,client \
 		--package darkfi-money-contract \
 		--test verification_bench $(FILTER)
 
-test: test-integration test-mint-pay-swap test-txs-verification test-genesis-mint
+test: test-integration test-mint-pay-swap test-txs-verification test-genesis-mint test-pow-reward
 
 test-no-run:
 	$(MAKE) test-integration ARGS=$(NO_RUN)

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

@@ -56,6 +56,9 @@ pub mod stake_v1;
 /// `Money::UnstakeV1` API
 pub mod unstake_v1;
 
+/// `Money::PoWRewardV1` API
+pub mod pow_reward_v1;
+
 // Wallet SQL table constant names. These have to represent the `wallet.sql`
 // SQL schema.
 // TODO: They should also be prefixed with the contract ID to avoid collisions.

+ 160 - 0
src/contract/money/src/client/pow_reward_v1.rs

@@ -0,0 +1,160 @@
+/* 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::{
+    zk::{Proof, ProvingKey},
+    zkas::ZkBinary,
+    Result,
+};
+use darkfi_sdk::{
+    blockchain::pow_expected_reward,
+    crypto::{note::AeadEncryptedNote, pasta_prelude::*, Keypair, PublicKey, DARK_TOKEN_ID},
+    pasta::pallas,
+};
+use log::{debug, info};
+use rand::rngs::OsRng;
+
+use crate::{
+    client::{
+        transfer_v1::{
+            create_transfer_mint_proof, TransactionBuilderClearInputInfo,
+            TransactionBuilderOutputInfo,
+        },
+        MoneyNote,
+    },
+    model::{ClearInput, Coin, MoneyTokenMintParamsV1, Output},
+};
+
+pub struct PoWRewardCallDebris {
+    pub params: MoneyTokenMintParamsV1,
+    pub proofs: Vec<Proof>,
+}
+
+pub struct PoWRewardRevealed {
+    pub coin: Coin,
+    pub value_commit: pallas::Point,
+    pub token_commit: pallas::Base,
+}
+
+impl PoWRewardRevealed {
+    pub fn to_vec(&self) -> Vec<pallas::Base> {
+        let valcom_coords = self.value_commit.to_affine().coordinates().unwrap();
+
+        // NOTE: It's important to keep these in the same order
+        // as the `constrain_instance` calls in the zkas code.
+        vec![self.coin.inner(), *valcom_coords.x(), *valcom_coords.y(), self.token_commit]
+    }
+}
+
+/// Struct holding necessary information to build a `Money::PoWRewardV1` contract call.
+pub struct PoWRewardCallBuilder {
+    /// Caller's keypair
+    pub keypair: Keypair,
+    /// Rewarded slot(block)
+    pub slot: u64,
+    /// Spend hook for the output
+    pub spend_hook: pallas::Base,
+    /// User data for the output
+    pub user_data: pallas::Base,
+    /// `Mint_V1` zkas circuit ZkBinary
+    pub mint_zkbin: ZkBinary,
+    /// Proving key for the `Mint_V1` zk circuit
+    pub mint_pk: ProvingKey,
+}
+
+impl PoWRewardCallBuilder {
+    fn _build(&self, value: u64) -> Result<PoWRewardCallDebris> {
+        debug!("Building Money::MintV1 contract call");
+
+        // In this call, we will build one clear input and one anonymous output.
+        // Only DARK_TOKEN_ID can be minted as PoW reward.
+        let token_id = *DARK_TOKEN_ID;
+
+        let input = TransactionBuilderClearInputInfo {
+            value,
+            token_id,
+            signature_secret: self.keypair.secret,
+        };
+
+        let output =
+            TransactionBuilderOutputInfo { value, token_id, public_key: self.keypair.public };
+
+        // We just create the commitment blinds here. We simply encofce
+        // that the clear input and the anon output have the same commitments.
+        // Not sure if this can be avoided, but also is it really necessary to avoid?
+        let value_blind = pallas::Scalar::random(&mut OsRng);
+        let token_blind = pallas::Base::random(&mut OsRng);
+
+        let c_input = ClearInput {
+            value: input.value,
+            token_id: input.token_id,
+            value_blind,
+            token_blind,
+            signature_public: PublicKey::from_secret(input.signature_secret),
+        };
+
+        let serial = pallas::Base::random(&mut OsRng);
+
+        info!("Creating token mint proof for output");
+        let (proof, public_inputs) = create_transfer_mint_proof(
+            &self.mint_zkbin,
+            &self.mint_pk,
+            &output,
+            value_blind,
+            token_blind,
+            serial,
+            self.spend_hook,
+            self.user_data,
+        )?;
+
+        let note = MoneyNote {
+            serial,
+            value: output.value,
+            token_id: output.token_id,
+            spend_hook: self.spend_hook,
+            user_data: self.user_data,
+            value_blind,
+            token_blind,
+            memo: vec![],
+        };
+
+        let encrypted_note = AeadEncryptedNote::encrypt(&note, &output.public_key, &mut OsRng)?;
+
+        let c_output = Output {
+            value_commit: public_inputs.value_commit,
+            token_commit: public_inputs.token_commit,
+            coin: public_inputs.coin,
+            note: encrypted_note,
+        };
+
+        let params = MoneyTokenMintParamsV1 { input: c_input, output: c_output };
+        let debris = PoWRewardCallDebris { params, proofs: vec![proof] };
+        Ok(debris)
+    }
+
+    pub fn build(&self) -> Result<PoWRewardCallDebris> {
+        let reward = pow_expected_reward(self.slot);
+        assert!(reward != 0);
+        self._build(reward)
+    }
+
+    /// This function should only be used for testing, as PoW reward values are predefined
+    pub fn build_with_custom_reward(&self, reward: u64) -> Result<PoWRewardCallDebris> {
+        self._build(reward)
+    }
+}

+ 16 - 7
src/contract/money/src/entrypoint.rs

@@ -84,6 +84,10 @@ use unstake_v1::{
     money_unstake_process_update_v1,
 };
 
+/// `Money::PoWReward` functions
+mod pow_reward_v1;
+use pow_reward_v1::{money_pow_reward_get_metadata_v1, money_pow_reward_process_instruction_v1};
+
 darkfi_sdk::define_contract!(
     init: init_contract,
     exec: process_instruction,
@@ -221,6 +225,11 @@ fn get_metadata(cid: ContractId, ix: &[u8]) -> ContractResult {
             let metadata = money_unstake_get_metadata_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&metadata)?)
         }
+
+        MoneyFunction::PoWRewardV1 => {
+            let metadata = money_pow_reward_get_metadata_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&metadata)?)
+        }
     }
 }
 
@@ -274,6 +283,11 @@ fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
             let update_data = money_unstake_process_instruction_v1(cid, call_idx, calls)?;
             Ok(set_return_data(&update_data)?)
         }
+
+        MoneyFunction::PoWRewardV1 => {
+            let update_data = money_pow_reward_process_instruction_v1(cid, call_idx, calls)?;
+            Ok(set_return_data(&update_data)?)
+        }
     }
 }
 
@@ -295,13 +309,8 @@ fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult {
             Ok(money_otcswap_process_update_v1(cid, update)?)
         }
 
-        MoneyFunction::GenesisMintV1 => {
-            // FIXME: GenesisMint uses the same update as `TokenMintV1`
-            let update: MoneyTokenMintUpdateV1 = deserialize(&update_data[1..])?;
-            Ok(money_token_mint_process_update_v1(cid, update)?)
-        }
-
-        MoneyFunction::TokenMintV1 => {
+        MoneyFunction::GenesisMintV1 | MoneyFunction::TokenMintV1 | MoneyFunction::PoWRewardV1 => {
+            // FIXME: GenesisMint and PoWReward use the same update as `TokenMintV1`
             let update: MoneyTokenMintUpdateV1 = deserialize(&update_data[1..])?;
             Ok(money_token_mint_process_update_v1(cid, update)?)
         }

+ 1 - 1
src/contract/money/src/entrypoint/genesis_mint_v1.rs

@@ -120,7 +120,7 @@ pub(crate) fn money_genesis_mint_process_instruction_v1(
     // Create a state update. We only need the new coin.
     let update = MoneyTokenMintUpdateV1 { coin: params.output.coin };
     let mut update_data = vec![];
-    update_data.write_u8(MoneyFunction::TokenMintV1 as u8)?;
+    update_data.write_u8(MoneyFunction::GenesisMintV1 as u8)?;
     update.encode(&mut update_data)?;
 
     Ok(update_data)

+ 145 - 0
src/contract/money/src/entrypoint/pow_reward_v1.rs

@@ -0,0 +1,145 @@
+/* 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_sdk::{
+    blockchain::{pow_expected_reward, POW_CUTOFF},
+    crypto::{pasta_prelude::*, pedersen_commitment_u64, poseidon_hash, ContractId, DARK_TOKEN_ID},
+    db::{db_contains_key, db_lookup},
+    error::ContractError,
+    msg,
+    pasta::pallas,
+    util::get_verifying_slot,
+    ContractCall,
+};
+use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
+
+use crate::{
+    error::MoneyError,
+    model::{MoneyTokenMintParamsV1, MoneyTokenMintUpdateV1},
+    MoneyFunction, MONEY_CONTRACT_COINS_TREE, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+};
+
+/// `get_metadata` function for `Money::PoWRewardV1`
+pub(crate) fn money_pow_reward_get_metadata_v1(
+    _cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: MoneyTokenMintParamsV1 = 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 pedersen commitment from the anonymous output
+    let value_coords = params.output.value_commit.to_affine().coordinates().unwrap();
+
+    zk_public_inputs.push((
+        MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string(),
+        vec![
+            params.output.coin.inner(),
+            *value_coords.x(),
+            *value_coords.y(),
+            params.output.token_commit,
+        ],
+    ));
+
+    // 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 `Money::PoWRewardV1`
+pub(crate) fn money_pow_reward_process_instruction_v1(
+    cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: MoneyTokenMintParamsV1 = deserialize(&self_.data[1..])?;
+
+    // Verify this contract call is verified against a slot(block height) before PoS transition,
+    // excluding genesis.
+    let verifying_slot = get_verifying_slot();
+    if verifying_slot == 0 || verifying_slot > POW_CUTOFF {
+        msg!(
+            "[PoWRewardV1] Error: Call is executed for slot {}(cutoff slot {})",
+            verifying_slot,
+            POW_CUTOFF
+        );
+        return Err(MoneyError::PoWRewardCallAfterCutoffSlot.into())
+    }
+
+    // Only DARK_TOKEN_ID can be minted as PoW reward.
+    if params.input.token_id != *DARK_TOKEN_ID {
+        msg!("[PoWRewardV1] Error: Clear input used non-native token");
+        return Err(MoneyError::TransferClearInputNonNativeToken.into())
+    }
+
+    // Verify reward value matches the expected one for this slot(block height)
+    let expected_reward = pow_expected_reward(verifying_slot);
+    if params.input.value != expected_reward {
+        msg!(
+            "[PoWRewardV1] Error: Reward value({}) is not the block height({}) expected one: {}",
+            params.input.value,
+            verifying_slot,
+            expected_reward
+        );
+        return Err(MoneyError::ValueMismatch.into())
+    }
+
+    // Access the necessary databases where there is information to
+    // validate this state transition.
+    let coins_db = db_lookup(cid, MONEY_CONTRACT_COINS_TREE)?;
+
+    // Check that the coin from the output hasn't existed before.
+    if db_contains_key(coins_db, &serialize(&params.output.coin))? {
+        msg!("[PoWRewardV1] Error: Duplicate coin in output");
+        return Err(MoneyError::DuplicateCoin.into())
+    }
+
+    // Verify that the value and token commitments match. In here we just
+    // confirm that the clear input and the anon output have the same
+    // commitments.
+    if pedersen_commitment_u64(params.input.value, params.input.value_blind) !=
+        params.output.value_commit
+    {
+        msg!("[PoWRewardV1] Error: Value commitment mismatch");
+        return Err(MoneyError::ValueMismatch.into())
+    }
+
+    if poseidon_hash([params.input.token_id.inner(), params.input.token_blind]) !=
+        params.output.token_commit
+    {
+        msg!("[PoWRewardV1] Error: Token commitment mismatch");
+        return Err(MoneyError::TokenMismatch.into())
+    }
+
+    // Create a state update. We only need the new coin.
+    let update = MoneyTokenMintUpdateV1 { coin: params.output.coin };
+    let mut update_data = vec![];
+    update_data.write_u8(MoneyFunction::PoWRewardV1 as u8)?;
+    update.encode(&mut update_data)?;
+
+    Ok(update_data)
+}

+ 4 - 0
src/contract/money/src/error.rs

@@ -117,6 +117,9 @@ pub enum MoneyError {
 
     #[error("Missing nullifier in set")]
     MissingNullifier,
+
+    #[error("Call is executed after cutoff slot")]
+    PoWRewardCallAfterCutoffSlot,
 }
 
 impl From<MoneyError> for ContractError {
@@ -154,6 +157,7 @@ impl From<MoneyError> for ContractError {
             MoneyError::PreviousCallInputMismatch => Self::Custom(30),
             MoneyError::GenesisCallNonGenesisSlot => Self::Custom(31),
             MoneyError::MissingNullifier => Self::Custom(32),
+            MoneyError::PoWRewardCallAfterCutoffSlot => Self::Custom(33),
         }
     }
 }

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

@@ -32,6 +32,7 @@ pub enum MoneyFunction {
     TokenFreezeV1 = 0x05,
     StakeV1 = 0x06,
     UnstakeV1 = 0x07,
+    PoWRewardV1 = 0x08,
 }
 
 impl TryFrom<u8> for MoneyFunction {
@@ -47,6 +48,7 @@ impl TryFrom<u8> for MoneyFunction {
             0x05 => Ok(Self::TokenFreezeV1),
             0x06 => Ok(Self::StakeV1),
             0x07 => Ok(Self::UnstakeV1),
+            0x08 => Ok(Self::PoWRewardV1),
             _ => Err(ContractError::InvalidFunction),
         }
     }

+ 155 - 0
src/contract/money/tests/pow_reward.rs

@@ -0,0 +1,155 @@
+/* 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/>.
+ */
+
+//! Test for PoW reward transaction verification correctness.
+//!
+//! We first reward Alice some native tokens, and then she send some of them to Bob.
+//!
+//! With this test, we want to confirm the PoW reward transactions execution works
+//! and generated tokens can be processed as usual between multiple parties,
+//! with detection of erroneous transactions.
+
+use darkfi::Result;
+use darkfi_contract_test_harness::{init_logger, Holder, TestHarness, TxAction};
+use darkfi_sdk::{blockchain::pow_expected_reward, crypto::DARK_TOKEN_ID};
+use log::info;
+
+#[test]
+fn pow_reward() -> Result<()> {
+    smol::block_on(async {
+        init_logger();
+
+        // Holders this test will use
+        const HOLDERS: [Holder; 3] = [Holder::Faucet, Holder::Alice, Holder::Bob];
+
+        // Slot to verify against
+        let mut current_slot = 0;
+
+        // Initialize harness
+        let mut th = TestHarness::new(&["money".to_string()]).await?;
+
+        let mut alice_owncoins = vec![];
+        let mut bob_owncoins = vec![];
+
+        // We are going to generate some erroneous transactions to
+        // test some malicious cases.
+        info!(target: "money", "[Malicious] =======================================");
+        info!(target: "money", "[Malicious] Building PoW reward tx for genesis slot");
+        info!(target: "money", "[Malicious] =======================================");
+        let (pow_reward_tx, _) = th.pow_reward(&Holder::Alice, current_slot, Some(0))?;
+
+        info!(target: "money", "[Malicious] =======================================");
+        info!(target: "money", "[Malicious] Checking PoW reward tx for genesis slot");
+        info!(target: "money", "[Malicious] =======================================");
+        th.execute_erroneous_txs(
+            TxAction::MoneyPoWReward,
+            &Holder::Alice,
+            &[pow_reward_tx.clone()],
+            current_slot,
+            1,
+        )
+        .await?;
+
+        current_slot += 1;
+        th.generate_slot(current_slot).await?;
+
+        let alice_reward = pow_expected_reward(current_slot);
+        info!(target: "money", "[Malicious] ================================");
+        info!(target: "money", "[Malicious] Building erroneous PoW reward tx");
+        info!(target: "money", "[Malicious] ================================");
+        let (pow_reward_tx, _) =
+            th.pow_reward(&Holder::Alice, current_slot, Some(alice_reward + 1))?;
+
+        info!(target: "money", "[Malicious] =======================================");
+        info!(target: "money", "[Malicious] Checking erroneous amount PoW reward tx");
+        info!(target: "money", "[Malicious] =======================================");
+        th.execute_erroneous_txs(
+            TxAction::MoneyPoWReward,
+            &Holder::Alice,
+            &[pow_reward_tx.clone()],
+            current_slot,
+            1,
+        )
+        .await?;
+
+        info!(target: "money", "[Alice] ======================");
+        info!(target: "money", "[Alice] Building PoW reward tx");
+        info!(target: "money", "[Alice] ======================");
+        let (pow_reward_tx, pow_reward_params) =
+            th.pow_reward(&Holder::Alice, current_slot, None)?;
+
+        for holder in &HOLDERS {
+            info!(target: "money", "[{holder:?}] =============================");
+            info!(target: "money", "[{holder:?}] Executing Alice PoW reward tx");
+            info!(target: "money", "[{holder:?}] =============================");
+            th.execute_pow_reward_tx(holder, &pow_reward_tx, &pow_reward_params, current_slot)
+                .await?;
+        }
+
+        th.assert_trees(&HOLDERS);
+
+        // Alice gathers her new owncoin
+        let alice_oc = th.gather_owncoin(&Holder::Alice, &pow_reward_params.output, None)?;
+        alice_owncoins.push(alice_oc);
+
+        // Now Alice can send a little bit of funds to Bob
+        let alice_send = alice_reward / 2;
+        info!(target: "money", "[Alice] ====================================================");
+        info!(target: "money", "[Alice] Building Money::Transfer params for a payment to Bob");
+        info!(target: "money", "[Alice] ====================================================");
+        let (transfer_tx, transfer_params, spent_coins) =
+            th.transfer(alice_send, &Holder::Alice, &Holder::Bob, &alice_owncoins, *DARK_TOKEN_ID)?;
+
+        // Validating transfer params
+        assert!(transfer_params.inputs.len() == 1);
+        assert!(transfer_params.outputs.len() == 2);
+        assert!(spent_coins.len() == 1);
+        alice_owncoins.retain(|x| x != &spent_coins[0]);
+        assert!(alice_owncoins.is_empty());
+
+        for holder in &HOLDERS {
+            info!(target: "money", "[{holder:?}] ==============================");
+            info!(target: "money", "[{holder:?}] Executing Alice2Bob payment tx");
+            info!(target: "money", "[{holder:?}] ==============================");
+            th.execute_transfer_tx(holder, &transfer_tx, &transfer_params, current_slot, true)
+                .await?;
+        }
+
+        th.assert_trees(&HOLDERS);
+
+        // Alice should now have one OwnCoin with the change from the above transaction.
+        let alice_oc = th.gather_owncoin(&Holder::Alice, &transfer_params.outputs[0], None)?;
+        alice_owncoins.push(alice_oc);
+
+        // Bob should have this new one.
+        let bob_oc = th.gather_owncoin(&Holder::Bob, &transfer_params.outputs[1], None)?;
+        bob_owncoins.push(bob_oc);
+
+        // Validating transaction outcomes
+        assert!(alice_owncoins.len() == 1);
+        assert!(bob_owncoins.len() == 1);
+        assert!(alice_owncoins[0].note.value == alice_reward - alice_send);
+        assert!(bob_owncoins[0].note.value == alice_send);
+
+        // Statistics
+        th.statistics();
+
+        // Thanks for reading
+        Ok(())
+    })
+}

+ 3 - 0
src/contract/test-harness/src/lib.rs

@@ -65,6 +65,7 @@ mod dao_vote;
 mod money_airdrop;
 mod money_genesis_mint;
 mod money_otc_swap;
+mod money_pow_reward;
 mod money_token;
 mod money_transfer;
 
@@ -108,6 +109,7 @@ pub enum TxAction {
     MoneyGenesisMint,
     MoneyTransfer,
     MoneyOtcSwap,
+    MoneyPoWReward,
     ConsensusGenesisStake,
     ConsensusStake,
     ConsensusProposal,
@@ -255,6 +257,7 @@ impl TestHarness {
         tx_action_benchmarks.insert(TxAction::MoneyGenesisMint, TxActionBenchmarks::default());
         tx_action_benchmarks.insert(TxAction::MoneyOtcSwap, TxActionBenchmarks::default());
         tx_action_benchmarks.insert(TxAction::MoneyTransfer, TxActionBenchmarks::default());
+        tx_action_benchmarks.insert(TxAction::MoneyPoWReward, TxActionBenchmarks::default());
         tx_action_benchmarks.insert(TxAction::ConsensusGenesisStake, TxActionBenchmarks::default());
         tx_action_benchmarks.insert(TxAction::ConsensusStake, TxActionBenchmarks::default());
         tx_action_benchmarks.insert(TxAction::ConsensusProposal, TxActionBenchmarks::default());

+ 109 - 0
src/contract/test-harness/src/money_pow_reward.rs

@@ -0,0 +1,109 @@
+/* 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 std::time::Instant;
+
+use darkfi::{tx::Transaction, Result};
+use darkfi_money_contract::{
+    client::pow_reward_v1::PoWRewardCallBuilder, model::MoneyTokenMintParamsV1, MoneyFunction,
+    MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+};
+use darkfi_sdk::{
+    crypto::{MerkleNode, MONEY_CONTRACT_ID},
+    pasta::pallas,
+    ContractCall,
+};
+use darkfi_serial::{serialize, Encodable};
+use rand::rngs::OsRng;
+
+use super::{Holder, TestHarness, TxAction};
+
+impl TestHarness {
+    pub fn pow_reward(
+        &mut self,
+        holder: &Holder,
+        slot: u64,
+        reward: Option<u64>,
+    ) -> Result<(Transaction, MoneyTokenMintParamsV1)> {
+        let wallet = self.holders.get(holder).unwrap();
+
+        let (mint_pk, mint_zkbin) =
+            self.proving_keys.get(&MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string()).unwrap();
+
+        let tx_action_benchmark =
+            self.tx_action_benchmarks.get_mut(&TxAction::MoneyPoWReward).unwrap();
+
+        let timer = Instant::now();
+
+        // We're just going to be using a zero spend-hook and user-data
+        let spend_hook = pallas::Base::zero();
+        let user_data = pallas::Base::zero();
+
+        let builder = PoWRewardCallBuilder {
+            keypair: wallet.keypair,
+            slot,
+            spend_hook,
+            user_data,
+            mint_zkbin: mint_zkbin.clone(),
+            mint_pk: mint_pk.clone(),
+        };
+
+        let debris = match reward {
+            Some(value) => builder.build_with_custom_reward(value)?,
+            None => builder.build()?,
+        };
+
+        let mut data = vec![MoneyFunction::PoWRewardV1 as u8];
+        debris.params.encode(&mut data)?;
+        let calls = vec![ContractCall { contract_id: *MONEY_CONTRACT_ID, data }];
+        let proofs = vec![debris.proofs];
+        let mut tx = Transaction { calls, proofs, signatures: vec![] };
+        let sigs = tx.create_sigs(&mut OsRng, &[wallet.keypair.secret])?;
+        tx.signatures = vec![sigs];
+        tx_action_benchmark.creation_times.push(timer.elapsed());
+
+        // Calculate transaction sizes
+        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((tx, debris.params))
+    }
+
+    pub async fn execute_pow_reward_tx(
+        &mut self,
+        holder: &Holder,
+        tx: &Transaction,
+        params: &MoneyTokenMintParamsV1,
+        slot: u64,
+    ) -> Result<()> {
+        let wallet = self.holders.get_mut(holder).unwrap();
+        let tx_action_benchmark =
+            self.tx_action_benchmarks.get_mut(&TxAction::MoneyPoWReward).unwrap();
+        let timer = Instant::now();
+
+        wallet.validator.read().await.add_transactions(&[tx.clone()], slot, true).await?;
+        wallet.money_merkle_tree.append(MerkleNode::from(params.output.coin.inner()));
+        tx_action_benchmark.verify_times.push(timer.elapsed());
+
+        Ok(())
+    }
+}

+ 24 - 0
src/sdk/src/blockchain.rs

@@ -117,3 +117,27 @@ impl Default for Slot {
         Self::new(0, PreviousSlot::default(), PidOutput::default(), pallas::Base::ZERO, 0, 0)
     }
 }
+
+// TODO: This values are experimental, should be replaced with the proper ones once defined
+pub const POW_CUTOFF: u64 = 1000000;
+pub const POS_START: u64 = 1000001;
+/// Auxiliary function to calculate provided block height(slot) expected PoW reward value.
+/// Genesis block(0) always returns reward value 0.
+/// A cut-off is used, signalling PoS start, after which reward value 0 is returned.
+pub fn pow_expected_reward(block_height: u64) -> u64 {
+    match block_height {
+        0 => 0,
+        1..=1000 => 20,
+        1001..=2000 => 18,
+        2001..=3000 => 16,
+        3001..=4000 => 14,
+        4001..=5000 => 12,
+        5001..=6000 => 10,
+        6001..=7000 => 8,
+        7001..=8000 => 6,
+        8001..=9000 => 4,
+        9001..=10000 => 2,
+        10001..=POW_CUTOFF => 1,
+        POS_START.. => 0,
+    }
+}