-
Notifications
You must be signed in to change notification settings - Fork 78
Expand file tree
/
Copy pathvalidator_sets.rs
More file actions
322 lines (298 loc) · 12.1 KB
/
validator_sets.rs
File metadata and controls
322 lines (298 loc) · 12.1 KB
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
use alloc::vec::Vec;
use borsh::{io, BorshSerialize, BorshDeserialize};
use sp_core::{ConstU32, bounded::BoundedVec};
use serai_primitives::{
BitVec,
crypto::{EmbeddedEllipticCurveKeys, SignedEmbeddedEllipticCurveKeys, KeyPair, Signature},
address::SeraiAddress,
balance::Amount,
network_id::*,
validator_sets::*,
};
/// The address used by the validator sets pallet.
pub fn address() -> SeraiAddress {
SeraiAddress::system(borsh::to_vec(b"ValidatorSets").unwrap())
}
/// Slash(es) to occur on-chain.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Slashes {
/// A single slash for a specific validator of the Serai network.
Serai {
/// The session the misbehavior occurred during.
session: Session,
/// The validator being slashed.
validator: SeraiAddress,
/// The reason for the slash, represented as an opaque byte blob.
///
/// This reason is not validated on-chain. Instead, with a trust assumption the validator set
/// for Serai is honest, an inherent transaction is used (where the Serai validators will only
/// build on a block which includes it if they agree with it). Not only is this reasonable, and
/// a trust assumption already in place for validators of external networks, it avoids binding
/// to the BABE/GRANDPA equivocation proofs within the on-chain protocol.
///
/// The byte blob is present so a validator who observed a fault may effectively communicate it
/// to the other validators. It also has the benefit of allowing public inspection of the
/// reason.
///
/// The reason being limited to `u16::MAX` does mean an equivocation proof larger than 64 KiB
/// cannot be communicated in this method. As an equivocation proof, as defined in
/// [`sp-consensus-babe`], is primarily a pair of headers, Serai benefits from having defined a
/// fixed-size header (removing the `Digest` from its canonical wire format). This allows
/// representing an `EquivocationProof` not with [`SubstrateHeader`] but
/// `(Header, Digest::new(substrate_header.digest().find(babe)))` which can be of bounded
/// length even while the digest as a whole remains unbounded (its own sin).
#[expect(clippy::as_conversions)]
reason: BoundedVec<u8, ConstU32<{ u16::MAX as u32 }>>,
},
/// A [`SlashReport`] from an external network.
ExternalNetwork {
/// The validator set which is reporting their slashes.
set: ExternalValidatorSet,
/// The slashes they're reporting.
slashes: SlashReport,
/// The signature confirming the validity of this slash report.
///
/// This is defined as a `Signature`. This may change in the future, as while `Signature` may
/// be the type used to sign transactions, this signature must support efficient proving by a
/// multi-party protocol. While currently, both concepts can be fulfilled by
/// `RistrettoSignature` (as versioned by `Signature`), in the future, potential upgrades may
/// diverge.
signature: Signature,
},
}
impl BorshSerialize for Slashes {
fn serialize<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
match self {
Slashes::Serai { session, validator, reason } => {
(NetworkId::Serai, session, validator).serialize(writer)?;
serai_primitives::sp_borsh::borsh_serialize_bounded_vec(reason, writer)
}
Slashes::ExternalNetwork { set, slashes, signature } => {
(set, slashes, signature).serialize(writer)
}
}
}
}
impl BorshDeserialize for Slashes {
fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
Ok(match NetworkId::deserialize_reader(reader)? {
NetworkId::Serai => Slashes::Serai {
session: <_>::deserialize_reader(reader)?,
validator: <_>::deserialize_reader(reader)?,
reason: serai_primitives::sp_borsh::borsh_deserialize_bounded_vec(reader)?,
},
NetworkId::External(network) => Slashes::ExternalNetwork {
set: ExternalValidatorSet { network, session: <_>::deserialize_reader(reader)? },
slashes: <_>::deserialize_reader(reader)?,
signature: <_>::deserialize_reader(reader)?,
},
})
}
}
/// A call to the validator sets module.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Call {
/// Set the keys for a validator set.
set_keys {
/// The network whose latest decided validator set is setting their keys.
network: ExternalNetworkId,
/// The keys being set.
key_pair: KeyPair,
/// The participants in the validator set who signed off on these keys.
signature_participants: BitVec<{ KeyShares::MAX_PER_SET_U64 }>,
/// The signature confirming these keys are valid.
///
/// This is defined as a `Signature`. This may change in the future, as while `Signature` may
/// be the type used to sign transactions, this signature must support verification for a
/// non-interactive aggregate key of the participating validators. While currently, both
/// concepts can be fulfilled by `RistrettoSignature` (as versioned by `Signature`), in the
/// future, potential upgrades may diverge.
signature: Signature,
},
/// Report a validator set's slashes onto Serai.
report_slashes(Slashes),
/// Set a validator's keys on embedded elliptic curves for a specific network.
set_embedded_elliptic_curve_keys {
/// The keys on the embedded elliptic curves.
keys: SignedEmbeddedEllipticCurveKeys,
},
/// Allocate stake to a network.
allocate {
/// The network to allocate stake to.
network: NetworkId,
/// The amount of stake to allocate.
amount: Amount,
},
/// Deallocate stake from a network.
///
/// This deallocation may be immediate or may be delayed depending on if the origin is an
/// active, or even recent, validator. If delayed, it will have to be claimed at a later time.
deallocate {
/// The network to deallocate stake from.
network: NetworkId,
/// The amount of stake to deallocate.
amount: Amount,
},
/// Claim a now-unlocked deallocation.
claim_deallocation {
/// The validator set which claiming the deallocation was delayed until.
deallocation: ValidatorSet,
},
}
impl Call {
pub(crate) fn is_signed(&self) -> bool {
match self {
Call::set_keys { .. } | Call::report_slashes { .. } => false,
Call::set_embedded_elliptic_curve_keys { .. } |
Call::allocate { .. } |
Call::deallocate { .. } |
Call::claim_deallocation { .. } => true,
}
}
}
/// An event from the validator sets module.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Event {
/// A new validator set was decided.
SetDecided {
/// The set decided.
set: ValidatorSet,
/// The validators decided to be included in the set.
validators: Vec<(SeraiAddress, KeyShares)>,
},
/// A validator set has set their keys.
SetKeys {
/// The set which set their keys.
set: ExternalValidatorSet,
/// The keys sets.
key_pair: KeyPair,
},
/// A validator set has accepted responsibility from the prior validator set.
AcceptedHandover {
/// The set which accepted responsibility from the prior set.
set: ValidatorSet,
},
/// A marker event for slashes having occured for a validator set.
///
/// In the case of `set.network == NetworkId::Serai`, this signifies a single validator was
/// slashed. In the case of `matches!(set.network, NetworkId::External(_))`, this signals the
/// set published a slash report _or_ a default slash report was used due to the publication
/// window timing out. This means every external validator set will trigger this event, as useful
/// for determining if a validator set has any outstanding operations (as reporting slashes will
/// be its final act).
Slashes {
/// The set for which slashes have occurred.
set: ValidatorSet,
},
/// A validator set their keys on an embedded elliptic curve for a network.
SetEmbeddedEllipticCurveKeys {
/// The validator which set their keys.
validator: SeraiAddress,
/// The embedded elliptic curve keys they set.
keys: EmbeddedEllipticCurveKeys,
},
/// A validator's allocation to a network has increased.
Allocation {
/// The validator who increased their allocation.
validator: SeraiAddress,
/// The network the stake was allocated to.
network: NetworkId,
/// The amount of stake allocated.
amount: Amount,
},
/// A validator's allocation to a network has decreased.
Deallocation {
/// The validator who decreased their allocation.
validator: SeraiAddress,
/// The network the stake was deallocated from.
network: NetworkId,
/// The amount of stake deallocated.
amount: Amount,
/// The timeline for this deallocation.
timeline: DeallocationTimeline,
},
/// A validator's delayed deallocation from a network has been claimed.
DelayedDeallocationClaimed {
/// The validator who claimed their deallocation.
validator: SeraiAddress,
/// The validator set the deallocation was delayed until.
deallocation: ValidatorSet,
},
}
#[test]
fn serialize_slashes() {
use alloc::{vec, vec::Vec};
use rand_core::{RngCore as _, OsRng};
use serai_primitives::crypto::RistrettoSignature;
let external_networks = ExternalNetworkId::all().collect::<Vec<_>>();
for _ in 0 .. 1000 {
{
#[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
let session = Session(OsRng.next_u64() as u32);
let mut validator = [0; 32];
OsRng.fill_bytes(&mut validator);
let validator = SeraiAddress(validator);
let mut reason = vec![0u8; usize::try_from(OsRng.next_u64() % u64::from(u16::MAX)).unwrap()];
OsRng.fill_bytes(&mut reason);
let slashes = Slashes::Serai { session, validator, reason: reason.try_into().unwrap() };
assert_eq!(
slashes,
Slashes::deserialize_reader(&mut borsh::to_vec(&slashes).unwrap().as_slice()).unwrap()
);
}
{
#[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
let network = external_networks[(OsRng.next_u64() as usize) % external_networks.len()];
#[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
let session = Session(OsRng.next_u64() as u32);
let mut slashes = vec![];
for _ in 0 .. (OsRng.next_u64() % KeyShares::MAX_PER_SET_U64) {
if (OsRng.next_u64() & 1) == 1 {
slashes.push(Slash::Fatal);
} else {
#[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
slashes.push(Slash::Points(OsRng.next_u64() as u32));
}
}
let slashes = SlashReport(slashes.try_into().unwrap());
let mut signature = [0; 64];
OsRng.fill_bytes(&mut signature);
let signature = Signature::from(RistrettoSignature(signature));
let slashes = Slashes::ExternalNetwork {
set: ExternalValidatorSet { network, session },
slashes,
signature,
};
assert_eq!(
slashes,
Slashes::deserialize_reader(&mut borsh::to_vec(&slashes).unwrap().as_slice()).unwrap()
);
}
}
}
#[cfg(feature = "substrate")]
#[test]
fn babe_grandpa_equivocation_lengths() {
use scale::MaxEncodedLen as _;
use sp_runtime::traits::Header as HeaderTrait;
use crate::{Header, SubstrateHeader};
// Check two headers, as part of an equivocation proof, fit within a slash's reason once encoded
{
let two_serai_headers_with_babe_digest =
2 * (Header::SIZE + sp_consensus_babe::digests::PreDigest::max_encoded_len());
// We check we have a wide margin
assert!(two_serai_headers_with_babe_digest < usize::from(u16::MAX / 2));
}
// For GRANDPA, the equivocation does not implement `MaxEncodedLen` but does not contain any
// heap-allocated types (such as `Digest`), so we simplify to a `size_of` with an even more
// aggressive bound. We should be _well_ below this regardless
{
let grandpa_equivocation = core::mem::size_of::<
sp_consensus_grandpa::EquivocationProof<
<SubstrateHeader as HeaderTrait>::Hash,
<SubstrateHeader as HeaderTrait>::Number,
>,
>();
assert!(grandpa_equivocation < usize::from(u16::MAX / 64));
}
}