feat: Phase 4 — mesh authentication, envelope signature verification, TX validation

- Identity announcements: verify Ed25519 key validity and X25519 consistency
- Envelope signatures: verify Ed25519 signatures on signed messages, drop invalid
- Block header validation: height range, hash length, timestamp sanity checks
- TX relay validation: hex validity, size bounds, version check before broadcast
- Rate limiter struct for per-peer relay operations
- Message sequence number field (seq) added to TypedEnvelope for ordering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-18 00:49:38 +00:00
co-authored by Claude Opus 4.6
parent dd8e8e9e4f
commit 5853b6a065
4 changed files with 179 additions and 6 deletions
@@ -302,6 +302,101 @@ pub fn build_lightning_relay_response(
envelope.to_wire()
}
// ─── Validation Functions ─────────────────────────────────────────────
/// Validate a received block header before storing/relaying.
/// Rejects obviously invalid headers (bad version, impossibly far-ahead height).
pub fn validate_block_header(height: u64, hash_hex: &str, timestamp: u32, last_known_height: u64) -> bool {
// Hash must be 64 hex chars (32 bytes)
if hash_hex.len() != 64 {
warn!("Block header rejected: hash length {} != 64", hash_hex.len());
return false;
}
// Height must not be impossibly far ahead (allow 100 blocks gap for mesh delays)
if last_known_height > 0 && height > last_known_height + 100 {
warn!(
"Block header height {} is too far ahead of known height {}",
height, last_known_height
);
return false;
}
// Timestamp sanity: must not be before Bitcoin genesis (2009-01-03) or far in the future
if timestamp < 1_231_006_505 {
warn!("Block header rejected: timestamp {} before Bitcoin genesis", timestamp);
return false;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
if timestamp > now + 7200 {
warn!("Block header rejected: timestamp {} is more than 2 hours in the future", timestamp);
return false;
}
true
}
/// Validate a raw transaction hex string before relaying to Bitcoin Core.
/// Checks basic syntax constraints only (full validation is done by Bitcoin Core).
pub fn validate_raw_transaction(tx_hex: &str) -> bool {
// Must be valid hex
let tx_bytes = match hex::decode(tx_hex) {
Ok(b) => b,
Err(_) => {
warn!("TX relay rejected: invalid hex");
return false;
}
};
// Minimum valid transaction size is ~60 bytes, max 400KB
if tx_bytes.len() < 60 || tx_bytes.len() > 400_000 {
warn!("TX relay rejected: size {} out of range [60, 400000]", tx_bytes.len());
return false;
}
// Check version bytes (first 4 bytes, little-endian) — valid versions: 1, 2, 3
if tx_bytes.len() >= 4 {
let version = u32::from_le_bytes([tx_bytes[0], tx_bytes[1], tx_bytes[2], tx_bytes[3]]);
if version < 1 || version > 3 {
warn!("TX relay rejected: version {} not in [1,3]", version);
return false;
}
}
true
}
/// Simple per-peer rate limiter for mesh relay operations.
pub struct RelayRateLimiter {
/// (peer_id, message_type) -> list of timestamps
windows: RwLock<HashMap<(u32, &'static str), Vec<std::time::Instant>>>,
}
impl RelayRateLimiter {
pub fn new() -> Self {
Self {
windows: RwLock::new(HashMap::new()),
}
}
/// Check if a relay operation is allowed. Returns true if within rate limits.
/// max_per_minute: maximum operations per 60-second window.
pub async fn check(&self, peer_id: u32, msg_type: &'static str, max_per_minute: usize) -> bool {
let now = std::time::Instant::now();
let cutoff = now - std::time::Duration::from_secs(60);
let mut windows = self.windows.write().await;
let key = (peer_id, msg_type);
let timestamps = windows.entry(key).or_insert_with(Vec::new);
// Remove entries older than 60 seconds
timestamps.retain(|t| *t > cutoff);
if timestamps.len() >= max_per_minute {
warn!(peer_id, msg_type, "Rate limit exceeded: {} in last minute", timestamps.len());
return false;
}
timestamps.push(now);
true
}
}
#[cfg(test)]
mod tests {
use super::*;