1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
//! Work unit construction: preimage prefix, nonce table, and midstate computation.
//!
//! Faithfully reproduces the C++ webminer's preimage format:
//! - JSON prefix padded to 48 raw bytes → 64 base64 bytes = one SHA256 block
//! - Nonce table: base64 of "000" through "999" (1000 entries × 4 chars each)
//! - Final suffix: "fQ==" (base64 of "}")
use base64::{engine::general_purpose::STANDARD, Engine};
use rand::RngCore;
use webylib::{Amount, SecretWebcash};
use super::sha256::Sha256Midstate;
// NonceTable is now in miner/nonce_table.rs (WASM-safe).
pub use super::nonce_table::NonceTable;
/// The final 4-byte suffix: base64 of "}".
pub const FINAL_SUFFIX: &[u8; 4] = b"fQ==";
/// A prepared work unit ready for mining.
pub struct WorkUnit {
/// SHA256 midstate after processing the 64-byte prefix block.
pub midstate: Sha256Midstate,
/// The full base64-encoded prefix string (64 bytes).
pub prefix_b64: String,
/// The webcash secret the miner keeps (mining_amount - subsidy_amount).
pub keep_secret: SecretWebcash,
/// The subsidy secret (paid to Webcash LLC).
pub subsidy_secret: SecretWebcash,
/// Current difficulty target.
pub difficulty: u32,
/// Unix timestamp when the work unit was created.
pub timestamp: f64,
}
impl WorkUnit {
/// Build a new work unit with fresh random secrets and current parameters.
///
/// This reproduces the C++ webminer's preimage construction exactly:
/// 1. Format JSON prefix with keep/subsidy secrets, difficulty, timestamp
/// 2. Pad to a multiple of 48 bytes (spaces, last char '1')
/// 3. Base64 encode (becomes 64 bytes = one SHA256 block)
/// 4. Compute midstate
pub fn new(difficulty: u32, mining_amount: Amount, subsidy_amount: Amount) -> Self {
Self::new_with_timestamp(difficulty, mining_amount, subsidy_amount, None)
}
/// Like `new` but lets the caller forward-date the embedded preimage
/// timestamp. Used by the adaptive reporter to absorb queue pressure
/// inside the server's ±2h timestamp window: when many solutions are
/// queued, the next batch gets `timestamp = now + offset` so that by
/// the time the reporter submits them, the embedded timestamp is still
/// within the server's acceptance band. Caller is responsible for
/// keeping the offset within ~+115 min (= ~+2h forward bound minus
/// a 5-min safety margin from maaku's reference server).
///
/// Pass `None` to use wall-clock now (the default behavior of `new`).
pub fn new_with_timestamp(
difficulty: u32,
mining_amount: Amount,
subsidy_amount: Amount,
timestamp_override: Option<f64>,
) -> Self {
let mut rng = rand::thread_rng();
// Generate random 32-byte secrets
let mut keep_sk = [0u8; 32];
let mut subsidy_sk = [0u8; 32];
rng.fill_bytes(&mut keep_sk);
rng.fill_bytes(&mut subsidy_sk);
let keep_amount = mining_amount - subsidy_amount;
let keep_str_full = format!("e{}:secret:{}", keep_amount, hex::encode(keep_sk));
let subsidy_str_full = format!("e{}:secret:{}", subsidy_amount, hex::encode(subsidy_sk));
let keep_secret = SecretWebcash::parse(&keep_str_full).expect("valid keep secret format");
let subsidy_secret =
SecretWebcash::parse(&subsidy_str_full).expect("valid subsidy secret format");
// Zero out raw key bytes
keep_sk.fill(0);
subsidy_sk.fill(0);
let timestamp = match timestamp_override {
Some(ts) => ts,
None => {
#[cfg(not(target_arch = "wasm32"))]
{
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64()
}
#[cfg(target_arch = "wasm32")]
{
js_sys::Date::now() / 1000.0
}
}
};
let keep_str = keep_secret.to_string();
let subsidy_str = subsidy_secret.to_string();
// Build the JSON prefix (matching C++ webminer format exactly)
let mut prefix = format!(
"{{\"legalese\": {{\"terms\": true}}, \"webcash\": [\"{}\", \"{}\"], \"subsidy\": [\"{}\"], \"difficulty\": {}, \"timestamp\": {}, \"nonce\": ",
keep_str, subsidy_str, subsidy_str, difficulty, timestamp
);
// Pad to multiple of 48 bytes (space-fill, last char '1')
let target_len = 48 * (1 + prefix.len() / 48);
while prefix.len() < target_len {
prefix.push(' ');
}
// Replace the last character with '1' to form a valid JSON nonce value
prefix.pop();
prefix.push('1');
// Base64 encode → must be a multiple of 64 bytes (one or more SHA256 blocks)
let prefix_b64 = STANDARD.encode(&prefix);
assert_eq!(
prefix_b64.len() % 64,
0,
"prefix_b64 must be a multiple of 64 bytes, got {}. Raw prefix was {} bytes.",
prefix_b64.len(),
prefix.len()
);
// Compute midstate from all prefix blocks
let midstate = Sha256Midstate::from_prefix(prefix_b64.as_bytes());
WorkUnit {
midstate,
prefix_b64,
keep_secret,
subsidy_secret,
difficulty,
timestamp,
}
}
/// Reconstruct the full preimage string from nonce indices.
///
/// Result = prefix_b64(64) + nonce1(4) + nonce2(4) + "fQ=="(4) = 76 chars.
pub fn preimage_string(&self, nonce_table: &NonceTable, n1: u16, n2: u16) -> String {
let mut s = String::with_capacity(76);
s.push_str(&self.prefix_b64);
s.push_str(std::str::from_utf8(nonce_table.get(n1)).unwrap());
s.push_str(std::str::from_utf8(nonce_table.get(n2)).unwrap());
s.push_str(std::str::from_utf8(FINAL_SUFFIX).unwrap());
s
}
/// Build the 12-byte tail for a given nonce pair.
pub fn build_tail(nonce_table: &NonceTable, n1: u16, n2: u16) -> [u8; 12] {
let mut tail = [0u8; 12];
tail[0..4].copy_from_slice(nonce_table.get(n1));
tail[4..8].copy_from_slice(nonce_table.get(n2));
tail[8..12].copy_from_slice(FINAL_SUFFIX);
tail
}
}
#[cfg(test)]
mod tests {
use super::*;
use sha2::{Digest, Sha256};
#[test]
fn nonce_table_first_entries() {
let table = NonceTable::new();
// "000" → base64 "MDAw"
assert_eq!(table.get(0), b"MDAw");
// "001" → base64 "MDAx"
assert_eq!(table.get(1), b"MDAx");
// "010" → base64 "MDEw"
assert_eq!(table.get(10), b"MDEw");
// "999" → base64 "OTk5"
assert_eq!(table.get(999), b"OTk5");
}
#[test]
fn nonce_table_matches_cpp_webminer() {
let table = NonceTable::new();
// The C++ webminer starts with "MDAwMDAx..." which is "MDAw" + "MDAx" = nonces[0]+nonces[1]
let nonce0 = table.get(0);
let nonce1 = table.get(1);
assert_eq!(std::str::from_utf8(nonce0).unwrap(), "MDAw");
assert_eq!(std::str::from_utf8(nonce1).unwrap(), "MDAx");
}
#[test]
fn final_suffix_decodes_to_closing_brace() {
let decoded = STANDARD.decode(FINAL_SUFFIX).unwrap();
assert_eq!(decoded, b"}");
}
#[test]
fn work_unit_prefix_is_multiple_of_64() {
let wu = WorkUnit::new(
28,
Amount::from_wats(20_000_000_000_000),
Amount::from_wats(1_000_000_000_000),
);
assert_eq!(
wu.prefix_b64.len() % 64,
0,
"prefix must be a multiple of 64 bytes"
);
assert!(
wu.prefix_b64.len() >= 64,
"prefix must be at least 64 bytes"
);
}
#[test]
fn work_unit_preimage_length() {
let table = NonceTable::new();
let wu = WorkUnit::new(
28,
Amount::from_wats(20_000_000_000_000),
Amount::from_wats(1_000_000_000_000),
);
let preimage = wu.preimage_string(&table, 0, 0);
// preimage = prefix_b64 + nonce1(4) + nonce2(4) + "fQ=="(4)
assert_eq!(preimage.len(), wu.prefix_b64.len() + 12);
}
#[test]
fn work_unit_preimage_hashes_correctly() {
let table = NonceTable::new();
let wu = WorkUnit::new(
28,
Amount::from_wats(20_000_000_000_000),
Amount::from_wats(1_000_000_000_000),
);
for n1 in [0u16, 42, 999] {
for n2 in [0u16, 500, 999] {
// Hash the full preimage with sha2
let preimage = wu.preimage_string(&table, n1, n2);
let ref_hash: [u8; 32] = Sha256::digest(preimage.as_bytes()).into();
// Hash via midstate
let tail = WorkUnit::build_tail(&table, n1, n2);
let our_hash = wu.midstate.finalize(&tail);
assert_eq!(our_hash, ref_hash, "mismatch at n1={}, n2={}", n1, n2);
}
}
}
}