Skip to content

Commit bd20ff1

Browse files
authored
Merge pull request #6 from disassembler/sl/queue-solution-submission
Feature: Asynchronous Solution Submission Queue
2 parents ee636c9 + d6d02b2 commit bd20ff1

File tree

9 files changed

+451
-116
lines changed

9 files changed

+451
-116
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "shadow-harvester" # 🚨 NEW: The project and library name
2+
name = "shadow-harvester"
33
version = "0.1.0"
44
edition = "2024"
55
license = "MIT Or Apache-2.0"

src/api.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ pub fn fetch_tandc(client: &blocking::Client, api_url: &str) -> Result<TandCResp
2222
response.json()
2323
}
2424

25-
// NEW FUNCTION: Parses the comma-separated CLI challenge string
2625
pub fn parse_cli_challenge_string(challenge_str: &str) -> Result<CliChallengeData, String> {
2726
let parts: Vec<&str> = challenge_str.split(',').collect();
2827

@@ -44,7 +43,6 @@ pub fn parse_cli_challenge_string(challenge_str: &str) -> Result<CliChallengeDat
4443

4544

4645
/// Performs the POST /register call using key/signature arguments.
47-
// RENAMED from register_address_mock
4846
pub fn register_address(
4947
client: &blocking::Client,
5048
api_url: &str,
@@ -139,7 +137,6 @@ pub fn submit_solution(
139137
}
140138

141139
/// Performs the POST /donate_to call.
142-
// RENAMED from donate_to_mock
143140
pub fn donate_to(
144141
client: &blocking::Client,
145142
api_url: &str,
@@ -187,7 +184,6 @@ pub fn donate_to(
187184
}
188185

189186
/// Fetches the raw Challenge Response object from the API.
190-
// NEW FUNCTION
191187
pub fn fetch_challenge_status(client: &blocking::Client, api_url: &str) -> Result<ChallengeResponse, String> {
192188
let url = format!("{}/challenge", api_url);
193189

@@ -202,7 +198,6 @@ pub fn fetch_challenge_status(client: &blocking::Client, api_url: &str) -> Resul
202198
}
203199

204200
/// Fetches and validates the active challenge parameters, returning data only if active.
205-
// RENAMED from fetch_challenge
206201
pub fn get_active_challenge_data(client: &blocking::Client, api_url: &str) -> Result<ChallengeData, String> {
207202
let challenge_response = fetch_challenge_status(client, api_url)?;
208203

src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub struct Cli {
2828
#[arg(long)]
2929
pub payment_key: Option<String>,
3030

31-
/// NEW: Automatically generate a new ephemeral key pair for every mining cycle.
31+
/// Automatically generate a new ephemeral key pair for every mining cycle.
3232
#[arg(long)]
3333
pub ephemeral_key: bool,
3434

src/data_types.rs

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ pub struct CliChallengeData {
128128
// CORE APPLICATION STRUCTS
129129
// ===============================================
130130

131-
// NEW STRUCT: Holds the common, validated state for the mining loops.
131+
// Holds the common, validated state for the mining loops.
132132
#[derive(Debug)]
133133
pub struct MiningContext<'a> {
134134
pub client: blocking::Client,
@@ -142,10 +142,20 @@ pub struct MiningContext<'a> {
142142
}
143143

144144

145-
// NEW: Define a result type for the mining cycle
145+
// Holds the data needed to submit a solution later.
146+
#[derive(Debug, Deserialize, Serialize, Clone)]
147+
pub struct PendingSolution {
148+
pub address: String,
149+
pub challenge_id: String,
150+
pub nonce: String,
151+
pub donation_address: Option<String>, // RE-ADDED this field
152+
}
153+
154+
// Define a result type for the mining cycle
146155
#[derive(Debug, PartialEq)]
147156
pub enum MiningResult {
148-
FoundAndSubmitted((serde_json::Value, Option<String>)),
157+
FoundAndQueued, // Solution found and saved to local queue
158+
#[allow(dead_code)] // The submitter thread produces this result conceptually when processing a queue item, but the miner never constructs it.
149159
AlreadySolved, // The solution was successfully submitted by someone else
150160
MiningFailed, // General mining or submission error (e.g., hash not found, transient API error)
151161
}
@@ -154,6 +164,8 @@ pub enum MiningResult {
154164
pub const FILE_NAME_CHALLENGE: &str = "challenge.json";
155165
pub const FILE_NAME_RECEIPT: &str = "receipt.json";
156166
pub const FILE_NAME_DONATION: &str = "donation.txt";
167+
// Removed: FILE_NAME_PENDING_SOLUTION is unused, path is derived from QUEUE_BASE_DIR
168+
pub const FILE_NAME_FOUND_SOLUTION: &str = "found.json"; // (Crash recovery file)
157169

158170

159171
#[derive(Debug, Clone, Copy)]
@@ -224,31 +236,98 @@ impl<'a> DataDir<'a> {
224236
Ok(())
225237
}
226238

227-
pub fn save_receipt(&self, base_dir: &str, challenge_id: &str, receipt: &serde_json::Value, donation: &Option<String>) -> Result<(), String> {
239+
// Only saves the receipt, donation logic removed
240+
pub fn save_receipt(&self, base_dir: &str, challenge_id: &str, receipt: &serde_json::Value) -> Result<(), String> {
228241
let mut path = self.receipt_dir(base_dir, challenge_id)?;
229242
path.push(FILE_NAME_RECEIPT);
230243

231244
let receipt_json = receipt.to_string();
232245

233-
// FIX: Use explicit file handling and sync to guarantee persistence.
234246
let mut file = std::fs::File::create(&path)
235247
.map_err(|e| format!("Could not create {}: {}", FILE_NAME_RECEIPT, e))?;
236248

237249
file.write_all(receipt_json.as_bytes())
238250
.map_err(|e| format!("Could not write to {}: {}", FILE_NAME_RECEIPT, e))?;
239251

240-
// CRITICAL: Force the OS to write the data to disk now.
241252
file.sync_all()
242253
.map_err(|e| format!("Could not sync {}: {}", FILE_NAME_RECEIPT, e))?;
243254

244-
if let Some(donation_id) = donation {
245-
path.pop();
246-
path.push(FILE_NAME_DONATION);
255+
// Donation file logic is intentionally removed here.
247256

248-
std::fs::write(&path, donation_id.as_bytes())
249-
.map_err(|e| format!("Could not write {}: {}", FILE_NAME_DONATION, e))?;
250-
}
257+
Ok(())
258+
}
259+
260+
// Saves a PendingSolution to the queue directory
261+
pub fn save_pending_solution(&self, base_dir: &str, solution: &PendingSolution) -> Result<(), String> {
262+
let mut path = PathBuf::from(base_dir);
263+
path.push("pending_submissions"); // Dedicated directory for the queue
264+
std::fs::create_dir_all(&path)
265+
.map_err(|e| format!("Could not create pending_submissions directory: {}", e))?;
266+
267+
// Use a unique file name based on challenge, address, and nonce
268+
path.push(format!("{}_{}_{}.json", solution.address, solution.challenge_id, solution.nonce));
269+
270+
let solution_json = serde_json::to_string(solution)
271+
.map_err(|e| format!("Could not serialize pending solution: {}", e))?;
272+
273+
std::fs::write(&path, solution_json)
274+
.map_err(|e| format!("Could not write pending solution file: {}", e))?;
251275

252276
Ok(())
253277
}
278+
279+
// Saves the temporary file indicating a solution was found but not queued/submitted
280+
pub fn save_found_solution(&self, base_dir: &str, challenge_id: &str, solution: &PendingSolution) -> Result<(), String> {
281+
let mut path = self.receipt_dir(base_dir, challenge_id)?; // Use receipt dir for local persistence
282+
path.push(FILE_NAME_FOUND_SOLUTION);
283+
284+
let solution_json = serde_json::to_string(solution)
285+
.map_err(|e| format!("Could not serialize found solution: {}", e))?;
286+
287+
// Use explicit file handling to guarantee persistence before returning success
288+
let mut file = std::fs::File::create(&path)
289+
.map_err(|e| format!("Could not create {}: {}", FILE_NAME_FOUND_SOLUTION, e))?;
290+
291+
file.write_all(solution_json.as_bytes())
292+
.map_err(|e| format!("Could not write to {}: {}", FILE_NAME_FOUND_SOLUTION, e))?;
293+
294+
file.sync_all()
295+
.map_err(|e| format!("Could not sync {}: {}", FILE_NAME_FOUND_SOLUTION, e))?;
296+
297+
Ok(())
298+
}
299+
300+
// Removes the temporary file
301+
pub fn delete_found_solution(&self, base_dir: &str, challenge_id: &str) -> Result<(), String> {
302+
let mut path = self.receipt_dir(base_dir, challenge_id)?;
303+
path.push(FILE_NAME_FOUND_SOLUTION);
304+
if path.exists() {
305+
std::fs::remove_file(&path)
306+
.map_err(|e| format!("Failed to delete {}: {}", FILE_NAME_FOUND_SOLUTION, e))?;
307+
}
308+
Ok(())
309+
}
310+
}
311+
312+
// Checks if an address/challenge has a pending submission file in the queue dir
313+
pub fn is_solution_pending_in_queue(base_dir: &str, address: &str, challenge_id: &str) -> Result<bool, String> {
314+
use std::path::PathBuf;
315+
316+
let mut path = PathBuf::from(base_dir);
317+
path.push("pending_submissions");
318+
319+
// Scan for any file that matches the address and challenge ID prefix
320+
if let Ok(entries) = std::fs::read_dir(&path) {
321+
for entry in entries.filter_map(|e| e.ok()) {
322+
if let Some(filename) = entry.file_name().to_str() {
323+
// Check if the filename starts with the required prefix and is a JSON file
324+
// The filename format is: address_challenge_id_nonce.json
325+
if filename.starts_with(&format!("{}_{}_", address, challenge_id)) && filename.ends_with(".json") {
326+
return Ok(true);
327+
}
328+
}
329+
}
330+
}
331+
// If the directory doesn't exist or no matching file is found
332+
Ok(false)
254333
}

src/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -587,8 +587,6 @@ pub fn scavenge(
587587
// Set start_nonce = thread_id
588588
let start_nonce = thread_id;
589589

590-
println!("Starting thread {} with initial nonce: {:016x} and step size: {}", thread_id, start_nonce, step_size);
591-
592590
s.spawn(move || {
593591
spin(params, sender, stop_signal, start_nonce, step_size)
594592
});

src/main.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// src/main.rs - Final Minimal Version
22

33
use clap::Parser;
4+
use std::thread; // ADDED
45

56
// Declare modules
67
mod api;
@@ -11,6 +12,7 @@ mod cardano;
1112
mod data_types;
1213
mod utils; // The helpers module
1314
mod mining;
15+
mod submitter;
1416

1517
use mining::{run_persistent_key_mining, run_mnemonic_sequential_mining, run_ephemeral_key_mining};
1618
use utils::{setup_app, print_mining_setup}; // Importing refactored helpers
@@ -27,6 +29,27 @@ fn run_app(cli: Cli) -> Result<(), String> {
2729
Err(e) => return Err(e),
2830
};
2931

32+
// --- Start Background Submitter Thread ---
33+
// Clone client, API URL, and data_dir for the background thread
34+
let submitter_handle = if let Some(base_dir) = context.data_dir {
35+
let client_clone = context.client.clone();
36+
let api_url_clone = context.api_url.clone();
37+
let data_dir_clone = base_dir.to_string();
38+
39+
println!("📦 Starting background submitter thread...");
40+
let handle = thread::spawn(move || {
41+
match submitter::run_submitter_thread(client_clone, api_url_clone, data_dir_clone) {
42+
Ok(_) => {},
43+
Err(e) => eprintln!("FATAL SUBMITTER ERROR: {}", e),
44+
}
45+
});
46+
Some(handle)
47+
} else {
48+
println!("⚠️ No --data-dir specified. Submissions will be synchronous (blocking) and lost on API error.");
49+
None
50+
};
51+
// ---------------------------------------------
52+
3053
// --- Pre-extract mnemonic logic ---
3154
let mnemonic: Option<String> = if let Some(mnemonic) = cli.mnemonic.clone() {
3255
Some(mnemonic)
@@ -56,7 +79,7 @@ fn run_app(cli: Cli) -> Result<(), String> {
5679
}
5780

5881
// 2. Determine Operation Mode and Start Mining
59-
if let Some(skey_hex) = cli.payment_key.as_ref() {
82+
let result = if let Some(skey_hex) = cli.payment_key.as_ref() {
6083
// Mode A: Persistent Key Mining
6184
run_persistent_key_mining(context, skey_hex)
6285
}
@@ -70,7 +93,12 @@ fn run_app(cli: Cli) -> Result<(), String> {
7093
} else {
7194
// This should be unreachable due to the validation in utils::setup_app
7295
Ok(())
73-
}
96+
};
97+
98+
// NOTE: In a production app, you would join the submitter thread here.
99+
// if let Some(handle) = submitter_handle { handle.join().unwrap(); }
100+
101+
result
74102
}
75103

76104
fn main() {

0 commit comments

Comments
 (0)