Browse Source

darkfid: support merge mining for a DAO

skoupidi 8 tháng trước cách đây
mục cha
commit
5754d4268f

+ 1 - 1
bin/darkfid/src/lib.rs

@@ -85,7 +85,7 @@ pub struct DarkfiNode {
     /// HTTP JSON-RPC connection tracker
     /// HTTP JSON-RPC connection tracker
     mm_rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
     mm_rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
     /// Merge mining block templates
     /// Merge mining block templates
-    mm_blocktemplates: Mutex<HashMap<[u8; 32], (BlockInfo, f64, SecretKey)>>,
+    mm_blocktemplates: Mutex<HashMap<Vec<u8>, (BlockInfo, f64, SecretKey)>>,
     /// PowRewardV1 ZK data
     /// PowRewardV1 ZK data
     powrewardv1_zk: PowRewardV1Zk,
     powrewardv1_zk: PowRewardV1Zk,
 }
 }

+ 121 - 16
bin/darkfid/src/rpc_xmr.rs

@@ -31,8 +31,11 @@ use darkfi::{
     util::encoding::base64,
     util::encoding::base64,
     validator::consensus::Proposal,
     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 hex::FromHex;
 use tinyjson::JsonValue;
 use tinyjson::JsonValue;
 use tracing::{error, info};
 use tracing::{error, info};
@@ -89,13 +92,13 @@ impl DarkfiNode {
     // merge mining.
     // merge mining.
     //
     //
     // **Request:**
     // **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
     // * `aux_hash`: Merge mining job that is currently being polled
     // * `height`  : Monero height
     // * `height`  : Monero height
     // * `prev_id` : Hash of the previous Monero block
     // * `prev_id` : Hash of the previous Monero block
     //
     //
     // **Response:**
     // **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_diff`: Mining difficulty (decimal number)
     // * `aux_hash`: A 32-byte hex-encoded hash of merge mined block
     // * `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()
             return JsonError::new(InvalidParams, None, id).into()
         };
         };
 
 
-        // Parse address
+        // Parse address mining configuration
         let Some(address) = params.get("address") else {
         let Some(address) = params.get("address") else {
             return JsonError::new(InvalidParams, Some("missing address".to_string()), id).into()
             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)
             return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id)
                 .into()
                 .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<String>, Option<String>)>(&address_bytes).await
+        else {
             return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id)
             return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id)
                 .into()
                 .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<pallas::Base> = 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
         // Parse aux_hash
         let Some(aux_hash) = params.get("aux_hash") else {
         let Some(aux_hash) = params.get("aux_hash") else {
@@ -184,7 +242,6 @@ impl DarkfiNode {
                 return JsonError::new(ErrorCode::InternalError, None, id).into()
                 return JsonError::new(ErrorCode::InternalError, None, id).into()
             }
             }
         };
         };
-        let address_bytes = address.to_bytes();
         if let Some((block, difficulty, _)) = mm_blocktemplates.get(&address_bytes) {
         if let Some((block, difficulty, _)) = mm_blocktemplates.get(&address_bytes) {
             let last_proposal = match extended_fork.last_proposal() {
             let last_proposal = match extended_fork.last_proposal() {
                 Ok(p) => p,
                 Ok(p) => p,
@@ -217,9 +274,17 @@ impl DarkfiNode {
 
 
         // At this point, we should query the Validator for a new blocktemplate.
         // At this point, we should query the Validator for a new blocktemplate.
         // We first need to construct `MinerRewardsRecipientConfig` from the
         // 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.
         // Now let's try to construct the blocktemplate.
         // Find the difficulty. Note we cast it to f64 here.
         // 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,
         // Now we have the blocktemplate. We'll mark it down in memory,
         // and then ship it to RPC.
         // and then ship it to RPC.
         let blockhash = blocktemplate.header.template_hash();
         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!(
         info!(
             target: "darkfid::rpc_xmr::xmr_merge_mining_get_aux_block",
             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([
         let response = JsonValue::from(HashMap::from([
@@ -308,7 +374,7 @@ impl DarkfiNode {
             return JsonError::new(InvalidParams, None, id).into()
             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 {
         let Some(aux_blob) = params.get("aux_blob") else {
             return JsonError::new(InvalidParams, Some("missing aux_blob".to_string()), id).into()
             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)
             return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id)
                 .into()
                 .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)
             return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id)
                 .into()
                 .into()
         };
         };
-        if PublicKey::from_bytes(address_bytes).is_err() {
+        let Ok((_, spend_hook, user_data)) =
+            deserialize_async::<(PublicKey, Option<String>, Option<String>)>(&address_bytes).await
+        else {
             return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id)
             return JsonError::new(InvalidParams, Some("invalid aux_blob format".to_string()), id)
                 .into()
                 .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
         // Parse aux_hash
         let Some(aux_hash) = params.get("aux_hash") else {
         let Some(aux_hash) = params.get("aux_hash") else {

+ 1 - 1
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
         // 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
     // Drop jobs not referencing active forks or last confirmed block

+ 15 - 6
bin/drk/src/cli_util.rs

@@ -152,7 +152,7 @@ pub fn generate_completions(shell: &str) -> Result<String> {
 
 
     let default_address = SubCommand::with_name("default-address")
     let default_address = SubCommand::with_name("default-address")
         .about("Set the default address in the wallet")
         .about("Set the default address in the wallet")
-        .arg(index);
+        .arg(index.clone());
 
 
     let secrets =
     let secrets =
         SubCommand::with_name("secrets").about("Print all the secret keys from the wallet");
         SubCommand::with_name("secrets").about("Print all the secret keys from the wallet");
@@ -164,6 +164,14 @@ pub fn generate_completions(shell: &str) -> Result<String> {
 
 
     let coins = SubCommand::with_name("coins").about("Print all the coins in the wallet");
     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![
     let wallet = SubCommand::with_name("wallet").about("Wallet operations").subcommands(vec![
         initialize,
         initialize,
         keygen,
         keygen,
@@ -175,6 +183,7 @@ pub fn generate_completions(shell: &str) -> Result<String> {
         import_secrets,
         import_secrets,
         tree,
         tree,
         coins,
         coins,
+        mining_config,
     ]);
     ]);
 
 
     // Spend
     // Spend
@@ -193,10 +202,6 @@ pub fn generate_completions(shell: &str) -> Result<String> {
 
 
     let recipient = Arg::with_name("recipient").help("Recipient address");
     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")
     let half_split = Arg::with_name("half-split")
         .long("half-split")
         .long("half-split")
         .help("Split the output coin into two equal halves");
         .help("Split the output coin into two equal halves");
@@ -302,7 +307,7 @@ pub fn generate_completions(shell: &str) -> Result<String> {
         .about("Create a generic proposal for a DAO")
         .about("Create a generic proposal for a DAO")
         .args(&[name.clone(), duration, user_data.clone()]);
         .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");
     let bulla = Arg::with_name("bulla").help("Bulla identifier for the proposal");
 
 
@@ -337,6 +342,9 @@ pub fn generate_completions(shell: &str) -> Result<String> {
     let spend_hook_cmd = SubCommand::with_name("spend-hook")
     let spend_hook_cmd = SubCommand::with_name("spend-hook")
         .about("Print the DAO contract base64-encoded 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![
     let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![
         create,
         create,
         view,
         view,
@@ -352,6 +360,7 @@ pub fn generate_completions(shell: &str) -> Result<String> {
         vote,
         vote,
         exec,
         exec,
         spend_hook_cmd,
         spend_hook_cmd,
+        mining_config,
     ]);
     ]);
 
 
     // AttachFee
     // AttachFee

+ 27 - 1
bin/drk/src/dao.rs

@@ -25,7 +25,10 @@ use rusqlite::types::Value;
 
 
 use darkfi::{
 use darkfi::{
     tx::{ContractCallLeaf, Transaction, TransactionBuilder},
     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},
     zk::{empty_witnesses, halo2::Field, ProvingKey, ZkCircuit},
     zkas::ZkBinary,
     zkas::ZkBinary,
     Error, Result,
     Error, Result,
@@ -55,6 +58,7 @@ use darkfi_money_contract::{
 use darkfi_sdk::{
 use darkfi_sdk::{
     bridgetree,
     bridgetree,
     crypto::{
     crypto::{
+        pasta_prelude::PrimeField,
         poseidon_hash,
         poseidon_hash,
         smt::{MemoryStorageFp, PoseidonFp, SmtMemoryFp, EMPTY_NODES_FP},
         smt::{MemoryStorageFp, PoseidonFp, SmtMemoryFp, EMPTY_NODES_FP},
         util::{fp_mod_fv, fp_to_u64},
         util::{fp_mod_fv, fp_to_u64},
@@ -1907,6 +1911,28 @@ impl Drk {
         Ok(balmap)
         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<String>) -> 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.
     /// Fetch all known DAO proposalss from the wallet.
     pub async fn get_proposals(&self) -> Result<Vec<ProposalRecord>> {
     pub async fn get_proposals(&self) -> Result<Vec<ProposalRecord>> {
         let rows = match self.wallet.query_multiple(&DAO_PROPOSALS_TABLE, &[], &[]) {
         let rows = match self.wallet.query_multiple(&DAO_PROPOSALS_TABLE, &[], &[]) {

+ 98 - 8
bin/drk/src/interactive.rs

@@ -150,7 +150,8 @@ fn completion(buffer: &str, lc: &mut Vec<String>) {
         lc.push(prefix.clone() + "wallet secrets");
         lc.push(prefix.clone() + "wallet secrets");
         lc.push(prefix.clone() + "wallet import-secrets");
         lc.push(prefix.clone() + "wallet import-secrets");
         lc.push(prefix.clone() + "wallet tree");
         lc.push(prefix.clone() + "wallet tree");
-        lc.push(prefix + "wallet coins");
+        lc.push(prefix.clone() + "wallet coins");
+        lc.push(prefix + "wallet mining-config");
         return
         return
     }
     }
 
 
@@ -193,7 +194,8 @@ fn completion(buffer: &str, lc: &mut Vec<String>) {
         lc.push(prefix.clone() + "dao proposal-import");
         lc.push(prefix.clone() + "dao proposal-import");
         lc.push(prefix.clone() + "dao vote");
         lc.push(prefix.clone() + "dao vote");
         lc.push(prefix.clone() + "dao exec");
         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
         return
     }
     }
 
 
@@ -333,13 +335,14 @@ fn hints(buffer: &str) -> Option<(String, i32, bool)> {
     let bold = false;
     let bold = false;
     match last {
     match last {
         "completions " => Some(("<shell>".to_string(), color, bold)),
         "completions " => Some(("<shell>".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(("<index>".to_string(), color, bold)),
         "wallet default-address " => Some(("<index>".to_string(), color, bold)),
+        "wallet mining-config " => Some(("<index> [spend_hook] [user_data]".to_string(), color, bold)),
         "unspend " => Some(("<coin>".to_string(), color, bold)),
         "unspend " => Some(("<coin>".to_string(), color, bold)),
         "transfer " => Some(("[--half-split] <amount> <token> <recipient> [spend_hook] [user_data]".to_string(), color, bold)),
         "transfer " => Some(("[--half-split] <amount> <token> <recipient> [spend_hook] [user_data]".to_string(), color, bold)),
         "otc " => Some(("(init|join|inspect|sign)".to_string(), color, bold)),
         "otc " => Some(("(init|join|inspect|sign)".to_string(), color, bold)),
         "otc init " => Some(("<value_pair> <token_pair>".to_string(), color, bold)),
         "otc init " => Some(("<value_pair> <token_pair>".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(("<proposer-limit> <quorum> <early-exec-quorum> <approval-ratio> <gov-token-id>".to_string(), color, bold)),
         "dao create " => Some(("<proposer-limit> <quorum> <early-exec-quorum> <approval-ratio> <gov-token-id>".to_string(), color, bold)),
         "dao import " => Some(("<name>".to_string(), color, bold)),
         "dao import " => Some(("<name>".to_string(), color, bold)),
         "dao list " => Some(("[name]".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)] <bulla>".to_string(), color, bold)),
         "dao proposal " => Some(("[--(export|mint-proposal)] <bulla>".to_string(), color, bold)),
         "dao vote " => Some(("<bulla> <vote> [vote-weight]".to_string(), color, bold)),
         "dao vote " => Some(("<bulla> <vote> [vote-weight]".to_string(), color, bold)),
         "dao exec " => Some(("[--early] <bulla>".to_string(), color, bold)),
         "dao exec " => Some(("[--early] <bulla>".to_string(), color, bold)),
+        "dao mining-config " => Some(("<name>".to_string(), color, bold)),
         "scan " => Some(("[--reset]".to_string(), color, bold)),
         "scan " => Some(("[--reset]".to_string(), color, bold)),
         "scan --reset " => Some(("<height>".to_string(), color, bold)),
         "scan --reset " => Some(("<height>".to_string(), color, bold)),
         "explorer " => Some(("(fetch-tx|simulate-tx|txs-history|clear-reverted|scanned-blocks)".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
     // Check correct command structure
     if parts.len() < 2 {
     if parts.len() < 2 {
         output.push(String::from("Malformed `wallet` command"));
         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
         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,
         "import-secrets" => handle_wallet_import_secrets(drk, input, output).await,
         "tree" => handle_wallet_tree(drk, output).await,
         "tree" => handle_wallet_tree(drk, output).await,
         "coins" => handle_wallet_coins(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(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<String>) {
     output.push(format!("{table}"));
     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<String>) {
+    // 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 <index> [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.
 /// Auxiliary function to define the spend command handling.
 async fn handle_spend(drk: &DrkPtr, input: &[String], output: &mut Vec<String>) {
 async fn handle_spend(drk: &DrkPtr, input: &[String], output: &mut Vec<String>) {
     let tx = match parse_tx_from_input(input).await {
     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
     // Check correct command structure
     if parts.len() < 2 {
     if parts.len() < 2 {
         output.push(String::from("Malformed `dao` command"));
         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
         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,
         "vote" => handle_dao_vote(drk, parts, output).await,
         "exec" => handle_dao_exec(drk, parts, output).await,
         "exec" => handle_dao_exec(drk, parts, output).await,
         "spend-hook" => handle_dao_spend_hook(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(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<String>) {
     output.push(format!("{spend_hook}"));
     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<String>) {
+    // 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 <name>"));
+        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.
 /// Auxiliary function to define the attach fee command handling.
 async fn handle_attach_fee(drk: &DrkPtr, input: &[String], output: &mut Vec<String>) {
 async fn handle_attach_fee(drk: &DrkPtr, input: &[String], output: &mut Vec<String>) {
     let mut tx = match parse_tx_from_input(input).await {
     let mut tx = match parse_tx_from_input(input).await {

+ 83 - 0
bin/drk/src/main.rs

@@ -251,6 +251,18 @@ enum WalletSubcmd {
 
 
     /// Print all the coins in the wallet
     /// Print all the coins in the wallet
     Coins,
     Coins,
+
+    /// Print a wallet address mining configuration
+    MiningConfig {
+        /// Identifier of the address
+        index: usize,
+
+        /// Optional contract spend hook to use
+        spend_hook: Option<String>,
+
+        /// Optional user data to use
+        user_data: Option<String>,
+    },
 }
 }
 
 
 #[derive(Clone, Debug, Deserialize, StructOpt)]
 #[derive(Clone, Debug, Deserialize, StructOpt)]
@@ -404,6 +416,12 @@ enum DaoSubcmd {
 
 
     /// Print the DAO contract base64-encoded spend hook
     /// Print the DAO contract base64-encoded spend hook
     SpendHook,
     SpendHook,
+
+    /// Print a DAO mining configuration
+    MiningConfig {
+        /// Name identifier for the DAO
+        name: String,
+    },
 }
 }
 
 
 #[derive(Clone, Debug, Deserialize, StructOpt)]
 #[derive(Clone, Debug, Deserialize, StructOpt)]
@@ -945,6 +963,50 @@ async fn realmain(args: Args, ex: ExecutorPtr) -> Result<()> {
 
 
                     println!("{table}");
                     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(())
             Ok(())
@@ -1934,6 +1996,27 @@ async fn realmain(args: Args, ex: ExecutorPtr) -> Result<()> {
 
 
                 Ok(())
                 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 => {
         Subcmd::AttachFee => {

+ 51 - 2
bin/drk/src/money.rs

@@ -27,6 +27,7 @@ use rusqlite::types::Value;
 
 
 use darkfi::{
 use darkfi::{
     tx::Transaction,
     tx::Transaction,
+    util::encoding::base64,
     validator::fees::compute_fee,
     validator::fees::compute_fee,
     zk::{halo2::Field, proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses, Proof},
     zk::{halo2::Field, proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses, Proof},
     zkas::ZkBinary,
     zkas::ZkBinary,
@@ -48,8 +49,8 @@ use darkfi_money_contract::{
 use darkfi_sdk::{
 use darkfi_sdk::{
     bridgetree::Position,
     bridgetree::Position,
     crypto::{
     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,
     dark_tree::DarkLeaf,
     pasta::pallas,
     pasta::pallas,
@@ -266,6 +267,54 @@ impl Drk {
         Ok(vec)
         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<FuncId>,
+        user_data: Option<pallas::Base>,
+        output: &mut Vec<String>,
+    ) -> 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.
     /// Fetch all secret keys from the wallet.
     pub async fn get_money_secrets(&self) -> Result<Vec<SecretKey>> {
     pub async fn get_money_secrets(&self) -> Result<Vec<SecretKey>> {
         let rows =
         let rows =

+ 1 - 1
bin/drk/src/token.rs

@@ -242,7 +242,7 @@ impl Drk {
             Ok(r) => r,
             Ok(r) => r,
             Err(e) => {
             Err(e) => {
                 return Err(Error::DatabaseError(format!(
                 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}"
                 )))
                 )))
             }
             }
         };
         };

+ 22 - 7
doc/src/testnet/dao.md

@@ -714,19 +714,34 @@ drk> dao balance DawnDAO
 ## Mining for a DAO
 ## Mining for a DAO
 
 
 A DAO can deploy its own mining nodes and/or other miners can choose to
 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
 ```toml
 # Put your DAO notes public key here
 # 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
 # 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
 After your miners have successfully mined confirmed blocks, you will

+ 48 - 2
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
 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
 on DarkFi, we'll need a DarkFi wallet address so make sure you have
 [initialized](node.md#wallet-initialization) your wallet and grab your
 [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
 We will also need `darkfid` running. Make sure you enable the RPC
 endpoint that will be used by p2pool in darkfid's config:
 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:
 parameters appended:
 
 
 ```shell
 ```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
 Now `p2pool` should communicate with both `monerod` and `darkfid` in
@@ -224,6 +236,40 @@ provided to `p2pool` merge-mine parameters.
 
 
 Happy mining!
 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
 [1]: https://github.com/monero-project/monero?tab=readme-ov-file#dependencies
 [2]: https://github.com/SChernykh/p2pool?tab=readme-ov-file#prerequisites
 [2]: https://github.com/SChernykh/p2pool?tab=readme-ov-file#prerequisites
 [3]: https://xmrig.com/docs/miner/build
 [3]: https://xmrig.com/docs/miner/build