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