feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch
- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation) - Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation) - Compact block header announcements (88 bytes, fits 160-byte LoRa limit) - Block header announcer: internet nodes auto-announce new blocks to Archy peers - TX relay: mesh-only nodes can broadcast transactions via internet-connected peers - Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back - Dead man's switch background task with configurable interval and signed alert broadcast - 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin - lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay) - Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling - Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons - TX/Lightning relay sends only to Archy peers (not broadcast to all devices) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4b7c765cd1
commit
d1ac098edb
@@ -165,27 +165,46 @@ impl Default for RelayTracker {
|
||||
|
||||
// ─── Block Header Announcement Builder ──────────────────────────────────
|
||||
|
||||
/// Build a signed block header announcement for mesh broadcast.
|
||||
/// Build a compact block header announcement for mesh broadcast.
|
||||
/// Uses raw binary (not CBOR) to fit within the 160-byte LoRa limit:
|
||||
/// height(8 LE) + hash_raw(32) + timestamp(4 LE) = 44 bytes payload
|
||||
/// Wrapped in unsigned TypedEnvelope (~25 bytes overhead) = ~69 total.
|
||||
pub fn build_block_header_announcement(
|
||||
height: u64,
|
||||
hash: &str,
|
||||
prev_hash: &str,
|
||||
_prev_hash: &str,
|
||||
timestamp: u32,
|
||||
our_did: &str,
|
||||
signing_key: &ed25519_dalek::SigningKey,
|
||||
_our_did: &str,
|
||||
_signing_key: &ed25519_dalek::SigningKey,
|
||||
) -> Result<Vec<u8>> {
|
||||
let header = BlockHeaderPayload {
|
||||
height,
|
||||
hash: hash.to_string(),
|
||||
prev_hash: prev_hash.to_string(),
|
||||
timestamp,
|
||||
announced_by: our_did.to_string(),
|
||||
};
|
||||
let payload = message_types::encode_payload(&header)?;
|
||||
let envelope = TypedEnvelope::new_signed(MeshMessageType::BlockHeader, payload, signing_key);
|
||||
let hash_bytes = hex::decode(hash).context("Invalid block hash hex")?;
|
||||
if hash_bytes.len() != 32 {
|
||||
anyhow::bail!("Block hash must be 32 bytes, got {}", hash_bytes.len());
|
||||
}
|
||||
|
||||
// Compact binary: height(8) + hash(32) + timestamp(4) = 44 bytes
|
||||
let mut payload = Vec::with_capacity(44);
|
||||
payload.extend_from_slice(&height.to_le_bytes());
|
||||
payload.extend_from_slice(&hash_bytes);
|
||||
payload.extend_from_slice(×tamp.to_le_bytes());
|
||||
|
||||
// Use unsigned envelope to save 64 bytes (no Ed25519 signature)
|
||||
let envelope = TypedEnvelope::new(MeshMessageType::BlockHeader, payload);
|
||||
envelope.to_wire()
|
||||
}
|
||||
|
||||
/// Decode a compact block header from raw binary payload.
|
||||
/// Returns (height, hash_hex, timestamp).
|
||||
pub fn decode_compact_block_header(payload: &[u8]) -> Result<(u64, String, u32)> {
|
||||
if payload.len() < 44 {
|
||||
anyhow::bail!("Compact block header too short: {} bytes", payload.len());
|
||||
}
|
||||
let height = u64::from_le_bytes(payload[0..8].try_into().unwrap());
|
||||
let hash_hex = hex::encode(&payload[8..40]);
|
||||
let timestamp = u32::from_le_bytes(payload[40..44].try_into().unwrap());
|
||||
Ok((height, hash_hex, timestamp))
|
||||
}
|
||||
|
||||
/// Build a TX relay request envelope.
|
||||
pub fn build_tx_relay_request(tx_hex: &str, request_id: u64) -> Result<Vec<u8>> {
|
||||
let payload = message_types::encode_payload(&TxRelayPayload {
|
||||
|
||||
Reference in New Issue
Block a user