use base64::{engine::general_purpose::STANDARD, Engine};
use rand::RngCore;
use webylib::{Amount, SecretWebcash};
use super::sha256::Sha256Midstate;
pub struct NonceTable {
data: Vec<u8>,
}
impl Default for NonceTable {
fn default() -> Self {
Self::new()
}
}
impl NonceTable {
pub fn new() -> Self {
let mut data = Vec::with_capacity(4000);
for i in 0u16..1000 {
let s = format!("{:03}", i);
let encoded = STANDARD.encode(&s);
assert_eq!(encoded.len(), 4, "base64 of 3-byte string must be 4 chars");
data.extend_from_slice(encoded.as_bytes());
}
assert_eq!(data.len(), 4000);
NonceTable { data }
}
pub fn get(&self, idx: u16) -> &[u8] {
let start = idx as usize * 4;
&self.data[start..start + 4]
}
pub fn get_u32(&self, idx: u16) -> u32 {
let b = self.get(idx);
u32::from_be_bytes([b[0], b[1], b[2], b[3]])
}
pub fn as_u32_slice(&self) -> Vec<u32> {
(0..1000).map(|i| self.get_u32(i)).collect()
}
}
impl Clone for NonceTable {
fn clone(&self) -> Self {
NonceTable {
data: self.data.clone(),
}
}
}
pub const FINAL_SUFFIX: &[u8; 4] = b"fQ==";
pub struct WorkUnit {
pub midstate: Sha256Midstate,
pub prefix_b64: String,
pub keep_secret: SecretWebcash,
pub subsidy_secret: SecretWebcash,
pub difficulty: u32,
pub timestamp: f64,
}
impl WorkUnit {
pub fn new(difficulty: u32, mining_amount: Amount, subsidy_amount: Amount) -> Self {
let mut rng = rand::thread_rng();
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");
keep_sk.fill(0);
subsidy_sk.fill(0);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64();
let keep_str = keep_secret.to_string();
let subsidy_str = subsidy_secret.to_string();
let mut prefix = format!(
"{{\"legalese\": {{\"terms\": true}}, \"webcash\": [\"{}\", \"{}\"], \"subsidy\": [\"{}\"], \"difficulty\": {}, \"timestamp\": {}, \"nonce\": ",
keep_str, subsidy_str, subsidy_str, difficulty, timestamp
);
let target_len = 48 * (1 + prefix.len() / 48);
while prefix.len() < target_len {
prefix.push(' ');
}
prefix.pop();
prefix.push('1');
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()
);
let midstate = Sha256Midstate::from_prefix(prefix_b64.as_bytes());
WorkUnit {
midstate,
prefix_b64,
keep_secret,
subsidy_secret,
difficulty,
timestamp,
}
}
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
}
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();
assert_eq!(table.get(0), b"MDAw");
assert_eq!(table.get(1), b"MDAx");
assert_eq!(table.get(10), b"MDEw");
assert_eq!(table.get(999), b"OTk5");
}
#[test]
fn nonce_table_matches_cpp_webminer() {
let table = NonceTable::new();
assert_eq!(std::str::from_utf8(&table.data[0..8]).unwrap(), "MDAwMDAx");
}
#[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);
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] {
let preimage = wu.preimage_string(&table, n1, n2);
let ref_hash: [u8; 32] = Sha256::digest(preimage.as_bytes()).into();
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);
}
}
}
}