From 5754d4268f0779e1d1d123dc94da062f6864cf6d Mon Sep 17 00:00:00 2001 From: skoupidi Date: Wed, 26 Nov 2025 17:26:15 +0200 Subject: [PATCH] darkfid: support merge mining for a DAO --- bin/darkfid/src/lib.rs | 2 +- bin/darkfid/src/rpc_xmr.rs | 137 ++++++++++++++++++++++++++---- bin/darkfid/src/task/consensus.rs | 2 +- bin/drk/src/cli_util.rs | 21 +++-- bin/drk/src/dao.rs | 28 +++++- bin/drk/src/interactive.rs | 106 +++++++++++++++++++++-- bin/drk/src/main.rs | 83 ++++++++++++++++++ bin/drk/src/money.rs | 53 +++++++++++- bin/drk/src/token.rs | 2 +- doc/src/testnet/dao.md | 29 +++++-- doc/src/testnet/merge-mining.md | 50 ++++++++++- 11 files changed, 468 insertions(+), 45 deletions(-) diff --git a/bin/darkfid/src/lib.rs b/bin/darkfid/src/lib.rs index d4f2bbcc8..5a6460af3 100644 --- a/bin/darkfid/src/lib.rs +++ b/bin/darkfid/src/lib.rs @@ -85,7 +85,7 @@ pub struct DarkfiNode { /// HTTP JSON-RPC connection tracker mm_rpc_connections: Mutex>, /// Merge mining block templates - mm_blocktemplates: Mutex>, + mm_blocktemplates: Mutex, (BlockInfo, f64, SecretKey)>>, /// PowRewardV1 ZK data powrewardv1_zk: PowRewardV1Zk, } diff --git a/bin/darkfid/src/rpc_xmr.rs b/bin/darkfid/src/rpc_xmr.rs index a0d57dee6..153394a2f 100644 --- a/bin/darkfid/src/rpc_xmr.rs +++ b/bin/darkfid/src/rpc_xmr.rs @@ -31,8 +31,11 @@ use darkfi::{ util::encoding::base64, validator::consensus::Proposal, }; -use darkfi_sdk::crypto::PublicKey; -use darkfi_serial::serialize_async; +use darkfi_sdk::{ + crypto::{pasta_prelude::PrimeField, FuncId, PublicKey}, + pasta::pallas, +}; +use darkfi_serial::{deserialize_async, serialize_async}; use hex::FromHex; use tinyjson::JsonValue; use tracing::{error, info}; @@ -89,13 +92,13 @@ impl DarkfiNode { // merge mining. // // **Request:** - // * `address` : A wallet address on the merge mined chain + // * `address` : A base-64 encoded wallet address mining configuration on the merge mined chain // * `aux_hash`: Merge mining job that is currently being polled // * `height` : Monero height // * `prev_id` : Hash of the previous Monero block // // **Response:** - // * `aux_blob`: The hex-encoded wallet address blob + // * `aux_blob`: The hex-encoded wallet address mining configuration blob // * `aux_diff`: Mining difficulty (decimal number) // * `aux_hash`: A 32-byte hex-encoded hash of merge mined block // @@ -112,7 +115,7 @@ impl DarkfiNode { return JsonError::new(InvalidParams, None, id).into() }; - // Parse address + // Parse address mining configuration let Some(address) = params.get("address") else { return JsonError::new(InvalidParams, Some("missing address".to_string()), id).into() }; @@ -120,10 +123,65 @@ impl DarkfiNode { return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id) .into() }; - let Ok(address) = PublicKey::from_str(address) else { + let Some(address_bytes) = base64::decode(address) else { return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id) .into() }; + let Ok((recipient, spend_hook, user_data)) = + deserialize_async::<(PublicKey, Option, Option)>(&address_bytes).await + else { + return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id) + .into() + }; + let spend_hook = match spend_hook { + Some(s) => match FuncId::from_str(&s) { + Ok(s) => Some(s), + Err(_) => { + return JsonError::new( + InvalidParams, + Some("invalid address format".to_string()), + id, + ) + .into() + } + }, + None => None, + }; + let user_data: Option = match user_data { + Some(u) => { + let Ok(bytes) = bs58::decode(&u).into_vec() else { + return JsonError::new( + InvalidParams, + Some("invalid address format".to_string()), + id, + ) + .into() + }; + let bytes: [u8; 32] = match bytes.try_into() { + Ok(b) => b, + Err(_) => { + return JsonError::new( + InvalidParams, + Some("invalid address format".to_string()), + id, + ) + .into() + } + }; + match pallas::Base::from_repr(bytes).into() { + Some(v) => Some(v), + None => { + return JsonError::new( + InvalidParams, + Some("invalid address format".to_string()), + id, + ) + .into() + } + } + } + None => None, + }; // Parse aux_hash let Some(aux_hash) = params.get("aux_hash") else { @@ -184,7 +242,6 @@ impl DarkfiNode { return JsonError::new(ErrorCode::InternalError, None, id).into() } }; - let address_bytes = address.to_bytes(); if let Some((block, difficulty, _)) = mm_blocktemplates.get(&address_bytes) { let last_proposal = match extended_fork.last_proposal() { Ok(p) => p, @@ -217,9 +274,17 @@ impl DarkfiNode { // At this point, we should query the Validator for a new blocktemplate. // We first need to construct `MinerRewardsRecipientConfig` from the - // address provided to us through the RPC. - let recipient_config = - MinerRewardsRecipientConfig { recipient: address, spend_hook: None, user_data: None }; + // address configuration provided to us through the RPC. + let recipient_str = format!("{recipient}"); + let spend_hook_str = match spend_hook { + Some(spend_hook) => format!("{spend_hook}"), + None => String::from("-"), + }; + let user_data_str = match user_data { + Some(user_data) => bs58::encode(user_data.to_repr()).into_string(), + None => String::from("-"), + }; + let recipient_config = MinerRewardsRecipientConfig { recipient, spend_hook, user_data }; // Now let's try to construct the blocktemplate. // Find the difficulty. Note we cast it to f64 here. @@ -260,10 +325,11 @@ impl DarkfiNode { // Now we have the blocktemplate. We'll mark it down in memory, // and then ship it to RPC. let blockhash = blocktemplate.header.template_hash(); - mm_blocktemplates.insert(address_bytes, (blocktemplate, difficulty, block_signing_secret)); + mm_blocktemplates + .insert(address_bytes.clone(), (blocktemplate, difficulty, block_signing_secret)); info!( target: "darkfid::rpc_xmr::xmr_merge_mining_get_aux_block", - "[RPC-XMR] Created new blocktemplate: address={address}, aux_hash={blockhash}, height={height}, prev_id={prev_id}" + "[RPC-XMR] Created new blocktemplate: address={recipient_str}, spend_hook={spend_hook_str}, user_data={user_data_str}, aux_hash={blockhash}, height={height}, prev_id={prev_id}" ); let response = JsonValue::from(HashMap::from([ @@ -308,7 +374,7 @@ impl DarkfiNode { return JsonError::new(InvalidParams, None, id).into() }; - // Parse address from aux_blob + // Parse address mining configuration from aux_blob let Some(aux_blob) = params.get("aux_blob") else { return JsonError::new(InvalidParams, Some("missing aux_blob".to_string()), id).into() }; @@ -316,15 +382,54 @@ impl DarkfiNode { return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id) .into() }; - let mut address_bytes = [0u8; 32]; - if hex::decode_to_slice(aux_blob, &mut address_bytes).is_err() { + let Ok(address_bytes) = hex::decode(aux_blob) else { return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id) .into() }; - if PublicKey::from_bytes(address_bytes).is_err() { + let Ok((_, spend_hook, user_data)) = + deserialize_async::<(PublicKey, Option, Option)>(&address_bytes).await + else { return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id) .into() }; + if let Some(spend_hook) = spend_hook { + if FuncId::from_str(&spend_hook).is_err() { + return JsonError::new( + InvalidParams, + Some("invalid aux_blob format".to_string()), + id, + ) + .into() + } + }; + if let Some(user_data) = user_data { + let Ok(bytes) = bs58::decode(&user_data).into_vec() else { + return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id) + .into() + }; + let bytes: [u8; 32] = match bytes.try_into() { + Ok(b) => b, + Err(_) => { + return JsonError::new( + InvalidParams, + Some("invalid aux_blob format".to_string()), + id, + ) + .into() + } + }; + let _: pallas::Base = match pallas::Base::from_repr(bytes).into() { + Some(v) => v, + None => { + return JsonError::new( + InvalidParams, + Some("invalid aux_blob format".to_string()), + id, + ) + .into() + } + }; + }; // Parse aux_hash let Some(aux_hash) = params.get("aux_hash") else { diff --git a/bin/darkfid/src/task/consensus.rs b/bin/darkfid/src/task/consensus.rs index 0ebc4c7a6..2a2f770e9 100644 --- a/bin/darkfid/src/task/consensus.rs +++ b/bin/darkfid/src/task/consensus.rs @@ -290,7 +290,7 @@ pub async fn clean_mm_blocktemplates(node: &DarkfiNodePtr) -> Result<()> { } // This job doesn't reference something so we drop it - dropped_templates.push(*key); + dropped_templates.push(key.clone()); } // Drop jobs not referencing active forks or last confirmed block diff --git a/bin/drk/src/cli_util.rs b/bin/drk/src/cli_util.rs index ad569d5dc..5dce531b2 100644 --- a/bin/drk/src/cli_util.rs +++ b/bin/drk/src/cli_util.rs @@ -152,7 +152,7 @@ pub fn generate_completions(shell: &str) -> Result { let default_address = SubCommand::with_name("default-address") .about("Set the default address in the wallet") - .arg(index); + .arg(index.clone()); let secrets = SubCommand::with_name("secrets").about("Print all the secret keys from the wallet"); @@ -164,6 +164,14 @@ pub fn generate_completions(shell: &str) -> Result { let coins = SubCommand::with_name("coins").about("Print all the coins in the wallet"); + let spend_hook = Arg::with_name("spend-hook").help("Optional contract spend hook to use"); + + let user_data = Arg::with_name("user-data").help("Optional user data to use"); + + let mining_config = SubCommand::with_name("mining-config") + .about("Print a wallet address mining configuration") + .args(&[index, spend_hook.clone(), user_data.clone()]); + let wallet = SubCommand::with_name("wallet").about("Wallet operations").subcommands(vec![ initialize, keygen, @@ -175,6 +183,7 @@ pub fn generate_completions(shell: &str) -> Result { import_secrets, tree, coins, + mining_config, ]); // Spend @@ -193,10 +202,6 @@ pub fn generate_completions(shell: &str) -> Result { let recipient = Arg::with_name("recipient").help("Recipient address"); - let spend_hook = Arg::with_name("spend-hook").help("Optional contract spend hook to use"); - - let user_data = Arg::with_name("user-data").help("Optional user data to use"); - let half_split = Arg::with_name("half-split") .long("half-split") .help("Split the output coin into two equal halves"); @@ -302,7 +307,7 @@ pub fn generate_completions(shell: &str) -> Result { .about("Create a generic proposal for a DAO") .args(&[name.clone(), duration, user_data.clone()]); - let proposals = SubCommand::with_name("proposals").about("List DAO proposals").args(&[name]); + let proposals = SubCommand::with_name("proposals").about("List DAO proposals").arg(&name); let bulla = Arg::with_name("bulla").help("Bulla identifier for the proposal"); @@ -337,6 +342,9 @@ pub fn generate_completions(shell: &str) -> Result { let spend_hook_cmd = SubCommand::with_name("spend-hook") .about("Print the DAO contract base64-encoded spend hook"); + let mining_config = + SubCommand::with_name("mining-config").about("Print a DAO mining configuration").arg(name); + let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![ create, view, @@ -352,6 +360,7 @@ pub fn generate_completions(shell: &str) -> Result { vote, exec, spend_hook_cmd, + mining_config, ]); // AttachFee diff --git a/bin/drk/src/dao.rs b/bin/drk/src/dao.rs index b8ef7467d..392ebd8e1 100644 --- a/bin/drk/src/dao.rs +++ b/bin/drk/src/dao.rs @@ -25,7 +25,10 @@ use rusqlite::types::Value; use darkfi::{ tx::{ContractCallLeaf, Transaction, TransactionBuilder}, - util::parse::{decode_base10, encode_base10}, + util::{ + encoding::base64, + parse::{decode_base10, encode_base10}, + }, zk::{empty_witnesses, halo2::Field, ProvingKey, ZkCircuit}, zkas::ZkBinary, Error, Result, @@ -55,6 +58,7 @@ use darkfi_money_contract::{ use darkfi_sdk::{ bridgetree, crypto::{ + pasta_prelude::PrimeField, poseidon_hash, smt::{MemoryStorageFp, PoseidonFp, SmtMemoryFp, EMPTY_NODES_FP}, util::{fp_mod_fv, fp_to_u64}, @@ -1907,6 +1911,28 @@ impl Drk { Ok(balmap) } + /// Fetch known unspent balances from the wallet for the given DAO name. + pub async fn dao_mining_config(&self, name: &str, output: &mut Vec) -> Result<()> { + let dao = self.get_dao_by_name(name).await?; + let recipient = dao.params.dao.notes_public_key; + let spend_hook = format!( + "{}", + FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 } + .to_func_id() + ); + let user_data = bs58::encode(dao.bulla().inner().to_repr()).into_string(); + output.push(String::from("DarkFi TOML configuration:")); + output.push(format!("recipient = \"{recipient}\"")); + output.push(format!("spend_hook = \"{spend_hook}\"")); + output.push(format!("user_data = \"{user_data}\"")); + output.push(String::from("\nP2Pool wallet address to use:")); + output.push( + base64::encode(&serialize(&(recipient, Some(spend_hook), Some(user_data)))).to_string(), + ); + + Ok(()) + } + /// Fetch all known DAO proposalss from the wallet. pub async fn get_proposals(&self) -> Result> { let rows = match self.wallet.query_multiple(&DAO_PROPOSALS_TABLE, &[], &[]) { diff --git a/bin/drk/src/interactive.rs b/bin/drk/src/interactive.rs index d03949228..adbd1a87a 100644 --- a/bin/drk/src/interactive.rs +++ b/bin/drk/src/interactive.rs @@ -150,7 +150,8 @@ fn completion(buffer: &str, lc: &mut Vec) { lc.push(prefix.clone() + "wallet secrets"); lc.push(prefix.clone() + "wallet import-secrets"); lc.push(prefix.clone() + "wallet tree"); - lc.push(prefix + "wallet coins"); + lc.push(prefix.clone() + "wallet coins"); + lc.push(prefix + "wallet mining-config"); return } @@ -193,7 +194,8 @@ fn completion(buffer: &str, lc: &mut Vec) { lc.push(prefix.clone() + "dao proposal-import"); lc.push(prefix.clone() + "dao vote"); lc.push(prefix.clone() + "dao exec"); - lc.push(prefix + "dao spend-hook"); + lc.push(prefix.clone() + "dao spend-hook"); + lc.push(prefix + "dao mining-config"); return } @@ -333,13 +335,14 @@ fn hints(buffer: &str) -> Option<(String, i32, bool)> { let bold = false; match last { "completions " => Some(("".to_string(), color, bold)), - "wallet " => Some(("(initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins)".to_string(), color, bold)), + "wallet " => Some(("(initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins|mining-config)".to_string(), color, bold)), "wallet default-address " => Some(("".to_string(), color, bold)), + "wallet mining-config " => Some((" [spend_hook] [user_data]".to_string(), color, bold)), "unspend " => Some(("".to_string(), color, bold)), "transfer " => Some(("[--half-split] [spend_hook] [user_data]".to_string(), color, bold)), "otc " => Some(("(init|join|inspect|sign)".to_string(), color, bold)), "otc init " => Some((" ".to_string(), color, bold)), - "dao " => Some(("(create|view|import|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook)".to_string(), color, bold)), + "dao " => Some(("(create|view|import|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook|mining-config)".to_string(), color, bold)), "dao create " => Some((" ".to_string(), color, bold)), "dao import " => Some(("".to_string(), color, bold)), "dao list " => Some(("[name]".to_string(), color, bold)), @@ -351,6 +354,7 @@ fn hints(buffer: &str) -> Option<(String, i32, bool)> { "dao proposal " => Some(("[--(export|mint-proposal)] ".to_string(), color, bold)), "dao vote " => Some((" [vote-weight]".to_string(), color, bold)), "dao exec " => Some(("[--early] ".to_string(), color, bold)), + "dao mining-config " => Some(("".to_string(), color, bold)), "scan " => Some(("[--reset]".to_string(), color, bold)), "scan --reset " => Some(("".to_string(), color, bold)), "explorer " => Some(("(fetch-tx|simulate-tx|txs-history|clear-reverted|scanned-blocks)".to_string(), color, bold)), @@ -774,7 +778,7 @@ async fn handle_wallet(drk: &DrkPtr, parts: &[&str], input: &[String], output: & // Check correct command structure if parts.len() < 2 { output.push(String::from("Malformed `wallet` command")); - output.push(String::from("Usage: wallet (initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins)")); + output.push(String::from("Usage: wallet (initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins|mining-config)")); return } @@ -790,9 +794,10 @@ async fn handle_wallet(drk: &DrkPtr, parts: &[&str], input: &[String], output: & "import-secrets" => handle_wallet_import_secrets(drk, input, output).await, "tree" => handle_wallet_tree(drk, output).await, "coins" => handle_wallet_coins(drk, output).await, + "mining-config" => handle_wallet_mining_config(drk, parts, output).await, _ => { output.push(format!("Unrecognized wallet subcommand: {}", parts[1])); - output.push(String::from("Usage: wallet (initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins)")); + output.push(String::from("Usage: wallet (initialize|keygen|balance|address|addresses|default-address|secrets|import-secrets|tree|coins|mining-config)")); } } } @@ -1064,6 +1069,76 @@ async fn handle_wallet_coins(drk: &DrkPtr, output: &mut Vec) { output.push(format!("{table}")); } +/// Auxiliary function to define the wallet mining config subcommand handling. +async fn handle_wallet_mining_config(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { + // Check correct command structure + if parts.len() < 3 || parts.len() > 5 { + output.push(String::from("Malformed `wallet mining-address` subcommand")); + output.push(String::from("Usage: wallet mining-config [spend_hook] [user_data]")); + return + } + + // Parse command + let mut index = 2; + let wallet_index = match usize::from_str(parts[index]) { + Ok(i) => i, + Err(e) => { + output.push(format!("Invalid address id: {e}")); + return + } + }; + index += 1; + + let spend_hook = if index < parts.len() { + match FuncId::from_str(parts[index]) { + Ok(s) => Some(s), + Err(e) => { + output.push(format!("Invalid spend hook: {e}")); + return + } + } + } else { + None + }; + index += 1; + + let user_data = if index < parts.len() { + let bytes = match bs58::decode(&parts[index]).into_vec() { + Ok(b) => b, + Err(e) => { + output.push(format!("Invalid user data: {e}")); + return + } + }; + + let bytes: [u8; 32] = match bytes.try_into() { + Ok(b) => b, + Err(e) => { + output.push(format!("Invalid user data: {e:?}")); + return + } + }; + + let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() { + Some(v) => v, + None => { + output.push(String::from("Invalid user data")); + return + } + }; + + Some(elem) + } else { + None + }; + + if let Err(e) = + drk.read().await.mining_config(wallet_index, spend_hook, user_data, output).await + { + output.push(format!("Failed to generate wallet mining configuration: {e}")); + } +} + /// Auxiliary function to define the spend command handling. async fn handle_spend(drk: &DrkPtr, input: &[String], output: &mut Vec) { let tx = match parse_tx_from_input(input).await { @@ -1379,7 +1454,7 @@ async fn handle_dao(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut // Check correct command structure if parts.len() < 2 { output.push(String::from("Malformed `dao` command")); - output.push(String::from("Usage: dao (create|view|import|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook)")); + output.push(String::from("Usage: dao (create|view|import|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook|mining-config)")); return } @@ -1399,9 +1474,10 @@ async fn handle_dao(drk: &DrkPtr, parts: &[&str], input: &[String], output: &mut "vote" => handle_dao_vote(drk, parts, output).await, "exec" => handle_dao_exec(drk, parts, output).await, "spend-hook" => handle_dao_spend_hook(parts, output).await, + "mining-config" => handle_dao_mining_config(drk, parts, output).await, _ => { output.push(format!("Unrecognized DAO subcommand: {}", parts[1])); - output.push(String::from("Usage: dao (create|view|import|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook)")); + output.push(String::from("Usage: dao (create|view|import|list|balance|mint|propose-transfer|propose-generic|proposals|proposal|proposal-import|vote|exec|spend-hook|mining-config)")); } } } @@ -2281,6 +2357,20 @@ async fn handle_dao_spend_hook(parts: &[&str], output: &mut Vec) { output.push(format!("{spend_hook}")); } +/// Auxiliary function to define the dao mining config subcommand handling. +async fn handle_dao_mining_config(drk: &DrkPtr, parts: &[&str], output: &mut Vec) { + // Check correct subcommand structure + if parts.len() != 3 { + output.push(String::from("Malformed `dao mining-config` subcommand")); + output.push(String::from("Usage: dao mining-config ")); + return + } + + if let Err(e) = drk.read().await.dao_mining_config(parts[2], output).await { + output.push(format!("Failed to generate DAO mining configuration: {e}")); + } +} + /// Auxiliary function to define the attach fee command handling. async fn handle_attach_fee(drk: &DrkPtr, input: &[String], output: &mut Vec) { let mut tx = match parse_tx_from_input(input).await { diff --git a/bin/drk/src/main.rs b/bin/drk/src/main.rs index 280764a81..69e3ff5be 100644 --- a/bin/drk/src/main.rs +++ b/bin/drk/src/main.rs @@ -251,6 +251,18 @@ enum WalletSubcmd { /// Print all the coins in the wallet Coins, + + /// Print a wallet address mining configuration + MiningConfig { + /// Identifier of the address + index: usize, + + /// Optional contract spend hook to use + spend_hook: Option, + + /// Optional user data to use + user_data: Option, + }, } #[derive(Clone, Debug, Deserialize, StructOpt)] @@ -404,6 +416,12 @@ enum DaoSubcmd { /// Print the DAO contract base64-encoded spend hook SpendHook, + + /// Print a DAO mining configuration + MiningConfig { + /// Name identifier for the DAO + name: String, + }, } #[derive(Clone, Debug, Deserialize, StructOpt)] @@ -945,6 +963,50 @@ async fn realmain(args: Args, ex: ExecutorPtr) -> Result<()> { println!("{table}"); } + + WalletSubcmd::MiningConfig { index, spend_hook, user_data } => { + let spend_hook = match spend_hook { + Some(s) => match FuncId::from_str(&s) { + Ok(s) => Some(s), + Err(e) => { + eprintln!("Invalid spend hook: {e}"); + exit(2); + } + }, + None => None, + }; + + let user_data = match user_data { + Some(u) => { + let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() { + Ok(b) => b, + Err(e) => { + eprintln!("Invalid user data: {e:?}"); + exit(2); + } + }; + + match pallas::Base::from_repr(bytes).into() { + Some(v) => Some(v), + None => { + eprintln!("Invalid user data"); + exit(2); + } + } + } + None => None, + }; + + let mut output = vec![]; + if let Err(e) = + drk.mining_config(index, spend_hook, user_data, &mut output).await + { + print_output(&output); + eprintln!("Failed to generate wallet mining configuration: {e}"); + exit(2); + } + print_output(&output); + } } Ok(()) @@ -1934,6 +1996,27 @@ async fn realmain(args: Args, ex: ExecutorPtr) -> Result<()> { Ok(()) } + + DaoSubcmd::MiningConfig { name } => { + let drk = new_wallet( + blockchain_config.cache_path, + blockchain_config.wallet_path, + blockchain_config.wallet_pass, + None, + &ex, + args.fun, + ) + .await; + let mut output = vec![]; + if let Err(e) = drk.dao_mining_config(&name, &mut output).await { + print_output(&output); + eprintln!("Failed to generate DAO mining configuration: {e}"); + exit(2); + } + print_output(&output); + + Ok(()) + } }, Subcmd::AttachFee => { diff --git a/bin/drk/src/money.rs b/bin/drk/src/money.rs index ef294434f..393f78192 100644 --- a/bin/drk/src/money.rs +++ b/bin/drk/src/money.rs @@ -27,6 +27,7 @@ use rusqlite::types::Value; use darkfi::{ tx::Transaction, + util::encoding::base64, validator::fees::compute_fee, zk::{halo2::Field, proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses, Proof}, zkas::ZkBinary, @@ -48,8 +49,8 @@ use darkfi_money_contract::{ use darkfi_sdk::{ bridgetree::Position, crypto::{ - note::AeadEncryptedNote, BaseBlind, FuncId, Keypair, MerkleNode, MerkleTree, PublicKey, - ScalarBlind, SecretKey, MONEY_CONTRACT_ID, + note::AeadEncryptedNote, pasta_prelude::PrimeField, BaseBlind, FuncId, Keypair, MerkleNode, + MerkleTree, PublicKey, ScalarBlind, SecretKey, MONEY_CONTRACT_ID, }, dark_tree::DarkLeaf, pasta::pallas, @@ -266,6 +267,54 @@ impl Drk { Ok(vec) } + /// Fetch provided index address from the wallet and generate its + /// mining configuration. + pub async fn mining_config( + &self, + idx: usize, + spend_hook: Option, + user_data: Option, + output: &mut Vec, + ) -> Result<()> { + let row = match self.wallet.query_single( + &MONEY_KEYS_TABLE, + &[MONEY_KEYS_COL_PUBLIC], + convert_named_params! {(MONEY_KEYS_COL_KEY_ID, idx)}, + ) { + Ok(r) => r, + Err(e) => { + return Err(Error::DatabaseError(format!( + "[mining_address] Address retrieval failed: {e}" + ))) + } + }; + + let Value::Blob(ref key_bytes) = row[0] else { + return Err(Error::ParseFailed("[mining_address] Key bytes parsing failed")) + }; + let public_key: PublicKey = deserialize_async(key_bytes).await?; + + let spend_hook = spend_hook.as_ref().map(|spend_hook| spend_hook.to_string()); + + let user_data = + user_data.as_ref().map(|user_data| bs58::encode(user_data.to_repr()).into_string()); + + output.push(String::from("DarkFi TOML configuration:")); + output.push(format!("recipient = \"{public_key}\"")); + match spend_hook { + Some(ref spend_hook) => output.push(format!("spend_hook = \"{spend_hook}\"")), + None => output.push(String::from("#spend_hook = \"\"")), + } + match user_data { + Some(ref user_data) => output.push(format!("user_data = \"{user_data}\"")), + None => output.push(String::from("#user_data = \"\"")), + } + output.push(String::from("\nP2Pool wallet address to use:")); + output.push(base64::encode(&serialize(&(public_key, spend_hook, user_data))).to_string()); + + Ok(()) + } + /// Fetch all secret keys from the wallet. pub async fn get_money_secrets(&self) -> Result> { let rows = diff --git a/bin/drk/src/token.rs b/bin/drk/src/token.rs index 3dd89b6e1..5f377cfd6 100644 --- a/bin/drk/src/token.rs +++ b/bin/drk/src/token.rs @@ -242,7 +242,7 @@ impl Drk { Ok(r) => r, Err(e) => { return Err(Error::DatabaseError(format!( - "[get_token_mint_authority] Token mint autority retrieval failed: {e}" + "[get_token_mint_authority] Token mint authority retrieval failed: {e}" ))) } }; diff --git a/doc/src/testnet/dao.md b/doc/src/testnet/dao.md index dce85c00f..8ef560cfe 100644 --- a/doc/src/testnet/dao.md +++ b/doc/src/testnet/dao.md @@ -714,19 +714,34 @@ drk> dao balance DawnDAO ## Mining for a DAO A DAO can deploy its own mining nodes and/or other miners can choose to -directly give their rewards towards one. To configure a `darkfid` -instance to mine for a DAO, set the corresponding fields(uncomment if -needed) as per example: +directly give their rewards towards one. To retrieve a DAO mining +configuration, execute: + +```shell +drk> dao mining-config {YOUR_DAO} + +DarkFi TOML configuration: +recipient = "{YOUR_DAO_NOTES_PUBLIC_KEY}" +spend_hook = "{DAO_CONTRACT_SPEND_HOOK}" +user_data = "{YOUR_DAO_BULLA}" + +P2Pool wallet address to use: +{YOUR_DAO_P2POOL_WALLET_ADDRESS_CONFIGURATION} +``` + +Then configure a `darkfid` instance to mine for a DAO, by setting the +corresponding fields(uncomment if needed) as per retrieved +configuration: ```toml # Put your DAO notes public key here -recipient = "YOUR_DAO_NOTES_PUBLIC_KEY_HERE" +recipient = "{YOUR_DAO_NOTES_PUBLIC_KEY}" -# Put the DAO contract spend hook from `drk dao spend-hook` here -spend_hook = "6iW9nywZYvyhcM7P1iLwYkh92rvYtREDsC8hgqf2GLuT" +# Put the DAO contract spend hook here +spend_hook = "{DAO_CONTRACT_SPEND_HOOK}" # Put your DAO bulla here -user_data = "YOUR_DAO_BULLA_HERE" +user_data = "{YOUR_DAO_BULLA}" ``` After your miners have successfully mined confirmed blocks, you will diff --git a/doc/src/testnet/merge-mining.md b/doc/src/testnet/merge-mining.md index d0b6e1086..2e6bdb719 100644 --- a/doc/src/testnet/merge-mining.md +++ b/doc/src/testnet/merge-mining.md @@ -195,7 +195,19 @@ Now that everything is in order, we can use `p2pool` with merge-mining enabled in order to merge mine DarkFi. For receiving mining rewards on DarkFi, we'll need a DarkFi wallet address so make sure you have [initialized](node.md#wallet-initialization) your wallet and grab your -address. +first address configuration: + +```shell +drk> wallet mining-configuration 1 + +DarkFi TOML configuration: +recipient = "{YOUR_DARKFI_WALLET_ADDRESS}" +#spend_hook = "" +#user_data = "" + +P2Pool wallet address to use: +{YOUR_P2POOL_WALLET_ADDRESS_CONFIGURATION} +``` We will also need `darkfid` running. Make sure you enable the RPC endpoint that will be used by p2pool in darkfid's config: @@ -211,7 +223,7 @@ Stop `p2pool` if it's running, and re-run it with the merge-mining parameters appended: ```shell -$ ./p2pool --host 127.0.0.1 --rpc-port 28081 --zmq-port 28083 --wallet {YOUR_MONERO_WALLET_ADDRESS_HERE} --stratum 127.0.0.1:3333 --data-dir ./p2pool-data --no-igd --merge-mine 127.0.0.1:8341 {YOUR_DARKFI_WALLET_ADDRESS_HERE} +$ ./p2pool --host 127.0.0.1 --rpc-port 28081 --zmq-port 28083 --wallet {YOUR_MONERO_WALLET_ADDRESS_HERE} --stratum 127.0.0.1:3333 --data-dir ./p2pool-data --no-igd --merge-mine 127.0.0.1:8341 {YOUR_P2POOL_WALLET_ADDRESS_CONFIGURATION_HERE} ``` Now `p2pool` should communicate with both `monerod` and `darkfid` in @@ -224,6 +236,40 @@ provided to `p2pool` merge-mine parameters. Happy mining! +## Merge mining for a DAO + +To retrieve a DAO merge mining configuration, execute: + +```shell +drk> dao mining-config {YOUR_DAO} + +DarkFi TOML configuration: +recipient = "{YOUR_DAO_NOTES_PUBLIC_KEY}" +spend_hook = "{DAO_CONTRACT_SPEND_HOOK}" +user_data = "{YOUR_DAO_BULLA}" + +P2Pool wallet address to use: +{YOUR_DAO_P2POOL_WALLET_ADDRESS_CONFIGURATION} +``` + +Stop `p2pool` if it's running, and re-run it with the merge-mining +parameters appended: + +```shell +$ ./p2pool --host 127.0.0.1 --rpc-port 28081 --zmq-port 28083 --wallet {YOUR_DAO_MONERO_WALLET_ADDRESS_HERE} --stratum 127.0.0.1:3333 --data-dir ./p2pool-data --no-igd --merge-mine 127.0.0.1:8341 {YOUR_DAO_P2POOL_WALLET_ADDRESS_CONFIGURATION} +``` + +After your miners have successfully mined confirmed blocks, you will +see the DAO `DRK` balance increasing: + +```shell +drk> dao balance {YOUR_DAO} + + Token ID | Aliases | Balance +----------------------------------------------+---------+--------- + 241vANigf1Cy3ytjM1KHXiVECxgxdK4yApddL8KcLssb | DRK | 80 +``` + [1]: https://github.com/monero-project/monero?tab=readme-ov-file#dependencies [2]: https://github.com/SChernykh/p2pool?tab=readme-ov-file#prerequisites [3]: https://xmrig.com/docs/miner/build