• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Neptune-Crypto / neptune-core / 15762735904

19 Jun 2025 04:46PM UTC coverage: 71.884% (-0.02%) from 71.908%
15762735904

Pull #618

github

web-flow
Merge b3086b9ad into 08ee86045
Pull Request #618: chore(deps): Upgrade dependency `tasm-lib`

4 of 4 new or added lines in 3 files covered. (100.0%)

21 existing lines in 5 files now uncovered.

20264 of 28190 relevant lines covered (71.88%)

506797.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.7
/src/models/peer.rs
1
pub(crate) mod handshake_data;
2
pub mod peer_block_notifications;
3
pub mod peer_info;
4
pub mod transaction_notification;
5
pub mod transfer_block;
6
pub mod transfer_transaction;
7

8
use std::fmt::Display;
9
use std::net::SocketAddr;
10
use std::time::SystemTime;
11

12
use handshake_data::HandshakeData;
13
use itertools::Itertools;
14
use num_bigint::BigUint;
15
use num_traits::ToPrimitive;
16
use num_traits::Zero;
17
use peer_block_notifications::PeerBlockNotification;
18
use rand::rngs::StdRng;
19
use rand::Rng;
20
use rand::RngCore;
21
use rand::SeedableRng;
22
use serde::Deserialize;
23
use serde::Serialize;
24
use tasm_lib::twenty_first::prelude::Mmr;
25
use tasm_lib::twenty_first::prelude::MmrMembershipProof;
26
use tasm_lib::twenty_first::util_types::mmr::mmr_accumulator::MmrAccumulator;
27
use tracing::debug;
28
use tracing::trace;
29
use tracing::warn;
30
use transaction_notification::TransactionNotification;
31
use transfer_transaction::TransferTransaction;
32
use twenty_first::math::digest::Digest;
33

34
use super::blockchain::block::block_header::BlockHeader;
35
use super::blockchain::block::block_header::BlockHeaderWithBlockHashWitness;
36
use super::blockchain::block::block_height::BlockHeight;
37
use super::blockchain::block::difficulty_control::Difficulty;
38
use super::blockchain::block::difficulty_control::ProofOfWork;
39
use super::blockchain::block::Block;
40
use super::channel::BlockProposalNotification;
41
use super::proof_abstractions::timestamp::Timestamp;
42
use super::state::transaction_kernel_id::TransactionKernelId;
43
use crate::config_models::network::Network;
44
use crate::models::blockchain::block::difficulty_control::max_cumulative_pow_after;
45
use crate::models::peer::transfer_block::TransferBlock;
46
use crate::prelude::twenty_first;
47

48
pub(crate) type InstanceId = u128;
49

50
pub(crate) const SYNC_CHALLENGE_POW_WITNESS_LENGTH: usize = 10;
51
pub(crate) const SYNC_CHALLENGE_NUM_BLOCK_PAIRS: usize = 10;
52

53
trait Sanction {
54
    fn severity(self) -> i32;
55
}
56
/// The reason for degrading a peer's standing
57
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
58
pub enum NegativePeerSanction {
59
    InvalidBlock((BlockHeight, Digest)),
60
    DifferentGenesis,
61
    ForkResolutionError((BlockHeight, u16, Digest)),
62
    SynchronizationTimeout,
63

64
    InvalidSyncChallenge,
65
    InvalidSyncChallengeResponse,
66
    TimedOutSyncChallengeResponse,
67
    UnexpectedSyncChallengeResponse,
68
    FishyPowEvolutionChallengeResponse,
69
    FishyDifficultiesChallengeResponse,
70

71
    FloodPeerListResponse,
72
    BlockRequestUnknownHeight,
73
    // Be careful about using this too much as it's bad for log opportunities
74
    InvalidMessage,
75
    NonMinedTransactionHasCoinbase,
76
    TooShortBlockBatch,
77
    ReceivedBatchBlocksOutsideOfSync,
78
    BatchBlocksInvalidStartHeight,
79
    BatchBlocksUnknownRequest,
80
    BatchBlocksRequestEmpty,
81
    BatchBlocksRequestTooManyDigests,
82

83
    InvalidTransaction,
84
    UnconfirmableTransaction,
85
    TransactionWithNegativeFee,
86
    DoubleSpendingTransaction,
87
    CannotApplyTransactionToMutatorSet,
88

89
    InvalidBlockMmrAuthentication,
90

91
    InvalidTransferBlock,
92

93
    BlockProposalNotFound,
94
    InvalidBlockProposal,
95
    NonFavorableBlockProposal,
96

97
    UnwantedMessage,
98

99
    NoStandingFoundMaybeCrash,
100
}
101

102
/// The reason for improving a peer's standing
103
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
104
pub enum PositivePeerSanction {
105
    // positive sanctions (standing-improving)
106
    // We only reward events that are unlikely to occur more frequently than the
107
    // target block frequency. This should make it impossible for an attacker
108
    // to quickly ramp up their standing with peers, provided that they are on
109
    // the global tip.
110
    ValidBlocks(usize),
111
    NewBlockProposal,
112
}
113

114
impl Display for NegativePeerSanction {
115
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
116
        let string = match self {
×
117
            NegativePeerSanction::InvalidBlock(_) => "invalid block",
×
118
            NegativePeerSanction::DifferentGenesis => "different genesis",
×
119
            NegativePeerSanction::ForkResolutionError(_) => "fork resolution error",
×
120
            NegativePeerSanction::SynchronizationTimeout => "synchronization timeout",
×
121
            NegativePeerSanction::FloodPeerListResponse => "flood peer list response",
×
122
            NegativePeerSanction::BlockRequestUnknownHeight => "block request unknown height",
×
123
            NegativePeerSanction::InvalidMessage => "invalid message",
×
124
            NegativePeerSanction::TooShortBlockBatch => "too short block batch",
×
125
            NegativePeerSanction::ReceivedBatchBlocksOutsideOfSync => {
126
                "received block batch outside of sync"
×
127
            }
128
            NegativePeerSanction::BatchBlocksInvalidStartHeight => {
129
                "invalid start height of batch blocks"
×
130
            }
131
            NegativePeerSanction::BatchBlocksUnknownRequest => "batch blocks unknown request",
×
132
            NegativePeerSanction::InvalidTransaction => "invalid transaction",
×
133
            NegativePeerSanction::UnconfirmableTransaction => "unconfirmable transaction",
×
134
            NegativePeerSanction::TransactionWithNegativeFee => "negative-fee transaction",
×
135
            NegativePeerSanction::DoubleSpendingTransaction => "double-spending transaction",
×
136
            NegativePeerSanction::CannotApplyTransactionToMutatorSet => {
137
                "cannot apply tx to mutator set"
×
138
            }
139
            NegativePeerSanction::NonMinedTransactionHasCoinbase => {
140
                "non-mined transaction has coinbase"
×
141
            }
142
            NegativePeerSanction::NoStandingFoundMaybeCrash => {
143
                "No standing found in map. Did peer task crash?"
×
144
            }
145
            NegativePeerSanction::BlockProposalNotFound => "Block proposal not found",
×
146
            NegativePeerSanction::InvalidBlockProposal => "Invalid block proposal",
×
147
            NegativePeerSanction::UnwantedMessage => "unwanted message",
×
148
            NegativePeerSanction::NonFavorableBlockProposal => "non-favorable block proposal",
×
149
            NegativePeerSanction::BatchBlocksRequestEmpty => "batch block request empty",
×
150
            NegativePeerSanction::InvalidSyncChallenge => "invalid sync challenge",
×
151
            NegativePeerSanction::InvalidSyncChallengeResponse => "invalid sync challenge response",
×
152
            NegativePeerSanction::UnexpectedSyncChallengeResponse => {
153
                "unexpected sync challenge response"
×
154
            }
155
            NegativePeerSanction::InvalidTransferBlock => "invalid transfer block",
×
156
            NegativePeerSanction::TimedOutSyncChallengeResponse => {
157
                "timed-out sync challenge response"
×
158
            }
159
            NegativePeerSanction::InvalidBlockMmrAuthentication => {
160
                "invalid block mmr authentication"
×
161
            }
162
            NegativePeerSanction::BatchBlocksRequestTooManyDigests => {
163
                "too many digests in batch block request"
×
164
            }
165
            NegativePeerSanction::FishyPowEvolutionChallengeResponse => "fishy pow evolution",
×
166
            NegativePeerSanction::FishyDifficultiesChallengeResponse => "fishy difficulties",
×
167
        };
168
        write!(f, "{string}")
×
169
    }
×
170
}
171

172
impl Display for PositivePeerSanction {
173
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
174
        let string = match self {
×
175
            PositivePeerSanction::ValidBlocks(_) => "valid blocks",
×
176
            PositivePeerSanction::NewBlockProposal => "new block proposal",
×
177
        };
178
        write!(f, "{string}")
×
179
    }
×
180
}
181

182
/// Used by main task to manage synchronizations/catch-up. Main task has
183
/// a value of this type for each connected peer.
184

185
#[derive(Debug, Clone, Copy)]
186
pub struct PeerSynchronizationState {
187
    pub claimed_max_height: BlockHeight,
188
    pub(crate) claimed_max_pow: ProofOfWork,
189
    pub synchronization_start: SystemTime,
190
    pub last_request_received: Option<SystemTime>,
191
}
192

193
impl PeerSynchronizationState {
194
    pub(crate) fn new(claimed_max_height: BlockHeight, claimed_max_pow: ProofOfWork) -> Self {
1✔
195
        Self {
1✔
196
            claimed_max_height,
1✔
197
            claimed_max_pow,
1✔
198
            synchronization_start: SystemTime::now(),
1✔
199
            last_request_received: None,
1✔
200
        }
1✔
201
    }
1✔
202
}
203

204
impl Sanction for NegativePeerSanction {
205
    fn severity(self) -> i32 {
10✔
206
        match self {
10✔
207
            NegativePeerSanction::InvalidBlock(_) => -10,
1✔
208
            NegativePeerSanction::DifferentGenesis => i32::MIN,
5✔
209
            NegativePeerSanction::ForkResolutionError((_height, count, _digest)) => {
1✔
210
                i32::from(count).saturating_mul(-1)
1✔
211
            }
212
            NegativePeerSanction::SynchronizationTimeout => -5,
×
213
            NegativePeerSanction::FloodPeerListResponse => -2,
×
214
            NegativePeerSanction::InvalidMessage => -2,
×
215
            NegativePeerSanction::TooShortBlockBatch => -2,
×
216
            NegativePeerSanction::ReceivedBatchBlocksOutsideOfSync => -2,
×
217
            NegativePeerSanction::BatchBlocksInvalidStartHeight => -2,
×
218
            NegativePeerSanction::BatchBlocksUnknownRequest => -10,
×
219
            NegativePeerSanction::BlockRequestUnknownHeight => -1,
1✔
220
            NegativePeerSanction::InvalidTransaction => -10,
×
221
            NegativePeerSanction::UnconfirmableTransaction => -2,
×
222
            NegativePeerSanction::TransactionWithNegativeFee => -22,
×
223
            NegativePeerSanction::DoubleSpendingTransaction => -14,
×
224
            NegativePeerSanction::CannotApplyTransactionToMutatorSet => -3,
×
225
            NegativePeerSanction::NonMinedTransactionHasCoinbase => -10,
×
226
            NegativePeerSanction::NoStandingFoundMaybeCrash => -10,
×
227
            NegativePeerSanction::BlockProposalNotFound => -1,
×
228
            NegativePeerSanction::InvalidBlockProposal => -10,
×
229
            NegativePeerSanction::UnwantedMessage => -1,
×
230
            NegativePeerSanction::NonFavorableBlockProposal => -1,
×
231
            NegativePeerSanction::BatchBlocksRequestEmpty => -10,
×
232
            NegativePeerSanction::InvalidSyncChallenge => -50,
2✔
233
            NegativePeerSanction::InvalidSyncChallengeResponse => -500,
×
234
            NegativePeerSanction::UnexpectedSyncChallengeResponse => -1,
×
235
            NegativePeerSanction::InvalidTransferBlock => -50,
×
236
            NegativePeerSanction::TimedOutSyncChallengeResponse => -50,
×
237
            NegativePeerSanction::InvalidBlockMmrAuthentication => -4,
×
238
            NegativePeerSanction::BatchBlocksRequestTooManyDigests => -50,
×
239
            NegativePeerSanction::FishyPowEvolutionChallengeResponse => -51,
×
240
            NegativePeerSanction::FishyDifficultiesChallengeResponse => -51,
×
241
        }
242
    }
10✔
243
}
244

245
/// The reason for changing a peer's standing.
246
///
247
/// Sanctions can be positive (rewards) or negative (punishments).
248
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
249
pub(crate) enum PeerSanction {
250
    Positive(PositivePeerSanction),
251
    Negative(NegativePeerSanction),
252
}
253

254
impl Sanction for PositivePeerSanction {
255
    fn severity(self) -> i32 {
18✔
256
        match self {
18✔
257
            PositivePeerSanction::ValidBlocks(number) => number
18✔
258
                .try_into()
18✔
259
                .map(|n: i32| n.saturating_mul(10))
18✔
260
                .unwrap_or(i32::MAX),
18✔
261
            PositivePeerSanction::NewBlockProposal => 7,
×
262
        }
263
    }
18✔
264
}
265

266
impl Sanction for PeerSanction {
267
    fn severity(self) -> i32 {
28✔
268
        match self {
28✔
269
            PeerSanction::Positive(positive_peer_sanction) => positive_peer_sanction.severity(),
18✔
270
            PeerSanction::Negative(negative_peer_sanction) => negative_peer_sanction.severity(),
10✔
271
        }
272
    }
28✔
273
}
274

275
/// This is the object that gets stored in the database to record how well a
276
/// peer has behaved so far.
277
//
278
// The most central methods are [PeerStanding::sanction] and
279
// [PeerStanding::is_bad].
280
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
281
pub struct PeerStanding {
282
    /// The actual standing. The higher, the better.
283
    pub standing: i32,
284
    pub latest_punishment: Option<(NegativePeerSanction, SystemTime)>,
285
    pub latest_reward: Option<(PositivePeerSanction, SystemTime)>,
286
    peer_tolerance: i32,
287
}
288
#[derive(Debug, Clone, Copy, Default)]
289
pub(crate) struct StandingExceedsBanThreshold;
290

291
impl Display for StandingExceedsBanThreshold {
292
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2✔
293
        write!(f, "standing exceeds ban threshold")
2✔
294
    }
2✔
295
}
296

297
impl std::error::Error for StandingExceedsBanThreshold {}
298

299
impl PeerStanding {
300
    pub(crate) fn new(peer_tolerance: u16) -> Self {
556✔
301
        assert!(peer_tolerance > 0, "peer tolerance must be positive");
556✔
302
        Self {
556✔
303
            peer_tolerance: i32::from(peer_tolerance),
556✔
304
            standing: 0,
556✔
305
            latest_punishment: None,
556✔
306
            latest_reward: None,
556✔
307
        }
556✔
308
    }
556✔
309

310
    /// Sanction peer. If (and only if) the peer is now in
311
    /// [bad standing](Self::is_bad), returns an error.
312
    pub(crate) fn sanction(
28✔
313
        &mut self,
28✔
314
        sanction: PeerSanction,
28✔
315
    ) -> Result<(), StandingExceedsBanThreshold> {
28✔
316
        self.standing = self
28✔
317
            .standing
28✔
318
            .saturating_add(sanction.severity())
28✔
319
            .clamp(-self.peer_tolerance, self.peer_tolerance);
28✔
320
        trace!(
28✔
321
            "new standing: {}, peer tolerance: {}",
×
322
            self.standing,
323
            self.peer_tolerance
324
        );
325
        let now = SystemTime::now();
28✔
326
        match sanction {
28✔
327
            PeerSanction::Negative(sanction) => self.latest_punishment = Some((sanction, now)),
10✔
328
            PeerSanction::Positive(sanction) => self.latest_reward = Some((sanction, now)),
18✔
329
        }
330

331
        self.is_good()
28✔
332
            .then_some(())
28✔
333
            .ok_or(StandingExceedsBanThreshold)
28✔
334
    }
28✔
335

336
    /// Clear peer standing record
337
    pub(crate) fn clear_standing(&mut self) {
8✔
338
        self.standing = 0;
8✔
339
        self.latest_punishment = None;
8✔
340
        self.latest_reward = None;
8✔
341
    }
8✔
342

343
    pub fn is_negative(&self) -> bool {
27✔
344
        self.standing.is_negative()
27✔
345
    }
27✔
346

347
    pub(crate) fn is_bad(&self) -> bool {
30✔
348
        self.standing <= -self.peer_tolerance
30✔
349
    }
30✔
350

351
    pub(crate) fn is_good(&self) -> bool {
28✔
352
        !self.is_bad()
28✔
353
    }
28✔
354
}
355

356
impl Display for PeerStanding {
357
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70✔
358
        write!(f, "{}", self.standing)
70✔
359
    }
70✔
360
}
361

362
/// A message sent between peers to inform them whether the connection was
363
/// accepted or refused (and if so, for what reason).
364
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
365
pub enum TransferConnectionStatus {
366
    Refused(ConnectionRefusedReason),
367
    Accepted,
368
}
369

370
/// A success code for internal use, pertaining to the establishment
371
/// of a connection to a peer.
372
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
373
pub enum InternalConnectionStatus {
374
    Refused(ConnectionRefusedReason),
375
    AcceptedMaxReached,
376
    Accepted,
377
}
378

379
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
380
pub enum ConnectionRefusedReason {
381
    AlreadyConnected,
382
    BadStanding,
383
    IncompatibleVersion,
384
    MaxPeerNumberExceeded,
385
    SelfConnect,
386
}
387

388
impl From<InternalConnectionStatus> for TransferConnectionStatus {
389
    fn from(value: InternalConnectionStatus) -> Self {
6✔
390
        match value {
6✔
391
            InternalConnectionStatus::Refused(connection_refused_reason) => {
3✔
392
                TransferConnectionStatus::Refused(connection_refused_reason)
3✔
393
            }
394
            InternalConnectionStatus::AcceptedMaxReached | InternalConnectionStatus::Accepted => {
395
                TransferConnectionStatus::Accepted
3✔
396
            }
397
        }
398
    }
6✔
399
}
400

401
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
402
pub struct BlockRequestBatch {
403
    /// Sorted list of most preferred blocks. The first digest is the block
404
    /// that the peer would prefer to build on top off, if it belongs to the
405
    /// canonical chain.
406
    pub(crate) known_blocks: Vec<Digest>,
407

408
    /// Indicates the maximum allowed number of blocks in the response.
409
    pub(crate) max_response_len: usize,
410

411
    /// The block MMR accumulator of the tip of the chain which the node is
412
    /// syncing towards. Its number of leafs is the block height the node is
413
    /// syncing towards.
414
    ///
415
    /// The receiver needs this value to know which MMR authentication paths to
416
    /// attach to the blocks in the response. These paths allow the receiver of
417
    /// a batch of blocks to verify that the received blocks are indeed
418
    /// ancestors to a given tip.
419
    pub(crate) anchor: MmrAccumulator,
420
}
421

422
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
423
pub(crate) struct BlockProposalRequest {
424
    pub(crate) body_mast_hash: Digest,
425
}
426

427
impl BlockProposalRequest {
428
    pub(crate) fn new(body_mast_hash: Digest) -> Self {
2✔
429
        Self { body_mast_hash }
2✔
430
    }
2✔
431
}
432

433
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
434
pub(crate) enum PeerMessage {
435
    Handshake(Box<(Vec<u8>, HandshakeData)>),
436
    Block(Box<TransferBlock>),
437
    BlockNotificationRequest,
438
    BlockNotification(PeerBlockNotification),
439
    BlockRequestByHeight(BlockHeight),
440
    BlockRequestByHash(Digest),
441

442
    BlockRequestBatch(BlockRequestBatch), // TODO: Consider restricting this in size
443
    BlockResponseBatch(Vec<(TransferBlock, MmrMembershipProof)>), // TODO: Consider restricting this in size
444
    UnableToSatisfyBatchRequest,
445

446
    SyncChallenge(SyncChallenge),
447
    SyncChallengeResponse(Box<SyncChallengeResponse>),
448

449
    BlockProposalNotification(BlockProposalNotification),
450

451
    BlockProposalRequest(BlockProposalRequest),
452

453
    BlockProposal(Box<Block>),
454

455
    /// Send a full transaction object to a peer.
456
    Transaction(Box<TransferTransaction>),
457
    /// Send a notification to a peer, informing it that this node stores the
458
    /// transaction with digest and timestamp specified in
459
    /// `TransactionNotification`.
460
    TransactionNotification(TransactionNotification),
461
    /// Send a request that this node would like a copy of the transaction with
462
    /// digest as specified by the argument.
463
    TransactionRequest(TransactionKernelId),
464
    PeerListRequest,
465
    /// (socket address, instance_id)
466
    PeerListResponse(Vec<(SocketAddr, u128)>),
467
    /// Inform peer that we are disconnecting them.
468
    Bye,
469
    ConnectionStatus(TransferConnectionStatus),
470
}
471

472
impl PeerMessage {
473
    pub fn get_type(&self) -> String {
240✔
474
        match self {
240✔
475
            PeerMessage::Handshake(_) => "handshake",
×
476
            PeerMessage::Block(_) => "block",
52✔
477
            PeerMessage::BlockNotificationRequest => "block notification request",
×
478
            PeerMessage::BlockNotification(_) => "block notification",
20✔
479
            PeerMessage::BlockRequestByHeight(_) => "block req by height",
20✔
480
            PeerMessage::BlockRequestByHash(_) => "block req by hash",
×
481
            PeerMessage::BlockRequestBatch(_) => "block req batch",
16✔
482
            PeerMessage::BlockResponseBatch(_) => "block resp batch",
×
483
            PeerMessage::Transaction(_) => "send",
7✔
484
            PeerMessage::TransactionNotification(_) => "transaction notification",
18✔
485
            PeerMessage::TransactionRequest(_) => "transaction request",
7✔
486
            PeerMessage::PeerListRequest => "peer list req",
7✔
487
            PeerMessage::PeerListResponse(_) => "peer list resp",
3✔
488
            PeerMessage::Bye => "bye",
80✔
489
            PeerMessage::ConnectionStatus(_) => "connection status",
×
490
            PeerMessage::BlockProposalNotification(_) => "block proposal notification",
2✔
491
            PeerMessage::BlockProposalRequest(_) => "block proposal request",
×
492
            PeerMessage::BlockProposal(_) => "block proposal",
2✔
493
            PeerMessage::UnableToSatisfyBatchRequest => "unable to satisfy batch request",
×
494
            PeerMessage::SyncChallenge(_) => "sync challenge",
4✔
495
            PeerMessage::SyncChallengeResponse(_) => "sync challenge response",
2✔
496
        }
497
        .to_string()
240✔
498
    }
240✔
499

500
    pub fn ignore_when_not_sync(&self) -> bool {
93✔
501
        match self {
93✔
502
            PeerMessage::Handshake(_) => false,
×
503
            PeerMessage::Block(_) => false,
20✔
504
            PeerMessage::BlockNotificationRequest => false,
×
505
            PeerMessage::BlockNotification(_) => false,
4✔
506
            PeerMessage::BlockRequestByHeight(_) => false,
4✔
507
            PeerMessage::BlockRequestByHash(_) => false,
×
508
            PeerMessage::BlockRequestBatch(_) => false,
8✔
509
            PeerMessage::BlockResponseBatch(_) => true,
×
510
            PeerMessage::Transaction(_) => false,
2✔
511
            PeerMessage::TransactionNotification(_) => false,
6✔
512
            PeerMessage::TransactionRequest(_) => false,
2✔
513
            PeerMessage::PeerListRequest => false,
2✔
514
            PeerMessage::PeerListResponse(_) => false,
×
515
            PeerMessage::Bye => false,
40✔
516
            PeerMessage::ConnectionStatus(_) => false,
×
517
            PeerMessage::BlockProposalNotification(_) => false,
1✔
518
            PeerMessage::BlockProposalRequest(_) => false,
×
519
            PeerMessage::BlockProposal(_) => false,
1✔
520
            PeerMessage::UnableToSatisfyBatchRequest => true,
×
521
            PeerMessage::SyncChallenge(_) => false,
2✔
522
            PeerMessage::SyncChallengeResponse(_) => false,
1✔
523
        }
524
    }
93✔
525

526
    /// Function to filter out messages that should not be handled while the client is syncing
527
    pub fn ignore_during_sync(&self) -> bool {
93✔
528
        match self {
93✔
529
            PeerMessage::Handshake(_) => false,
×
530
            PeerMessage::Block(_) => true,
20✔
531
            PeerMessage::BlockNotificationRequest => false,
×
532
            PeerMessage::BlockNotification(_) => false,
4✔
533
            PeerMessage::BlockRequestByHeight(_) => false,
4✔
534
            PeerMessage::BlockRequestByHash(_) => false,
×
535
            PeerMessage::BlockRequestBatch(_) => false,
8✔
536
            PeerMessage::BlockResponseBatch(_) => false,
×
537
            PeerMessage::Transaction(_) => true,
2✔
538
            PeerMessage::TransactionNotification(_) => false,
6✔
539
            PeerMessage::TransactionRequest(_) => false,
2✔
540
            PeerMessage::PeerListRequest => false,
2✔
UNCOV
541
            PeerMessage::PeerListResponse(_) => false,
×
542
            PeerMessage::Bye => false,
40✔
543
            PeerMessage::ConnectionStatus(_) => false,
×
544
            PeerMessage::BlockProposalNotification(_) => true,
1✔
545
            PeerMessage::BlockProposalRequest(_) => true,
×
546
            PeerMessage::BlockProposal(_) => true,
1✔
547
            PeerMessage::UnableToSatisfyBatchRequest => false,
×
548
            PeerMessage::SyncChallenge(_) => false,
2✔
549
            PeerMessage::SyncChallengeResponse(_) => false,
1✔
550
        }
551
    }
93✔
552
}
553

554
/// `MutablePeerState` contains information about the peer's blockchain state.
555
/// Under normal conditions, this information varies across time.
556
#[derive(Clone, Debug)]
557
pub struct MutablePeerState {
558
    pub highest_shared_block_height: BlockHeight,
559
    pub fork_reconciliation_blocks: Vec<Block>,
560
    pub(crate) sync_challenge: Option<IssuedSyncChallenge>,
561

562
    /// Timestamp for the last successful sync challenge response.
563
    ///
564
    /// Used to prevent issuing multiple sync challenges in short succession.
565
    pub(crate) successful_sync_challenge_response_time: Option<Timestamp>,
566
}
567

568
impl MutablePeerState {
569
    pub fn new(block_height: BlockHeight) -> Self {
48✔
570
        Self {
48✔
571
            highest_shared_block_height: block_height,
48✔
572
            fork_reconciliation_blocks: vec![],
48✔
573
            sync_challenge: None,
48✔
574
            successful_sync_challenge_response_time: None,
48✔
575
        }
48✔
576
    }
48✔
577
}
578

579
#[derive(Debug, Clone, Copy)]
580
pub(crate) struct IssuedSyncChallenge {
581
    pub(crate) challenge: SyncChallenge,
582
    pub(crate) issued_at: Timestamp,
583
    pub(crate) accumulated_pow: ProofOfWork,
584
}
585
impl IssuedSyncChallenge {
586
    pub(crate) fn new(
1✔
587
        challenge: SyncChallenge,
1✔
588
        claimed_pow: ProofOfWork,
1✔
589
        timestamp: Timestamp,
1✔
590
    ) -> Self {
1✔
591
        Self {
1✔
592
            challenge,
1✔
593
            issued_at: timestamp,
1✔
594
            accumulated_pow: claimed_pow,
1✔
595
        }
1✔
596
    }
1✔
597
}
598

599
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
600
pub(crate) struct SyncChallenge {
601
    pub(crate) tip_digest: Digest,
602

603
    /// Block heights of the child blocks, for which the peer must respond with
604
    /// (parent, child) blocks. Assumed to be ordered from small to big.
605
    pub(crate) challenges: [BlockHeight; SYNC_CHALLENGE_NUM_BLOCK_PAIRS],
606
}
607

608
impl SyncChallenge {
609
    /// Generate a `SyncChallenge`.
610
    ///
611
    /// Sample 10 block heights, 5 each from two distributions:
612
    ///  1. An exponential distribution smaller than the peer's claimed height
613
    ///     but skewed towards this number.
614
    ///  2. A uniform distribution between own tip height and the peer's claimed
615
    ///     height.
616
    ///
617
    /// # Panics
618
    ///
619
    ///  - Panics if the difference in height between own tip and peer's tip is
620
    ///    less than 10.
621
    pub(crate) fn generate(
2✔
622
        block_notification: &PeerBlockNotification,
2✔
623
        own_tip_height: BlockHeight,
2✔
624
        randomness: [u8; 32],
2✔
625
    ) -> Self {
2✔
626
        let mut rng = StdRng::from_seed(randomness);
2✔
627
        let mut heights = vec![];
2✔
628

629
        assert!(
2✔
630
            block_notification.height - own_tip_height >= 10,
2✔
631
            "Cannot issue sync challenge when height difference ({} - {} = {}) is less than 10.",
×
632
            block_notification.height,
633
            own_tip_height,
634
            block_notification.height - own_tip_height
×
635
        );
636

637
        // sample 5 block heights skewed towards peer's claimed tip height
638
        while heights.len() < 5 {
20✔
639
            let distance = rng.next_u64().leading_zeros() * 31
18✔
640
                + rng.next_u64().leading_zeros() * 7
18✔
641
                + rng.next_u64().leading_zeros() * 3
18✔
642
                + rng.next_u64().leading_zeros()
18✔
643
                + 1;
18✔
644
            let Some(height) = block_notification.height.checked_sub(distance.into()) else {
18✔
645
                continue;
6✔
646
            };
647

648
            // Don't require peer to send genesis block, as that's impossible.
649
            if height <= 1.into() {
12✔
650
                continue;
2✔
651
            }
10✔
652
            heights.push(height);
10✔
653
        }
654

655
        // sample 5 block heights uniformly from the interval between own tip
656
        // height and peer's claimed tip height
657
        let interval = u64::from(own_tip_height)..u64::from(block_notification.height);
2✔
658
        while heights.len() < 10 {
12✔
659
            let height = rng.random_range(interval.clone()).into();
10✔
660

661
            // Don't require peer to send genesis block, as that's impossible.
662
            if height <= 1.into() {
10✔
663
                continue;
×
664
            }
10✔
665
            heights.push(height);
10✔
666
        }
667

668
        // sort from small to big as that makes some validation checks easier.
669
        heights.sort();
2✔
670

671
        Self {
2✔
672
            tip_digest: block_notification.hash,
2✔
673
            challenges: heights.try_into().unwrap(),
2✔
674
        }
2✔
675
    }
2✔
676
}
677

678
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
679
pub(crate) struct SyncChallengeResponse {
680
    /// (parent, child) blocks. blocks are assumed to be ordered from small to
681
    /// big block height.
682
    pub(crate) blocks: [(TransferBlock, TransferBlock); SYNC_CHALLENGE_NUM_BLOCK_PAIRS],
683

684
    /// Membership proof of the child blocks, relative to the tip-MMR (after
685
    /// appending digest of tip). Must match ordering of blocks.
686
    pub(crate) membership_proofs: [MmrMembershipProof; SYNC_CHALLENGE_NUM_BLOCK_PAIRS],
687

688
    pub(crate) tip_parent: TransferBlock,
689
    pub(crate) tip: TransferBlock,
690

691
    /// Pow-witnesses from tip and X blocks back, in reverse-chronological
692
    /// order. So a witness to the `tip` hash should be the 1st element in this
693
    /// array.
694
    pub(crate) pow_witnesses: [BlockHeaderWithBlockHashWitness; SYNC_CHALLENGE_POW_WITNESS_LENGTH],
695
}
696

697
impl SyncChallengeResponse {
698
    fn pow_witnesses_form_chain_from_tip(
22✔
699
        tip_digest: Digest,
22✔
700
        pow_witnesses: &[BlockHeaderWithBlockHashWitness; SYNC_CHALLENGE_POW_WITNESS_LENGTH],
22✔
701
    ) -> bool {
22✔
702
        let tip_header_with_witness = &pow_witnesses[0];
22✔
703
        let mut is_chain_from_tip = tip_header_with_witness.hash() == tip_digest;
22✔
704
        for (child, parent) in pow_witnesses.iter().tuple_windows() {
198✔
705
            is_chain_from_tip &= child.is_successor_of(parent);
198✔
706
        }
198✔
707

708
        is_chain_from_tip
22✔
709
    }
22✔
710

711
    /// Determine whether the `SyncChallengeResponse` answers the given
712
    /// `IssuedSyncChallenge`, and not some other one.
713
    pub(crate) fn matches(&self, network: Network, issued_challenge: IssuedSyncChallenge) -> bool {
1✔
714
        let Ok(tip_parent) = Block::try_from(self.tip_parent.clone()) else {
1✔
715
            return false;
×
716
        };
717
        let Ok(tip) = Block::try_from(self.tip.clone()) else {
1✔
718
            return false;
×
719
        };
720

721
        let pow_witnesses_form_chain_from_tip =
1✔
722
            Self::pow_witnesses_form_chain_from_tip(tip.hash(), &self.pow_witnesses);
1✔
723

724
        self.blocks
1✔
725
            .iter()
1✔
726
            .zip(issued_challenge.challenge.challenges.iter())
1✔
727
            .all(|((_, child), challenge_height)| child.header.height == *challenge_height)
10✔
728
            && issued_challenge.challenge.tip_digest == tip.hash()
1✔
729
            && issued_challenge.accumulated_pow == tip.header().cumulative_proof_of_work
1✔
730
            && tip.has_proof_of_work(network, tip_parent.header())
1✔
731
            && pow_witnesses_form_chain_from_tip
1✔
732
    }
1✔
733

734
    /// Determine whether the proofs in `SyncChallengeResponse` are valid. Also
735
    /// checks proof-of-work.
736
    pub(crate) async fn is_valid(&self, now: Timestamp, network: Network) -> bool {
1✔
737
        let Ok(tip_predecessor) = Block::try_from(self.tip_parent.clone()) else {
1✔
738
            return false;
×
739
        };
740
        let Ok(tip) = Block::try_from(self.tip.clone()) else {
1✔
741
            return false;
×
742
        };
743
        if !tip.is_valid(&tip_predecessor, now, network).await
1✔
744
            || !tip.has_proof_of_work(network, tip_predecessor.header())
1✔
745
        {
746
            return false;
×
747
        }
1✔
748

749
        let mut mmra_anchor = tip.body().block_mmr_accumulator.to_owned();
1✔
750
        mmra_anchor.append(tip.hash());
1✔
751
        for ((parent, child), membership_proof) in
10✔
752
            self.blocks.iter().zip(self.membership_proofs.iter())
1✔
753
        {
754
            let Ok(child) = Block::try_from(child.clone()) else {
10✔
755
                return false;
×
756
            };
757
            if !membership_proof.verify(
10✔
758
                child.header().height.into(),
10✔
759
                child.hash(),
10✔
760
                &mmra_anchor.peaks(),
10✔
761
                mmra_anchor.num_leafs(),
10✔
762
            ) {
10✔
763
                return false;
×
764
            }
10✔
765

766
            let Ok(parent) = Block::try_from(parent.clone()) else {
10✔
767
                return false;
×
768
            };
769

770
            if !child.is_valid(&parent, now, network).await
10✔
771
                || !child.has_proof_of_work(network, parent.header())
10✔
772
            {
773
                return false;
×
774
            }
10✔
775
        }
776

777
        true
1✔
778
    }
1✔
779

780
    /// Determine whether the claimed evolution of the cumulative proof-of-work
781
    /// is a) possible, and b) likely, given the difficulties.
782
    pub(crate) fn check_pow(&self, network: Network, own_tip_height: BlockHeight) -> bool {
1✔
783
        let genesis_header = BlockHeader::genesis(network);
1✔
784
        let parent_triples = [(
1✔
785
            genesis_header.height,
1✔
786
            genesis_header.cumulative_proof_of_work,
1✔
787
            genesis_header.difficulty,
1✔
788
        )]
1✔
789
        .into_iter()
1✔
790
        .chain(self.blocks.iter().map(|(child, _parent)| {
10✔
791
            (
10✔
792
                child.header.height,
10✔
793
                child.header.cumulative_proof_of_work,
10✔
794
                child.header.difficulty,
10✔
795
            )
10✔
796
        }))
10✔
797
        .chain([(
1✔
798
            self.tip_parent.header.height,
1✔
799
            self.tip_parent.header.cumulative_proof_of_work,
1✔
800
            self.tip_parent.header.difficulty,
1✔
801
        )])
1✔
802
        .collect_vec();
1✔
803
        let cumulative_pow_evolution_okay = parent_triples.iter().copied().tuple_windows().all(
1✔
804
            |((start_height, start_cpow, start_difficulty), (stop_height, stop_cpow, _))| {
11✔
805
                let max_pow = max_cumulative_pow_after(
11✔
806
                    start_cpow,
11✔
807
                    start_difficulty,
11✔
808
                    (stop_height - start_height)
11✔
809
                        .try_into()
11✔
810
                        .expect("difference of block heights guaranteed to be non-negative"),
11✔
811
                    network.target_block_interval(),
11✔
812
                    network.minimum_block_time(),
11✔
813
                );
814
                // cpow must increase for each block, and is upward-bounded. But
815
                // since response may contain duplicates, allow equality.
816
                max_pow >= stop_cpow && start_cpow <= stop_cpow
11✔
817
            },
11✔
818
        );
819

820
        let first = self.blocks[0].0.header;
1✔
821
        let last = self.tip.header;
1✔
822
        let total_pow_increase = BigUint::from(last.cumulative_proof_of_work)
1✔
823
            - BigUint::from(first.cumulative_proof_of_work);
1✔
824
        let span = last.height - first.height;
1✔
825
        let average_difficulty = total_pow_increase.to_f64().unwrap() / (span as f64);
1✔
826
        debug_assert!(
1✔
827
            average_difficulty > 0.0,
1✔
828
            "Average difficulty must be positive. Got: {average_difficulty}"
×
829
        );
830

831
        // In principle, the cumulative proof-of-work could have been boosted by
832
        // a small number of outlying large difficulties. We require here that
833
        // "enough" observed difficulties are above this average. This strategy
834
        // is a heuristic and its use implies false positives: some evolutions
835
        // will be flagged as fishy, even though they came about legally.
836
        //
837
        // To quantify the heuristic somewhat: Suppose we are okay with assuming
838
        // that for all honest responders 10% of the difficulties must be larger
839
        // than the mean; and otherwise the node should flag the sync challenge
840
        // response as fishy. Then the probability of observing k above-mean
841
        // difficulties out of a random selection of 22, is
842
        //   {22 choose k} * (0.1)^k * (0.9)^(22-k) .
843
        // And in particular:
844
        //   k: probability
845
        //   ----------------------
846
        //   0: 0.09847709021836118
847
        //   1: 0.24072177608932735
848
        //   2: 0.28084207210421525
849
        //   3: 0.20803116452164094
850
        //   4: 0.10979422571975492
851
        //   5: 0.043917690287901975 .
852
        //
853
        // The tip is included in the below check, so if *it* doesn't have an
854
        // above average difficulty, something is almost certainly off.
855

856
        let too_few_above_mean_difficulties = !own_tip_height.is_genesis()
1✔
857
            && self
1✔
858
                .blocks
1✔
859
                .iter()
1✔
860
                .flat_map(|(l, r)| [l, r])
10✔
861
                .chain([&self.tip_parent, &self.tip])
1✔
862
                .map(|b| b.header.difficulty)
1✔
863
                .filter(|d| BigUint::from(*d).to_f64().unwrap() >= average_difficulty)
22✔
864
                .count()
1✔
865
                == 0;
866

867
        if too_few_above_mean_difficulties {
1✔
868
            warn!("Too few above mean difficulties.");
×
869
        }
1✔
870

871
        if !cumulative_pow_evolution_okay {
1✔
872
            warn!("Impossible evolution of cumulative pow.");
×
873
            for (start, stop) in parent_triples.into_iter().tuple_windows() {
×
874
                let upper_bound = max_cumulative_pow_after(
×
875
                    start.1,
×
876
                    start.2,
×
877
                    (stop.0 - start.0).try_into().unwrap(),
×
878
                    network.target_block_interval(),
×
879
                    network.minimum_block_time(),
×
880
                );
881
                debug!(
×
882
                    "start ({} / {} / {}) -> stop ({} / {} / {}) with max {}",
×
883
                    start.0, start.1, start.2, stop.0, stop.1, stop.2, upper_bound
884
                );
885
            }
886
        }
1✔
887

888
        cumulative_pow_evolution_okay && !too_few_above_mean_difficulties
1✔
889
    }
1✔
890

891
    /// Check whether the claimed difficulties are large enough relative to that
892
    /// of our own tip.
893
    ///
894
    /// Sum all verified difficulties and verify that this number is larger than
895
    /// our own tip difficulty. This inequality guarantees that the successful
896
    /// attacker must have spent at least one block's worth of guessing power to
897
    /// produce the malicious chain, and probably much more.
898
    pub(crate) fn check_difficulty(&self, own_tip_difficulty: Difficulty) -> bool {
1✔
899
        let own_tip_difficulty = ProofOfWork::zero() + own_tip_difficulty;
1✔
900
        let mut fork_relative_cumpow = ProofOfWork::zero();
1✔
901
        for (_parent, child) in &self.blocks {
11✔
902
            fork_relative_cumpow = fork_relative_cumpow + child.header.difficulty;
10✔
903
        }
10✔
904

905
        fork_relative_cumpow > own_tip_difficulty
1✔
906
    }
1✔
907
}
908

909
#[cfg(test)]
910
#[cfg_attr(coverage_nightly, coverage(off))]
911
mod tests {
912
    use macro_rules_attr::apply;
913
    use rand::random;
914

915
    use super::*;
916
    use crate::models::blockchain::block::block_header::HeaderToBlockHashWitness;
917
    use crate::models::blockchain::block::Block;
918
    use crate::tests::shared::blocks::fake_valid_sequence_of_blocks_for_tests;
919
    use crate::tests::shared_tokio_runtime;
920

921
    impl PeerStanding {
922
        pub fn init(
923
            standing: i32,
924
            latest_punishment: Option<(NegativePeerSanction, SystemTime)>,
925
            latest_reward: Option<(PositivePeerSanction, SystemTime)>,
926
            peer_tolerance: i32,
927
        ) -> PeerStanding {
928
            Self {
929
                standing,
930
                latest_punishment,
931
                latest_reward,
932
                peer_tolerance,
933
            }
934
        }
935
    }
936

937
    #[apply(shared_tokio_runtime)]
938
    async fn sync_challenge_response_pow_witnesses_must_be_a_chain() {
939
        let network = Network::Main;
940
        let genesis = Block::genesis(network);
941
        let mut rng = rand::rng();
942
        let ten_blocks: [Block; SYNC_CHALLENGE_POW_WITNESS_LENGTH] =
943
            fake_valid_sequence_of_blocks_for_tests(
944
                &genesis,
945
                Timestamp::minutes(20),
946
                rng.random(),
947
                network,
948
            )
949
            .await;
950

951
        let to_pow_witness = |block: &Block| {
952
            BlockHeaderWithBlockHashWitness::new(
953
                *block.header(),
954
                HeaderToBlockHashWitness::from(block),
955
            )
956
        };
957

958
        let mut i = SYNC_CHALLENGE_POW_WITNESS_LENGTH;
959
        let mut block;
960
        let mut valid_pow_chain = vec![];
961
        while valid_pow_chain.len() < SYNC_CHALLENGE_POW_WITNESS_LENGTH {
962
            i -= 1;
963
            block = &ten_blocks[i];
964
            valid_pow_chain.push(to_pow_witness(block));
965
        }
966

967
        let tip = &ten_blocks[SYNC_CHALLENGE_POW_WITNESS_LENGTH - 1];
968
        let valid_pow_chain: [BlockHeaderWithBlockHashWitness; SYNC_CHALLENGE_POW_WITNESS_LENGTH] =
969
            valid_pow_chain.try_into().unwrap();
970
        assert!(SyncChallengeResponse::pow_witnesses_form_chain_from_tip(
971
            tip.hash(),
972
            &valid_pow_chain
973
        ));
974

975
        for j in 0..SYNC_CHALLENGE_POW_WITNESS_LENGTH {
976
            let mut invalid_pow_chain = valid_pow_chain.clone();
977
            invalid_pow_chain[j].header.prev_block_digest = random();
978
            assert!(!SyncChallengeResponse::pow_witnesses_form_chain_from_tip(
979
                tip.hash(),
980
                &invalid_pow_chain
981
            ));
982
        }
983

984
        for j in 0..SYNC_CHALLENGE_POW_WITNESS_LENGTH {
985
            let mut invalid_pow_chain = valid_pow_chain.clone();
986
            invalid_pow_chain[j].header.nonce = random();
987
            assert!(!SyncChallengeResponse::pow_witnesses_form_chain_from_tip(
988
                tip.hash(),
989
                &invalid_pow_chain
990
            ));
991
        }
992
    }
993
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc