/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 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 . */ use std::{collections::HashMap, str::FromStr}; use rand::rngs::OsRng; use tinyjson::JsonValue; use tracing::info; use darkfi::{ blockchain::{BlockInfo, Header, HeaderHash}, rpc::jsonrpc::JsonSubscriber, tx::{ContractCallLeaf, Transaction, TransactionBuilder}, util::{ encoding::base64, time::{NanoTimestamp, Timestamp}, }, validator::{ consensus::Fork, pow::{RANDOMX_KEY_CHANGE_DELAY, RANDOMX_KEY_CHANGING_HEIGHT}, verification::apply_producer_transaction, ValidatorPtr, }, zk::{empty_witnesses, ProvingKey, ZkCircuit}, zkas::ZkBinary, Error, Result, }; use darkfi_money_contract::{ client::pow_reward_v1::PoWRewardCallBuilder, model::MoneyPoWRewardParamsV1, MoneyFunction, MONEY_CONTRACT_ZKAS_MINT_NS_V1, }; use darkfi_sdk::{ crypto::{ keypair::{Address, Keypair, Network, SecretKey}, pasta_prelude::PrimeField, FuncId, MerkleTree, MONEY_CONTRACT_ID, }, fee::accumulate_fee, pasta::pallas, ContractCall, }; use darkfi_serial::{deserialize_async, Encodable}; use crate::error::RpcError; /// Auxiliary structure representing node miner rewards recipient configuration. #[derive(Debug, Clone)] pub struct MinerRewardsRecipientConfig { /// Wallet mining address to receive mining rewards pub recipient: Address, /// Optional contract spend hook to use in the mining reward pub spend_hook: Option, /// Optional contract user data to use in the mining reward. /// This is not arbitrary data. pub user_data: Option, } impl MinerRewardsRecipientConfig { pub fn new( recipient: Address, spend_hook: Option, user_data: Option, ) -> Self { Self { recipient, spend_hook, user_data } } /// Auxiliary function to convert provided string to its /// `MinerRewardsRecipientConfig`. Supports parsing both a normal /// `Address` and a `base64` encoded mining configuration. Also /// verifies it corresponds to the provided `Network`. pub async fn from_str(network: &Network, address: &str) -> std::result::Result { // Try to parse the string as an `Address` if let Ok(recipient) = Address::from_str(address) { if recipient.network() != *network { return Err(RpcError::MinerInvalidRecipientPrefix) } return Ok(Self { recipient, spend_hook: None, user_data: None }) } // Try to parse the string as a `base64` encoded mining // configuration let Some(address_bytes) = base64::decode(address) else { return Err(RpcError::MinerInvalidWalletConfig) }; let Ok((recipient, spend_hook, user_data)) = deserialize_async::<(String, Option, Option)>(&address_bytes).await else { return Err(RpcError::MinerInvalidWalletConfig) }; let Ok(recipient) = Address::from_str(&recipient) else { return Err(RpcError::MinerInvalidRecipient) }; if recipient.network() != *network { return Err(RpcError::MinerInvalidRecipientPrefix) } let spend_hook = match spend_hook { Some(s) => match FuncId::from_str(&s) { Ok(s) => Some(s), Err(_) => return Err(RpcError::MinerInvalidSpendHook), }, None => None, }; let user_data: Option = match user_data { Some(u) => { let Ok(bytes) = bs58::decode(&u).into_vec() else { return Err(RpcError::MinerInvalidUserData) }; let bytes: [u8; 32] = match bytes.try_into() { Ok(b) => b, Err(_) => return Err(RpcError::MinerInvalidUserData), }; match pallas::Base::from_repr(bytes).into() { Some(v) => Some(v), None => return Err(RpcError::MinerInvalidUserData), } } None => None, }; Ok(Self { recipient, spend_hook, user_data }) } } /// Auxiliary structure representing a block template for mining. #[derive(Debug, Clone)] pub struct BlockTemplate { /// Block that is being mined pub block: BlockInfo, /// New kvdb trees opened the overlay this block was generated pub new_trees: Vec, /// RandomX current and next keys pair pub randomx_keys: (HeaderHash, Option), /// Compacted block mining target pub target: Vec, /// Block difficulty pub difficulty: f64, /// Ephemeral signing secret for this blocktemplate pub secret: SecretKey, /// Flag indicating if this template has been submitted pub submitted: bool, } impl BlockTemplate { fn new( block: BlockInfo, new_trees: Vec, randomx_keys: (HeaderHash, Option), target: Vec, difficulty: f64, secret: SecretKey, ) -> Self { Self { block, new_trees, randomx_keys, target, difficulty, secret, submitted: false } } pub fn job_notification(&self) -> (String, JsonValue) { let block_hash = hex::encode(self.block.header.hash().inner()).to_string(); let mut job = HashMap::from([ ( "blob".to_string(), JsonValue::from(hex::encode(self.block.header.to_block_hashing_blob()).to_string()), ), ("job_id".to_string(), JsonValue::from(block_hash.clone())), ("height".to_string(), JsonValue::from(self.block.header.height as f64)), ("target".to_string(), JsonValue::from(hex::encode(&self.target))), ("algo".to_string(), JsonValue::from(String::from("rx/0"))), ( "seed_hash".to_string(), JsonValue::from(hex::encode(self.randomx_keys.0.inner()).to_string()), ), ]); if let Some(next_randomx_key) = self.randomx_keys.1 { job.insert( "next_seed_hash".to_string(), JsonValue::from(hex::encode(next_randomx_key.inner()).to_string()), ); } (block_hash, JsonValue::from(job)) } /// Return block full reward. /// /// Note: always check if block contains transactions before /// calling this function. pub async fn reward(&self) -> Result { let Some(producer_tx) = self.block.txs.last() else { return Err(Error::BlockContainsNoTransactions( self.block.header.template_hash().as_string(), )) }; let Some(call) = producer_tx.calls.first() else { return Err(Error::ParseFailed("producer transaction contains no calls")) }; if !call.data.is_money_pow_reward() { return Err(Error::ParseFailed("producer transaction is not Money::PoWRewardV1")) } let params: MoneyPoWRewardParamsV1 = deserialize_async(&call.data.data[1..]).await?; Ok(params.input.value) } /// Return block fees. /// /// Note: always check if block contains transactions before /// calling this function. pub async fn fees(&self) -> Result { let mut fees = 0; 'outer: for tx in &self.block.txs[..self.block.txs.len() - 1] { for call in &tx.calls { if !call.data.is_money_fee() { continue } fees = accumulate_fee(fees, call.data.money_fee_value()?)?; continue 'outer } } Ok(fees) } /// Return block reward excluding fees and fees values. pub async fn reward_excluding_fees_and_fees(&self) -> Result<(u64, u64)> { if self.block.txs.is_empty() { return Err(Error::BlockContainsNoTransactions( self.block.header.template_hash().as_string(), )) } let fees = self.fees().await?; let reward = self.reward().await?.checked_sub(fees).ok_or(Error::SubtractionUnderflow)?; Ok((reward, fees)) } } /// Auxiliary structure representing a native miner client record. #[derive(Debug, Clone)] pub struct MinerClient { /// Miner wallet template key pub wallet: String, /// Miner recipient configuration pub config: MinerRewardsRecipientConfig, /// Current mining job pub job: String, /// Connection publisher to push new jobs pub publisher: JsonSubscriber, } impl MinerClient { pub fn new(wallet: &str, config: &MinerRewardsRecipientConfig, job: &str) -> (String, Self) { let mut hasher = blake3::Hasher::new(); hasher.update(wallet.as_bytes()); hasher.update(&NanoTimestamp::current_time().inner().to_le_bytes()); let client_id = hex::encode(hasher.finalize().as_bytes()).to_string(); let publisher = JsonSubscriber::new("job"); ( client_id, Self { wallet: String::from(wallet), config: config.clone(), job: job.to_owned(), publisher, }, ) } } /// ZK data used to generate the "coinbase" transaction in a block pub struct PowRewardV1Zk { pub zkbin: ZkBinary, pub provingkey: ProvingKey, } impl PowRewardV1Zk { pub async fn new(validator: &ValidatorPtr) -> Result { info!( target: "darkfid::registry::model::PowRewardV1Zk::new", "Generating PowRewardV1 ZkCircuit and ProvingKey...", ); let validator = validator.read().await; let (zkbin, _) = validator .blockchain .contracts .get_zkas(&MONEY_CONTRACT_ID, MONEY_CONTRACT_ZKAS_MINT_NS_V1)?; let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin); let provingkey = ProvingKey::build(zkbin.k, &circuit); Ok(Self { zkbin, provingkey }) } } /// Auxiliary function to generate next mining block template, in an /// atomic manner. pub async fn generate_next_block_template( extended_fork: &mut Fork, recipient_config: &MinerRewardsRecipientConfig, zkbin: &ZkBinary, pk: &ProvingKey, verify_fees: bool, ) -> Result { // Grab forks' last block proposal(previous) let last_proposal = extended_fork.last_proposal()?; // Grab forks' next block height let next_block_height = last_proposal.block.header.height + 1; // Grab forks' RandomX keys for that height let randomx_keys = if next_block_height > RANDOMX_KEY_CHANGING_HEIGHT && next_block_height % RANDOMX_KEY_CHANGING_HEIGHT == RANDOMX_KEY_CHANGE_DELAY { ( extended_fork .module .darkfi_rx_keys .1 .ok_or_else(|| Error::ParseFailed("darkfi_rx_keys.1 unwrap() error"))?, None, ) } else { extended_fork.module.darkfi_rx_keys }; // Grab forks' next mine target and difficulty let (target, difficulty) = extended_fork.module.next_mine_target_and_difficulty()?; // The target should be compacted to 8 bytes. We'll send the MSB. let target_bytes = target.to_bytes_le(); let mut padded = [0u8; 32]; let len = target_bytes.len().min(32); padded[..len].copy_from_slice(&target_bytes[..len]); let target = padded[24..32].to_vec(); // Cast difficulty to f64. This should always work. let difficulty = difficulty.to_string().parse()?; // Grab forks' unproposed transactions let (mut txs, _, fees) = extended_fork.unproposed_txs(next_block_height, verify_fees).await?; // Create an ephemeral block signing keypair. Its secret key will // be stored in the PowReward transaction's encrypted note for // later retrieval. It is encrypted towards the recipient's public // key. let block_signing_keypair = Keypair::random(&mut OsRng); // Generate reward transaction let tx = generate_transaction( next_block_height, fees, &block_signing_keypair, recipient_config, zkbin, pk, )?; // Apply producer transaction in the forks' overlay let _ = apply_producer_transaction( &extended_fork.overlay, next_block_height, extended_fork.module.target, &tx, &mut MerkleTree::new(1), ) .await?; txs.push(tx); // Grab the updated contracts states root let diff = extended_fork.overlay.lock().unwrap().overlay.lock().unwrap().diff(&extended_fork.diffs)?; let state_root = extended_fork.overlay.lock().unwrap().contracts.update_state_monotree(&diff)?; // Generate the new header let mut header = Header::new(last_proposal.hash, next_block_height, 0, Timestamp::current_time()); header.state_root = state_root; // Generate the block let mut next_block = BlockInfo::new_empty(header); // Add transactions to the block next_block.append_txs(txs); Ok(BlockTemplate::new( next_block, diff.new_trees(), randomx_keys, target, difficulty, block_signing_keypair.secret, )) } /// Auxiliary function to generate a Money::PoWReward transaction. fn generate_transaction( block_height: u32, fees: u64, block_signing_keypair: &Keypair, recipient_config: &MinerRewardsRecipientConfig, zkbin: &ZkBinary, pk: &ProvingKey, ) -> Result { // Build the transaction debris let debris = PoWRewardCallBuilder { signature_keypair: *block_signing_keypair, block_height, fees, recipient: Some(*recipient_config.recipient.public_key()), spend_hook: recipient_config.spend_hook, user_data: recipient_config.user_data, mint_zkbin: zkbin.clone(), mint_pk: pk.clone(), } .build()?; // Generate and sign the actual transaction let mut data = vec![MoneyFunction::PoWRewardV1 as u8]; debris.params.encode(&mut data)?; let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data }; let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?; let mut tx = tx_builder.build()?; let sigs = tx.create_sigs(&[block_signing_keypair.secret])?; tx.signatures = vec![sigs]; Ok(tx) }