mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-26 02:38:40 +01: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:
@@ -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?
|
||||
|
||||
Reference in New Issue
Block a user