blvm-node 0.1.2

Bitcoin Commons BLVM: Minimal Bitcoin node implementation using blvm-protocol and blvm-consensus
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
//! Protocol Extensions for UTXO Commitments
//!
//! Extends Bitcoin P2P protocol with UTXO commitment messages:
//! - GetUTXOSet: Request UTXO set at specific height
//! - UTXOSet: Response with UTXO commitment
//! - GetUTXOProof: Request Merkle proof for a specific UTXO
//! - UTXOProof: Response with Merkle proof
//! - GetFilteredBlock: Request filtered (spam-free) block
//! - FilteredBlock: Response with filtered transactions

use crate::network::protocol::*;
use crate::network::txhash::calculate_txid;
use crate::node::mempool::MempoolManager;
use crate::storage::Storage;
use crate::utils::option_to_result;
use anyhow::Result;
#[cfg(feature = "utxo-commitments")]
use blvm_protocol::spam_filter::SpamFilter;
#[cfg(feature = "utxo-commitments")]
use blvm_protocol::types::{OutPoint, UTXO};
#[cfg(feature = "utxo-commitments")]
use blvm_protocol::utxo_commitments::merkle_tree::UtxoMerkleTree;
use hex;
use std::sync::Arc;

/// Handle GetUTXOSet message
///
/// Responds with UTXO commitment at the requested height.
/// 1. Load UTXO set at requested height from storage
/// 2. Build Merkle tree from UTXO set
/// 3. Generate commitment from Merkle tree
/// 4. Return UTXOSet response
pub async fn handle_get_utxo_set(
    message: GetUTXOSetMessage,
    storage: Option<Arc<Storage>>,
) -> Result<UTXOSetMessage> {
    let storage = match storage {
        Some(s) => s,
        None => {
            // Storage is required for UTXO commitments
            return Err(anyhow::anyhow!(
                "Storage not available: UTXO commitments require storage to be initialized"
            ));
        }
    };

    // Get UTXO set from storage
    let utxo_set = storage.utxos().get_all_utxos()?;
    let utxo_count = utxo_set.len() as u64;

    // Get block hash and height
    let block_height = message.height;
    let block_hash = if block_height == 0 || message.block_hash == [0; 32] {
        // Use current tip if not specified
        storage.chain().get_tip_hash()?.unwrap_or([0; 32])
    } else {
        message.block_hash
    };

    // Build Merkle tree from UTXO set
    #[cfg(feature = "utxo-commitments")]
    let mut utxo_tree = UtxoMerkleTree::new()
        .map_err(|e| anyhow::anyhow!("Failed to create UTXO Merkle tree: {:?}", e))?;

    #[cfg(feature = "utxo-commitments")]
    for (outpoint, utxo) in &utxo_set {
        utxo_tree
            .insert(*outpoint, utxo.as_ref().clone())
            .map_err(|e| anyhow::anyhow!("Failed to insert UTXO into tree: {:?}", e))?;
    }

    // Generate commitment
    #[cfg(feature = "utxo-commitments")]
    let commitment = utxo_tree.generate_commitment(block_hash, block_height);

    #[cfg(not(feature = "utxo-commitments"))]
    let commitment = {
        // Calculate total supply (only needed when utxo-commitments feature is disabled)
        let total_supply: u64 = utxo_set.values().map(|utxo| utxo.value as u64).sum();
        crate::network::protocol::UTXOCommitment {
            merkle_root: [0; 32],
            total_supply,
            utxo_count,
            block_height,
            block_hash,
        }
    };

    // Generate request_id (use hash of message as ID since GetUTXOSetMessage doesn't have one)
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};
    let mut hasher = DefaultHasher::new();
    message.height.hash(&mut hasher);
    message.block_hash.hash(&mut hasher);
    let request_id = hasher.finish();

    Ok(UTXOSetMessage {
        request_id, // Generate ID from message content
        commitment: UTXOCommitment {
            merkle_root: commitment.merkle_root,
            total_supply: commitment.total_supply,
            utxo_count: commitment.utxo_count,
            block_height: commitment.block_height,
            block_hash: commitment.block_hash,
        },
        utxo_count,
        is_complete: true,
        chunk_id: None,
    })
}

/// Handle GetFilteredBlock message
///
/// Returns a block with spam transactions filtered out.
/// Optionally includes BIP158 compact block filter if requested.
/// 1. Load block at requested hash from block store
/// 2. Apply spam filter based on preferences
/// 3. Generate UTXO commitment for filtered block
/// 4. Generate BIP158 filter if requested
/// 5. Return filtered transactions with commitment and optional filter
pub async fn handle_get_filtered_block(
    message: GetFilteredBlockMessage,
    storage: Option<Arc<Storage>>,
    filter_service: Option<&crate::network::filter_service::BlockFilterService>,
) -> Result<FilteredBlockMessage> {
    let request_id = message.request_id; // Store for response

    // Get block from storage
    let (block, block_height) = if let Some(ref storage) = storage {
        // Get block by hash
        let block_opt = storage.blocks().get_block(&message.block_hash)?;
        let block = option_to_result(
            block_opt,
            &format!(
                "Block not found: block hash {} not in storage",
                hex::encode(message.block_hash)
            ),
        )
        .map_err(|e| anyhow::anyhow!("Failed to get block from storage: {}", e))?;
        // Get block height from chain state
        let height = storage.chain().get_height()?.unwrap_or(0);
        // Try to find exact height by iterating backwards from tip
        // For now, use tip height as approximation
        (block, height)
    } else {
        // Storage is required for filtered blocks
        return Err(anyhow::anyhow!(
            "Storage not available: filtered blocks require storage to be initialized"
        ));
    };

    // Create spam filter from preferences (use Default for new fields, override from message)
    #[cfg(feature = "utxo-commitments")]
    let spam_filter_config = {
        let mut config = blvm_protocol::spam_filter::SpamFilterConfig::default();
        config.filter_ordinals = message.filter_preferences.filter_ordinals;
        config.filter_dust = message.filter_preferences.filter_dust;
        config.filter_brc20 = message.filter_preferences.filter_brc20;
        config.dust_threshold = message.filter_preferences.min_output_value as i64;
        config.min_output_value = message.filter_preferences.min_output_value as i64;
        config
    };
    #[cfg(feature = "utxo-commitments")]
    let spam_filter = SpamFilter::with_config(spam_filter_config);
    #[cfg(feature = "utxo-commitments")]
    let (filtered_txs, spam_summary_from_filter) = spam_filter.filter_block(&block.transactions);
    #[cfg(not(feature = "utxo-commitments"))]
    let (filtered_txs, spam_summary_from_filter): (
        Vec<blvm_protocol::Transaction>,
        crate::network::protocol::SpamSummary,
    ) = (
        block.transactions.to_vec(),
        crate::network::protocol::SpamSummary {
            filtered_count: 0,
            filtered_size: 0,
            by_type: crate::network::protocol::SpamBreakdown {
                ordinals: 0,
                inscriptions: 0,
                dust: 0,
                brc20: 0,
            },
        },
    );

    // Convert spam summary to protocol types
    let spam_summary = SpamSummary {
        filtered_count: spam_summary_from_filter.filtered_count,
        filtered_size: spam_summary_from_filter.filtered_size,
        by_type: SpamBreakdown {
            ordinals: spam_summary_from_filter.by_type.ordinals,
            inscriptions: spam_summary_from_filter.by_type.inscriptions,
            dust: spam_summary_from_filter.by_type.dust,
            brc20: spam_summary_from_filter.by_type.brc20,
        },
    };

    // Generate transaction indices (positions of filtered transactions in original block)
    let mut transaction_indices = Vec::new();
    let filtered_txids: std::collections::HashSet<_> =
        filtered_txs.iter().map(calculate_txid).collect();
    for (original_idx, tx) in block.transactions.iter().enumerate() {
        let txid = calculate_txid(tx);
        if filtered_txids.contains(&txid) {
            transaction_indices.push(original_idx as u32);
        }
    }

    // Build UTXO tree from filtered transactions to generate commitment
    #[cfg(feature = "utxo-commitments")]
    let mut utxo_tree = UtxoMerkleTree::new()
        .map_err(|e| anyhow::anyhow!("Failed to create UTXO Merkle tree: {:?}", e))?;

    #[cfg(feature = "utxo-commitments")]
    // Add outputs from filtered transactions
    for (tx_idx, tx) in filtered_txs.iter().enumerate() {
        let txid = calculate_txid(tx);
        let is_coinbase_tx = transaction_indices.get(tx_idx) == Some(&0);
        for (output_idx, output) in tx.outputs.iter().enumerate() {
            use blvm_protocol::OutPoint;
            let outpoint = OutPoint {
                hash: txid,
                index: output_idx as u32,
            };
            use blvm_protocol::UTXO;
            let utxo = UTXO {
                value: output.value,
                script_pubkey: output.script_pubkey.as_slice().into(),
                height: block_height, // Use the block height from the message
                is_coinbase: is_coinbase_tx,
            };
            if let Err(e) = utxo_tree.insert(outpoint, utxo) {
                // Log error but continue
                tracing::warn!("Failed to insert UTXO into tree: {:?}", e);
            }
        }
    }

    // Generate commitment for filtered block
    #[cfg(feature = "utxo-commitments")]
    let commitment = utxo_tree.generate_commitment(message.block_hash, block_height);

    #[cfg(not(feature = "utxo-commitments"))]
    let commitment = crate::network::protocol::UTXOCommitment {
        merkle_root: [0; 32],
        total_supply: 0,
        utxo_count: 0,
        block_height,
        block_hash: message.block_hash,
    };

    // Generate BIP158 filter if requested and service available
    let bip158_filter = if message.include_bip158_filter {
        filter_service.and({
            // BIP158 filter support requires BlockFilterService integration
            // This is intentionally not implemented as filter service integration
            // is handled at a higher level in the node architecture
            None
        })
    } else {
        None
    };

    Ok(FilteredBlockMessage {
        request_id, // Echo request_id for matching
        header: block.header.clone(),
        commitment: UTXOCommitment {
            merkle_root: commitment.merkle_root,
            total_supply: commitment.total_supply,
            utxo_count: commitment.utxo_count,
            block_height: commitment.block_height,
            block_hash: commitment.block_hash,
        },
        transactions: filtered_txs,
        transaction_indices,
        spam_summary,
        bip158_filter,
    })
}

/// Serialize GetUTXOSet message to protocol format
pub fn serialize_get_utxo_set(message: &GetUTXOSetMessage) -> Result<Vec<u8>> {
    use crate::network::protocol::ProtocolParser;
    ProtocolParser::serialize_message(&ProtocolMessage::GetUTXOSet(message.clone()))
}

/// Deserialize UTXOSet message from protocol format
pub fn deserialize_utxo_set(data: &[u8]) -> Result<UTXOSetMessage> {
    use crate::network::protocol::ProtocolParser;
    match ProtocolParser::parse_message(data)? {
        ProtocolMessage::UTXOSet(msg) => Ok(msg),
        _ => Err(anyhow::anyhow!("Expected UTXOSet message")),
    }
}

/// Handle GetUTXOProof message
///
/// Responds with Merkle proof for the requested UTXO.
/// 1. Load UTXO set from storage
/// 2. Build Merkle tree from UTXO set
/// 3. Generate proof for requested outpoint
/// 4. Return UTXOProof response
#[cfg(feature = "utxo-commitments")]
pub async fn handle_get_utxo_proof(
    message: crate::network::protocol::GetUTXOProofMessage,
    storage: Option<Arc<Storage>>,
) -> Result<crate::network::protocol::UTXOProofMessage> {
    let storage = match storage {
        Some(s) => s,
        None => {
            return Err(anyhow::anyhow!(
                "Storage not available: UTXO proof generation requires storage"
            ));
        }
    };

    // Get UTXO set from storage
    let utxo_set = storage.utxos().get_all_utxos()?;

    // Build Merkle tree from UTXO set
    let mut utxo_tree = UtxoMerkleTree::new()
        .map_err(|e| anyhow::anyhow!("Failed to create UTXO Merkle tree: {:?}", e))?;

    for (outpoint, utxo) in &utxo_set {
        utxo_tree
            .insert(*outpoint, utxo.as_ref().clone())
            .map_err(|e| anyhow::anyhow!("Failed to insert UTXO into tree: {:?}", e))?;
    }

    // Create OutPoint from message
    use blvm_protocol::types::OutPoint;
    let outpoint = OutPoint {
        hash: message.tx_hash,
        index: message.output_index,
    };

    // Find UTXO in set
    let utxo = utxo_set
        .get(&outpoint)
        .ok_or_else(|| anyhow::anyhow!("UTXO not found for outpoint"))?;

    // Generate proof
    let proof = utxo_tree
        .generate_proof(&outpoint)
        .map_err(|e| anyhow::anyhow!("Failed to generate proof: {:?}", e))?;

    // Serialize proof to bytes (use custom format to avoid serde version conflicts)
    let proof_bytes = UtxoMerkleTree::serialize_proof_for_wire(proof)
        .map_err(|e| anyhow::anyhow!("Failed to serialize proof: {:?}", e))?;

    Ok(crate::network::protocol::UTXOProofMessage {
        request_id: message.request_id,
        tx_hash: message.tx_hash,
        output_index: message.output_index,
        value: utxo.value,
        script_pubkey: utxo.script_pubkey.as_ref().into(),
        height: utxo.height,
        is_coinbase: utxo.is_coinbase,
        proof: proof_bytes,
    })
}

/// Serialize GetUTXOProof message to protocol format
pub fn serialize_get_utxo_proof(
    message: &crate::network::protocol::GetUTXOProofMessage,
) -> Result<Vec<u8>> {
    use crate::network::protocol::ProtocolParser;
    ProtocolParser::serialize_message(&ProtocolMessage::GetUTXOProof(message.clone()))
}

/// Deserialize UTXOProof message from protocol format
pub fn deserialize_utxo_proof(data: &[u8]) -> Result<crate::network::protocol::UTXOProofMessage> {
    use crate::network::protocol::ProtocolParser;
    match ProtocolParser::parse_message(data)? {
        ProtocolMessage::UTXOProof(msg) => Ok(msg),
        _ => Err(anyhow::anyhow!("Expected UTXOProof message")),
    }
}

// Erlay (BIP330) protocol message handlers

/// Handle SendTxRcncl message (Erlay negotiation)
///
/// Responds to Erlay capability announcement and negotiates parameters.
#[cfg(feature = "erlay")]
pub async fn handle_send_tx_rcncl(
    message: crate::network::protocol::SendTxRcnclMessage,
    _storage: Option<Arc<Storage>>,
) -> Result<()> {
    // Store Erlay parameters for this peer
    // In a real implementation, this would be stored in peer state
    debug!(
        "Received Erlay negotiation: version={}, min_field={}, max_field={}",
        message.version, message.min_field_size, message.max_field_size
    );

    // Negotiate field size (use minimum of both peers' max)
    // For now, just acknowledge
    Ok(())
}

/// Handle ReqRecon message (Erlay reconciliation request)
///
/// Initiates transaction set reconciliation with a peer.
#[cfg(feature = "erlay")]
pub async fn handle_req_recon(
    message: crate::network::protocol::ReqReconMessage,
    storage: Option<Arc<Storage>>,
    mempool: Option<Arc<MempoolManager>>,
) -> Result<crate::network::protocol::ReqSktMessage> {
    use crate::network::erlay::ErlayTxSet;

    // Get local transaction set from mempool
    let mut tx_set = ErlayTxSet::new();

    if let Some(mempool_mgr) = mempool {
        // Get all transactions from mempool and add their hashes to the set
        let transactions = mempool_mgr.get_transactions();
        for tx in transactions {
            let tx_hash = calculate_txid(&tx);
            tx_set.add(tx_hash);
        }
        debug!(
            "Erlay: Populated tx_set with {} transactions from mempool",
            tx_set.size()
        );
    } else {
        warn!("MempoolManager not available for Erlay reconciliation, using empty set");
    }

    // Create reconciliation sketch
    let local_sketch = tx_set
        .create_reconciliation_sketch(message.local_set_size as usize)
        .map_err(|e| anyhow::anyhow!("Failed to create reconciliation sketch: {}", e))?;

    // Return ReqSkt message with our sketch
    Ok(crate::network::protocol::ReqSktMessage {
        salt: message.salt,
        remote_set_size: tx_set.size() as u32,
        field_size: message.field_size,
    })
}

/// Handle ReqSkt message (Erlay sketch request)
///
/// Responds with reconciliation sketch.
#[cfg(feature = "erlay")]
pub async fn handle_req_skt(
    message: crate::network::protocol::ReqSktMessage,
    storage: Option<Arc<Storage>>,
    mempool: Option<Arc<MempoolManager>>,
) -> Result<crate::network::protocol::SketchMessage> {
    use crate::network::erlay::ErlayTxSet;

    // Get local transaction set from mempool
    let mut tx_set = ErlayTxSet::new();

    if let Some(mempool_mgr) = mempool {
        // Get all transactions from mempool and add their hashes to the set
        let transactions = mempool_mgr.get_transactions();
        for tx in transactions {
            let tx_hash = calculate_txid(&tx);
            tx_set.add(tx_hash);
        }
        debug!(
            "Erlay: Populated tx_set with {} transactions for sketch",
            tx_set.size()
        );
    } else {
        warn!("MempoolManager not available for Erlay sketch, using empty set");
    }

    // Create sketch
    let sketch = tx_set
        .create_reconciliation_sketch(message.remote_set_size as usize)
        .map_err(|e| anyhow::anyhow!("Failed to create sketch: {}", e))?;

    Ok(crate::network::protocol::SketchMessage {
        salt: message.salt,
        sketch,
        field_size: message.field_size,
    })
}

/// Handle Sketch message (Erlay reconciliation sketch)
///
/// Processes reconciliation sketch and identifies missing transactions.
#[cfg(feature = "erlay")]
pub async fn handle_sketch(
    message: crate::network::protocol::SketchMessage,
    storage: Option<Arc<Storage>>,
    mempool: Option<Arc<MempoolManager>>,
) -> Result<Vec<blvm_protocol::Hash>> {
    use crate::network::erlay::ErlayTxSet;

    // Get local transaction set from mempool
    let mut tx_set = ErlayTxSet::new();

    if let Some(mempool_mgr) = mempool {
        // Get all transactions from mempool and add their hashes to the set
        let transactions = mempool_mgr.get_transactions();
        for tx in transactions {
            let tx_hash = calculate_txid(&tx);
            tx_set.add(tx_hash);
        }
        debug!(
            "Erlay: Populated tx_set with {} transactions for reconciliation",
            tx_set.size()
        );
    } else {
        warn!("MempoolManager not available for Erlay reconciliation, using empty set");
    }

    // Create our local sketch
    let local_sketch = tx_set
        .create_reconciliation_sketch(0)
        .map_err(|e| anyhow::anyhow!("Failed to create local sketch: {}", e))?;

    // Reconcile sets
    let missing_txs = tx_set
        .reconcile_with_peer(&local_sketch, &message.sketch)
        .map_err(|e| anyhow::anyhow!("Failed to reconcile sets: {}", e))?;

    debug!(
        "Erlay: Reconciliation found {} missing transactions",
        missing_txs.len()
    );
    Ok(missing_txs)
}

/// Serialize GetFilteredBlock message to protocol format
pub fn serialize_get_filtered_block(message: &GetFilteredBlockMessage) -> Result<Vec<u8>> {
    use crate::network::protocol::ProtocolParser;
    ProtocolParser::serialize_message(&ProtocolMessage::GetFilteredBlock(message.clone()))
}

/// Deserialize FilteredBlock message from protocol format
pub fn deserialize_filtered_block(data: &[u8]) -> Result<FilteredBlockMessage> {
    use crate::network::protocol::ProtocolParser;
    match ProtocolParser::parse_message(data)? {
        ProtocolMessage::FilteredBlock(msg) => Ok(msg),
        _ => Err(anyhow::anyhow!("Expected FilteredBlock message")),
    }
}