Harden validation, improve runtime security and refactor core services
Rust / build (push) Canceled after 0s

This commit is contained in:
thakares committed 2026-06-04 19:58:21 +05:30
1 parent aebef4c623
commit 8d119ac00e
39 files changed
+528 -158

No files matched your search

Generated
+15
View File
@@ -296,6 +296,7 @@ dependencies = [
"base64", "base64",
"clap", "clap",
"clap_complete", "clap_complete",
"dashmap",
"ed25519-dalek", "ed25519-dalek",
"hex", "hex",
"r2d2", "r2d2",
@@ -509,6 +510,20 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "dashmap"
version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.10" version = "0.7.10"
+5
View File
@@ -24,4 +24,9 @@ ENV CHRONOSEAL_DB_PATH=/var/lib/chronoseal/chronoseal.sqlite
ENV CHRONOSEAL_FRONTEND_DIR=/usr/share/chronoseal/frontend ENV CHRONOSEAL_FRONTEND_DIR=/usr/share/chronoseal/frontend
ENV CHRONOSEAL_PID_FILE=/run/chronoseal.pid ENV CHRONOSEAL_PID_FILE=/run/chronoseal.pid
RUN useradd -r -s /bin/false chronoseal
USER chronoseal
HEALTHCHECK --interval=30s --timeout=3s CMD chronoseal health || exit 1
CMD ["chronoseal", "run"] CMD ["chronoseal", "run"]
+133 -46
View File
@@ -117,7 +117,11 @@ fn test_entropy() -> EntropyData {
} }
} }
fn do_handshake(client: &reqwest::blocking::Client, base_url: &str, sk: &SigningKey) -> Result<InitResponse> { fn do_handshake(
client: &reqwest::blocking::Client,
base_url: &str,
sk: &SigningKey,
) -> Result<InitResponse> {
let pk_hex = hex::encode(sk.verifying_key().to_bytes()); let pk_hex = hex::encode(sk.verifying_key().to_bytes());
let init_req = InitRequest { public_key: pk_hex }; let init_req = InitRequest { public_key: pk_hex };
let resp = client let resp = client
@@ -126,7 +130,10 @@ fn do_handshake(client: &reqwest::blocking::Client, base_url: &str, sk: &Signing
.send()?; .send()?;
if !resp.status().is_success() { if !resp.status().is_success() {
return Err(anyhow!("Handshake failed with HTTP status: {}", resp.status())); return Err(anyhow!(
"Handshake failed with HTTP status: {}",
resp.status()
));
} }
let init_resp: InitResponse = resp.json()?; let init_resp: InitResponse = resp.json()?;
@@ -137,11 +144,17 @@ fn run_built_in_scenarios(client: &reqwest::blocking::Client, base_url: &str) ->
let mut failures = 0; let mut failures = 0;
let scenarios = [ let scenarios = [
("valid_progression", run_valid_progression as fn(&reqwest::blocking::Client, &str) -> Result<()>), (
"valid_progression",
run_valid_progression as fn(&reqwest::blocking::Client, &str) -> Result<()>,
),
("stale_replay", run_stale_replay), ("stale_replay", run_stale_replay),
("invalid_signature", run_invalid_signature), ("invalid_signature", run_invalid_signature),
("invalid_vm_stack", run_invalid_vm_stack), ("invalid_vm_stack", run_invalid_vm_stack),
("invalid_mutation_commitment", run_invalid_mutation_commitment), (
"invalid_mutation_commitment",
run_invalid_mutation_commitment,
),
("drifted_timestamp", run_drifted_timestamp), ("drifted_timestamp", run_drifted_timestamp),
("concurrent_heartbeat", run_concurrent_heartbeat), ("concurrent_heartbeat", run_concurrent_heartbeat),
("rate_limit_trigger", run_rate_limit_trigger), ("rate_limit_trigger", run_rate_limit_trigger),
@@ -197,7 +210,8 @@ fn run_valid_progression(client: &reqwest::blocking::Client, base_url: &str) ->
init.mutation_rounds, init.mutation_rounds,
)?; )?;
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, mutation_step); let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, mutation_step);
let timestamp = current_time_ms(); let timestamp = current_time_ms();
let entropy = test_entropy(); let entropy = test_entropy();
@@ -215,13 +229,14 @@ fn run_valid_progression(client: &reqwest::blocking::Client, base_url: &str) ->
sign_request(&sk, &mut req)?; sign_request(&sk, &mut req)?;
let resp = client let resp = client.post(format!("{}/hb", base_url)).json(&req).send()?;
.post(format!("{}/hb", base_url))
.json(&req)
.send()?;
if !resp.status().is_success() { if !resp.status().is_success() {
return Err(anyhow!("Step {} /hb returned HTTP error: {}", step, resp.status())); return Err(anyhow!(
"Step {} /hb returned HTTP error: {}",
step,
resp.status()
));
} }
let hb_resp: HeartbeatResponse = resp.json()?; let hb_resp: HeartbeatResponse = resp.json()?;
@@ -230,9 +245,15 @@ fn run_valid_progression(client: &reqwest::blocking::Client, base_url: &str) ->
} }
// Verify it was a successful validation (not a silent rejection) // Verify it was a successful validation (not a silent rejection)
let next_salt = hb_resp.next_salt.ok_or_else(|| anyhow!("Step {} was silently rejected", step))?; let next_salt = hb_resp
let next_step = hb_resp.next_mutation_step.ok_or_else(|| anyhow!("Step {} missing next mutation step", step))?; .next_salt
let next_order = hb_resp.next_mutation_order_b64.ok_or_else(|| anyhow!("Step {} missing next mutation order", step))?; .ok_or_else(|| anyhow!("Step {} was silently rejected", step))?;
let next_step = hb_resp
.next_mutation_step
.ok_or_else(|| anyhow!("Step {} missing next mutation step", step))?;
let next_order = hb_resp
.next_mutation_order_b64
.ok_or_else(|| anyhow!("Step {} missing next mutation order", step))?;
println!("Step {} successful. Salt rotated: {}", step, next_salt); println!("Step {} successful. Salt rotated: {}", step, next_salt);
@@ -265,12 +286,21 @@ fn run_stale_replay(client: &reqwest::blocking::Client, base_url: &str) -> Resul
let sk = SigningKey::generate(&mut csprng); let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?; let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?; let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes); let stack_state = shared::vm::execute(&opcodes);
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?; let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap(); let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?; let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step); &gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest { let mut req = HeartbeatRequest {
session_id: init.session_id.clone(), session_id: init.session_id.clone(),
@@ -296,7 +326,9 @@ fn run_stale_replay(client: &reqwest::blocking::Client, base_url: &str) -> Resul
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?; let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
let hb2: HeartbeatResponse = resp2.json()?; let hb2: HeartbeatResponse = resp2.json()?;
if hb2.next_salt.is_some() { if hb2.next_salt.is_some() {
return Err(anyhow!("Replayed heartbeat was successfully accepted (broken replay protection)")); return Err(anyhow!(
"Replayed heartbeat was successfully accepted (broken replay protection)"
));
} }
println!("Stale replay correctly rejected."); println!("Stale replay correctly rejected.");
@@ -308,12 +340,21 @@ fn run_invalid_signature(client: &reqwest::blocking::Client, base_url: &str) ->
let sk = SigningKey::generate(&mut csprng); let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?; let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?; let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes); let stack_state = shared::vm::execute(&opcodes);
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?; let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap(); let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?; let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step); &gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest { let mut req = HeartbeatRequest {
session_id: init.session_id.clone(), session_id: init.session_id.clone(),
@@ -344,10 +385,16 @@ fn run_invalid_vm_stack(client: &reqwest::blocking::Client, base_url: &str) -> R
let sk = SigningKey::generate(&mut csprng); let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?; let init = do_handshake(client, base_url, &sk)?;
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?; let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap(); let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?; let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step); &gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest { let mut req = HeartbeatRequest {
session_id: init.session_id.clone(), session_id: init.session_id.clone(),
@@ -375,12 +422,18 @@ fn run_invalid_vm_stack(client: &reqwest::blocking::Client, base_url: &str) -> R
Ok(()) Ok(())
} }
fn run_invalid_mutation_commitment(client: &reqwest::blocking::Client, base_url: &str) -> Result<()> { fn run_invalid_mutation_commitment(
client: &reqwest::blocking::Client,
base_url: &str,
) -> Result<()> {
let mut csprng = OsRng; let mut csprng = OsRng;
let sk = SigningKey::generate(&mut csprng); let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?; let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?; let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes); let stack_state = shared::vm::execute(&opcodes);
let mut req = HeartbeatRequest { let mut req = HeartbeatRequest {
@@ -411,12 +464,21 @@ fn run_drifted_timestamp(client: &reqwest::blocking::Client, base_url: &str) ->
let sk = SigningKey::generate(&mut csprng); let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?; let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?; let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes); let stack_state = shared::vm::execute(&opcodes);
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?; let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap(); let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?; let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step); &gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest { let mut req = HeartbeatRequest {
session_id: init.session_id.clone(), session_id: init.session_id.clone(),
@@ -446,12 +508,21 @@ fn run_concurrent_heartbeat(client: &reqwest::blocking::Client, base_url: &str)
let sk = SigningKey::generate(&mut csprng); let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?; let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?; let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes); let stack_state = shared::vm::execute(&opcodes);
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?; let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap(); let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?; let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step); &gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest { let mut req = HeartbeatRequest {
session_id: init.session_id.clone(), session_id: init.session_id.clone(),
@@ -470,10 +541,8 @@ fn run_concurrent_heartbeat(client: &reqwest::blocking::Client, base_url: &str)
let client_clone = client.clone(); let client_clone = client.clone();
let req_clone = req.clone(); let req_clone = req.clone();
let url_clone = format!("{}/hb", base_url); let url_clone = format!("{}/hb", base_url);
let handle = std::thread::spawn(move || { let handle = std::thread::spawn(move || client_clone.post(&url_clone).json(&req_clone).send());
client_clone.post(&url_clone).json(&req_clone).send()
});
let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?; let resp2 = client.post(format!("{}/hb", base_url)).json(&req).send()?;
let resp1_res = handle.join().map_err(|_| anyhow!("Thread panicked"))?; let resp1_res = handle.join().map_err(|_| anyhow!("Thread panicked"))?;
@@ -485,7 +554,10 @@ fn run_concurrent_heartbeat(client: &reqwest::blocking::Client, base_url: &str)
// One must succeed and one must fail (silent rejection) because of CAS check // One must succeed and one must fail (silent rejection) because of CAS check
let successes = (hb1.next_salt.is_some() as usize) + (hb2.next_salt.is_some() as usize); let successes = (hb1.next_salt.is_some() as usize) + (hb2.next_salt.is_some() as usize);
if successes != 1 { if successes != 1 {
return Err(anyhow!("Expected exactly one concurrent heartbeat to succeed. Got: {}", successes)); return Err(anyhow!(
"Expected exactly one concurrent heartbeat to succeed. Got: {}",
successes
));
} }
println!("Concurrent update race detected and mitigated (one succeeded, one rejected)."); println!("Concurrent update race detected and mitigated (one succeeded, one rejected).");
@@ -497,12 +569,21 @@ fn run_rate_limit_trigger(client: &reqwest::blocking::Client, base_url: &str) ->
let sk = SigningKey::generate(&mut csprng); let sk = SigningKey::generate(&mut csprng);
let init = do_handshake(client, base_url, &sk)?; let init = do_handshake(client, base_url, &sk)?;
let opcodes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64)?; let opcodes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)?;
let stack_state = shared::vm::execute(&opcodes); let stack_state = shared::vm::execute(&opcodes);
let order = shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?; let order =
shared::vm_extensions::decode_order_b64(init.mutation_step, &init.mutation_order_b64)?;
let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap(); let gene_state = shared::gene::new_state(init.gene_size as usize).unwrap();
let candidate = shared::vm_extensions::apply_program_clone_with_rounds(&gene_state, &order.program, init.mutation_rounds)?; let candidate = shared::vm_extensions::apply_program_clone_with_rounds(
let commitment = shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step); &gene_state,
&order.program,
init.mutation_rounds,
)?;
let commitment =
shared::gene::commitment_hex_with_context(&candidate, &init.session_id, init.mutation_step);
let mut req = HeartbeatRequest { let mut req = HeartbeatRequest {
session_id: init.session_id.clone(), session_id: init.session_id.clone(),
@@ -534,14 +615,20 @@ fn run_rate_limit_trigger(client: &reqwest::blocking::Client, base_url: &str) ->
} }
if !rate_limited { if !rate_limited {
return Err(anyhow!("Rate limiter was not triggered after 35 rapid requests")); return Err(anyhow!(
"Rate limiter was not triggered after 35 rapid requests"
));
} }
println!("Rate limiter correctly triggered."); println!("Rate limiter correctly triggered.");
Ok(()) Ok(())
} }
fn run_file_scenario(_client: &reqwest::blocking::Client, _base_url: &str, file_path: &str) -> Result<()> { fn run_file_scenario(
_client: &reqwest::blocking::Client,
_base_url: &str,
file_path: &str,
) -> Result<()> {
let scenario_content = std::fs::read_to_string(file_path)?; let scenario_content = std::fs::read_to_string(file_path)?;
let scenario: serde_json::Value = serde_json::from_str(&scenario_content)?; let scenario: serde_json::Value = serde_json::from_str(&scenario_content)?;
+1
View File
@@ -27,6 +27,7 @@ RestrictSUIDSGID=yes
LockPersonality=yes LockPersonality=yes
SystemCallArchitectures=native SystemCallArchitectures=native
ReadWritePaths=/run/chronoseal.pid ReadWritePaths=/run/chronoseal.pid
ReadWritePaths=/var/lib/chronoseal
# Logging # Logging
StandardOutput=journal StandardOutput=journal
+1 -1
View File
@@ -161,7 +161,7 @@ Content-Type: application/json
| `stack_state.ip` | number | yes | VM instruction pointer as an unsigned 16-bit value | | `stack_state.ip` | number | yes | VM instruction pointer as an unsigned 16-bit value |
| `fingerprint.aspectRatio` | string | yes | Screen aspect ratio; server accepts numeric strings in range `0.5..=3.0` | | `fingerprint.aspectRatio` | string | yes | Screen aspect ratio; server accepts numeric strings in range `0.5..=3.0` |
| `fingerprint.devicePixelRatio` | string | yes | Device pixel ratio; server accepts numeric strings in range `(0, 5]` | | `fingerprint.devicePixelRatio` | string | yes | Device pixel ratio; server accepts numeric strings in range `(0, 5]` |
| `fingerprint.hardwareConcurrency` | number | yes | Positive hardware concurrency value | | `fingerprint.hardwareConcurrency` | number | yes | Hardware concurrency value; server accepts integers in range `1..=256` |
| `mutation_step` | number | yes | Mutation step currently expected by the server | | `mutation_step` | number | yes | Mutation step currently expected by the server |
| `gene_commitment` | string | yes | Context-bound commitment produced by the WASM mutation preview | | `gene_commitment` | string | yes | Context-bound commitment produced by the WASM mutation preview |
| `signature` | string | yes | Ed25519 signature over the canonical payload | | `signature` | string | yes | Ed25519 signature over the canonical payload |
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" width="296.99997mm" viewBox="0 0 1122.5196 793.7008" enable-background="new 0 0 1254 1254" xml:space="preserve" height="210mm" sodipodi:docname="logo1.svg" inkscape:export-filename="logo1.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96" inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:zoom="1.1654266" inkscape:cx="561.16791" inkscape:cy="396.85039" inkscape:window-width="2048" inkscape:window-height="1205" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="Layer_1"></sodipodi:namedview><defs id="defs44"></defs><path fill="none" opacity="1" stroke="none" d="m 791.89377,632.01366 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23208 63.40558,32.40548 95.07782,48.66778 14.943,7.6726 29.74964,15.6123 44.73053,23.2091 8.44464,4.2823 16.79547,9.1534 25.75525,11.9324 18.81989,5.8372 37.10712,3.9335 54.83185,-5.5578 25.51599,-13.6634 51.41541,-26.6141 77.2135,-39.7465 30.82071,-15.6894 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path2"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89376,550.60089 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 -61.76651,-29.85993 -123.47361,-59.84277 -185.21183,-89.76129 -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77522,34.39209 -143.33662,69.23047 -215.00562,103.84463 -8.4082,4.06091 -14.3217,9.84101 -14.3998,19.74008 -0.079,10.00498 5.5228,16.22797 14.0898,20.54743 12.3277,6.21567 24.5123,12.71484 37.3422,19.41369 7.5143,3.93381 14.4228,7.5896 21.3786,11.15274 31.6877,16.23212 63.4056,32.40552 95.0778,48.66782 14.943,7.67261 29.7496,15.61227 44.73052,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path45" style="fill:#e1e1e4;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,469.18814 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20.46771 -0.14127,-9.82202 -6.25309,-15.51495 -14.66742,-19.5827 C 767.4184,378.3558 705.7113,348.37296 643.97308,318.45444 c -10.48242,-5.07981 -20.81335,-10.5555 -31.58477,-14.93764 -19.71186,-8.01936 -39.87799,-8.40881 -59.18305,0.84148 -71.77527,34.39209 -143.33667,69.23047 -215.00567,103.84463 -8.40815,4.06091 -14.32166,9.84101 -14.39978,19.74008 -0.079,10.00498 5.52282,16.22797 14.08978,20.54743 12.3277,6.21567 24.51239,12.71484 37.34228,19.41369 7.51429,3.93381 14.42273,7.5896 21.37854,11.15274 31.68775,16.23212 63.40558,32.40552 95.07782,48.66782 14.943,7.67261 29.74964,15.61227 44.73053,23.20911 8.44464,4.28232 16.79547,9.15341 25.75525,11.93237 18.81989,5.83722 37.10712,3.93353 54.83185,-5.55777 25.51599,-13.66336 51.41541,-26.61407 77.2135,-39.74655 30.82071,-15.68933 61.7009,-31.26196 92.58359,-46.82907 1.56006,-0.78641 3.38776,-1.0419 5.09082,-1.54462 z" id="path46" style="fill:#b0b2b8;fill-opacity:1"></path><path fill="#4b4d51" opacity="1" stroke="none" d="m 791.89377,387.77539 c 1.04068,-1.03091 1.90903,-2.38956 3.14896,-3.04166 11.48425,-6.03955 22.98721,-12.04745 34.57742,-17.88034 8.52002,-4.28775 14.37558,-10.49713 14.23218,-20Line truncated

After

Width:  |  Height:  |  Size: 8.1 KiB

+21
View File
@@ -0,0 +1,21 @@
{
"name": "MyWebSite",
"short_name": "MySite",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

-3
View File
@@ -77,7 +77,6 @@ async function sendHeartbeat() {
const sig = sign_message(msg); const sig = sign_message(msg);
if (!sig) { if (!sig) {
discard_gene_preview(); discard_gene_preview();
console.error('Keypair not initialised — skipping heartbeat');
return; return;
} }
const resp = await sendRequest('/hb', 'POST', { const resp = await sendRequest('/hb', 'POST', {
@@ -107,11 +106,9 @@ async function sendHeartbeat() {
pendingMutationOrderB64 = resp.next_mutation_order_b64; pendingMutationOrderB64 = resp.next_mutation_order_b64;
} else { } else {
discard_gene_preview(); discard_gene_preview();
console.warn('Heartbeat rejected');
} }
} catch (e) { } catch (e) {
discard_gene_preview(); discard_gene_preview();
console.error(e);
} finally { } finally {
scheduleNext(); scheduleNext();
} }
+1
View File
@@ -2,6 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self'; style-src 'self' 'unsafe-inline'">
<title>Anti-Scraper Demo</title> <title>Anti-Scraper Demo</title>
</head> </head>
<body> <body>
+1 -3
View File
@@ -1,5 +1,3 @@
import { initHeartbeat } from './heartbeat.js'; import { initHeartbeat } from './heartbeat.js';
(async () => { initHeartbeat().catch(() => {});
await initHeartbeat();
})();
+1
View File
@@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
set -euo pipefail
echo "Starting server with static frontend serving..." echo "Starting server with static frontend serving..."
cd ../server cd ../server
cargo run --release cargo run --release
+1
View File
@@ -31,4 +31,5 @@ base64 = "0.22"
rand = "0.8" rand = "0.8"
ed25519-dalek = "2" ed25519-dalek = "2"
redis = { version = "0.29", features = ["r2d2"] } redis = { version = "0.29", features = ["r2d2"] }
dashmap = "6"
+2 -4
View File
@@ -20,12 +20,10 @@ pub async fn cleanup_loop(state: Arc<AppState>) {
} }
} }
// Evict stale rate-limiter entries to prevent unbounded HashMap growth. // Evict stale rate-limiter entries to prevent unbounded map growth.
{ {
let window_secs = state.get_config().rate_limit_window_secs; let window_secs = state.get_config().rate_limit_window_secs;
let mut rl = state.rate_limiter.lock().await; state.rate_limiter.evict_stale(window_secs);
rl.evict_stale(window_secs);
} }
} }
} }
+5 -2
View File
@@ -137,7 +137,9 @@ impl Config {
size: self.gene_size, size: self.gene_size,
}); });
} }
if !(1..=shared::constants::MAX_MUTATION_ROUNDS).contains(&self.mutation_rounds) { if !(shared::constants::MIN_MUTATION_ROUNDS..=shared::constants::MAX_MUTATION_ROUNDS)
.contains(&self.mutation_rounds)
{
return Err(ConfigError::InvalidMutationRounds { return Err(ConfigError::InvalidMutationRounds {
rounds: self.mutation_rounds, rounds: self.mutation_rounds,
}); });
@@ -274,7 +276,8 @@ impl std::fmt::Display for ConfigError {
Self::InvalidMutationRounds { rounds } => { Self::InvalidMutationRounds { rounds } => {
write!( write!(
f, f,
"invalid mutation rounds {rounds}; expected 1..={}", "invalid mutation rounds {rounds}; expected {}..={}",
shared::constants::MIN_MUTATION_ROUNDS,
shared::constants::MAX_MUTATION_ROUNDS shared::constants::MAX_MUTATION_ROUNDS
) )
} }
-1
View File
@@ -44,4 +44,3 @@ pub fn verify_signature(
pk.verify_strict(message.as_bytes(), &sig)?; pk.verify_strict(message.as_bytes(), &sig)?;
Ok(()) Ok(())
} }
+7
View File
@@ -25,12 +25,19 @@ pub enum SessionError {
#[error("Invalid gene configuration: {0}")] #[error("Invalid gene configuration: {0}")]
InvalidGeneConfiguration(String), InvalidGeneConfiguration(String),
#[error("Rate limited")]
RateLimited,
} }
impl IntoResponse for SessionError { impl IntoResponse for SessionError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
let (status, error_message) = match self { let (status, error_message) = match self {
SessionError::InvalidPublicKeyLength => (StatusCode::BAD_REQUEST, self.to_string()), SessionError::InvalidPublicKeyLength => (StatusCode::BAD_REQUEST, self.to_string()),
SessionError::RateLimited => (
StatusCode::TOO_MANY_REQUESTS,
"Too many requests".to_string(),
),
_ => ( _ => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(), "Internal server error".to_string(),
+58 -3
View File
@@ -1,5 +1,10 @@
use shared::protocol::Fingerprint; use shared::protocol::Fingerprint;
const MIN_ASPECT_RATIO: f64 = 0.5;
const MAX_ASPECT_RATIO: f64 = 3.0;
const MAX_DEVICE_PIXEL_RATIO: f64 = 5.0;
const MAX_HARDWARE_CONCURRENCY: u32 = 256;
/// Validates the browser fingerprint fields submitted by the client. /// Validates the browser fingerprint fields submitted by the client.
/// ///
/// Checks basic screen aspect ratio thresholds, device pixel ratio limits, /// Checks basic screen aspect ratio thresholds, device pixel ratio limits,
@@ -9,16 +14,66 @@ use shared::protocol::Fingerprint;
/// * `fp` - The client's hardware and screen layout fingerprint. /// * `fp` - The client's hardware and screen layout fingerprint.
pub fn validate(fp: &Fingerprint) -> Result<(), Box<dyn std::error::Error>> { pub fn validate(fp: &Fingerprint) -> Result<(), Box<dyn std::error::Error>> {
let ar: f64 = fp.aspect_ratio.parse().map_err(|_| "ar")?; let ar: f64 = fp.aspect_ratio.parse().map_err(|_| "ar")?;
if !(0.5..=3.0).contains(&ar) { if !ar.is_finite() || !(MIN_ASPECT_RATIO..=MAX_ASPECT_RATIO).contains(&ar) {
return Err("aspect ratio".into()); return Err("aspect ratio".into());
} }
let dpr: f64 = fp.device_pixel_ratio.parse().map_err(|_| "dpr")?; let dpr: f64 = fp.device_pixel_ratio.parse().map_err(|_| "dpr")?;
if dpr <= 0.0 || dpr > 5.0 { if !dpr.is_finite() || dpr <= 0.0 || dpr > MAX_DEVICE_PIXEL_RATIO {
return Err("dpr".into()); return Err("dpr".into());
} }
if fp.hardware_concurrency == 0 {
if fp.hardware_concurrency == 0 || fp.hardware_concurrency > MAX_HARDWARE_CONCURRENCY {
return Err("hw".into()); return Err("hw".into());
} }
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
fn fingerprint(
aspect_ratio: impl Into<String>,
device_pixel_ratio: impl Into<String>,
hardware_concurrency: u32,
) -> Fingerprint {
Fingerprint {
aspect_ratio: aspect_ratio.into(),
device_pixel_ratio: device_pixel_ratio.into(),
hardware_concurrency,
}
}
#[test]
fn accepts_valid_fingerprint() {
assert!(validate(&fingerprint("1.7777777778", "2", 8)).is_ok());
}
#[test]
fn accepts_boundary_values() {
assert!(validate(&fingerprint("0.5", "1", 1)).is_ok());
assert!(validate(&fingerprint("3.0", "5.0", MAX_HARDWARE_CONCURRENCY)).is_ok());
}
#[test]
fn rejects_invalid_aspect_ratios() {
for aspect_ratio in ["not-a-number", "NaN", "inf", "0.49", "3.01"] {
assert!(validate(&fingerprint(aspect_ratio, "2", 8)).is_err());
}
}
#[test]
fn rejects_invalid_device_pixel_ratios() {
for device_pixel_ratio in ["not-a-number", "NaN", "inf", "0", "-1", "5.01"] {
assert!(validate(&fingerprint("1.77", device_pixel_ratio, 8)).is_err());
}
}
#[test]
fn rejects_invalid_hardware_concurrency() {
assert!(validate(&fingerprint("1.77", "2", 0)).is_err());
assert!(validate(&fingerprint("1.77", "2", MAX_HARDWARE_CONCURRENCY + 1)).is_err());
}
}
-3
View File
@@ -30,9 +30,6 @@ async fn main() {
async fn try_main() -> Result<(), Box<dyn std::error::Error>> { async fn try_main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse(); let cli = Cli::parse();
if let Some(config_path) = cli.globals.config.as_deref() {
std::env::set_var("CHRONOSEAL_CONFIG", config_path);
}
let log_filter = cli.globals.log.as_deref().unwrap_or("info"); let log_filter = cli.globals.log.as_deref().unwrap_or("info");
let log_file = log_file_for_command(&cli); let log_file = log_file_for_command(&cli);
let _log_guard = init_logging(log_filter, log_file)?; let _log_guard = init_logging(log_filter, log_file)?;
+22
View File
@@ -9,3 +9,25 @@ pub async fn log_request(req: Request, next: Next) -> Response {
tracing::info!("{} {} -> {}", method, uri, response.status()); tracing::info!("{} {} -> {}", method, uri, response.status());
response response
} }
/// Injects defensive HTTP response headers on every response.
///
/// These headers mitigate several classes of attacks:
/// - `X-Content-Type-Options: nosniff` — prevents MIME-type sniffing.
/// - `X-Frame-Options: DENY` — blocks clickjacking via framing.
/// - `Referrer-Policy: no-referrer` — suppresses referrer leakage.
/// - `X-XSS-Protection: 0` — disables legacy XSS auditors (can introduce bugs).
/// - `Permissions-Policy` — restricts powerful browser features.
pub async fn security_headers(req: Request, next: Next) -> Response {
let mut response = next.run(req).await;
let headers = response.headers_mut();
headers.insert("x-content-type-options", "nosniff".parse().unwrap());
headers.insert("x-frame-options", "DENY".parse().unwrap());
headers.insert("referrer-policy", "no-referrer".parse().unwrap());
headers.insert("x-xss-protection", "0".parse().unwrap());
headers.insert(
"permissions-policy",
"camera=(), microphone=(), geolocation=()".parse().unwrap(),
);
response
}
+20 -15
View File
@@ -1,17 +1,20 @@
use std::collections::HashMap; use dashmap::DashMap;
use std::time::Instant; use std::time::Instant;
/// A simple, in-memory sliding-window rate limiter for tracking client heartbeat frequency. /// A lock-free, concurrent sliding-window rate limiter backed by `DashMap`.
///
/// All public methods take `&self` (no `&mut self`), so the limiter can live in
/// an `Arc<AppState>` without a `Mutex` wrapper.
pub struct RateLimiter { pub struct RateLimiter {
/// Maps session identifiers to request counts and window start timestamps. /// Maps rate-limit keys to request counts and window start timestamps.
buckets: HashMap<String, (u32, Instant)>, buckets: DashMap<String, (u32, Instant)>,
} }
impl RateLimiter { impl RateLimiter {
/// Creates a new, empty `RateLimiter`. /// Creates a new, empty `RateLimiter`.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
buckets: HashMap::new(), buckets: DashMap::new(),
} }
} }
@@ -20,19 +23,21 @@ impl RateLimiter {
/// Returns `true` if allowed, or `false` if the rate limit is exceeded. /// Returns `true` if allowed, or `false` if the rate limit is exceeded.
/// ///
/// # Arguments /// # Arguments
/// * `key` - The unique identifier to rate-limit (e.g., session ID). /// * `key` - The unique identifier to rate-limit (e.g., client IP address).
/// * `limit` - The maximum number of allowed requests per window. /// * `limit` - The maximum number of allowed requests per window.
/// * `window_secs` - The length of the sliding-window in seconds. /// * `window_secs` - The length of the sliding-window in seconds.
pub fn check(&mut self, key: &str, limit: u32, window_secs: u64) -> bool { pub fn check(&self, key: &str, limit: u32, window_secs: u64) -> bool {
let now = Instant::now(); let now = Instant::now();
let entry = self.buckets.entry(key.to_string()).or_insert((0, now)); let mut entry = self.buckets.entry(key.to_string()).or_insert((0, now));
if now.duration_since(entry.1).as_secs() >= window_secs { let (count, ts) = entry.value_mut();
*entry = (1, now); if now.duration_since(*ts).as_secs() >= window_secs {
*count = 1;
*ts = now;
true true
} else if entry.0 >= limit { } else if *count >= limit {
false false
} else { } else {
entry.0 += 1; *count += 1;
true true
} }
} }
@@ -43,7 +48,7 @@ impl RateLimiter {
/// ///
/// # Arguments /// # Arguments
/// * `window_secs` - The active rate-limiting window duration in seconds. /// * `window_secs` - The active rate-limiting window duration in seconds.
pub fn evict_stale(&mut self, window_secs: u64) { pub fn evict_stale(&self, window_secs: u64) {
let now = Instant::now(); let now = Instant::now();
self.buckets self.buckets
.retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < window_secs); .retain(|_, (_, ts)| now.duration_since(*ts).as_secs() < window_secs);
@@ -58,7 +63,7 @@ mod tests {
#[test] #[test]
fn test_rate_limiter() { fn test_rate_limiter() {
let mut rl = RateLimiter::new(); let rl = RateLimiter::new();
// Limit of 2 requests per 1 second window // Limit of 2 requests per 1 second window
assert!(rl.check("user1", 2, 1)); assert!(rl.check("user1", 2, 1));
assert!(rl.check("user1", 2, 1)); assert!(rl.check("user1", 2, 1));
@@ -72,7 +77,7 @@ mod tests {
#[test] #[test]
fn test_rate_limiter_eviction() { fn test_rate_limiter_eviction() {
let mut rl = RateLimiter::new(); let rl = RateLimiter::new();
assert!(rl.check("user1", 1, 1)); assert!(rl.check("user1", 1, 1));
assert_eq!(rl.buckets.len(), 1); assert_eq!(rl.buckets.len(), 1);
+64 -16
View File
@@ -8,21 +8,51 @@ pub async fn handler(
Json(payload): Json<HeartbeatRequest>, Json(payload): Json<HeartbeatRequest>,
) -> (StatusCode, Json<HeartbeatResponse>) { ) -> (StatusCode, Json<HeartbeatResponse>) {
let start_http = std::time::Instant::now(); let start_http = std::time::Instant::now();
state.heartbeats_total.fetch_add(1, std::sync::atomic::Ordering::Relaxed); state
.heartbeats_total
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
// Rate limiting // Cap entropy events to prevent oversized payloads from exhausting memory.
if payload.entropy_data.events.len() > 1000 {
let http_dur = start_http.elapsed().as_nanos() as u64;
state
.http_latency_ns
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
state
.http_ops_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return (
StatusCode::OK,
Json(HeartbeatResponse {
status: "ok".into(),
next_salt: None,
next_mutation_step: None,
next_mutation_order_b64: None,
}),
);
}
// Rate limiting (lock-free via DashMap)
{ {
let (limit, window_secs) = { let (limit, window_secs) = {
let cfg = state.get_config(); let cfg = state.get_config();
(cfg.rate_limit_count, cfg.rate_limit_window_secs) (cfg.rate_limit_count, cfg.rate_limit_window_secs)
}; };
let mut rl = state.rate_limiter.lock().await; if !state
if !rl.check(&payload.session_id, limit, window_secs) { .rate_limiter
.check(&payload.session_id, limit, window_secs)
{
tracing::debug!("Rate limit hit: {}", payload.session_id); tracing::debug!("Rate limit hit: {}", payload.session_id);
state.verification_failures_total.fetch_add(1, std::sync::atomic::Ordering::Relaxed); state
.verification_failures_total
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let http_dur = start_http.elapsed().as_nanos() as u64; let http_dur = start_http.elapsed().as_nanos() as u64;
state.http_latency_ns.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed); state
state.http_ops_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); .http_latency_ns
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
state
.http_ops_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return ( return (
StatusCode::OK, StatusCode::OK,
Json(HeartbeatResponse { Json(HeartbeatResponse {
@@ -39,8 +69,12 @@ pub async fn handler(
let start_db = std::time::Instant::now(); let start_db = std::time::Instant::now();
let db_res = crate::session::verify_heartbeat(&state.db_pool, &config, &payload); let db_res = crate::session::verify_heartbeat(&state.db_pool, &config, &payload);
let db_dur = start_db.elapsed().as_nanos() as u64; let db_dur = start_db.elapsed().as_nanos() as u64;
state.storage_latency_ns.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed); state
state.storage_ops_count.fetch_add(2, std::sync::atomic::Ordering::Relaxed); // read + write .storage_latency_ns
.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed);
state
.storage_ops_count
.fetch_add(2, std::sync::atomic::Ordering::Relaxed); // read + write
let outcome = match db_res { let outcome = match db_res {
Ok(result) => ( Ok(result) => (
@@ -54,15 +88,21 @@ pub async fn handler(
), ),
Err(e) => { Err(e) => {
tracing::warn!("Heartbeat failed for {}: {}", payload.session_id, e); tracing::warn!("Heartbeat failed for {}: {}", payload.session_id, e);
state.verification_failures_total.fetch_add(1, std::sync::atomic::Ordering::Relaxed); state
.verification_failures_total
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
match &e { match &e {
crate::errors::VerificationError::ChainBroken => { crate::errors::VerificationError::ChainBroken => {
state.replay_attempts_total.fetch_add(1, std::sync::atomic::Ordering::Relaxed); state
.replay_attempts_total
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
} }
crate::errors::VerificationError::MutationCommitmentMismatch crate::errors::VerificationError::MutationCommitmentMismatch
| crate::errors::VerificationError::MutationProgram(_) | crate::errors::VerificationError::MutationProgram(_)
| crate::errors::VerificationError::GeneState(_) => { | crate::errors::VerificationError::GeneState(_) => {
state.mutation_failures_total.fetch_add(1, std::sync::atomic::Ordering::Relaxed); state
.mutation_failures_total
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
} }
_ => {} _ => {}
} }
@@ -79,8 +119,12 @@ pub async fn handler(
}; };
let http_dur = start_http.elapsed().as_nanos() as u64; let http_dur = start_http.elapsed().as_nanos() as u64;
state.http_latency_ns.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed); state
state.http_ops_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); .http_latency_ns
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
state
.http_ops_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
outcome outcome
} }
@@ -138,7 +182,11 @@ mod tests {
}, },
], ],
}; };
let program_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &init.opcodes_b64).unwrap(); let program_bytes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&init.opcodes_b64,
)
.unwrap();
let stack_state = shared::vm::execute(&program_bytes); let stack_state = shared::vm::execute(&program_bytes);
let order = let order =
@@ -176,7 +224,7 @@ mod tests {
let pool = crate::storage::init_pool(Path::new(":memory:")).unwrap(); let pool = crate::storage::init_pool(Path::new(":memory:")).unwrap();
let state = Arc::new(AppState { let state = Arc::new(AppState {
db_pool: pool.clone(), db_pool: pool.clone(),
rate_limiter: tokio::sync::Mutex::new(crate::ratelimit::RateLimiter::new()), rate_limiter: crate::ratelimit::RateLimiter::new(),
config: std::sync::RwLock::new(config.clone()), config: std::sync::RwLock::new(config.clone()),
heartbeats_total: std::sync::atomic::AtomicU64::new(0), heartbeats_total: std::sync::atomic::AtomicU64::new(0),
verification_failures_total: std::sync::atomic::AtomicU64::new(0), verification_failures_total: std::sync::atomic::AtomicU64::new(0),
+22 -5
View File
@@ -10,18 +10,35 @@ pub async fn handler(
) -> Result<Json<InitResponse>, SessionError> { ) -> Result<Json<InitResponse>, SessionError> {
let start_http = std::time::Instant::now(); let start_http = std::time::Instant::now();
let config = state.get_config(); let config = state.get_config();
// Rate limit session creation by public key to prevent storage exhaustion.
if !state.rate_limiter.check(
&payload.public_key,
config.rate_limit_count,
config.rate_limit_window_secs,
) {
return Err(SessionError::RateLimited);
}
let start_db = std::time::Instant::now(); let start_db = std::time::Instant::now();
let resp = crate::session::create_session(&state.db_pool, &config, &payload.public_key); let resp = crate::session::create_session(&state.db_pool, &config, &payload.public_key);
let db_dur = start_db.elapsed().as_nanos() as u64; let db_dur = start_db.elapsed().as_nanos() as u64;
state.storage_latency_ns.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed); state
state.storage_ops_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); .storage_latency_ns
.fetch_add(db_dur, std::sync::atomic::Ordering::Relaxed);
state
.storage_ops_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let resp = resp?; let resp = resp?;
let http_dur = start_http.elapsed().as_nanos() as u64; let http_dur = start_http.elapsed().as_nanos() as u64;
state.http_latency_ns.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed); state
state.http_ops_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); .http_latency_ns
.fetch_add(http_dur, std::sync::atomic::Ordering::Relaxed);
state
.http_ops_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(Json(resp)) Ok(Json(resp))
} }
+55 -15
View File
@@ -5,17 +5,19 @@ use crate::{
routes, session, routes, session,
storage::{self, StoreStats}, storage::{self, StoreStats},
}; };
use axum::{http::StatusCode, response::IntoResponse, routing::get, Json, Router}; use axum::{
extract::ConnectInfo, http::StatusCode, response::IntoResponse, routing::get, Json, Router,
};
use serde::Serialize; use serde::Serialize;
use std::{ use std::{
fs, fs,
io::{Read, Write}, io::{Read, Write},
net::{SocketAddr, TcpStream}, net::{IpAddr, SocketAddr, TcpStream},
path::Path, path::Path,
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
use tokio::sync::{Mutex, Notify}; use tokio::sync::Notify;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@@ -144,7 +146,7 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
let db_pool = init_db_pool(&config)?; let db_pool = init_db_pool(&config)?;
let state = Arc::new(session::AppState { let state = Arc::new(session::AppState {
db_pool, db_pool,
rate_limiter: Mutex::new(RateLimiter::new()), rate_limiter: RateLimiter::new(),
config: std::sync::RwLock::new(config.clone()), config: std::sync::RwLock::new(config.clone()),
heartbeats_total: std::sync::atomic::AtomicU64::new(0), heartbeats_total: std::sync::atomic::AtomicU64::new(0),
verification_failures_total: std::sync::atomic::AtomicU64::new(0), verification_failures_total: std::sync::atomic::AtomicU64::new(0),
@@ -170,7 +172,11 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
tower_http::services::ServeDir::new(&config.frontend_dir), tower_http::services::ServeDir::new(&config.frontend_dir),
) )
.layer(tower_http::cors::CorsLayer::permissive()) .layer(tower_http::cors::CorsLayer::permissive())
.layer(axum::middleware::from_fn(
crate::middleware::security_headers,
))
.layer(axum::middleware::from_fn(crate::middleware::log_request)) .layer(axum::middleware::from_fn(crate::middleware::log_request))
.layer(axum::extract::DefaultBodyLimit::max(64 * 1024)) // 64 KiB
.with_state(state.clone()); .with_state(state.clone());
let addr: SocketAddr = config.bind.parse()?; let addr: SocketAddr = config.bind.parse()?;
@@ -178,9 +184,12 @@ pub async fn run_daemon(config: Config) -> Result<(), Box<dyn std::error::Error>
info!(bind = %config.bind, "chronoseal daemon started"); info!(bind = %config.bind, "chronoseal daemon started");
let shutdown = signal_task(state.clone()); let shutdown = signal_task(state.clone());
let result = axum::serve(listener, app) let result = axum::serve(
.with_graceful_shutdown(shutdown) listener,
.await; app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown)
.await;
remove_pid_file(&config.pid_file); remove_pid_file(&config.pid_file);
result?; result?;
@@ -277,8 +286,12 @@ async fn health_handler() -> impl IntoResponse {
} }
async fn stats_handler( async fn stats_handler(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>, axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
) -> Result<Json<StoreStats>, (StatusCode, String)> { ) -> Result<Json<StoreStats>, (StatusCode, String)> {
if !is_loopback(addr.ip()) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
state state
.db_pool .db_pool
.stats() .stats()
@@ -287,25 +300,45 @@ async fn stats_handler(
} }
async fn metrics_handler( async fn metrics_handler(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
axum::extract::State(state): axum::extract::State<Arc<session::AppState>>, axum::extract::State(state): axum::extract::State<Arc<session::AppState>>,
) -> Result<String, (StatusCode, String)> { ) -> Result<String, (StatusCode, String)> {
if !is_loopback(addr.ip()) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let stats = state let stats = state
.db_pool .db_pool
.stats() .stats()
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?; .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
let heartbeats = state.heartbeats_total.load(std::sync::atomic::Ordering::Relaxed); let heartbeats = state
let ver_failures = state.verification_failures_total.load(std::sync::atomic::Ordering::Relaxed); .heartbeats_total
let mut_failures = state.mutation_failures_total.load(std::sync::atomic::Ordering::Relaxed); .load(std::sync::atomic::Ordering::Relaxed);
let replays = state.replay_attempts_total.load(std::sync::atomic::Ordering::Relaxed); let ver_failures = state
.verification_failures_total
.load(std::sync::atomic::Ordering::Relaxed);
let mut_failures = state
.mutation_failures_total
.load(std::sync::atomic::Ordering::Relaxed);
let replays = state
.replay_attempts_total
.load(std::sync::atomic::Ordering::Relaxed);
let store_ns = state.storage_latency_ns.load(std::sync::atomic::Ordering::Relaxed) as f64; let store_ns = state
.storage_latency_ns
.load(std::sync::atomic::Ordering::Relaxed) as f64;
let store_sum = store_ns / 1_000_000_000.0; let store_sum = store_ns / 1_000_000_000.0;
let store_count = state.storage_ops_count.load(std::sync::atomic::Ordering::Relaxed); let store_count = state
.storage_ops_count
.load(std::sync::atomic::Ordering::Relaxed);
let http_ns = state.http_latency_ns.load(std::sync::atomic::Ordering::Relaxed) as f64; let http_ns = state
.http_latency_ns
.load(std::sync::atomic::Ordering::Relaxed) as f64;
let http_sum = http_ns / 1_000_000_000.0; let http_sum = http_ns / 1_000_000_000.0;
let http_count = state.http_ops_count.load(std::sync::atomic::Ordering::Relaxed); let http_count = state
.http_ops_count
.load(std::sync::atomic::Ordering::Relaxed);
Ok(format!( Ok(format!(
"# HELP chronoseal_active_sessions Active ChronoSeal sessions\n\ "# HELP chronoseal_active_sessions Active ChronoSeal sessions\n\
@@ -355,6 +388,13 @@ async fn metrics_handler(
)) ))
} }
fn is_loopback(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => v4.is_loopback(),
IpAddr::V6(v6) => v6.is_loopback(),
}
}
async fn signal_task(state: Arc<session::AppState>) { async fn signal_task(state: Arc<session::AppState>) {
let shutdown = Arc::new(Notify::new()); let shutdown = Arc::new(Notify::new());
+7 -4
View File
@@ -1,6 +1,6 @@
pub struct AppState { pub struct AppState {
pub db_pool: crate::storage::DbPool, pub db_pool: crate::storage::DbPool,
pub rate_limiter: tokio::sync::Mutex<crate::ratelimit::RateLimiter>, pub rate_limiter: crate::ratelimit::RateLimiter,
pub config: std::sync::RwLock<crate::config::Config>, pub config: std::sync::RwLock<crate::config::Config>,
pub heartbeats_total: std::sync::atomic::AtomicU64, pub heartbeats_total: std::sync::atomic::AtomicU64,
pub verification_failures_total: std::sync::atomic::AtomicU64, pub verification_failures_total: std::sync::atomic::AtomicU64,
@@ -271,7 +271,6 @@ mod tests {
} }
} }
fn test_fingerprint() -> Fingerprint { fn test_fingerprint() -> Fingerprint {
Fingerprint { Fingerprint {
aspect_ratio: "1.77".to_string(), aspect_ratio: "1.77".to_string(),
@@ -323,8 +322,12 @@ mod tests {
vm_extensions::apply_program_clone(&client.committed_gene_state, &order.program) vm_extensions::apply_program_clone(&client.committed_gene_state, &order.program)
.unwrap(); .unwrap();
let entropy = test_entropy(); let entropy = test_entropy();
let program_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &client.opcodes_b64).unwrap(); let program_bytes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&client.opcodes_b64,
)
.unwrap();
let stack = shared::vm::execute(&program_bytes); let stack = shared::vm::execute(&program_bytes);
let mut req = HeartbeatRequest { let mut req = HeartbeatRequest {
+54 -24
View File
@@ -1,8 +1,8 @@
use crate::config::Config; use crate::config::Config;
use redis::Commands;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path; use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use redis::Commands;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoreStats { pub struct StoreStats {
@@ -54,11 +54,12 @@ impl DbPool {
crate::config::DbType::Valkey => { crate::config::DbType::Valkey => {
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR") let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
.unwrap_or_else(|_| "127.0.0.1:6666".to_string()); .unwrap_or_else(|_| "127.0.0.1:6666".to_string());
let connection_string = if addr.starts_with("redis://") || addr.starts_with("rediss://") { let connection_string =
addr.clone() if addr.starts_with("redis://") || addr.starts_with("rediss://") {
} else { addr.clone()
format!("redis://{}", addr) } else {
}; format!("redis://{}", addr)
};
let client = redis::Client::open(connection_string)?; let client = redis::Client::open(connection_string)?;
let pool = r2d2::Pool::builder().build(client)?; let pool = r2d2::Pool::builder().build(client)?;
Ok(DbPool::Valkey(ValkeyStore { Ok(DbPool::Valkey(ValkeyStore {
@@ -354,9 +355,19 @@ impl ValkeyStore {
redis::pipe() redis::pipe()
.atomic() .atomic()
.cmd("SET").arg(&key).arg(&value).arg("EX").arg(ttl_seconds) .cmd("SET")
.cmd("ZADD").arg(&self.index_key).arg(record.expires_at).arg(&record.session_id) .arg(&key)
.cmd("ZADD").arg("sessions:chain_lengths").arg(record.chain_length).arg(&record.session_id) .arg(&value)
.arg("EX")
.arg(ttl_seconds)
.cmd("ZADD")
.arg(&self.index_key)
.arg(record.expires_at)
.arg(&record.session_id)
.cmd("ZADD")
.arg("sessions:chain_lengths")
.arg(record.chain_length)
.arg(&record.session_id)
.query::<()>(&mut *conn)?; .query::<()>(&mut *conn)?;
Ok(()) Ok(())
} }
@@ -400,9 +411,19 @@ impl ValkeyStore {
let response: Option<()> = redis::pipe() let response: Option<()> = redis::pipe()
.atomic() .atomic()
.cmd("SET").arg(&key).arg(&value).arg("EX").arg(ttl_seconds) .cmd("SET")
.cmd("ZADD").arg(&self.index_key).arg(record.expires_at).arg(&record.session_id) .arg(&key)
.cmd("ZADD").arg("sessions:chain_lengths").arg(record.chain_length).arg(&record.session_id) .arg(&value)
.arg("EX")
.arg(ttl_seconds)
.cmd("ZADD")
.arg(&self.index_key)
.arg(record.expires_at)
.arg(&record.session_id)
.cmd("ZADD")
.arg("sessions:chain_lengths")
.arg(record.chain_length)
.arg(&record.session_id)
.query(&mut *conn)?; .query(&mut *conn)?;
match response { match response {
@@ -422,8 +443,12 @@ impl ValkeyStore {
if !expired_ids.is_empty() { if !expired_ids.is_empty() {
redis::pipe() redis::pipe()
.atomic() .atomic()
.cmd("ZREM").arg(&self.index_key).arg(&expired_ids) .cmd("ZREM")
.cmd("ZREM").arg("sessions:chain_lengths").arg(&expired_ids) .arg(&self.index_key)
.arg(&expired_ids)
.cmd("ZREM")
.arg("sessions:chain_lengths")
.arg(&expired_ids)
.query::<()>(&mut *conn)?; .query::<()>(&mut *conn)?;
} }
Ok(()) Ok(())
@@ -435,8 +460,12 @@ impl ValkeyStore {
let sessions: u64 = conn.zcard(&self.index_key)?; let sessions: u64 = conn.zcard(&self.index_key)?;
let expired_sessions: u64 = conn.zcount(&self.index_key, 0, now)?; let expired_sessions: u64 = conn.zcount(&self.index_key, 0, now)?;
let max_chain_length_res: Vec<(String, u64)> = conn.zrevrange_withscores("sessions:chain_lengths", 0, 0)?; let max_chain_length_res: Vec<(String, u64)> =
let max_chain_length = max_chain_length_res.first().map(|(_, score)| *score).unwrap_or(0); conn.zrevrange_withscores("sessions:chain_lengths", 0, 0)?;
let max_chain_length = max_chain_length_res
.first()
.map(|(_, score)| *score)
.unwrap_or(0);
Ok(StoreStats { Ok(StoreStats {
sessions, sessions,
@@ -449,7 +478,7 @@ impl ValkeyStore {
pub fn current_time_ms() -> u64 { pub fn current_time_ms() -> u64 {
SystemTime::now() SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .expect("system clock is before UNIX epoch; check system time")
.as_millis() as u64 .as_millis() as u64
} }
@@ -459,7 +488,8 @@ mod valkey_tests {
#[test] #[test]
fn test_valkey_store_operations() { fn test_valkey_store_operations() {
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR").unwrap_or_else(|_| "127.0.0.1:6379".to_string()); let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
.unwrap_or_else(|_| "127.0.0.1:6379".to_string());
let connection_string = format!("redis://{}", addr); let connection_string = format!("redis://{}", addr);
let client = match redis::Client::open(connection_string) { let client = match redis::Client::open(connection_string) {
Ok(c) => c, Ok(c) => c,
@@ -522,10 +552,11 @@ mod valkey_tests {
#[test] #[test]
fn test_valkey_pool_concurrency() { fn test_valkey_pool_concurrency() {
use std::thread;
use std::sync::Arc; use std::sync::Arc;
use std::thread;
let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR").unwrap_or_else(|_| "127.0.0.1:6379".to_string()); let addr = std::env::var("CHRONOSEAL_VALKEY_ADDR")
.unwrap_or_else(|_| "127.0.0.1:6379".to_string());
let connection_string = format!("redis://{}", addr); let connection_string = format!("redis://{}", addr);
let client = match redis::Client::open(connection_string) { let client = match redis::Client::open(connection_string) {
Ok(c) => c, Ok(c) => c,
@@ -589,7 +620,8 @@ mod valkey_tests {
// Cleanup // Cleanup
let mut conn = store_arc.pool.get().unwrap(); let mut conn = store_arc.pool.get().unwrap();
for t in 0..10 { for t in 0..10 {
let _: Result<(), _> = conn.del(store_arc.session_key(&format!("valkey_concurrent_{}", t))); let _: Result<(), _> =
conn.del(store_arc.session_key(&format!("valkey_concurrent_{}", t)));
} }
let _: Result<(), _> = conn.del(&store_arc.index_key); let _: Result<(), _> = conn.del(&store_arc.index_key);
} }
@@ -598,8 +630,8 @@ mod valkey_tests {
#[cfg(test)] #[cfg(test)]
mod sqlite_tests { mod sqlite_tests {
use super::*; use super::*;
use std::thread;
use std::sync::Arc; use std::sync::Arc;
use std::thread;
#[test] #[test]
fn test_sqlite_pool_concurrency() { fn test_sqlite_pool_concurrency() {
@@ -655,5 +687,3 @@ mod sqlite_tests {
let _ = std::fs::remove_file(db_path); let _ = std::fs::remove_file(db_path);
} }
} }
-1
View File
@@ -49,7 +49,6 @@ pub fn validate_mouse(
Ok(()) Ok(())
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
-1
View File
@@ -80,7 +80,6 @@ pub fn execute_mutation_order(
vm_extensions::execute_program(state, &order.program) vm_extensions::execute_program(state, &order.program)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+1
View File
@@ -10,3 +10,4 @@ pub const MAX_MUTATION_ROUNDS: u8 = 10;
pub const MAX_MUTATION_INSTRUCTION_BUDGET: usize = 2048; pub const MAX_MUTATION_INSTRUCTION_BUDGET: usize = 2048;
pub const HASH_OPCODE_INSTRUCTION_COST: usize = 16; pub const HASH_OPCODE_INSTRUCTION_COST: usize = 16;
pub const SOFT_CAP_DURATION_MS: u128 = 50; pub const SOFT_CAP_DURATION_MS: u128 = 50;
pub const MAX_STACK_DEPTH: usize = 64;
+2 -3
View File
@@ -37,8 +37,8 @@ pub fn next_chain_hash(
stack: &StackState, stack: &StackState,
salt: &[u8], salt: &[u8],
) -> Vec<u8> { ) -> Vec<u8> {
let entropy_bytes = serde_json::to_vec(entropy).unwrap(); let entropy_bytes = serde_json::to_vec(entropy).unwrap_or_default();
let stack_bytes = serde_json::to_vec(stack).unwrap(); let stack_bytes = serde_json::to_vec(stack).unwrap_or_default();
let entropy_hash = blake3::hash(&entropy_bytes); let entropy_hash = blake3::hash(&entropy_bytes);
let stack_hash = blake3::hash(&stack_bytes); let stack_hash = blake3::hash(&stack_bytes);
@@ -63,4 +63,3 @@ pub fn hash_stack(stack: &[u32]) -> u32 {
let hash = blake3::hash(&data); let hash = blake3::hash(&data);
u32::from_le_bytes(hash.as_bytes()[..4].try_into().unwrap()) u32::from_le_bytes(hash.as_bytes()[..4].try_into().unwrap())
} }
-1
View File
@@ -116,4 +116,3 @@ pub struct StackState {
/// The final instruction pointer location at program completion or termination. /// The final instruction pointer location at program completion or termination.
pub ip: u16, pub ip: u16,
} }
+12
View File
@@ -25,6 +25,9 @@ pub fn execute(program: &[u8]) -> StackState {
program[ip + 3], program[ip + 3],
]); ]);
ip += 4; ip += 4;
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
break;
}
stack.push(val); stack.push(val);
} }
0x01..=0x07 => { 0x01..=0x07 => {
@@ -43,6 +46,9 @@ pub fn execute(program: &[u8]) -> StackState {
0x07 => a.rotate_left(b % 32), 0x07 => a.rotate_left(b % 32),
_ => unreachable!(), _ => unreachable!(),
}; };
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
break;
}
stack.push(r); stack.push(r);
} }
0x08 => { 0x08 => {
@@ -50,11 +56,17 @@ pub fn execute(program: &[u8]) -> StackState {
break; break;
} }
let a = stack.pop().unwrap(); let a = stack.pop().unwrap();
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
break;
}
stack.push(!a); stack.push(!a);
} }
0x09 => { 0x09 => {
let r = crate::hashing::hash_stack(&stack); let r = crate::hashing::hash_stack(&stack);
stack.clear(); stack.clear();
if stack.len() >= crate::constants::MAX_STACK_DEPTH {
break;
}
stack.push(r); stack.push(r);
} }
_ => break, _ => break,
-1
View File
@@ -269,7 +269,6 @@ pub fn apply_program(state: &mut GeneState, program: &[u8]) -> Result<(), Mutati
Ok(()) Ok(())
} }
/// Executes the mutation program on the mutable `GeneState` reference for multiple rounds. /// Executes the mutation program on the mutable `GeneState` reference for multiple rounds.
/// ///
/// Implements a soft instruction cost-budget cap check to prevent hostile/inefficient /// Implements a soft instruction cost-budget cap check to prevent hostile/inefficient
+8 -2
View File
@@ -51,8 +51,14 @@ pub fn compute_next_hash(
) -> String { ) -> String {
let prev = hex::decode(prev_hash_hex).unwrap_or_default(); let prev = hex::decode(prev_hash_hex).unwrap_or_default();
let salt = hex::decode(salt_hex).unwrap_or_default(); let salt = hex::decode(salt_hex).unwrap_or_default();
let entropy = serde_json::from_str::<shared::protocol::EntropyData>(entropy_data_json).unwrap(); let entropy = match serde_json::from_str::<shared::protocol::EntropyData>(entropy_data_json) {
let stack = serde_json::from_str::<shared::protocol::StackState>(stack_state_json).unwrap(); Ok(v) => v,
Err(_) => return String::new(),
};
let stack = match serde_json::from_str::<shared::protocol::StackState>(stack_state_json) {
Ok(v) => v,
Err(_) => return String::new(),
};
let new = shared::hashing::next_chain_hash(&prev, timestamp, &entropy, &stack, &salt); let new = shared::hashing::next_chain_hash(&prev, timestamp, &entropy, &stack, &salt);
hex::encode(new) hex::encode(new)
} }
+8 -4
View File
@@ -4,11 +4,15 @@ use wasm_bindgen::prelude::*;
#[wasm_bindgen] #[wasm_bindgen]
pub fn run_program(program_b64: &str) -> JsValue { pub fn run_program(program_b64: &str) -> JsValue {
use base64::Engine; use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD let bytes = match base64::engine::general_purpose::STANDARD.decode(program_b64) {
.decode(program_b64) Ok(v) => v,
.unwrap(); Err(_) => return JsValue::NULL,
};
let state = execute(&bytes); let state = execute(&bytes);
serde_wasm_bindgen::to_value(&state).unwrap() match serde_wasm_bindgen::to_value(&state) {
Ok(v) => v,
Err(_) => JsValue::NULL,
}
} }
fn execute(program: &[u8]) -> StackState { fn execute(program: &[u8]) -> StackState {