jam-std-common 0.1.28

Common datatypes and utilities for the JAM nodes and tooling
Documentation
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
use crate::{
	hash_encoded, hash_raw, header::OffendersMark, AvailabilityAssurance, EpochIndex, ErasureRoot,
	TicketEnvelope,
};
use bounded_collections::ConstU32;
use bytes::Bytes;
use codec::{Compact, CompactLen, ConstEncodedLen, Decode, Encode, MaxEncodedLen};
use jam_types::{
	max_report_elective_data, max_work_items, val_count, AuthTrace, AuthorizerHash, BoundedVec,
	CoreCount, CoreIndex, ExtrinsicHash, FixedVec, MaxDependencies, MaxTicketsPerBlock,
	MaxWorkItems, RefineContext, SegmentTreeRoot, ServiceId, Slot, UnsignedGas, ValIndex,
	ValSuperMajority, VecMap, WorkDigest, WorkPackageHash, WorkReportHash, VALS_PER_CORE,
};

pub const MAX_VERDICTS_COUNT: usize = 16;
pub const MAX_OFFENSES_COUNT: usize = 16;

/// A bunch of preimages.
pub type PreimagesXt = Vec<Preimage>;
/// A collection of preimage info (hash + requestor).
pub type LightPreimagesXt = Vec<LightPreimage>;
/// A collection of ticket envelopes.
pub type TicketsXt = BoundedVec<TicketEnvelope, MaxTicketsPerBlock>;
/// A bunch of assurances.
pub type AssurancesXt = BoundedVec<AvailabilityAssurance, jam_types::ValCount>;
/// A bunch of guarantees.
pub type GuaranteesXt = BoundedVec<ReportGuarantee, CoreCount>;
/// A collection of lightweight report info (hash + slot + signatures).
pub type LightGuaranteesXt = BoundedVec<LightReportGuarantee, CoreCount>;

/// The disputes extrinsic.
#[derive(Clone, Encode, Decode, Debug, Default, MaxEncodedLen)]
pub struct DisputesXt {
	/// Disputed work reports together with their judgements.
	pub verdicts: BoundedVec<Verdict, ConstU32<{ MAX_VERDICTS_COUNT as u32 }>>,
	/// Validators who guaranteed a work report subsequently determined to be invalid.
	pub culprits: BoundedVec<CulpritProof, ConstU32<{ MAX_OFFENSES_COUNT as u32 }>>,
	/// Validators who audited a work report in conflict with the final dispute resolution.
	pub faults: BoundedVec<FaultProof, ConstU32<{ MAX_OFFENSES_COUNT as u32 }>>,
}

impl DisputesXt {
	pub fn offenders_mark(&self) -> OffendersMark {
		let offenders: Vec<_> = self
			.culprits
			.iter()
			.map(|v| v.key)
			.chain(self.faults.iter().map(|v| v.key))
			.collect();
		offenders.try_into().expect("OffendersMark bounds equal culprits + faults")
	}
	pub fn implies_offenders_mark(&self, offenders_mark: &OffendersMark) -> bool {
		self.culprits
			.iter()
			.map(|v| &v.key)
			.chain(self.faults.iter().map(|v| &v.key))
			.eq(offenders_mark.iter())
	}
}

/// Judgements coming from a supermajority of either the active validators
/// set (κ) or the previous epoch's validator set (λ).
pub type VerdictVotes = FixedVec<Judgement, ValSuperMajority>;

/// Collection of judgements for a given target work report.
#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
pub struct Verdict {
	/// Target work report.
	pub target: WorkReportHash,
	/// Epoch index of the prior state or one less depending on the key set
	/// used to sign the votes.
	pub age: EpochIndex,
	/// Verdict votes collection.
	pub votes: VerdictVotes,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VerdictKind {
	Good,
	Bad,
	Wonky,
}

impl Verdict {
	/// Returns the kind of verdict (good/bad/wonky) based on the vote split, or `Err` if the vote
	/// split is invalid.
	pub fn kind(&self) -> Result<VerdictKind, ()> {
		let valid_count = self.votes.iter().filter(|v| v.vote).count();
		if valid_count == self.votes.len() {
			Ok(VerdictKind::Good)
		} else if valid_count == 0 {
			Ok(VerdictKind::Bad)
		} else if valid_count as u16 == val_count() / 3 {
			Ok(VerdictKind::Wonky)
		} else {
			Err(())
		}
	}
}

#[derive(Clone, Encode, Decode, Debug, PartialEq, MaxEncodedLen)]
pub struct Judgement {
	pub vote: bool,
	pub index: ValIndex,
	pub signature: super::ed25519::Signature,
}

#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
pub struct CulpritProof {
	pub report_hash: WorkReportHash,
	pub key: super::ed25519::Public,
	pub signature: super::ed25519::Signature,
}

#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
pub struct FaultProof {
	pub report_hash: WorkReportHash,
	pub vote: bool,
	pub key: super::ed25519::Public,
	pub signature: super::ed25519::Signature,
}

#[derive(Clone, Encode, Decode, Debug, Default)]
pub struct Extrinsic {
	pub tickets: TicketsXt,
	pub preimages: PreimagesXt,
	pub guarantees: GuaranteesXt,
	pub assurances: AssurancesXt,
	pub disputes: DisputesXt,
}

#[derive(Clone, Encode, Decode, Debug, Default)]
pub struct LightExtrinsic {
	pub tickets: TicketsXt,
	pub preimages: LightPreimagesXt,
	pub guarantees: LightGuaranteesXt,
	pub assurances: AssurancesXt,
	pub disputes: DisputesXt,
}

impl Extrinsic {
	pub fn guarantees_prehashed(&self) -> Vec<(WorkReportHash, Slot, &GuaranteeSignatures)> {
		self.guarantees
			.iter()
			.map(|r| (r.report.hash(), r.slot, &r.signatures))
			.collect()
	}

	pub fn hash(&self) -> ExtrinsicHash {
		let tickets_hash = hash_encoded(&self.tickets);
		let disputes_hash = hash_encoded(&self.disputes);
		let preimages_hash = hash_encoded(&self.preimages);
		let assurances_hash = hash_encoded(&self.assurances);
		let guarantees_hash = hash_encoded(&self.guarantees_prehashed());
		let top = (tickets_hash, preimages_hash, guarantees_hash, assurances_hash, disputes_hash);
		hash_encoded(&top).into()
	}

	pub fn encode_as_light<T: codec::Output + ?Sized>(&self, dest: &mut T) {
		self.tickets.encode_to(dest);
		let size = codec::Compact::<u32>(self.preimages.len() as u32);
		size.encode_to(dest);
		for preimage in &self.preimages {
			preimage.as_light().encode_to(dest);
		}
		let size = codec::Compact::<u32>(self.guarantees.len() as u32);
		size.encode_to(dest);
		for guarantee in &self.guarantees {
			guarantee.as_light().encode_to(dest);
		}
		self.assurances.encode_to(dest);
		self.disputes.encode_to(dest);
	}
}

#[derive(Clone, Debug, Eq, PartialOrd, Ord, Encode)]
pub struct Preimage {
	pub requester: ServiceId,
	blob: Bytes,
	#[codec(skip)]
	hash: jam_types::Hash,
}

impl PartialEq<Preimage> for Preimage {
	fn eq(&self, other: &Preimage) -> bool {
		self.requester == other.requester && self.hash == other.hash
	}
}

impl Preimage {
	pub fn as_light(&self) -> LightPreimage {
		LightPreimage { requester: self.requester, hash: self.hash() }
	}
}

impl Decode for Preimage {
	fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
		let requester: ServiceId = Decode::decode(input)?;
		let blob: Bytes = Decode::decode(input)?;
		let hash = hash_raw(&blob);
		Ok(Preimage { requester, blob, hash })
	}
}

impl Preimage {
	/// Utility to create preimage for testing: calculate hash of blob internally.
	/// Jam code do not need it as it is either known (persistence) or calculated in `decode`
	/// method.
	pub fn new_testing(requester: ServiceId, blob: impl Into<Bytes>) -> Self {
		let blob: Bytes = blob.into();
		let hash = hash_raw(&blob);
		Self { requester, blob, hash }
	}

	/// Warning hash parameter for this method MUST be the hash of the blob (we only add it as
	/// parameter to avoid hash computation when already checked, for instance when read from db).
	pub fn new_with_hash(
		requester: ServiceId,
		blob: impl Into<Bytes>,
		hash: jam_types::Hash,
	) -> Self {
		let blob: Bytes = blob.into();
		debug_assert_eq!(hash_raw(&blob), hash);
		Self { requester, blob, hash }
	}

	pub fn hash(&self) -> jam_types::Hash {
		self.hash
	}

	pub fn blob(&self) -> &Bytes {
		&self.blob
	}

	pub fn encoded_len(blob_len: usize) -> usize {
		core::mem::size_of::<ServiceId>() +
			Compact::<u64>::compact_len(&(blob_len as u64)) +
			blob_len
	}
}

/// Preimage data stored in db, actual blob is part of state.
#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct LightPreimage {
	pub requester: ServiceId,
	pub hash: jam_types::Hash,
}

/// A Work Report along with a guarantee of its correctness from staked sources.
#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct ReportGuarantee {
	/// The Work Report which is being attested.
	pub report: WorkReport,
	/// The slot following production of the report.
	/// The validator set and hence the meaning of the
	/// validator indices in `signatures` is inferred from this. The indices must be a subset of
	/// those assigned to `report.core_index` during the slot.
	pub slot: Slot,
	/// The signatures from the guarantors whose message is the hash of the `report`.
	/// The order of the signatures is the same order as the validators appear in
	/// the epochal validator set.
	pub signatures: GuaranteeSignatures,
}

impl ReportGuarantee {
	pub fn as_light(&self) -> LightReportGuaranteeEncode<'_> {
		LightReportGuaranteeEncode {
			report_hash: self.report.hash(),
			slot: self.slot,
			signatures: &self.signatures,
		}
	}
}

/// Report guarantee but only with a hash pointing to work report.
#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct LightReportGuarantee {
	pub report_hash: WorkReportHash,
	pub slot: Slot,
	pub signatures: GuaranteeSignatures,
}

#[derive(Encode, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct LightReportGuaranteeEncode<'a> {
	pub report_hash: WorkReportHash,
	pub slot: Slot,
	pub signatures: &'a GuaranteeSignatures,
}

pub type GuaranteeSignatures = BoundedVec<ValSignature, ConstU32<{ VALS_PER_CORE as u32 }>>;

#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, MaxEncodedLen)]
pub struct ValSignature {
	pub val_index: ValIndex,
	pub signature: super::ed25519::Signature,
}

impl ConstEncodedLen for ValSignature {}

/// Secure reference to a Work Package.
#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, MaxEncodedLen)]
pub struct WorkPackageSpec {
	/// The hash of the Work Package.
	pub hash: WorkPackageHash,
	/// The length in bytes of Work Bundle.
	pub len: u32,
	/// The erasure root of the Work Bundle and export-segment pieces.
	pub erasure_root: ErasureRoot,
	/// The segment root of the Work Package.
	pub exports_root: SegmentTreeRoot,
	/// The number of segments exported by the Work Package.
	pub exports_count: u16,
}

impl WorkPackageSpec {
	pub fn wp_srl(&self) -> (WorkPackageHash, SegmentTreeRoot) {
		(self.hash, self.exports_root)
	}
}

/// Execution report of a Work Package, mainly comprising the Results from the Refinement
/// of its Work Items.
#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq)]
pub struct WorkReport {
	/// The specification of the underlying Work Package.
	pub package_spec: WorkPackageSpec,
	/// The context of the underlying Work Package.
	pub context: RefineContext,
	/// The Core index under which the Work Package was Refined to generate the Report.
	#[codec(compact)]
	pub core_index: CoreIndex,
	/// The authorizer under which this Work Package got executed. For the Work Package to be
	/// validly reported, this must appear in the Core's authorizer queue at the time of reporting.
	pub authorizer_hash: AuthorizerHash,
	/// The amount of gas actually used by the IsAuthorized call.
	#[codec(compact)]
	pub auth_gas_used: UnsignedGas,
	/// The output of the authorizer under which this Work Package got executed.
	pub auth_output: AuthTrace,
	/// The segment-root lookup dictionary.
	pub sr_lookup: VecMap<WorkPackageHash, SegmentTreeRoot>,
	/// The results of the evaluation of the Items in the underlying Work Package.
	pub results: BoundedVec<WorkDigest, MaxWorkItems>,
}

impl WorkReport {
	/// Report hash.
	pub fn hash(&self) -> WorkReportHash {
		hash_encoded(self).into()
	}

	/// Report total gas requirement.
	pub fn gas(&self) -> UnsignedGas {
		self.results.iter().map(|r| r.accumulate_gas).sum()
	}

	/// Report dependencies derived from both context prerequisites and the segments root
	/// lookup dictionary.
	pub fn deps(&self) -> impl Iterator<Item = &WorkPackageHash> {
		self.sr_lookup.keys().chain(self.context.prerequisites.iter())
	}

	/// Count report dependencies derived from both context prerequisites and the segments root
	/// lookup dictionary.
	pub fn dep_count(&self) -> usize {
		self.sr_lookup.len() + self.context.prerequisites.len()
	}

	/// Determine if the report size is within the limit.
	pub fn check_size(&self) -> bool {
		let total: usize =
			self.results.iter().filter_map(|r| Some(r.result.as_ref().ok()?.len())).sum();
		total + self.auth_output.len() <= max_report_elective_data() as usize
	}

	/// Work package hash for this report.
	pub fn package_hash(&self) -> WorkPackageHash {
		self.package_spec.hash
	}
}

impl MaxEncodedLen for WorkReport {
	fn max_encoded_len() -> usize {
		let mut max = WorkPackageSpec::max_encoded_len() +
			RefineContext::max_encoded_len() +
			codec::Compact::<CoreIndex>::max_encoded_len() +
			AuthorizerHash::max_encoded_len() +
			AuthTrace::max_encoded_len() +
			BoundedVec::<(WorkPackageHash, SegmentTreeRoot), MaxDependencies>::max_encoded_len() + // sr_lookup
			BoundedVec::<WorkDigest, MaxWorkItems>::max_encoded_len() +
			codec::Compact::<UnsignedGas>::max_encoded_len(); // auth_gas_used

		// In the max expression above, the max_report_elective_data() bound is effectively applied
		// separately to auth_output and item results. It actually applies to the combined size of
		// these -- they cannot _all_ have maximum length. Account for this.
		max -= max_work_items() * (max_report_elective_data() as usize);

		max
	}
}

pub mod tests_utils {
	use super::*;
	use bounded_collections::TryCollect;
	use jam_types::{max_dependencies, RefineLoad};

	// The largest possible work-report, or at least very close -- it might be possible to get
	// a few bytes more by eg rebalancing the output Vec lengths, as Vec lengths use compact
	// encoding.
	//
	// This is actually a bit larger than the largest valid report as both the prerequisites
	// set and the sr_lookup map have max_dependencies() entries; the max_dependencies() limit
	// is supposed to apply to the combined count. WorkReport::max_encoded_len() doesn't
	// account for this though, so we don't bother accounting for it here either.
	pub fn largest_report() -> WorkReport {
		let largest_spec = WorkPackageSpec {
			hash: Default::default(),
			len: u32::MAX,
			erasure_root: Default::default(),
			exports_root: Default::default(),
			exports_count: u16::MAX,
		};

		let max_outputs = 1 + max_work_items(); // AuthTrace + WorkOutputs
		let output = |i| {
			let mut size = (max_report_elective_data() as usize) / max_outputs;
			if i == 0 {
				size += (max_report_elective_data() as usize) % max_outputs;
			}
			vec![0; size]
		};

		WorkReport {
			package_spec: largest_spec,
			context: RefineContext::largest(),
			core_index: CoreIndex::MAX,
			authorizer_hash: Default::default(),
			auth_output: output(0).into(),
			sr_lookup: (0..max_dependencies())
				.map(|i| ([i as u8; 32].into(), Default::default()))
				.collect(),
			results: (0..max_work_items())
				.map(|i| WorkDigest {
					service: ServiceId::MAX,
					code_hash: Default::default(),
					payload_hash: Default::default(),
					accumulate_gas: UnsignedGas::MAX,
					result: Ok(output(1 + i).into()),
					refine_load: RefineLoad {
						gas_used: UnsignedGas::MAX,
						imports: u16::MAX,
						extrinsic_count: u16::MAX,
						extrinsic_size: u32::MAX,
						exports: u16::MAX,
					},
				})
				.try_collect()
				.expect("max workitem should be this vec bound"),
			auth_gas_used: UnsignedGas::MAX,
		}
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use jam_types::{max_work_items, MAX_PREIMAGE_BLOB_LEN, MAX_PREIMAGE_LEN};

	#[test]
	fn max_preimage_blob_len_is_correct() {
		let preimage = Preimage::new_testing(ServiceId::MAX, vec![123_u8; MAX_PREIMAGE_BLOB_LEN]);
		let encoded_len = preimage.encode().len();
		assert_eq!(MAX_PREIMAGE_LEN, encoded_len);
	}

	#[test]
	fn preimage_encoded_len_works() {
		for blob_len in (0..MAX_PREIMAGE_BLOB_LEN).step_by(997) {
			let preimage = Preimage::new_testing(ServiceId::MAX, vec![123_u8; blob_len]);
			let expected_len = Preimage::encoded_len(blob_len);
			let actual_len = preimage.encode().len();
			assert_eq!(expected_len, actual_len);
		}
	}

	#[test]
	fn report_max_encoded_len() {
		let largest_report = super::tests_utils::largest_report();
		assert_eq!(largest_report.package_spec.encoded_size(), WorkPackageSpec::max_encoded_len());

		let largest_report_size = largest_report.encoded_size();

		// WorkReport::max_encoded_len() assumes each "output" Vec can be the maximum length when
		// calculating how many bytes are needed for encoding Vec lengths in the worst case. The
		// Vecs cannot all be the maximum length though due to the shared nature of
		// max_report_elective_data(). To account for this discrepancy, we allow
		// WorkReport::max_encoded_len() to overshoot largest_report_size by 1 for each such Vec.
		let max = WorkReport::max_encoded_len();
		assert!(largest_report_size <= max);
		let max_outputs = 1 + max_work_items(); // AuthTrace + WorkOutputs
		assert!((largest_report_size + max_outputs) >= max);
	}
}