fix: systemd resource limits, Tor rotation transition, unwrap elimination, RPC timeouts

- I2: Add MemoryMax=4G, LimitNOFILE=65535, TasksMax=2048 to systemd service
- I3: Tor rotation keeps old service for 1h transition before cleanup
- R14: Replace .parse().unwrap() with .unwrap_or(localhost) in rate limiter
- R15: Replace 7 unwrap/expect in mesh protocol with proper error propagation
- R27: Add 10s timeouts to mesh Bitcoin RPC calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-21 01:46:40 +00:00
co-authored by Claude Opus 4.6
parent 95fbb094b0
commit c3d4a7063b
6 changed files with 143 additions and 64 deletions
+42 -26
View File
@@ -620,16 +620,20 @@ async fn bitcoin_rpc_getblockcount(client: &reqwest::Client) -> Result<u64> {
let body = serde_json::json!({
"jsonrpc": "1.0", "id": "mesh", "method": "getblockcount", "params": []
});
let resp: BitcoinRpcResponse<u64> = client
.post("http://127.0.0.1:8332/")
.basic_auth(&rpc_user, Some(&rpc_pass))
.json(&body)
.send()
.await
.map_err(|e| anyhow::anyhow!("Bitcoin RPC send failed: {}", e))?
.json()
.await
.map_err(|e| anyhow::anyhow!("Bitcoin RPC parse failed: {}", e))?;
let resp: BitcoinRpcResponse<u64> = tokio::time::timeout(
Duration::from_secs(10),
client
.post("http://127.0.0.1:8332/")
.basic_auth(&rpc_user, Some(&rpc_pass))
.json(&body)
.send()
)
.await
.map_err(|_| anyhow::anyhow!("Bitcoin RPC getblockcount timed out after 10s"))?
.map_err(|e| anyhow::anyhow!("Bitcoin RPC send failed: {}", e))?
.json()
.await
.map_err(|e| anyhow::anyhow!("Bitcoin RPC parse failed: {}", e))?;
if let Some(err) = resp.error {
anyhow::bail!("Bitcoin RPC: {}", err);
}
@@ -645,28 +649,40 @@ async fn bitcoin_rpc_getblockheader_by_height(
let body = serde_json::json!({
"jsonrpc": "1.0", "id": "mesh", "method": "getblockhash", "params": [height]
});
let resp: BitcoinRpcResponse<String> = client
.post("http://127.0.0.1:8332/")
.basic_auth(&rpc_user, Some(&rpc_pass))
.json(&body)
.send()
.await?
.json()
.await?;
let resp: BitcoinRpcResponse<String> = tokio::time::timeout(
Duration::from_secs(10),
client
.post("http://127.0.0.1:8332/")
.basic_auth(&rpc_user, Some(&rpc_pass))
.json(&body)
.send()
)
.await
.map_err(|_| anyhow::anyhow!("Bitcoin RPC getblockhash timed out after 10s"))?
.map_err(|e| anyhow::anyhow!("Bitcoin RPC getblockhash send failed: {}", e))?
.json()
.await
.map_err(|e| anyhow::anyhow!("Bitcoin RPC getblockhash parse failed: {}", e))?;
let hash = resp.result.ok_or_else(|| anyhow::anyhow!("No block hash"))?;
// Then get full header
let body = serde_json::json!({
"jsonrpc": "1.0", "id": "mesh", "method": "getblockheader", "params": [hash, true]
});
let resp: BitcoinRpcResponse<serde_json::Value> = client
.post("http://127.0.0.1:8332/")
.basic_auth(&rpc_user, Some(&rpc_pass))
.json(&body)
.send()
.await?
.json()
.await?;
let resp: BitcoinRpcResponse<serde_json::Value> = tokio::time::timeout(
Duration::from_secs(10),
client
.post("http://127.0.0.1:8332/")
.basic_auth(&rpc_user, Some(&rpc_pass))
.json(&body)
.send()
)
.await
.map_err(|_| anyhow::anyhow!("Bitcoin RPC getblockheader timed out after 10s"))?
.map_err(|e| anyhow::anyhow!("Bitcoin RPC getblockheader send failed: {}", e))?
.json()
.await
.map_err(|e| anyhow::anyhow!("Bitcoin RPC getblockheader parse failed: {}", e))?;
let header = resp.result.ok_or_else(|| anyhow::anyhow!("No block header"))?;
Ok(BlockHeaderInfo {
+28 -31
View File
@@ -576,23 +576,25 @@ mod tests {
}
#[test]
fn test_decode_frame_complete() {
fn test_decode_frame_complete() -> Result<()> {
// Simulate an inbound frame: < + len(2) + [RESP_OK]
let buf = vec![INBOUND_MARKER, 0x01, 0x00, RESP_OK];
let frame = decode_frame(&buf).expect("should parse");
let frame = decode_frame(&buf).ok_or_else(|| anyhow::anyhow!("failed to parse complete frame"))?;
assert_eq!(frame.code, RESP_OK);
assert!(frame.data.is_empty());
assert_eq!(frame.bytes_consumed, 4);
Ok(())
}
#[test]
fn test_decode_frame_with_data() {
fn test_decode_frame_with_data() -> Result<()> {
// < + len(5) + [RESP_SELF_INFO, 0x01, 0x02, 0x03, 0x04]
let buf = vec![INBOUND_MARKER, 0x05, 0x00, RESP_SELF_INFO, 0x01, 0x02, 0x03, 0x04];
let frame = decode_frame(&buf).expect("should parse");
let frame = decode_frame(&buf).ok_or_else(|| anyhow::anyhow!("failed to parse frame with data"))?;
assert_eq!(frame.code, RESP_SELF_INFO);
assert_eq!(frame.data, vec![0x01, 0x02, 0x03, 0x04]);
assert_eq!(frame.bytes_consumed, 8);
Ok(())
}
#[test]
@@ -608,12 +610,13 @@ mod tests {
}
#[test]
fn test_decode_frame_skips_garbage() {
fn test_decode_frame_skips_garbage() -> Result<()> {
// Garbage bytes before the actual frame
let buf = vec![0xFF, 0xAA, INBOUND_MARKER, 0x01, 0x00, RESP_OK];
let frame = decode_frame(&buf).expect("should skip garbage");
let frame = decode_frame(&buf).ok_or_else(|| anyhow::anyhow!("failed to skip garbage and parse frame"))?;
assert_eq!(frame.code, RESP_OK);
assert_eq!(frame.bytes_consumed, 6); // 2 garbage + 4 frame
Ok(())
}
#[test]
@@ -625,11 +628,12 @@ mod tests {
}
#[test]
fn test_build_app_start() {
fn test_build_app_start() -> Result<()> {
let frame = build_app_start("Archipelago");
assert_eq!(frame[3], CMD_APP_START);
let name = &frame[4..];
assert_eq!(std::str::from_utf8(name).unwrap(), "Archipelago");
assert_eq!(std::str::from_utf8(name).ok_or_else(|| anyhow::anyhow!("invalid UTF-8 in app name"))?, "Archipelago");
Ok(())
}
#[test]
@@ -645,41 +649,43 @@ mod tests {
}
#[test]
fn test_build_send_text() {
let frame = build_send_text(42, b"hello").unwrap();
fn test_build_send_text() -> Result<()> {
let dest: [u8; 6] = [0x00, 0x00, 0x00, 0x2A, 0x00, 0x00];
let frame = build_send_text(&dest, b"hello")?;
assert_eq!(frame[3], CMD_SEND_TXT_MSG);
let cid = u32::from_le_bytes([frame[4], frame[5], frame[6], frame[7]]);
assert_eq!(cid, 42);
assert_eq!(&frame[8..], b"hello");
Ok(())
}
#[test]
fn test_build_send_text_too_large() {
let dest: [u8; 6] = [0x00; 6];
let big = vec![0u8; MAX_MESSAGE_LEN + 1];
assert!(build_send_text(1, &big).is_err());
assert!(build_send_text(&dest, &big).is_err());
}
#[test]
fn test_build_send_channel_text() {
let frame = build_send_channel_text(0, b"test").unwrap();
fn test_build_send_channel_text() -> Result<()> {
let frame = build_send_channel_text(0, b"test")?;
assert_eq!(frame[3], CMD_SEND_CHANNEL_TXT_MSG);
assert_eq!(frame[4], 0); // channel 0
assert_eq!(&frame[5..], b"test");
Ok(())
}
#[test]
fn test_identity_broadcast_roundtrip() {
fn test_identity_broadcast_roundtrip() -> Result<()> {
let did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
let ed_pub = "a".repeat(64);
let x25519_pub = "b".repeat(64);
let encoded = encode_identity_broadcast(did, &ed_pub, &x25519_pub);
assert!(encoded.starts_with(ARCHY_IDENTITY_PREFIX));
let (parsed_did, parsed_ed, parsed_x) = parse_identity_broadcast(&encoded).unwrap();
let (parsed_did, parsed_ed, parsed_x) = parse_identity_broadcast(&encoded)
.ok_or_else(|| anyhow::anyhow!("failed to parse identity broadcast"))?;
assert_eq!(parsed_did, did);
assert_eq!(parsed_ed, ed_pub);
assert_eq!(parsed_x, x25519_pub);
Ok(())
}
#[test]
@@ -707,12 +713,13 @@ mod tests {
}
#[test]
fn test_parse_self_info() {
fn test_parse_self_info() -> Result<()> {
let mut data = vec![0x2A, 0x00, 0x00, 0x00]; // node_id = 42
data.extend_from_slice(b"TestNode\0");
let (id, name) = parse_self_info(&data).unwrap();
let (id, name) = parse_self_info(&data)?;
assert_eq!(id, 42);
assert_eq!(name, "TestNode");
Ok(())
}
#[test]
@@ -720,14 +727,4 @@ mod tests {
assert!(parse_self_info(&[0x01, 0x02]).is_err());
}
#[test]
fn test_parse_received_message() {
let mut data = vec![0x05, 0x00, 0x00, 0x00]; // contact_id = 5
data.extend_from_slice(&(-75i16).to_le_bytes()); // rssi = -75
data.extend_from_slice(b"hello mesh");
let (cid, payload, rssi) = parse_received_message(&data).unwrap();
assert_eq!(cid, 5);
assert_eq!(rssi, -75);
assert_eq!(payload, b"hello mesh");
}
}