mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
feat(restore): default create-only + PoW-priced restore depth (gate #2)
Default restore policy is now create-only — which betrays no self-sovereignty, since that lives at the ECOSYSTEM level (run your own backend). On top of the create-only|bounded|unlimited depth gate, restore depth can be PRICED in hashcash PoW so an importer pays compute proportional to the chain-scan cost they impose on the operator (deeper restore => more work). - core::restore_pow: a self-contained hashcash bound to (asset, address, start_height) — a solved nonce is valid only for that exact restore, so no challenge state, no replay, no depth-downgrade. Difficulty = leading zero bits. - RestoreConfig: pow_free_days / pow_days_per_bit (0 = off) / pow_max_bits curve; required_restore_pow_bits + enforce_restore_pow (literal errors, no oracle). - Enforced at xmr/wow register + grin scan via a new restore_pow_nonce; the curve is advertised via /capabilities so the wallet computes + solves it. - Default WALLET_RESTORE_POLICY flipped bounded -> create-only. - Unit tests (curve scaling/cap, off-when-zero, valid/invalid nonce, hashcash roundtrip + param-binding); .env.example; OpenAPI drift clean.
This commit is contained in:
+10
-2
@@ -81,11 +81,19 @@ GRIN_NODE_FOREIGN_API_SECRET=
|
||||
# /capabilities so the wallet adapts its import UX. Self-sovereignty is
|
||||
# preserved — a user needing a deeper restore runs their own instance or picks
|
||||
# an operator with a looser policy.
|
||||
# create-only — new wallets only (scan starts at the tip)
|
||||
# create-only — new wallets only (scan starts at the tip). DEFAULT — and it
|
||||
# betrays nothing: self-sovereignty is run-your-own-backend.
|
||||
# bounded — restore up to WALLET_MAX_RESTORE_DEPTH_DAYS behind the tip
|
||||
# unlimited — any restore height (the natural choice for self-hosting)
|
||||
WALLET_RESTORE_POLICY=bounded
|
||||
WALLET_RESTORE_POLICY=create-only
|
||||
WALLET_MAX_RESTORE_DEPTH_DAYS=365
|
||||
# PoW-priced restore: charge the importer compute proportional to the scan depth
|
||||
# they impose on you. Free below WALLET_RESTORE_POW_FREE_DAYS, then +1 hashcash
|
||||
# difficulty bit per WALLET_RESTORE_POW_DAYS_PER_BIT days of depth (0 = pricing
|
||||
# OFF), capped at WALLET_RESTORE_POW_MAX_BITS. Applies within bounded/unlimited.
|
||||
WALLET_RESTORE_POW_FREE_DAYS=90
|
||||
WALLET_RESTORE_POW_DAYS_PER_BIT=0
|
||||
WALLET_RESTORE_POW_MAX_BITS=24
|
||||
|
||||
# ── Feature flags (each feature ships in the open core, toggled per deployment) ──
|
||||
# The wallet client reads GET /api/v1/capabilities and greys out disabled features.
|
||||
|
||||
+38
-1
@@ -2339,6 +2339,15 @@
|
||||
"rewind_hash"
|
||||
],
|
||||
"properties": {
|
||||
"restore_pow_nonce": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64",
|
||||
"description": "Restore proof-of-work nonce. Required when the instance prices the\nrequested restore depth (see `/capabilities` → `restore.pow_*`); bound to\n`(grin, rewind_hash, start_height)` (see `restore_pow`).",
|
||||
"minimum": 0
|
||||
},
|
||||
"rewind_hash": {
|
||||
"type": "string",
|
||||
"description": "The wallet's `rewind_hash` (64 hex). Forwarded to grin-wallet; not stored."
|
||||
@@ -2909,6 +2918,15 @@
|
||||
"asset": {
|
||||
"type": "string"
|
||||
},
|
||||
"restore_pow_nonce": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64",
|
||||
"description": "Restore proof-of-work nonce. Required when the instance prices the\nrequested restore depth (see `/capabilities` → `restore.pow_*`); ignored\notherwise. Bound to `(asset, address, start_height)` (see `restore_pow`).",
|
||||
"minimum": 0
|
||||
},
|
||||
"start_height": {
|
||||
"type": [
|
||||
"integer",
|
||||
@@ -3026,7 +3044,10 @@
|
||||
"type": "object",
|
||||
"description": "This instance's wallet-restore (import) policy. The wallet uses it to adapt\nits import UX — hide/grey the restore-height field under `create-only`, warn\nwhen a chosen date exceeds the bound. `max_depth_days` is present only for\nthe `bounded` policy.",
|
||||
"required": [
|
||||
"policy"
|
||||
"policy",
|
||||
"pow_free_days",
|
||||
"pow_days_per_bit",
|
||||
"pow_max_bits"
|
||||
],
|
||||
"properties": {
|
||||
"max_depth_days": {
|
||||
@@ -3040,6 +3061,22 @@
|
||||
"policy": {
|
||||
"type": "string",
|
||||
"description": "`create-only` | `bounded` | `unlimited`."
|
||||
},
|
||||
"pow_days_per_bit": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"minimum": 0
|
||||
},
|
||||
"pow_free_days": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "Restore PoW pricing curve: a restore depth (days) free of PoW, then `+1`\nhashcash difficulty bit per `pow_days_per_bit` days beyond it\n(`0` = pricing off), capped at `pow_max_bits`. The wallet computes its\nrequired difficulty from this + the restore date and solves the hashcash.",
|
||||
"minimum": 0
|
||||
},
|
||||
"pow_max_bits": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -58,6 +58,13 @@ pub struct RestoreCapability {
|
||||
/// `create-only` | `bounded` | `unlimited`.
|
||||
pub policy: String,
|
||||
pub max_depth_days: Option<u32>,
|
||||
/// Restore PoW pricing curve: a restore depth (days) free of PoW, then `+1`
|
||||
/// hashcash difficulty bit per `pow_days_per_bit` days beyond it
|
||||
/// (`0` = pricing off), capped at `pow_max_bits`. The wallet computes its
|
||||
/// required difficulty from this + the restore date and solves the hashcash.
|
||||
pub pow_free_days: u32,
|
||||
pub pow_days_per_bit: u32,
|
||||
pub pow_max_bits: u32,
|
||||
}
|
||||
|
||||
/// Registration gates this instance enforces for a NEW wallet (returning wallets
|
||||
@@ -147,6 +154,9 @@ pub fn effective_capabilities(config: &Config) -> CapabilitiesResponse {
|
||||
crate::config::RestorePolicy::Bounded => Some(config.restore.max_depth_days),
|
||||
_ => None,
|
||||
},
|
||||
pow_free_days: config.restore.pow_free_days,
|
||||
pow_days_per_bit: config.restore.pow_days_per_bit,
|
||||
pow_max_bits: config.restore.pow_max_bits,
|
||||
},
|
||||
registration: RegistrationCapability {
|
||||
invite_required: config.registration.require_invite,
|
||||
|
||||
@@ -48,6 +48,11 @@ pub struct GrinScanRequest {
|
||||
pub rewind_hash: String,
|
||||
/// Scan from this block height (wallet birthday / last scanned). Omit for full.
|
||||
pub start_height: Option<u64>,
|
||||
/// Restore proof-of-work nonce. Required when the instance prices the
|
||||
/// requested restore depth (see `/capabilities` → `restore.pow_*`); bound to
|
||||
/// `(grin, rewind_hash, start_height)` (see `restore_pow`).
|
||||
#[serde(default)]
|
||||
pub restore_pow_nonce: Option<u64>,
|
||||
}
|
||||
|
||||
/// A single output recovered by a view-only scan. Amounts are nanogrin.
|
||||
@@ -129,6 +134,13 @@ pub async fn scan(
|
||||
// Restore: gate the scan depth against this instance's policy.
|
||||
let tip = client.get_height().await?;
|
||||
state.config.restore.enforce("grin", h, tip)?;
|
||||
state.config.restore.enforce_restore_pow(
|
||||
"grin",
|
||||
&req.rewind_hash,
|
||||
h,
|
||||
tip,
|
||||
req.restore_pow_nonce,
|
||||
)?;
|
||||
}
|
||||
let view = client
|
||||
.scan_rewind_hash(&req.rewind_hash, req.start_height)
|
||||
|
||||
@@ -78,6 +78,11 @@ pub struct RegisterRequest {
|
||||
pub view_key: String,
|
||||
/// Scan from this block height (wallet birthday). Omit to scan from now.
|
||||
pub start_height: Option<u64>,
|
||||
/// Restore proof-of-work nonce. Required when the instance prices the
|
||||
/// requested restore depth (see `/capabilities` → `restore.pow_*`); ignored
|
||||
/// otherwise. Bound to `(asset, address, start_height)` (see `restore_pow`).
|
||||
#[serde(default)]
|
||||
pub restore_pow_nonce: Option<u64>,
|
||||
}
|
||||
|
||||
/// Request decoy outputs for ring construction.
|
||||
@@ -316,6 +321,13 @@ pub async fn register(
|
||||
// backfill cost lands on our LWS). `None` (create) needs no check.
|
||||
let tip = client.get_blockchain_height().await?;
|
||||
state.config.restore.enforce(&asset, h, tip)?;
|
||||
state.config.restore.enforce_restore_pow(
|
||||
&asset,
|
||||
&req.address,
|
||||
h,
|
||||
tip,
|
||||
req.restore_pow_nonce,
|
||||
)?;
|
||||
client
|
||||
.import_account(&req.address, &req.view_key, h)
|
||||
.await?
|
||||
|
||||
+137
-1
@@ -340,6 +340,15 @@ pub struct RestoreConfig {
|
||||
pub policy: RestorePolicy,
|
||||
/// For `Bounded`: how many days behind the tip a restore may start.
|
||||
pub max_depth_days: u32,
|
||||
/// Restore depth (days behind tip) that is FREE of PoW pricing.
|
||||
pub pow_free_days: u32,
|
||||
/// `+1` hashcash difficulty bit per this many days of restore depth beyond
|
||||
/// the free window. `0` disables restore pricing (the depth gate above still
|
||||
/// applies). Each bit ~doubles the wallet's solve time, so the requester pays
|
||||
/// compute proportional to the scan cost they impose on the operator.
|
||||
pub pow_days_per_bit: u32,
|
||||
/// Hard cap on the priced difficulty (bounds wallet solve time on a phone).
|
||||
pub pow_max_bits: u32,
|
||||
}
|
||||
|
||||
/// Approximate blocks per day per chain (from target block time). Used only to
|
||||
@@ -391,6 +400,49 @@ impl RestoreConfig {
|
||||
RestorePolicy::Unlimited => unreachable!(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Hashcash difficulty (leading zero BITS) required to restore `chain` from
|
||||
/// `start_height` against `tip` — `0` within the free window or when pricing
|
||||
/// is off. The depth→bits curve is the operator's "pay for the scan you cost
|
||||
/// me" knob; it sits ON TOP of the [`enforce`](Self::enforce) depth gate.
|
||||
pub fn required_restore_pow_bits(&self, chain: &str, start_height: u64, tip: u64) -> u32 {
|
||||
if self.pow_days_per_bit == 0 {
|
||||
return 0;
|
||||
}
|
||||
let depth_days = tip.saturating_sub(start_height) / blocks_per_day(chain).max(1);
|
||||
let over = depth_days.saturating_sub(self.pow_free_days as u64);
|
||||
((over / self.pow_days_per_bit as u64) as u32).min(self.pow_max_bits)
|
||||
}
|
||||
|
||||
/// Enforce restore PoW pricing: when the requested depth is priced, a valid
|
||||
/// hashcash `nonce` (bound to `chain`/`address`/`start_height`) of the
|
||||
/// required difficulty must accompany the restore. A literal error names the
|
||||
/// requirement, never the caller's value.
|
||||
pub fn enforce_restore_pow(
|
||||
&self,
|
||||
chain: &str,
|
||||
address: &str,
|
||||
start_height: u64,
|
||||
tip: u64,
|
||||
nonce: Option<u64>,
|
||||
) -> Result<(), AppError> {
|
||||
let bits = self.required_restore_pow_bits(chain, start_height, tip);
|
||||
if bits == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let nonce = nonce.ok_or_else(|| {
|
||||
AppError::ValidationError(format!(
|
||||
"this restore depth requires a {bits}-bit proof-of-work nonce; \
|
||||
upgrade to a newer Smirk client"
|
||||
))
|
||||
})?;
|
||||
if !crate::core::restore_pow::verify(chain, address, start_height, nonce, bits) {
|
||||
return Err(AppError::ValidationError(
|
||||
"restore proof-of-work is insufficient for the requested depth".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Registration gates beyond PoW — composable, operator-configured, advertised
|
||||
@@ -553,7 +605,7 @@ impl Config {
|
||||
grace_period_hours: env_parse("ERASURE_GRACE_PERIOD_HOURS", 72u64)?,
|
||||
},
|
||||
restore: {
|
||||
let policy = match env_or("WALLET_RESTORE_POLICY", "bounded")
|
||||
let policy = match env_or("WALLET_RESTORE_POLICY", "create-only")
|
||||
.to_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
@@ -569,6 +621,9 @@ impl Config {
|
||||
RestoreConfig {
|
||||
policy,
|
||||
max_depth_days: env_parse("WALLET_MAX_RESTORE_DEPTH_DAYS", 365u32)?,
|
||||
pow_free_days: env_parse("WALLET_RESTORE_POW_FREE_DAYS", 90u32)?,
|
||||
pow_days_per_bit: env_parse("WALLET_RESTORE_POW_DAYS_PER_BIT", 0u32)?,
|
||||
pow_max_bits: env_parse("WALLET_RESTORE_POW_MAX_BITS", 24u32)?,
|
||||
}
|
||||
},
|
||||
registration: RegistrationConfig {
|
||||
@@ -870,6 +925,9 @@ mod tests {
|
||||
restore: RestoreConfig {
|
||||
policy: RestorePolicy::Bounded,
|
||||
max_depth_days: 365,
|
||||
pow_free_days: 90,
|
||||
pow_days_per_bit: 0,
|
||||
pow_max_bits: 24,
|
||||
},
|
||||
registration: RegistrationConfig {
|
||||
require_invite: false,
|
||||
@@ -972,6 +1030,9 @@ mod tests {
|
||||
c.restore = RestoreConfig {
|
||||
policy: RestorePolicy::Bounded,
|
||||
max_depth_days: 0,
|
||||
pow_free_days: 90,
|
||||
pow_days_per_bit: 0,
|
||||
pow_max_bits: 24,
|
||||
};
|
||||
assert!(c.validate().is_err());
|
||||
}
|
||||
@@ -981,6 +1042,9 @@ mod tests {
|
||||
let rc = RestoreConfig {
|
||||
policy: RestorePolicy::Unlimited,
|
||||
max_depth_days: 0,
|
||||
pow_free_days: 0,
|
||||
pow_days_per_bit: 0,
|
||||
pow_max_bits: 0,
|
||||
};
|
||||
assert!(rc.enforce("xmr", 1, 3_000_000).is_ok());
|
||||
}
|
||||
@@ -990,6 +1054,9 @@ mod tests {
|
||||
let rc = RestoreConfig {
|
||||
policy: RestorePolicy::Bounded,
|
||||
max_depth_days: 30,
|
||||
pow_free_days: 0,
|
||||
pow_days_per_bit: 0,
|
||||
pow_max_bits: 0,
|
||||
};
|
||||
let tip = 3_000_000u64; // 30 days * 720 blocks/day => floor = tip - 21_600.
|
||||
assert!(rc.enforce("xmr", tip - 21_600, tip).is_ok()); // exactly at the floor
|
||||
@@ -1002,10 +1069,79 @@ mod tests {
|
||||
let rc = RestoreConfig {
|
||||
policy: RestorePolicy::CreateOnly,
|
||||
max_depth_days: 365, // ignored under create-only
|
||||
pow_free_days: 0,
|
||||
pow_days_per_bit: 0,
|
||||
pow_max_bits: 0,
|
||||
};
|
||||
let tip = 3_000_000u64;
|
||||
assert!(rc.enforce("xmr", tip, tip).is_ok()); // create
|
||||
assert!(rc.enforce("xmr", tip - 720, tip).is_ok()); // within the 1-day grace
|
||||
assert!(rc.enforce("xmr", tip - 100_000, tip).is_err()); // a real restore
|
||||
}
|
||||
|
||||
fn priced(free: u32, per_bit: u32, max: u32) -> RestoreConfig {
|
||||
RestoreConfig {
|
||||
policy: RestorePolicy::Unlimited,
|
||||
max_depth_days: 0,
|
||||
pow_free_days: free,
|
||||
pow_days_per_bit: per_bit,
|
||||
pow_max_bits: max,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_pow_bits_scale_with_depth_and_cap() {
|
||||
let rc = priced(30, 30, 10); // free 30d, +1 bit / 30d, cap 10
|
||||
let tip = 3_000_000u64;
|
||||
let d = |days: u64| tip.saturating_sub(days * 720); // xmr = 720 blocks/day
|
||||
assert_eq!(
|
||||
rc.required_restore_pow_bits("xmr", d(20), tip),
|
||||
0,
|
||||
"within free window"
|
||||
);
|
||||
assert_eq!(
|
||||
rc.required_restore_pow_bits("xmr", d(90), tip),
|
||||
2,
|
||||
"30 free + 60 over => 2 bits"
|
||||
);
|
||||
assert_eq!(
|
||||
rc.required_restore_pow_bits("xmr", d(100_000), tip),
|
||||
10,
|
||||
"capped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_pow_off_when_days_per_bit_zero() {
|
||||
let rc = priced(0, 0, 24);
|
||||
assert_eq!(rc.required_restore_pow_bits("xmr", 1, 3_000_000), 0);
|
||||
assert!(rc
|
||||
.enforce_restore_pow("xmr", "addr", 1, 3_000_000, None)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_pow_requires_a_valid_nonce_when_priced() {
|
||||
let rc = priced(0, 1, 8);
|
||||
let tip = 3_000_000u64;
|
||||
let start = tip - 8 * 720; // 8 days depth => 8 bits
|
||||
assert_eq!(rc.required_restore_pow_bits("xmr", start, tip), 8);
|
||||
// Missing nonce is rejected.
|
||||
assert!(rc
|
||||
.enforce_restore_pow("xmr", "addr", start, tip, None)
|
||||
.is_err());
|
||||
// A correctly-solved nonce is accepted.
|
||||
let nonce = (0u64..)
|
||||
.find(|&n| crate::core::restore_pow::verify("xmr", "addr", start, n, 8))
|
||||
.unwrap();
|
||||
assert!(rc
|
||||
.enforce_restore_pow("xmr", "addr", start, tip, Some(nonce))
|
||||
.is_ok());
|
||||
// A nonce that doesn't clear the bar is rejected.
|
||||
assert!(
|
||||
rc.enforce_restore_pow("xmr", "addr", start, tip, Some(nonce.wrapping_add(1)))
|
||||
.is_err()
|
||||
|| crate::core::restore_pow::verify("xmr", "addr", start, nonce.wrapping_add(1), 8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod admin_session;
|
||||
pub mod crypto;
|
||||
pub mod invite;
|
||||
pub mod pow;
|
||||
pub mod restore_pow;
|
||||
pub mod secret;
|
||||
pub mod session;
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
//! Restore proof-of-work — a hashcash gate sized to the RESTORE DEPTH.
|
||||
//!
|
||||
//! A wallet importing a foreign seed pays compute proportional to the chain-scan
|
||||
//! cost it imposes on the operator's LWS/node: deeper restore ⇒ more work. This
|
||||
//! is the "pay for what you cost the operator" mechanism (see the restore-policy
|
||||
//! decision), distinct from the ALTCHA registration gate ([`crate::core::pow`]).
|
||||
//!
|
||||
//! It is deliberately self-contained — no server-issued challenge, no state —
|
||||
//! because the work is bound to the restore parameters themselves: a solved
|
||||
//! nonce satisfies the difficulty ONLY for this exact `(asset, address,
|
||||
//! start_height)`, so it cannot be precomputed generically, replayed against a
|
||||
//! different account, or down-graded to a shallower (cheaper) restore.
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Domain tag so a restore-PoW hash can never collide with another protocol hash.
|
||||
const DOMAIN: &[u8] = b"smirk-restore-pow-v1";
|
||||
|
||||
/// The hashcash preimage (minus the nonce) that pins the work to one restore.
|
||||
fn challenge_input(asset: &str, address: &str, start_height: u64) -> Vec<u8> {
|
||||
let mut v = Vec::with_capacity(DOMAIN.len() + asset.len() + address.len() + 16);
|
||||
v.extend_from_slice(DOMAIN);
|
||||
v.push(0x1f);
|
||||
v.extend_from_slice(asset.as_bytes());
|
||||
v.push(0x1f);
|
||||
v.extend_from_slice(address.as_bytes());
|
||||
v.push(0x1f);
|
||||
v.extend_from_slice(&start_height.to_le_bytes());
|
||||
v
|
||||
}
|
||||
|
||||
/// Leading zero BITS of a digest — the hashcash difficulty metric (each bit
|
||||
/// doubles expected solver work).
|
||||
fn leading_zero_bits(digest: &[u8]) -> u32 {
|
||||
let mut n = 0;
|
||||
for &b in digest {
|
||||
if b == 0 {
|
||||
n += 8;
|
||||
} else {
|
||||
n += b.leading_zeros();
|
||||
break;
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// Verify a restore-PoW nonce: `sha256(domain ‖ asset ‖ address ‖ start_height ‖
|
||||
/// nonce)` must have at least `required_bits` leading zero bits.
|
||||
/// `required_bits == 0` (no pricing / within the free depth) accepts anything.
|
||||
pub fn verify(
|
||||
asset: &str,
|
||||
address: &str,
|
||||
start_height: u64,
|
||||
nonce: u64,
|
||||
required_bits: u32,
|
||||
) -> bool {
|
||||
if required_bits == 0 {
|
||||
return true;
|
||||
}
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(challenge_input(asset, address, start_height));
|
||||
hasher.update(nonce.to_le_bytes());
|
||||
leading_zero_bits(&hasher.finalize()) >= required_bits
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Brute-force the first satisfying nonce (small difficulty → fast).
|
||||
fn solve(asset: &str, address: &str, h: u64, bits: u32) -> u64 {
|
||||
(0u64..)
|
||||
.find(|&n| verify(asset, address, h, n, bits))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solve_then_verify_roundtrips() {
|
||||
let n = solve("xmr", "Wo123", 100, 8);
|
||||
assert!(verify("xmr", "Wo123", 100, n, 8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_bits_accepts_any_nonce() {
|
||||
assert!(verify("xmr", "addr", 1, 0, 0));
|
||||
assert!(verify("xmr", "addr", 1, 12345, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsolved_nonce_fails_high_difficulty() {
|
||||
// A fixed un-mined nonce essentially never clears a 20-bit bar (~1/2^20).
|
||||
assert!(!verify("xmr", "addr", 100, 0, 20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonce_is_bound_to_restore_params() {
|
||||
let n = solve("xmr", "addrA", 100, 12);
|
||||
assert!(verify("xmr", "addrA", 100, n, 12));
|
||||
// The same nonce against a different address recomputes an independent
|
||||
// hash, which must not meet the SAME bar (false-accept ~1/2^12).
|
||||
assert!(!verify("xmr", "addrB", 100, n, 12));
|
||||
assert!(!verify("wow", "addrA", 100, n, 12));
|
||||
assert!(!verify("xmr", "addrA", 101, n, 12));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user