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
//! Dandelion++: Privacy-Preserving Transaction Relay
//!
//! Specification: https://github.com/bitcoin/bips/blob/master/bip-0156.mediawiki (Dandelion)
//! Extended version: Dandelion++ with improved anonymity guarantees
//!
//! Orange Paper Section 10.6: Implementation invariants verified via blvm-spec-lock.
//!
//! Dandelion++ operates in two phases:
//! 1. Stem Phase: Transaction relayed along a random path (obscures origin)
//! 2. Fluff Phase: Transaction broadcast to all peers (standard diffusion)
//!
//! This provides formal anonymity guarantees against transaction origin analysis.

#[cfg(feature = "dandelion")]
use blvm_spec_lock::spec_locked;

use blvm_protocol::Hash;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use std::collections::HashMap;
use std::time::{Duration, Instant};
use tracing::debug;

/// Dandelion relay state
pub struct DandelionRelay<C: Clock = SystemClock> {
    /// Active stem paths per peer (peer_id -> next_stem_peer)
    stem_paths: HashMap<String, StemPath>,
    /// Transactions in stem phase (tx_hash -> stem_state)
    stem_txs: HashMap<Hash, StemState>,
    /// Stem phase timeout (default: 10 seconds)
    stem_timeout: Duration,
    /// Probability of fluffing at each hop (default: 0.1 = 10%)
    fluff_probability: f64,
    /// Maximum stem hops before forced fluff (default: 2)
    max_stem_hops: u8,
    /// RNG for deterministic testing
    rng: StdRng,
    /// Clock for deterministic testing
    clock: C,
}

/// Stem path for a peer
#[derive(Debug, Clone)]
struct StemPath {
    /// Next peer in the stem path
    next_peer: String,
    /// When this path expires
    expiry: Instant,
    /// Current hop count
    hop_count: u8,
}

/// Stem phase state for a transaction
#[derive(Debug, Clone)]
pub struct StemState {
    /// Current peer in stem path
    current_peer: String,
    /// Next peer in stem path (if any)
    next_peer: Option<String>,
    /// When stem phase started
    stem_start: Instant,
    /// Number of hops so far
    hops: u8,
    /// Original source (for metrics only, not used in routing)
    source_peer: Option<String>,
}

/// Dandelion phase
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DandelionPhase {
    /// Stem phase: relay to next stem peer
    Stem,
    /// Fluff phase: broadcast to all peers
    Fluff,
}

impl Default for DandelionRelay<SystemClock> {
    fn default() -> Self {
        Self::new()
    }
}

pub trait Clock: Clone + Send + Sync + 'static {
    fn now(&self) -> Instant;
}

#[derive(Clone)]
pub struct SystemClock;
impl Clock for SystemClock {
    fn now(&self) -> Instant {
        Instant::now()
    }
}

impl DandelionRelay<SystemClock> {
    /// Create a new Dandelion++ relay
    pub fn new() -> Self {
        Self {
            stem_paths: HashMap::new(),
            stem_txs: HashMap::new(),
            stem_timeout: Duration::from_secs(10),
            fluff_probability: 0.1, // 10% chance to fluff at each hop
            max_stem_hops: 2,
            rng: StdRng::from_entropy(),
            clock: SystemClock,
        }
    }

    /// Create with custom parameters
    pub fn with_params(stem_timeout: Duration, fluff_probability: f64, max_stem_hops: u8) -> Self {
        Self {
            stem_paths: HashMap::new(),
            stem_txs: HashMap::new(),
            stem_timeout,
            fluff_probability,
            max_stem_hops,
            rng: StdRng::from_entropy(),
            clock: SystemClock,
        }
    }
}

impl<C: Clock> DandelionRelay<C> {
    /// Create with custom rng and clock (for tests)
    pub fn with_rng_and_clock(rng: StdRng, clock: C) -> Self {
        Self {
            stem_paths: HashMap::new(),
            stem_txs: HashMap::new(),
            stem_timeout: Duration::from_secs(10),
            fluff_probability: 0.1,
            max_stem_hops: 2,
            rng,
            clock,
        }
    }

    /// Test helper: set stem timeout
    pub fn set_stem_timeout(&mut self, timeout: Duration) {
        self.stem_timeout = timeout;
    }

    /// Test helper: set fluff probability
    pub fn set_fluff_probability(&mut self, p: f64) {
        self.fluff_probability = p;
    }

    /// Test helper: set max stem hops
    pub fn set_max_stem_hops(&mut self, hops: u8) {
        self.max_stem_hops = hops;
    }

    /// Test helper: update clock
    pub fn set_clock(&mut self, clock: C) {
        self.clock = clock;
    }

    /// Initialize stem path for a peer (called during peer handshake)
    pub fn initialize_stem_path(
        &mut self,
        peer_id: String,
        available_peers: &[String],
    ) -> Option<String> {
        // Select a random peer (not self) for stem path
        let rng = &mut self.rng;
        let candidates: Vec<_> = available_peers.iter().filter(|p| *p != &peer_id).collect();

        if candidates.is_empty() {
            return None;
        }

        let next_peer = candidates[rng.gen_range(0..candidates.len())].clone();

        let path = StemPath {
            next_peer: next_peer.clone(),
            expiry: self.clock.now() + Duration::from_secs(600), // 10 minute path expiry
            hop_count: 0,
        };

        self.stem_paths.insert(peer_id.clone(), path);
        debug!(
            "Initialized Dandelion stem path: {} -> {}",
            peer_id, next_peer
        );

        Some(next_peer)
    }

    /// Update stem path (rotate after each relay)
    pub fn update_stem_path(&mut self, peer_id: &str, available_peers: &[String]) {
        if let Some(path) = self.stem_paths.get_mut(peer_id) {
            // Select new next peer (different from current)
            let rng = &mut self.rng;
            let candidates: Vec<_> = available_peers
                .iter()
                .filter(|p| *p != peer_id && *p != &path.next_peer)
                .collect();

            if !candidates.is_empty() {
                path.next_peer = candidates[rng.gen_range(0..candidates.len())].clone();
                path.hop_count += 1;
                debug!(
                    "Updated Dandelion stem path: {} -> {} (hop {})",
                    peer_id, path.next_peer, path.hop_count
                );
            }
        }
    }

    /// Start stem phase for a transaction.
    /// Orange Paper 10.6: Single stem state per tx (|stem_states(tx)| ≤ 1).
    #[spec_locked("10.6")]
    pub fn start_stem_phase(
        &mut self,
        tx_hash: Hash,
        current_peer: String,
        available_peers: &[String],
    ) -> Option<String> {
        // Get or create stem path for current peer
        let next_peer = if let Some(path) = self.stem_paths.get(&current_peer) {
            if path.expiry > Instant::now() {
                Some(path.next_peer.clone())
            } else {
                // Path expired, create new one
                self.initialize_stem_path(current_peer.clone(), available_peers)
            }
        } else {
            self.initialize_stem_path(current_peer.clone(), available_peers)
        };

        if let Some(next) = next_peer.as_ref() {
            let stem_state = StemState {
                current_peer: current_peer.clone(),
                next_peer: Some(next.clone()),
                stem_start: self.clock.now(),
                hops: 0,
                source_peer: Some(current_peer.clone()),
            };

            self.stem_txs.insert(tx_hash, stem_state);
            debug!(
                "Started Dandelion stem phase for tx {}: {} -> {}",
                hex::encode(tx_hash),
                current_peer,
                next
            );
        }

        next_peer
    }

    /// Check if transaction should fluff (transition to broadcast phase).
    /// Orange Paper 10.6: Timeout enforcement (elapsed > stem_timeout ⟹ Fluff).
    #[spec_locked("10.6")]
    pub fn should_fluff(&mut self, tx_hash: &Hash) -> bool {
        if let Some(state) = self.stem_txs.get(tx_hash) {
            // Check timeout
            if self.clock.now().duration_since(state.stem_start) >= self.stem_timeout {
                debug!("Dandelion stem timeout for tx {}", hex::encode(tx_hash));
                return true;
            }

            // Check max hops
            if state.hops >= self.max_stem_hops {
                debug!("Dandelion max hops reached for tx {}", hex::encode(tx_hash));
                return true;
            }

            // Random fluff decision (with fluff_probability)
            if self.rng.gen::<f64>() < self.fluff_probability {
                debug!("Dandelion random fluff for tx {}", hex::encode(tx_hash));
                return true;
            }
        }

        false
    }

    /// Advance stem phase (move to next peer).
    /// Orange Paper 10.6: Bounded stem length (stem_hops(tx) ≤ max_stem_hops).
    #[spec_locked("10.6")]
    pub fn advance_stem(&mut self, tx_hash: Hash, available_peers: &[String]) -> Option<String> {
        if let Some(state) = self.stem_txs.get_mut(&tx_hash) {
            state.hops += 1;
            let current_peer = state.current_peer.clone();
            // drop mutable borrow of state before borrowing self mutably again
            // Update stem path for current peer
            let _ = state;
            self.update_stem_path(&current_peer, available_peers);
            if let Some(state2) = self.stem_txs.get_mut(&tx_hash) {
                if let Some(path) = self.stem_paths.get(&current_peer) {
                    let next = path.next_peer.clone();
                    state2.current_peer = next.clone();
                    state2.next_peer = Some(next.clone());
                    debug!(
                        "Advanced Dandelion stem for tx {}: hop {} -> {}",
                        hex::encode(tx_hash),
                        state2.hops,
                        next
                    );
                    return Some(next);
                }
            }
        }

        None
    }

    /// Transition transaction to fluff phase.
    /// Orange Paper 10.6: Eventual fluff (removes from stem, enters broadcast phase).
    #[spec_locked("10.6")]
    pub fn transition_to_fluff(&mut self, tx_hash: Hash) -> DandelionPhase {
        self.stem_txs.remove(&tx_hash);
        debug!(
            "Transitioned tx {} to Dandelion fluff phase",
            hex::encode(tx_hash)
        );
        DandelionPhase::Fluff
    }

    /// Get current phase for a transaction.
    /// Orange Paper 10.6: phase = Stem ⟹ single-peer relay (no broadcast).
    #[spec_locked("10.6")]
    pub fn get_phase(&self, tx_hash: &Hash) -> Option<DandelionPhase> {
        if self.stem_txs.contains_key(tx_hash) {
            Some(DandelionPhase::Stem)
        } else {
            Some(DandelionPhase::Fluff)
        }
    }

    /// Get next stem peer for a transaction (if in stem phase)
    pub fn get_stem_peer(&self, tx_hash: &Hash) -> Option<String> {
        self.stem_txs.get(tx_hash).and_then(|s| s.next_peer.clone())
    }

    /// Clean up expired stem paths and transactions
    pub fn cleanup_expired(&mut self) {
        let now = self.clock.now();

        // Clean expired stem paths
        self.stem_paths.retain(|_, path| path.expiry > now);

        // Clean expired stem transactions (should have fluffed)
        self.stem_txs.retain(|_, state| {
            self.clock.now().duration_since(state.stem_start) < self.stem_timeout * 2
        });
    }

    /// Get statistics
    pub fn get_stats(&self) -> DandelionStats {
        DandelionStats {
            active_stem_paths: self.stem_paths.len(),
            stem_transactions: self.stem_txs.len(),
            stem_timeout_secs: self.stem_timeout.as_secs(),
            fluff_probability: self.fluff_probability,
            max_stem_hops: self.max_stem_hops,
        }
    }
}

/// Dandelion statistics
#[derive(Debug, Clone)]
pub struct DandelionStats {
    pub active_stem_paths: usize,
    pub stem_transactions: usize,
    pub stem_timeout_secs: u64,
    pub fluff_probability: f64,
    pub max_stem_hops: u8,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Clone)]
    struct TestClock {
        now: Instant,
    }
    impl TestClock {
        fn new(start: Instant) -> Self {
            Self { now: start }
        }
        fn advance(&mut self, d: Duration) {
            self.now += d;
        }
    }
    impl Clock for TestClock {
        fn now(&self) -> Instant {
            self.now
        }
    }

    fn peers() -> Vec<String> {
        vec!["p1".into(), "p2".into(), "p3".into(), "p4".into()]
    }

    #[test]
    fn stem_initialization_and_advance() {
        let rng = StdRng::seed_from_u64(42);
        let clock = TestClock::new(Instant::now());
        let mut d: DandelionRelay<TestClock> =
            DandelionRelay::with_rng_and_clock(rng, clock.clone());
        let next = d.initialize_stem_path("p1".into(), &peers());
        assert!(next.is_some());

        // Start stem for a tx
        let tx = [1u8; 32];
        let hop = d.start_stem_phase(tx, "p1".into(), &peers());
        assert!(hop.is_some());

        // Advance once
        let _ = d.advance_stem(tx, &peers());
        assert_eq!(d.get_phase(&tx), Some(DandelionPhase::Stem));
    }

    #[test]
    fn timeout_triggers_fluff() {
        let rng = StdRng::seed_from_u64(7);
        let start = Instant::now();
        let mut clock = TestClock::new(start);
        let mut d: DandelionRelay<TestClock> =
            DandelionRelay::with_rng_and_clock(rng, clock.clone());

        // Make timeout very short
        d.stem_timeout = Duration::from_millis(50);

        let tx = [2u8; 32];
        let _ = d.start_stem_phase(tx, "p2".into(), &peers());
        assert_eq!(d.get_phase(&tx), Some(DandelionPhase::Stem));

        // Advance clock past timeout
        clock.advance(Duration::from_millis(60));
        d.clock = clock.clone();
        assert!(d.should_fluff(&tx));
    }
}