Skip to main content

jam_std_common/
extrinsic.rs

1use crate::{
2	hash_encoded, hash_raw, header::OffendersMark, AvailabilityAssurance, EpochIndex, ErasureRoot,
3	TicketEnvelope,
4};
5use bounded_collections::ConstU32;
6use bytes::Bytes;
7use codec::{Compact, CompactLen, ConstEncodedLen, Decode, Encode, MaxEncodedLen};
8use jam_types::{
9	max_report_elective_data, max_work_items, val_count, AuthTrace, AuthorizerHash, BoundedVec,
10	CoreCount, CoreIndex, ExtrinsicHash, FixedVec, MaxDependencies, MaxTicketsPerBlock,
11	MaxWorkItems, RefineContext, SegmentTreeRoot, ServiceId, Slot, UnsignedGas, ValIndex,
12	ValSuperMajority, VecMap, WorkDigest, WorkPackageHash, WorkReportHash, VALS_PER_CORE,
13};
14
15pub const MAX_VERDICTS_COUNT: usize = 16;
16pub const MAX_OFFENSES_COUNT: usize = 16;
17
18/// A bunch of preimages.
19pub type PreimagesXt = Vec<Preimage>;
20/// A collection of preimage info (hash + requestor).
21pub type LightPreimagesXt = Vec<LightPreimage>;
22/// A collection of ticket envelopes.
23pub type TicketsXt = BoundedVec<TicketEnvelope, MaxTicketsPerBlock>;
24/// A bunch of assurances.
25pub type AssurancesXt = BoundedVec<AvailabilityAssurance, jam_types::ValCount>;
26/// A bunch of guarantees.
27pub type GuaranteesXt = BoundedVec<ReportGuarantee, CoreCount>;
28/// A collection of lightweight report info (hash + slot + signatures).
29pub type LightGuaranteesXt = BoundedVec<LightReportGuarantee, CoreCount>;
30
31/// The disputes extrinsic.
32#[derive(Clone, Encode, Decode, Debug, Default, MaxEncodedLen)]
33pub struct DisputesXt {
34	/// Disputed work reports together with their judgements.
35	pub verdicts: BoundedVec<Verdict, ConstU32<{ MAX_VERDICTS_COUNT as u32 }>>,
36	/// Validators who guaranteed a work report subsequently determined to be invalid.
37	pub culprits: BoundedVec<CulpritProof, ConstU32<{ MAX_OFFENSES_COUNT as u32 }>>,
38	/// Validators who audited a work report in conflict with the final dispute resolution.
39	pub faults: BoundedVec<FaultProof, ConstU32<{ MAX_OFFENSES_COUNT as u32 }>>,
40}
41
42impl DisputesXt {
43	pub fn offenders_mark(&self) -> OffendersMark {
44		let offenders: Vec<_> = self
45			.culprits
46			.iter()
47			.map(|v| v.key)
48			.chain(self.faults.iter().map(|v| v.key))
49			.collect();
50		offenders.try_into().expect("OffendersMark bounds equal culprits + faults")
51	}
52	pub fn implies_offenders_mark(&self, offenders_mark: &OffendersMark) -> bool {
53		self.culprits
54			.iter()
55			.map(|v| &v.key)
56			.chain(self.faults.iter().map(|v| &v.key))
57			.eq(offenders_mark.iter())
58	}
59}
60
61/// Judgements coming from a supermajority of either the active validators
62/// set (κ) or the previous epoch's validator set (λ).
63pub type VerdictVotes = FixedVec<Judgement, ValSuperMajority>;
64
65/// Collection of judgements for a given target work report.
66#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
67pub struct Verdict {
68	/// Target work report.
69	pub target: WorkReportHash,
70	/// Epoch index of the prior state or one less depending on the key set
71	/// used to sign the votes.
72	pub age: EpochIndex,
73	/// Verdict votes collection.
74	pub votes: VerdictVotes,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum VerdictKind {
79	Good,
80	Bad,
81	Wonky,
82}
83
84impl Verdict {
85	/// Returns the kind of verdict (good/bad/wonky) based on the vote split, or `Err` if the vote
86	/// split is invalid.
87	pub fn kind(&self) -> Result<VerdictKind, ()> {
88		let valid_count = self.votes.iter().filter(|v| v.vote).count();
89		if valid_count == self.votes.len() {
90			Ok(VerdictKind::Good)
91		} else if valid_count == 0 {
92			Ok(VerdictKind::Bad)
93		} else if valid_count as u16 == val_count() / 3 {
94			Ok(VerdictKind::Wonky)
95		} else {
96			Err(())
97		}
98	}
99}
100
101#[derive(Clone, Encode, Decode, Debug, PartialEq, MaxEncodedLen)]
102pub struct Judgement {
103	pub vote: bool,
104	pub index: ValIndex,
105	pub signature: super::ed25519::Signature,
106}
107
108#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
109pub struct CulpritProof {
110	pub report_hash: WorkReportHash,
111	pub key: super::ed25519::Public,
112	pub signature: super::ed25519::Signature,
113}
114
115#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
116pub struct FaultProof {
117	pub report_hash: WorkReportHash,
118	pub vote: bool,
119	pub key: super::ed25519::Public,
120	pub signature: super::ed25519::Signature,
121}
122
123#[derive(Clone, Encode, Decode, Debug, Default)]
124pub struct Extrinsic {
125	pub tickets: TicketsXt,
126	pub preimages: PreimagesXt,
127	pub guarantees: GuaranteesXt,
128	pub assurances: AssurancesXt,
129	pub disputes: DisputesXt,
130}
131
132#[derive(Clone, Encode, Decode, Debug, Default)]
133pub struct LightExtrinsic {
134	pub tickets: TicketsXt,
135	pub preimages: LightPreimagesXt,
136	pub guarantees: LightGuaranteesXt,
137	pub assurances: AssurancesXt,
138	pub disputes: DisputesXt,
139}
140
141impl Extrinsic {
142	pub fn guarantees_prehashed(&self) -> Vec<(WorkReportHash, Slot, &GuaranteeSignatures)> {
143		self.guarantees
144			.iter()
145			.map(|r| (r.report.hash(), r.slot, &r.signatures))
146			.collect()
147	}
148
149	pub fn hash(&self) -> ExtrinsicHash {
150		let tickets_hash = hash_encoded(&self.tickets);
151		let disputes_hash = hash_encoded(&self.disputes);
152		let preimages_hash = hash_encoded(&self.preimages);
153		let assurances_hash = hash_encoded(&self.assurances);
154		let guarantees_hash = hash_encoded(&self.guarantees_prehashed());
155		let top = (tickets_hash, preimages_hash, guarantees_hash, assurances_hash, disputes_hash);
156		hash_encoded(&top).into()
157	}
158
159	pub fn encode_as_light<T: codec::Output + ?Sized>(&self, dest: &mut T) {
160		self.tickets.encode_to(dest);
161		let size = codec::Compact::<u32>(self.preimages.len() as u32);
162		size.encode_to(dest);
163		for preimage in &self.preimages {
164			preimage.as_light().encode_to(dest);
165		}
166		let size = codec::Compact::<u32>(self.guarantees.len() as u32);
167		size.encode_to(dest);
168		for guarantee in &self.guarantees {
169			guarantee.as_light().encode_to(dest);
170		}
171		self.assurances.encode_to(dest);
172		self.disputes.encode_to(dest);
173	}
174}
175
176#[derive(Clone, Debug, Eq, PartialOrd, Ord, Encode)]
177pub struct Preimage {
178	pub requester: ServiceId,
179	blob: Bytes,
180	#[codec(skip)]
181	hash: jam_types::Hash,
182}
183
184impl PartialEq<Preimage> for Preimage {
185	fn eq(&self, other: &Preimage) -> bool {
186		self.requester == other.requester && self.hash == other.hash
187	}
188}
189
190impl Preimage {
191	pub fn as_light(&self) -> LightPreimage {
192		LightPreimage { requester: self.requester, hash: self.hash() }
193	}
194}
195
196impl Decode for Preimage {
197	fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
198		let requester: ServiceId = Decode::decode(input)?;
199		let blob: Bytes = Decode::decode(input)?;
200		let hash = hash_raw(&blob);
201		Ok(Preimage { requester, blob, hash })
202	}
203}
204
205impl Preimage {
206	/// Utility to create preimage for testing: calculate hash of blob internally.
207	/// Jam code do not need it as it is either known (persistence) or calculated in `decode`
208	/// method.
209	pub fn new_testing(requester: ServiceId, blob: impl Into<Bytes>) -> Self {
210		let blob: Bytes = blob.into();
211		let hash = hash_raw(&blob);
212		Self { requester, blob, hash }
213	}
214
215	/// Warning hash parameter for this method MUST be the hash of the blob (we only add it as
216	/// parameter to avoid hash computation when already checked, for instance when read from db).
217	pub fn new_with_hash(
218		requester: ServiceId,
219		blob: impl Into<Bytes>,
220		hash: jam_types::Hash,
221	) -> Self {
222		let blob: Bytes = blob.into();
223		debug_assert_eq!(hash_raw(&blob), hash);
224		Self { requester, blob, hash }
225	}
226
227	pub fn hash(&self) -> jam_types::Hash {
228		self.hash
229	}
230
231	pub fn blob(&self) -> &Bytes {
232		&self.blob
233	}
234
235	pub fn encoded_len(blob_len: usize) -> usize {
236		core::mem::size_of::<ServiceId>() +
237			Compact::<u64>::compact_len(&(blob_len as u64)) +
238			blob_len
239	}
240}
241
242/// Preimage data stored in db, actual blob is part of state.
243#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
244#[cfg_attr(test, derive(PartialEq, Eq))]
245pub struct LightPreimage {
246	pub requester: ServiceId,
247	pub hash: jam_types::Hash,
248}
249
250/// A Work Report along with a guarantee of its correctness from staked sources.
251#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
252#[cfg_attr(test, derive(PartialEq, Eq))]
253pub struct ReportGuarantee {
254	/// The Work Report which is being attested.
255	pub report: WorkReport,
256	/// The slot following production of the report.
257	/// The validator set and hence the meaning of the
258	/// validator indices in `signatures` is inferred from this. The indices must be a subset of
259	/// those assigned to `report.core_index` during the slot.
260	pub slot: Slot,
261	/// The signatures from the guarantors whose message is the hash of the `report`.
262	/// The order of the signatures is the same order as the validators appear in
263	/// the epochal validator set.
264	pub signatures: GuaranteeSignatures,
265}
266
267impl ReportGuarantee {
268	pub fn as_light(&self) -> LightReportGuaranteeEncode<'_> {
269		LightReportGuaranteeEncode {
270			report_hash: self.report.hash(),
271			slot: self.slot,
272			signatures: &self.signatures,
273		}
274	}
275}
276
277/// Report guarantee but only with a hash pointing to work report.
278#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
279#[cfg_attr(test, derive(PartialEq, Eq))]
280pub struct LightReportGuarantee {
281	pub report_hash: WorkReportHash,
282	pub slot: Slot,
283	pub signatures: GuaranteeSignatures,
284}
285
286#[derive(Encode, Debug)]
287#[cfg_attr(test, derive(PartialEq, Eq))]
288pub struct LightReportGuaranteeEncode<'a> {
289	pub report_hash: WorkReportHash,
290	pub slot: Slot,
291	pub signatures: &'a GuaranteeSignatures,
292}
293
294pub type GuaranteeSignatures = BoundedVec<ValSignature, ConstU32<{ VALS_PER_CORE as u32 }>>;
295
296#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, MaxEncodedLen)]
297pub struct ValSignature {
298	pub val_index: ValIndex,
299	pub signature: super::ed25519::Signature,
300}
301
302impl ConstEncodedLen for ValSignature {}
303
304/// Secure reference to a Work Package.
305#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, MaxEncodedLen)]
306pub struct WorkPackageSpec {
307	/// The hash of the Work Package.
308	pub hash: WorkPackageHash,
309	/// The length in bytes of Work Bundle.
310	pub len: u32,
311	/// The erasure root of the Work Bundle and export-segment pieces.
312	pub erasure_root: ErasureRoot,
313	/// The segment root of the Work Package.
314	pub exports_root: SegmentTreeRoot,
315	/// The number of segments exported by the Work Package.
316	pub exports_count: u16,
317}
318
319impl WorkPackageSpec {
320	pub fn wp_srl(&self) -> (WorkPackageHash, SegmentTreeRoot) {
321		(self.hash, self.exports_root)
322	}
323}
324
325/// Execution report of a Work Package, mainly comprising the Results from the Refinement
326/// of its Work Items.
327#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq)]
328pub struct WorkReport {
329	/// The specification of the underlying Work Package.
330	pub package_spec: WorkPackageSpec,
331	/// The context of the underlying Work Package.
332	pub context: RefineContext,
333	/// The Core index under which the Work Package was Refined to generate the Report.
334	#[codec(compact)]
335	pub core_index: CoreIndex,
336	/// The authorizer under which this Work Package got executed. For the Work Package to be
337	/// validly reported, this must appear in the Core's authorizer queue at the time of reporting.
338	pub authorizer_hash: AuthorizerHash,
339	/// The amount of gas actually used by the IsAuthorized call.
340	#[codec(compact)]
341	pub auth_gas_used: UnsignedGas,
342	/// The output of the authorizer under which this Work Package got executed.
343	pub auth_output: AuthTrace,
344	/// The segment-root lookup dictionary.
345	pub sr_lookup: VecMap<WorkPackageHash, SegmentTreeRoot>,
346	/// The results of the evaluation of the Items in the underlying Work Package.
347	pub results: BoundedVec<WorkDigest, MaxWorkItems>,
348}
349
350impl WorkReport {
351	/// Report hash.
352	pub fn hash(&self) -> WorkReportHash {
353		hash_encoded(self).into()
354	}
355
356	/// Report total gas requirement.
357	pub fn gas(&self) -> UnsignedGas {
358		self.results.iter().map(|r| r.accumulate_gas).sum()
359	}
360
361	/// Report dependencies derived from both context prerequisites and the segments root
362	/// lookup dictionary.
363	pub fn deps(&self) -> impl Iterator<Item = &WorkPackageHash> {
364		self.sr_lookup.keys().chain(self.context.prerequisites.iter())
365	}
366
367	/// Count report dependencies derived from both context prerequisites and the segments root
368	/// lookup dictionary.
369	pub fn dep_count(&self) -> usize {
370		self.sr_lookup.len() + self.context.prerequisites.len()
371	}
372
373	/// Determine if the report size is within the limit.
374	pub fn check_size(&self) -> bool {
375		let total: usize =
376			self.results.iter().filter_map(|r| Some(r.result.as_ref().ok()?.len())).sum();
377		total + self.auth_output.len() <= max_report_elective_data() as usize
378	}
379
380	/// Work package hash for this report.
381	pub fn package_hash(&self) -> WorkPackageHash {
382		self.package_spec.hash
383	}
384}
385
386impl MaxEncodedLen for WorkReport {
387	fn max_encoded_len() -> usize {
388		let mut max = WorkPackageSpec::max_encoded_len() +
389			RefineContext::max_encoded_len() +
390			codec::Compact::<CoreIndex>::max_encoded_len() +
391			AuthorizerHash::max_encoded_len() +
392			AuthTrace::max_encoded_len() +
393			BoundedVec::<(WorkPackageHash, SegmentTreeRoot), MaxDependencies>::max_encoded_len() + // sr_lookup
394			BoundedVec::<WorkDigest, MaxWorkItems>::max_encoded_len() +
395			codec::Compact::<UnsignedGas>::max_encoded_len(); // auth_gas_used
396
397		// In the max expression above, the max_report_elective_data() bound is effectively applied
398		// separately to auth_output and item results. It actually applies to the combined size of
399		// these -- they cannot _all_ have maximum length. Account for this.
400		max -= max_work_items() * (max_report_elective_data() as usize);
401
402		max
403	}
404}
405
406pub mod tests_utils {
407	use super::*;
408	use bounded_collections::TryCollect;
409	use jam_types::{max_dependencies, RefineLoad};
410
411	// The largest possible work-report, or at least very close -- it might be possible to get
412	// a few bytes more by eg rebalancing the output Vec lengths, as Vec lengths use compact
413	// encoding.
414	//
415	// This is actually a bit larger than the largest valid report as both the prerequisites
416	// set and the sr_lookup map have max_dependencies() entries; the max_dependencies() limit
417	// is supposed to apply to the combined count. WorkReport::max_encoded_len() doesn't
418	// account for this though, so we don't bother accounting for it here either.
419	pub fn largest_report() -> WorkReport {
420		let largest_spec = WorkPackageSpec {
421			hash: Default::default(),
422			len: u32::MAX,
423			erasure_root: Default::default(),
424			exports_root: Default::default(),
425			exports_count: u16::MAX,
426		};
427
428		let max_outputs = 1 + max_work_items(); // AuthTrace + WorkOutputs
429		let output = |i| {
430			let mut size = (max_report_elective_data() as usize) / max_outputs;
431			if i == 0 {
432				size += (max_report_elective_data() as usize) % max_outputs;
433			}
434			vec![0; size]
435		};
436
437		WorkReport {
438			package_spec: largest_spec,
439			context: RefineContext::largest(),
440			core_index: CoreIndex::MAX,
441			authorizer_hash: Default::default(),
442			auth_output: output(0).into(),
443			sr_lookup: (0..max_dependencies())
444				.map(|i| ([i as u8; 32].into(), Default::default()))
445				.collect(),
446			results: (0..max_work_items())
447				.map(|i| WorkDigest {
448					service: ServiceId::MAX,
449					code_hash: Default::default(),
450					payload_hash: Default::default(),
451					accumulate_gas: UnsignedGas::MAX,
452					result: Ok(output(1 + i).into()),
453					refine_load: RefineLoad {
454						gas_used: UnsignedGas::MAX,
455						imports: u16::MAX,
456						extrinsic_count: u16::MAX,
457						extrinsic_size: u32::MAX,
458						exports: u16::MAX,
459					},
460				})
461				.try_collect()
462				.expect("max workitem should be this vec bound"),
463			auth_gas_used: UnsignedGas::MAX,
464		}
465	}
466}
467
468#[cfg(test)]
469mod tests {
470	use super::*;
471	use jam_types::{max_work_items, MAX_PREIMAGE_BLOB_LEN, MAX_PREIMAGE_LEN};
472
473	#[test]
474	fn max_preimage_blob_len_is_correct() {
475		let preimage = Preimage::new_testing(ServiceId::MAX, vec![123_u8; MAX_PREIMAGE_BLOB_LEN]);
476		let encoded_len = preimage.encode().len();
477		assert_eq!(MAX_PREIMAGE_LEN, encoded_len);
478	}
479
480	#[test]
481	fn preimage_encoded_len_works() {
482		for blob_len in (0..MAX_PREIMAGE_BLOB_LEN).step_by(997) {
483			let preimage = Preimage::new_testing(ServiceId::MAX, vec![123_u8; blob_len]);
484			let expected_len = Preimage::encoded_len(blob_len);
485			let actual_len = preimage.encode().len();
486			assert_eq!(expected_len, actual_len);
487		}
488	}
489
490	#[test]
491	fn report_max_encoded_len() {
492		let largest_report = super::tests_utils::largest_report();
493		assert_eq!(largest_report.package_spec.encoded_size(), WorkPackageSpec::max_encoded_len());
494
495		let largest_report_size = largest_report.encoded_size();
496
497		// WorkReport::max_encoded_len() assumes each "output" Vec can be the maximum length when
498		// calculating how many bytes are needed for encoding Vec lengths in the worst case. The
499		// Vecs cannot all be the maximum length though due to the shared nature of
500		// max_report_elective_data(). To account for this discrepancy, we allow
501		// WorkReport::max_encoded_len() to overshoot largest_report_size by 1 for each such Vec.
502		let max = WorkReport::max_encoded_len();
503		assert!(largest_report_size <= max);
504		let max_outputs = 1 + max_work_items(); // AuthTrace + WorkOutputs
505		assert!((largest_report_size + max_outputs) >= max);
506	}
507}