darkfid: support merge mining for a DAO

This commit is contained in:
skoupidi
2025-11-26 17:26:15 +02:00
parent 334c90e45d
commit 5754d4268f
11 changed files with 468 additions and 45 deletions

View File

@@ -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,
} }

View File

@@ -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_sdk::{
use darkfi_serial::serialize_async; 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) return JsonError::new(InvalidParams, Some("invalid address format".to_string()), id)
.into() .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)
.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. // address configuration provided to us through the RPC.
let recipient_config = let recipient_str = format!("{recipient}");
MinerRewardsRecipientConfig { recipient: address, spend_hook: None, user_data: None }; 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]; let Ok(address_bytes) = hex::decode(aux_blob) else {
if hex::decode_to_slice(aux_blob, &mut address_bytes).is_err() {
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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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, &[], &[]) {

View File

@@ -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 {

View File

@@ -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 => {

View File

@@ -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, note::AeadEncryptedNote, pasta_prelude::PrimeField, BaseBlind, FuncId, Keypair, MerkleNode,
ScalarBlind, SecretKey, MONEY_CONTRACT_ID, 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 =

View File

@@ -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}"
))) )))
} }
}; };

View File

@@ -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` directly give their rewards towards one. To retrieve a DAO mining
instance to mine for a DAO, set the corresponding fields(uncomment if configuration, execute:
needed) as per example:
```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 # Put the DAO contract spend hook here
spend_hook = "6iW9nywZYvyhcM7P1iLwYkh92rvYtREDsC8hgqf2GLuT" 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

View File

@@ -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