Skip to content

Commit

Permalink
feat: add JS/python POW solvers
Browse files Browse the repository at this point in the history
+ update error messages from the pow claim endpoint
  • Loading branch information
Zk2u committed Sep 17, 2024
1 parent ca53369 commit 9761e2b
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 5 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name = "alpen-faucet"
version = "0.1.0"
edition = "2021"

[workspace]
resolver = "2"
members = [".", "utils/html-solver"]

[dependencies]
alloy = { version = "0.3.5", default-features = false, features = [
"std",
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ fn count_leading_zeros(data: &[u8]) -> u8 {
}
```

For those who are compiler challenged, there are 0-dependency JavaScript and Python implementations of solvers in the `utils` directory. You can easily run the JS solver in your browser by running `just html-solver` and then navigating to http://localhost:3001.

Once you find a solution, hex encode it and use it in a claim for either L1 or L2 funds:

### L1
Expand Down
3 changes: 3 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ enable-hooks:
echo "#!/bin/sh\njust pre-commit" > .git/hooks/pre-commit

pre-commit: fmt clippy t

html-solver:
cargo r -p html-solver
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ async fn claim_l1(
};

// num hashes on average to solve challenge: 2^15
if !Challenge::valid(&ip, SETTINGS.pow_difficulty, solution.0) {
return Err((StatusCode::BAD_REQUEST, "Bad solution".to_string()));
if let Err(e) = Challenge::valid(&ip, SETTINGS.pow_difficulty, solution.0) {
return Err((StatusCode::BAD_REQUEST, format!("{e:?}")))
}

let address = address.require_network(SETTINGS.network).map_err(|_| {
Expand Down
18 changes: 15 additions & 3 deletions src/pow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,25 @@ use concurrent_map::{CasFailure, ConcurrentMap};
use parking_lot::{Mutex, MutexGuard};
use rand::{thread_rng, Rng};
use sha2::{Digest, Sha256};
use terrors::OneOf;
use tokio::time::sleep;

use crate::err;

pub struct Challenge {
nonce: Nonce,
expires_at: Instant,
}

const TTL: Duration = Duration::from_secs(20);

#[derive(Debug)]
pub struct NonceNotFound;
#[derive(Debug)]
pub struct BadProofOfWork;
#[derive(Debug)]
pub struct AlreadyClaimed;

impl Challenge {
/// Retrieves a proof-of-work challenge for the given Ipv4 address.
///
Expand Down Expand Up @@ -48,12 +58,13 @@ impl Challenge {
}

/// Validates the proof of work solution by the client.
pub fn valid(ip: &Ipv4Addr, difficulty: u8, solution: Solution) -> bool {
pub fn valid(ip: &Ipv4Addr, difficulty: u8, solution: Solution) -> Result<(), OneOf<(NonceNotFound, BadProofOfWork, AlreadyClaimed)>> {
let ns = nonce_set();
let raw_ip = ip.to_bits();
let nonce = match ns.get(&raw_ip) {
Some((nonce, claimed)) if !claimed => nonce,
_ => return false,
Some(_) => return err!(AlreadyClaimed),
None => return err!(NonceNotFound),
};
let mut hasher = Sha256::new();
hasher.update(b"alpen labs faucet 2024");
Expand All @@ -62,8 +73,9 @@ impl Challenge {
let pow_valid = count_leading_zeros(&hasher.finalize()) >= difficulty;
if pow_valid {
ns.insert(raw_ip, (nonce, true));
return Ok(());
}
pow_valid
err!(BadProofOfWork)
}

pub fn nonce(&self) -> [u8; 16] {
Expand Down
8 changes: 8 additions & 0 deletions utils/html-solver/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "html-solver"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["rt", "macros"] }
hyper = { version = "0.14", features = ["server", "http1", "tcp"] }
50 changes: 50 additions & 0 deletions utils/html-solver/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use hyper::{
service::{make_service_fn, service_fn},
Body, Request, Response, Server, StatusCode,
};
use std::{convert::Infallible, fs, net::SocketAddr, path::Path};

async fn serve_file(req: Request<Body>) -> Result<Response<Body>, Infallible> {
let path = match req.uri().path() {
"/" => "static/index.html",
path => &path[1..], // strip leading '/'
};

let file_path = Path::new(path);

match fs::read(file_path) {
Ok(contents) => {
let mime_type = match file_path.extension().and_then(|ext| ext.to_str()) {
Some("html") => "text/html",
Some("js") => "application/javascript",
_ => "application/octet-stream",
};

Ok(Response::builder()
.header("Content-Type", mime_type)
.body(Body::from(contents))
.unwrap())
}
Err(_) => Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from("404 Not Found"))
.unwrap()),
}
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 3001));

let make_svc = make_service_fn(|_conn| {
async { Ok::<_, Infallible>(service_fn(serve_file)) }
});

let server = Server::bind(&addr).serve(make_svc);

println!("Listening on http://{}", addr);

if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
}
75 changes: 75 additions & 0 deletions utils/html-solver/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PoW Solver</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.container {
max-width: 600px;
margin: auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
input, button {
margin: 10px 0;
padding: 10px;
width: calc(100% - 22px);
}
button {
cursor: pointer;
}
.result {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
}
</style>
</head>
<body>
<div class="container">
<h1>Proof-of-Work Solver</h1>
<form id="powForm">
<label for="nonce">Nonce (16-byte hex string):</label>
<input type="text" id="nonce" required pattern="[0-9a-fA-F]{32}" title="16-byte hex string (32 hex characters)">

<label for="difficulty">Difficulty (0-255):</label>
<input type="number" id="difficulty" required min="0" max="255">

<button type="submit">Solve PoW</button>
</form>
<div id="result" class="result" style="display: none;"></div>
</div>

<script>
if (window.Worker) {
const worker = new Worker('static/solver.js');

document.getElementById('powForm').addEventListener('submit', function(event) {
event.preventDefault();
const nonce = document.getElementById('nonce').value;
const difficulty = parseInt(document.getElementById('difficulty').value, 10);
document.getElementById('result').style.display = 'none';
document.getElementById('result').textContent = 'Solving...';

worker.postMessage({ nonce, difficulty });

worker.onmessage = function(event) {
document.getElementById('result').textContent = 'Solution: ' + event.data.solution;
document.getElementById('result').style.display = 'block';
};
});
} else {
alert('Your browser does not support Web Workers.');
}
</script>
</body>
</html>
50 changes: 50 additions & 0 deletions utils/html-solver/static/solver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
async function sha256(data) {
const buffer = await crypto.subtle.digest('SHA-256', data);
return new Uint8Array(buffer);
}

function countLeadingZeros(data) {
let leadingZeros = 0;
for (let byte of data) {
if (byte === 0) {
leadingZeros += 8;
} else {
leadingZeros += byte.toString(2).padStart(8, '0').indexOf('1');
break;
}
}
return leadingZeros;
}

async function findSolution(nonce, difficulty) {
const salt = new Uint8Array([
0x61, 0x6c, 0x70, 0x65, 0x6e, 0x20, 0x6c, 0x61,
0x62, 0x73, 0x20, 0x66, 0x61, 0x75, 0x63, 0x65,
0x74, 0x20, 0x32, 0x30, 0x32, 0x34
]);
nonce = new Uint8Array(nonce.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
let solution = new Uint8Array(8);

while (true) {
const hashInput = new Uint8Array([...salt, ...nonce, ...solution]);
const hash = await sha256(hashInput);
if (countLeadingZeros(hash) >= difficulty) {
return Array.from(solution).map(byte => byte.toString(16).padStart(2, '0')).join('');
}
// Increment solution
for (let i = 7; i >= 0; i--) {
if (solution[i] < 0xFF) {
solution[i]++;
break;
} else {
solution[i] = 0;
}
}
}
}

onmessage = async function(event) {
const { nonce, difficulty } = event.data;
const solution = await findSolution(nonce, difficulty);
postMessage({ solution });
};
43 changes: 43 additions & 0 deletions utils/solver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import hashlib

# SHA-256 hashing function
def sha256(data):
return hashlib.sha256(data).digest()

# Count leading zeros in a byte array
def count_leading_zeros(data):
leading_zeros = 0
for byte in data:
if byte == 0:
leading_zeros += 8
else:
leading_zeros += bin(byte).find('1')
break
return leading_zeros

# Find solution
def find_solution(nonce, difficulty):
salt = bytes.fromhex("616c70656e206c616273206661756365742032303234")
nonce = bytes.fromhex(nonce)
solution = bytearray(8)

while True:
hash_input = salt + nonce + solution
hash = sha256(hash_input)
print(hash.hex())
print(count_leading_zeros(hash))
if count_leading_zeros(hash) >= difficulty:
return solution.hex()
# Increment solution
for i in range(7, -1, -1):
if solution[i] < 0xFF:
solution[i] += 1
break
else:
solution[i] = 0

# Example usage
nonce = "4bbbefa849c59704f7f13745ca47161a" # Replace with actual nonce
difficulty = 17 # Replace with actual difficulty
solution = find_solution(nonce, difficulty)
print("Solution:", solution)

0 comments on commit 9761e2b

Please sign in to comment.