darkfid/registry/
model.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{collections::HashMap, str::FromStr};
20
21use rand::rngs::OsRng;
22use tinyjson::JsonValue;
23use tracing::info;
24
25use darkfi::{
26    blockchain::{BlockInfo, Header, HeaderHash},
27    rpc::jsonrpc::JsonSubscriber,
28    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
29    util::{
30        encoding::base64,
31        time::{NanoTimestamp, Timestamp},
32    },
33    validator::{consensus::Fork, verification::apply_producer_transaction, ValidatorPtr},
34    zk::{empty_witnesses, ProvingKey, ZkCircuit},
35    zkas::ZkBinary,
36    Error, Result,
37};
38use darkfi_money_contract::{
39    client::pow_reward_v1::PoWRewardCallBuilder, MoneyFunction, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
40};
41use darkfi_sdk::{
42    crypto::{
43        keypair::{Address, Keypair, Network, SecretKey},
44        pasta_prelude::PrimeField,
45        FuncId, MerkleTree, MONEY_CONTRACT_ID,
46    },
47    pasta::pallas,
48    ContractCall,
49};
50use darkfi_serial::{deserialize_async, Encodable};
51
52use crate::error::RpcError;
53
54/// Auxiliary structure representing node miner rewards recipient configuration.
55#[derive(Debug, Clone)]
56pub struct MinerRewardsRecipientConfig {
57    /// Wallet mining address to receive mining rewards
58    pub recipient: Address,
59    /// Optional contract spend hook to use in the mining reward
60    pub spend_hook: Option<FuncId>,
61    /// Optional contract user data to use in the mining reward.
62    /// This is not arbitrary data.
63    pub user_data: Option<pallas::Base>,
64}
65
66impl MinerRewardsRecipientConfig {
67    pub fn new(
68        recipient: Address,
69        spend_hook: Option<FuncId>,
70        user_data: Option<pallas::Base>,
71    ) -> Self {
72        Self { recipient, spend_hook, user_data }
73    }
74
75    /// Auxiliary function to convert provided string to its
76    /// `MinerRewardsRecipientConfig`. Supports parsing both a normal
77    /// `Address` and a `base64` encoded mining configuration. Also
78    /// verifies it corresponds to the provided `Network`.
79    pub async fn from_str(network: &Network, address: &str) -> std::result::Result<Self, RpcError> {
80        // Try to parse the string as an `Address`
81        if let Ok(recipient) = Address::from_str(address) {
82            if recipient.network() != *network {
83                return Err(RpcError::MinerInvalidRecipientPrefix)
84            }
85            return Ok(Self { recipient, spend_hook: None, user_data: None })
86        }
87
88        // Try to parse the string as a `base64` encoded mining
89        // configuration
90        let Some(address_bytes) = base64::decode(address) else {
91            return Err(RpcError::MinerInvalidWalletConfig)
92        };
93        let Ok((recipient, spend_hook, user_data)) =
94            deserialize_async::<(String, Option<String>, Option<String>)>(&address_bytes).await
95        else {
96            return Err(RpcError::MinerInvalidWalletConfig)
97        };
98        let Ok(recipient) = Address::from_str(&recipient) else {
99            return Err(RpcError::MinerInvalidRecipient)
100        };
101        if recipient.network() != *network {
102            return Err(RpcError::MinerInvalidRecipientPrefix)
103        }
104        let spend_hook = match spend_hook {
105            Some(s) => match FuncId::from_str(&s) {
106                Ok(s) => Some(s),
107                Err(_) => return Err(RpcError::MinerInvalidSpendHook),
108            },
109            None => None,
110        };
111        let user_data: Option<pallas::Base> = match user_data {
112            Some(u) => {
113                let Ok(bytes) = bs58::decode(&u).into_vec() else {
114                    return Err(RpcError::MinerInvalidUserData)
115                };
116                let bytes: [u8; 32] = match bytes.try_into() {
117                    Ok(b) => b,
118                    Err(_) => return Err(RpcError::MinerInvalidUserData),
119                };
120                match pallas::Base::from_repr(bytes).into() {
121                    Some(v) => Some(v),
122                    None => return Err(RpcError::MinerInvalidUserData),
123                }
124            }
125            None => None,
126        };
127
128        Ok(Self { recipient, spend_hook, user_data })
129    }
130}
131
132/// Auxiliary structure representing a block template for mining.
133#[derive(Debug, Clone)]
134pub struct BlockTemplate {
135    /// Block that is being mined
136    pub block: BlockInfo,
137    /// RandomX current and next keys pair
138    pub randomx_keys: (HeaderHash, HeaderHash),
139    /// Compacted block mining target
140    pub target: Vec<u8>,
141    /// Block difficulty
142    pub difficulty: f64,
143    /// Ephemeral signing secret for this blocktemplate
144    pub secret: SecretKey,
145    /// Flag indicating if this template has been submitted
146    pub submitted: bool,
147}
148
149impl BlockTemplate {
150    fn new(
151        block: BlockInfo,
152        randomx_keys: (HeaderHash, HeaderHash),
153        target: Vec<u8>,
154        difficulty: f64,
155        secret: SecretKey,
156    ) -> Self {
157        Self { block, randomx_keys, target, difficulty, secret, submitted: false }
158    }
159
160    pub fn job_notification(&self) -> (String, JsonValue) {
161        let block_hash = hex::encode(self.block.header.hash().inner()).to_string();
162        let mut job = HashMap::from([
163            (
164                "blob".to_string(),
165                JsonValue::from(hex::encode(self.block.header.to_block_hashing_blob()).to_string()),
166            ),
167            ("job_id".to_string(), JsonValue::from(block_hash.clone())),
168            ("height".to_string(), JsonValue::from(self.block.header.height as f64)),
169            ("target".to_string(), JsonValue::from(hex::encode(&self.target))),
170            ("algo".to_string(), JsonValue::from(String::from("rx/0"))),
171            (
172                "seed_hash".to_string(),
173                JsonValue::from(hex::encode(self.randomx_keys.0.inner()).to_string()),
174            ),
175        ]);
176        if self.randomx_keys.0 != self.randomx_keys.1 {
177            job.insert(
178                "next_seed_hash".to_string(),
179                JsonValue::from(hex::encode(self.randomx_keys.1.inner()).to_string()),
180            );
181        }
182        (block_hash, JsonValue::from(job))
183    }
184}
185
186/// Auxiliary structure representing a native miner client record.
187#[derive(Debug, Clone)]
188pub struct MinerClient {
189    /// Miner wallet template key
190    pub wallet: String,
191    /// Miner recipient configuration
192    pub config: MinerRewardsRecipientConfig,
193    /// Current mining job
194    pub job: String,
195    /// Connection publisher to push new jobs
196    pub publisher: JsonSubscriber,
197}
198
199impl MinerClient {
200    pub fn new(wallet: &str, config: &MinerRewardsRecipientConfig, job: &str) -> (String, Self) {
201        let mut hasher = blake3::Hasher::new();
202        hasher.update(wallet.as_bytes());
203        hasher.update(&NanoTimestamp::current_time().inner().to_le_bytes());
204        let client_id = hex::encode(hasher.finalize().as_bytes()).to_string();
205        let publisher = JsonSubscriber::new("job");
206        (
207            client_id,
208            Self {
209                wallet: String::from(wallet),
210                config: config.clone(),
211                job: job.to_owned(),
212                publisher,
213            },
214        )
215    }
216}
217
218/// ZK data used to generate the "coinbase" transaction in a block
219pub struct PowRewardV1Zk {
220    pub zkbin: ZkBinary,
221    pub provingkey: ProvingKey,
222}
223
224impl PowRewardV1Zk {
225    pub fn new(validator: &ValidatorPtr) -> Result<Self> {
226        info!(
227            target: "darkfid::registry::model::PowRewardV1Zk::new",
228            "Generating PowRewardV1 ZkCircuit and ProvingKey...",
229        );
230
231        let (zkbin, _) = validator.blockchain.contracts.get_zkas(
232            &validator.blockchain.sled_db,
233            &MONEY_CONTRACT_ID,
234            MONEY_CONTRACT_ZKAS_MINT_NS_V1,
235        )?;
236
237        let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin);
238        let provingkey = ProvingKey::build(zkbin.k, &circuit);
239
240        Ok(Self { zkbin, provingkey })
241    }
242}
243
244/// Auxiliary function to generate next mining block template, in an
245/// atomic manner.
246pub async fn generate_next_block_template(
247    extended_fork: &mut Fork,
248    recipient_config: &MinerRewardsRecipientConfig,
249    zkbin: &ZkBinary,
250    pk: &ProvingKey,
251    verify_fees: bool,
252) -> Result<BlockTemplate> {
253    // Grab forks' last block proposal(previous)
254    let last_proposal = extended_fork.last_proposal()?;
255
256    // Grab forks' next block height
257    let next_block_height = last_proposal.block.header.height + 1;
258
259    // Grab forks' next mine target and difficulty
260    let (target, difficulty) = extended_fork.module.next_mine_target_and_difficulty()?;
261
262    // The target should be compacted to 8 bytes. We'll send the MSB.
263    let target_bytes = target.to_bytes_le();
264    let mut padded = [0u8; 32];
265    let len = target_bytes.len().min(32);
266    padded[..len].copy_from_slice(&target_bytes[..len]);
267    let target = padded[24..32].to_vec();
268
269    // Cast difficulty to f64. This should always work.
270    let difficulty = difficulty.to_string().parse()?;
271
272    // Grab forks' unproposed transactions
273    let (mut txs, _, fees) = extended_fork.unproposed_txs(next_block_height, verify_fees).await?;
274
275    // Create an ephemeral block signing keypair. Its secret key will
276    // be stored in the PowReward transaction's encrypted note for
277    // later retrieval. It is encrypted towards the recipient's public
278    // key.
279    let block_signing_keypair = Keypair::random(&mut OsRng);
280
281    // Generate reward transaction
282    let tx = generate_transaction(
283        next_block_height,
284        fees,
285        &block_signing_keypair,
286        recipient_config,
287        zkbin,
288        pk,
289    )?;
290
291    // Apply producer transaction in the forks' overlay
292    let _ = apply_producer_transaction(
293        &extended_fork.overlay,
294        next_block_height,
295        extended_fork.module.target,
296        &tx,
297        &mut MerkleTree::new(1),
298    )
299    .await?;
300    txs.push(tx);
301
302    // Grab the updated contracts states root
303    let diff =
304        extended_fork.overlay.lock().unwrap().overlay.lock().unwrap().diff(&extended_fork.diffs)?;
305    extended_fork
306        .overlay
307        .lock()
308        .unwrap()
309        .contracts
310        .update_state_monotree(&diff, &mut extended_fork.state_monotree)?;
311    let Some(state_root) = extended_fork.state_monotree.get_headroot()? else {
312        return Err(Error::ContractsStatesRootNotFoundError);
313    };
314
315    // Generate the new header
316    let mut header =
317        Header::new(last_proposal.hash, next_block_height, 0, Timestamp::current_time());
318    header.state_root = state_root;
319
320    // Generate the block
321    let mut next_block = BlockInfo::new_empty(header);
322
323    // Add transactions to the block
324    next_block.append_txs(txs);
325
326    Ok(BlockTemplate::new(
327        next_block,
328        extended_fork.module.darkfi_rx_keys,
329        target,
330        difficulty,
331        block_signing_keypair.secret,
332    ))
333}
334
335/// Auxiliary function to generate a Money::PoWReward transaction.
336fn generate_transaction(
337    block_height: u32,
338    fees: u64,
339    block_signing_keypair: &Keypair,
340    recipient_config: &MinerRewardsRecipientConfig,
341    zkbin: &ZkBinary,
342    pk: &ProvingKey,
343) -> Result<Transaction> {
344    // Build the transaction debris
345    let debris = PoWRewardCallBuilder {
346        signature_keypair: *block_signing_keypair,
347        block_height,
348        fees,
349        recipient: Some(*recipient_config.recipient.public_key()),
350        spend_hook: recipient_config.spend_hook,
351        user_data: recipient_config.user_data,
352        mint_zkbin: zkbin.clone(),
353        mint_pk: pk.clone(),
354    }
355    .build()?;
356
357    // Generate and sign the actual transaction
358    let mut data = vec![MoneyFunction::PoWRewardV1 as u8];
359    debris.params.encode(&mut data)?;
360    let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
361    let mut tx_builder =
362        TransactionBuilder::new(ContractCallLeaf { call, proofs: debris.proofs }, vec![])?;
363    let mut tx = tx_builder.build()?;
364    let sigs = tx.create_sigs(&[block_signing_keypair.secret])?;
365    tx.signatures = vec![sigs];
366
367    Ok(tx)
368}