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
//! Peer reputation management

/// The default reputation of a peer
pub const DEFAULT_REPUTATION: Reputation = 0;

/// The minimal unit we're measuring reputation
const REPUTATION_UNIT: i32 = -1024;

/// The reputation value below which new connection from/to peers are rejected.
pub const BANNED_REPUTATION: i32 = 50 * REPUTATION_UNIT;

/// The reputation change to apply to a peer that dropped the connection.
const REMOTE_DISCONNECT_REPUTATION_CHANGE: i32 = 4 * REPUTATION_UNIT;

/// The reputation change to apply to a peer that we failed to connect to.
const FAILED_TO_CONNECT_REPUTATION_CHANGE: i32 = 25 * REPUTATION_UNIT;

/// The reputation change to apply to a peer that failed to respond in time.
const TIMEOUT_REPUTATION_CHANGE: i32 = 4 * REPUTATION_UNIT;

/// The reputation change to apply to a peer that sent a bad message.
const BAD_MESSAGE_REPUTATION_CHANGE: i32 = 16 * REPUTATION_UNIT;

/// The reputation change applies to a peer that has sent a transaction (full or hash) that we
/// already know about and have already previously received from that peer.
///
/// Note: this appears to be quite common in practice, so by default this is 0, which doesn't
/// apply any changes to the peer's reputation, effectively ignoring it.
const ALREADY_SEEN_TRANSACTION_REPUTATION_CHANGE: i32 = 0;

/// The reputation change to apply to a peer which violates protocol rules: minimal reputation
const BAD_PROTOCOL_REPUTATION_CHANGE: i32 = i32::MIN;

/// The reputation change to apply to a peer that sent a bad announcement.
// todo: current value is a hint, needs to be set properly
const BAD_ANNOUNCEMENT_REPUTATION_CHANGE: i32 = REPUTATION_UNIT;

/// The maximum reputation change that can be applied to a trusted peer.
/// This is used to prevent a single bad message from a trusted peer to cause a significant change.
/// This gives a trusted peer more leeway when interacting with the node, which is useful for in
/// custom setups. By not setting this to `0` we still allow trusted peer penalization but less than
/// untrusted peers.
pub const MAX_TRUSTED_PEER_REPUTATION_CHANGE: Reputation = 2 * REPUTATION_UNIT;

/// Returns `true` if the given reputation is below the [`BANNED_REPUTATION`] threshold
#[inline]
pub const fn is_banned_reputation(reputation: i32) -> bool {
    reputation < BANNED_REPUTATION
}

/// The type that tracks the reputation score.
pub type Reputation = i32;

/// Various kinds of reputation changes.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ReputationChangeKind {
    /// Received an unspecific bad message from the peer
    BadMessage,
    /// Peer sent a bad block.
    ///
    /// Note: this will we only used in pre-merge, pow consensus, since after no more block announcements are sent via devp2p: [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#devp2p)
    BadBlock,
    /// Peer sent a bad transaction message. E.g. Transactions which weren't recoverable.
    BadTransactions,
    /// Peer sent a bad announcement message, e.g. invalid transaction type for the configured
    /// network.
    BadAnnouncement,
    /// Peer sent a message that included a hash or transaction that we already received from the
    /// peer.
    ///
    /// According to the [Eth spec](https://github.com/ethereum/devp2p/blob/master/caps/eth.md):
    ///
    /// > A node should never send a transaction back to a peer that it can determine already knows
    /// > of it (either because it was previously sent or because it was informed from this peer
    /// > originally). This is usually achieved by remembering a set of transaction hashes recently
    /// > relayed by the peer.
    AlreadySeenTransaction,
    /// Peer failed to respond in time.
    Timeout,
    /// Peer does not adhere to network protocol rules.
    BadProtocol,
    /// Failed to establish a connection to the peer.
    FailedToConnect,
    /// Connection dropped by peer.
    Dropped,
    /// Reset the reputation to the default value.
    Reset,
    /// Apply a reputation change by value
    Other(Reputation),
}

impl ReputationChangeKind {
    /// Returns true if the reputation change is a [`ReputationChangeKind::Reset`].
    pub const fn is_reset(&self) -> bool {
        matches!(self, Self::Reset)
    }

    /// Returns true if the reputation change is [`ReputationChangeKind::Dropped`].
    pub const fn is_dropped(&self) -> bool {
        matches!(self, Self::Dropped)
    }
}

/// How the [`ReputationChangeKind`] are weighted.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct ReputationChangeWeights {
    /// Weight for [`ReputationChangeKind::BadMessage`]
    pub bad_message: Reputation,
    /// Weight for [`ReputationChangeKind::BadBlock`]
    pub bad_block: Reputation,
    /// Weight for [`ReputationChangeKind::BadTransactions`]
    pub bad_transactions: Reputation,
    /// Weight for [`ReputationChangeKind::AlreadySeenTransaction`]
    pub already_seen_transactions: Reputation,
    /// Weight for [`ReputationChangeKind::Timeout`]
    pub timeout: Reputation,
    /// Weight for [`ReputationChangeKind::BadProtocol`]
    pub bad_protocol: Reputation,
    /// Weight for [`ReputationChangeKind::FailedToConnect`]
    pub failed_to_connect: Reputation,
    /// Weight for [`ReputationChangeKind::Dropped`]
    pub dropped: Reputation,
    /// Weight for [`ReputationChangeKind::BadAnnouncement`]
    pub bad_announcement: Reputation,
}

// === impl ReputationChangeWeights ===

impl ReputationChangeWeights {
    /// Returns the quantifiable [`ReputationChange`] for the given [`ReputationChangeKind`] using
    /// the configured weights
    pub fn change(&self, kind: ReputationChangeKind) -> ReputationChange {
        match kind {
            ReputationChangeKind::BadMessage => self.bad_message.into(),
            ReputationChangeKind::BadBlock => self.bad_block.into(),
            ReputationChangeKind::BadTransactions => self.bad_transactions.into(),
            ReputationChangeKind::AlreadySeenTransaction => self.already_seen_transactions.into(),
            ReputationChangeKind::Timeout => self.timeout.into(),
            ReputationChangeKind::BadProtocol => self.bad_protocol.into(),
            ReputationChangeKind::FailedToConnect => self.failed_to_connect.into(),
            ReputationChangeKind::Dropped => self.dropped.into(),
            ReputationChangeKind::Reset => DEFAULT_REPUTATION.into(),
            ReputationChangeKind::Other(val) => val.into(),
            ReputationChangeKind::BadAnnouncement => self.bad_announcement.into(),
        }
    }
}

impl Default for ReputationChangeWeights {
    fn default() -> Self {
        Self {
            bad_block: BAD_MESSAGE_REPUTATION_CHANGE,
            bad_transactions: BAD_MESSAGE_REPUTATION_CHANGE,
            already_seen_transactions: ALREADY_SEEN_TRANSACTION_REPUTATION_CHANGE,
            bad_message: BAD_MESSAGE_REPUTATION_CHANGE,
            timeout: TIMEOUT_REPUTATION_CHANGE,
            bad_protocol: BAD_PROTOCOL_REPUTATION_CHANGE,
            failed_to_connect: FAILED_TO_CONNECT_REPUTATION_CHANGE,
            dropped: REMOTE_DISCONNECT_REPUTATION_CHANGE,
            bad_announcement: BAD_ANNOUNCEMENT_REPUTATION_CHANGE,
        }
    }
}

/// Represents a change in a peer's reputation.
#[derive(Debug, Copy, Clone, Default)]
pub struct ReputationChange(Reputation);

// === impl ReputationChange ===

impl ReputationChange {
    /// Helper type for easier conversion
    #[inline]
    pub const fn as_i32(self) -> Reputation {
        self.0
    }
}

impl From<ReputationChange> for Reputation {
    fn from(value: ReputationChange) -> Self {
        value.0
    }
}

impl From<Reputation> for ReputationChange {
    fn from(value: Reputation) -> Self {
        Self(value)
    }
}

/// Outcomes when a reputation change is applied to a peer
#[derive(Debug, Clone, Copy)]
pub enum ReputationChangeOutcome {
    /// Nothing to do.
    None,
    /// Ban the peer.
    Ban,
    /// Ban and disconnect
    DisconnectAndBan,
    /// Unban the peer
    Unban,
}