Skip to main content

jam_std_common/
extrinsic.rs

1use crate::{
2	AvailabilityAssurance, EpochIndex, ErasureRoot, TicketEnvelope, hash_encoded, hash_raw,
3	header::OffendersMark,
4};
5use bounded_collections::ConstU32;
6use bytes::Bytes;
7use codec::{Compact, CompactLen, ConstEncodedLen, Decode, Encode, MaxEncodedLen};
8use jam_types::{
9	AuthTrace, AuthorizerHash, BoundedVec, CoreCount, CoreIndex, ExtrinsicHash, MaxDependencies,
10	MaxTicketsPerBlock, MaxValSuperMajority, MaxWorkItems, RefineContext, SegmentTreeRoot,
11	ServiceId, Slot, UnsignedGas, VALS_PER_CORE, ValIndex, VecMap, WorkDigest, WorkPackageHash,
12	WorkReportHash, max_report_elective_data, max_work_items,
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::MaxValCount>;
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 = BoundedVec<Judgement, MaxValSuperMajority>;
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	///
88	/// Assumes `votes` holds a validator supermajority (`⌊2V/3⌋ + 1` votes, checked by the
89	/// caller), so the wonky threshold `⌊V/3⌋` is derived from the votes count itself.
90	pub fn kind(&self) -> Result<VerdictKind, ()> {
91		let valid_count = self.votes.iter().filter(|v| v.vote).count();
92		if valid_count == self.votes.len() {
93			Ok(VerdictKind::Good)
94		} else if valid_count == 0 {
95			Ok(VerdictKind::Bad)
96		} else if valid_count == (self.votes.len() - 1) / 2 {
97			Ok(VerdictKind::Wonky)
98		} else {
99			Err(())
100		}
101	}
102}
103
104#[derive(Clone, Encode, Decode, Debug, PartialEq, MaxEncodedLen)]
105pub struct Judgement {
106	pub vote: bool,
107	pub index: ValIndex,
108	pub signature: super::ed25519::Signature,
109}
110
111// This represents a culprit where we have received a signature for a guarantee
112// of a report `report_hash` that was found in a dispute to be Bad.
113#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
114pub struct CulpritProof {
115	pub report_hash: WorkReportHash,
116	pub key: super::ed25519::Public,
117	pub signature: super::ed25519::Signature,
118}
119
120// This represents a fault where we have received a signature for report `report_hash`
121// claiming it was valid if `vote == True` and invalid if `vote == False`,
122// while we determined in a dispute that actually the report's validity is otherwise.
123#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
124pub struct FaultProof {
125	pub report_hash: WorkReportHash,
126	pub vote: bool,
127	pub key: super::ed25519::Public,
128	pub signature: super::ed25519::Signature,
129}
130
131#[derive(Clone, Encode, Decode, Debug, Default)]
132pub struct Extrinsic {
133	pub tickets: TicketsXt,
134	pub preimages: PreimagesXt,
135	pub guarantees: GuaranteesXt,
136	pub assurances: AssurancesXt,
137	pub disputes: DisputesXt,
138}
139
140#[derive(Clone, Encode, Decode, Debug, Default)]
141pub struct LightExtrinsic {
142	pub tickets: TicketsXt,
143	pub preimages: LightPreimagesXt,
144	pub guarantees: LightGuaranteesXt,
145	pub assurances: AssurancesXt,
146	pub disputes: DisputesXt,
147}
148
149impl Extrinsic {
150	pub fn guarantees_prehashed(&self) -> Vec<(WorkReportHash, Slot, &GuaranteeSignatures)> {
151		self.guarantees
152			.iter()
153			.map(|r| (r.report.hash(), r.slot, &r.signatures))
154			.collect()
155	}
156
157	pub fn preimages_prehashed(&self) -> Vec<(ServiceId, jam_types::Hash)> {
158		self.preimages.iter().map(|r| (r.requester, r.hash)).collect()
159	}
160
161	pub fn hash(&self) -> ExtrinsicHash {
162		let tickets_hash = hash_encoded(&self.tickets);
163		let disputes_hash = hash_encoded(&self.disputes);
164		let preimages_hash = hash_encoded(&self.preimages_prehashed());
165		let assurances_hash = hash_encoded(&self.assurances);
166		let guarantees_hash = hash_encoded(&self.guarantees_prehashed());
167		let top = (tickets_hash, preimages_hash, guarantees_hash, assurances_hash, disputes_hash);
168		hash_encoded(&top).into()
169	}
170
171	pub fn encode_as_light<T: codec::Output + ?Sized>(&self, dest: &mut T) {
172		self.tickets.encode_to(dest);
173		let size = codec::Compact::<u32>(self.preimages.len() as u32);
174		size.encode_to(dest);
175		for preimage in &self.preimages {
176			preimage.as_light().encode_to(dest);
177		}
178		let size = codec::Compact::<u32>(self.guarantees.len() as u32);
179		size.encode_to(dest);
180		for guarantee in &self.guarantees {
181			guarantee.as_light().encode_to(dest);
182		}
183		self.assurances.encode_to(dest);
184		self.disputes.encode_to(dest);
185	}
186}
187
188#[derive(Clone, Debug, Eq, PartialOrd, Ord, Encode)]
189pub struct Preimage {
190	pub requester: ServiceId,
191	blob: Bytes,
192	#[codec(skip)]
193	hash: jam_types::Hash,
194}
195
196impl PartialEq<Preimage> for Preimage {
197	fn eq(&self, other: &Preimage) -> bool {
198		self.requester == other.requester && self.hash == other.hash
199	}
200}
201
202impl Preimage {
203	pub fn as_light(&self) -> LightPreimage {
204		LightPreimage { requester: self.requester, hash: self.hash() }
205	}
206}
207
208impl Decode for Preimage {
209	fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
210		let requester: ServiceId = Decode::decode(input)?;
211		let blob: Bytes = Decode::decode(input)?;
212		let hash = hash_raw(&blob);
213		Ok(Preimage { requester, blob, hash })
214	}
215}
216
217impl Preimage {
218	/// Utility to create preimage for testing: calculate hash of blob internally.
219	/// Jam code do not need it as it is either known (persistence) or calculated in `decode`
220	/// method.
221	pub fn new_testing(requester: ServiceId, blob: impl Into<Bytes>) -> Self {
222		let blob: Bytes = blob.into();
223		let hash = hash_raw(&blob);
224		Self { requester, blob, hash }
225	}
226
227	/// Warning hash parameter for this method MUST be the hash of the blob (we only add it as
228	/// parameter to avoid hash computation when already checked, for instance when read from db).
229	pub fn new_with_hash(
230		requester: ServiceId,
231		blob: impl Into<Bytes>,
232		hash: jam_types::Hash,
233	) -> Self {
234		let blob: Bytes = blob.into();
235		debug_assert_eq!(hash_raw(&blob), hash);
236		Self { requester, blob, hash }
237	}
238
239	pub fn hash(&self) -> jam_types::Hash {
240		self.hash
241	}
242
243	pub fn blob(&self) -> &Bytes {
244		&self.blob
245	}
246
247	pub fn encoded_len(blob_len: usize) -> usize {
248		core::mem::size_of::<ServiceId>() +
249			Compact::<u64>::compact_len(&(blob_len as u64)) +
250			blob_len
251	}
252}
253
254/// Preimage data stored in db, actual blob is part of state.
255#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
256#[cfg_attr(test, derive(PartialEq, Eq))]
257pub struct LightPreimage {
258	pub requester: ServiceId,
259	pub hash: jam_types::Hash,
260}
261
262/// A Work Report along with a guarantee of its correctness from staked sources.
263#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, MaxEncodedLen)]
264pub struct ReportGuarantee {
265	/// The Work Report which is being attested.
266	pub report: WorkReport,
267	/// Timeslot of the guarantee: the slot at which the guarantors attested to the report. It
268	/// determines the guarantor rotation, and hence the validator set the indices in `signatures`
269	/// refer to; the indices must be a subset of those assigned to `report.core_index` at this
270	/// slot. Must fall within the including block's rotation or the one before.
271	pub slot: Slot,
272	/// The signatures from the guarantors whose message is the hash of the `report`.
273	/// The order of the signatures is the same order as the validators appear in
274	/// the epochal validator set.
275	pub signatures: GuaranteeSignatures,
276}
277
278impl ReportGuarantee {
279	pub fn as_light(&self) -> LightReportGuaranteeEncode<'_> {
280		LightReportGuaranteeEncode {
281			report_hash: self.report.hash(),
282			slot: self.slot,
283			signatures: &self.signatures,
284		}
285	}
286}
287
288/// Report guarantee but only with a hash pointing to work report.
289#[derive(Clone, Encode, Decode, Debug, MaxEncodedLen)]
290#[cfg_attr(test, derive(PartialEq, Eq))]
291pub struct LightReportGuarantee {
292	pub report_hash: WorkReportHash,
293	pub slot: Slot,
294	pub signatures: GuaranteeSignatures,
295}
296
297#[derive(Encode, Debug)]
298#[cfg_attr(test, derive(PartialEq, Eq))]
299pub struct LightReportGuaranteeEncode<'a> {
300	pub report_hash: WorkReportHash,
301	pub slot: Slot,
302	pub signatures: &'a GuaranteeSignatures,
303}
304
305pub type GuaranteeSignatures = BoundedVec<ValSignature, ConstU32<{ VALS_PER_CORE as u32 }>>;
306
307#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, MaxEncodedLen)]
308pub struct ValSignature {
309	pub val_index: ValIndex,
310	pub signature: super::ed25519::Signature,
311}
312
313impl ConstEncodedLen for ValSignature {}
314
315/// Secure reference to a Work Package.
316#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, MaxEncodedLen)]
317pub struct WorkPackageSpec {
318	/// The hash of the Work Package.
319	pub hash: WorkPackageHash,
320	/// The length in bytes of Work Bundle.
321	pub len: u32,
322	/// The erasure root of the Work Bundle and export-segment pieces.
323	pub erasure_root: ErasureRoot,
324	/// The total number of erasure-coded chunks, one per assuring validator.
325	///
326	/// Determines the erasure-coding rate used for the Work Bundle and export segments; a
327	/// report may only be included on-chain if this matches the active validator-set size.
328	pub erasure_shards: ValIndex,
329	/// The segment root of the Work Package.
330	pub exports_root: SegmentTreeRoot,
331	/// The number of segments exported by the Work Package.
332	pub exports_count: u16,
333}
334
335impl WorkPackageSpec {
336	pub fn wp_srl(&self) -> (WorkPackageHash, SegmentTreeRoot) {
337		(self.hash, self.exports_root)
338	}
339}
340
341/// Execution report of a Work Package, mainly comprising the Results from the Refinement
342/// of its Work Items.
343#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq)]
344pub struct WorkReport {
345	/// The specification of the underlying Work Package.
346	pub package_spec: WorkPackageSpec,
347	/// The context of the underlying Work Package.
348	pub context: RefineContext,
349	/// The Core index under which the Work Package was Refined to generate the Report.
350	#[codec(compact)]
351	pub core_index: CoreIndex,
352	/// The authorizer under which this Work Package got executed. For the Work Package to be
353	/// validly reported, this must appear in the Core's authorizer queue at the time of reporting.
354	pub authorizer_hash: AuthorizerHash,
355	/// The amount of gas actually used by the IsAuthorized call.
356	#[codec(compact)]
357	pub auth_gas_used: UnsignedGas,
358	/// The output of the authorizer under which this Work Package got executed.
359	pub auth_output: AuthTrace,
360	/// The segment-root lookup dictionary.
361	pub sr_lookup: VecMap<WorkPackageHash, SegmentTreeRoot>,
362	/// The results of the evaluation of the Items in the underlying Work Package.
363	pub results: BoundedVec<WorkDigest, MaxWorkItems>,
364}
365
366impl WorkReport {
367	/// Report hash.
368	pub fn hash(&self) -> WorkReportHash {
369		hash_encoded(self).into()
370	}
371
372	/// Saturating sum of the `accumulate_gas` of all digests.
373	pub fn gas(&self) -> UnsignedGas {
374		self.results
375			.iter()
376			.fold(0, |gas: UnsignedGas, r| gas.saturating_add(r.accumulate_gas))
377	}
378
379	/// Report dependencies derived from both context prerequisites and the segments root
380	/// lookup dictionary.
381	pub fn deps(&self) -> impl Iterator<Item = &WorkPackageHash> {
382		self.sr_lookup.keys().chain(self.context.prerequisites.iter())
383	}
384
385	/// Count report dependencies derived from both context prerequisites and the segments root
386	/// lookup dictionary.
387	pub fn dep_count(&self) -> usize {
388		self.sr_lookup.len() + self.context.prerequisites.len()
389	}
390
391	/// Determine if the report size is within the limit.
392	pub fn check_size(&self) -> bool {
393		let total: usize =
394			self.results.iter().filter_map(|r| Some(r.result.as_ref().ok()?.len())).sum();
395		total + self.auth_output.len() <= max_report_elective_data() as usize
396	}
397
398	/// Work package hash for this report.
399	pub fn package_hash(&self) -> WorkPackageHash {
400		self.package_spec.hash
401	}
402}
403
404impl MaxEncodedLen for WorkReport {
405	fn max_encoded_len() -> usize {
406		let mut max = WorkPackageSpec::max_encoded_len() +
407			RefineContext::max_encoded_len() +
408			codec::Compact::<CoreIndex>::max_encoded_len() +
409			AuthorizerHash::max_encoded_len() +
410			AuthTrace::max_encoded_len() +
411			BoundedVec::<(WorkPackageHash, SegmentTreeRoot), MaxDependencies>::max_encoded_len() + // sr_lookup
412			BoundedVec::<WorkDigest, MaxWorkItems>::max_encoded_len() +
413			codec::Compact::<UnsignedGas>::max_encoded_len(); // auth_gas_used
414
415		// In the max expression above, the max_report_elective_data() bound is effectively applied
416		// separately to auth_output and item results. It actually applies to the combined size of
417		// these -- they cannot _all_ have maximum length. Account for this.
418		max -= max_work_items() * (max_report_elective_data() as usize);
419
420		max
421	}
422}
423
424pub mod tests_utils {
425	use super::*;
426	use bounded_collections::TryCollect;
427	use jam_types::{RefineLoad, max_dependencies};
428
429	// The largest possible work-report, or at least very close -- it might be possible to get
430	// a few bytes more by eg rebalancing the output Vec lengths, as Vec lengths use compact
431	// encoding.
432	//
433	// This is actually a bit larger than the largest valid report as both the prerequisites
434	// set and the sr_lookup map have max_dependencies() entries; the max_dependencies() limit
435	// is supposed to apply to the combined count. WorkReport::max_encoded_len() doesn't
436	// account for this though, so we don't bother accounting for it here either.
437	pub fn largest_report() -> WorkReport {
438		let largest_spec = WorkPackageSpec {
439			hash: Default::default(),
440			len: u32::MAX,
441			erasure_root: Default::default(),
442			erasure_shards: ValIndex::MAX,
443			exports_root: Default::default(),
444			exports_count: u16::MAX,
445		};
446
447		let max_outputs = 1 + max_work_items(); // AuthTrace + WorkOutputs
448		let output = |i| {
449			let mut size = (max_report_elective_data() as usize) / max_outputs;
450			if i == 0 {
451				size += (max_report_elective_data() as usize) % max_outputs;
452			}
453			vec![0; size]
454		};
455
456		WorkReport {
457			package_spec: largest_spec,
458			context: RefineContext::largest(),
459			core_index: CoreIndex::MAX,
460			authorizer_hash: Default::default(),
461			auth_output: output(0).into(),
462			sr_lookup: (0..max_dependencies())
463				.map(|i| ([i as u8; 32].into(), Default::default()))
464				.collect(),
465			results: (0..max_work_items())
466				.map(|i| WorkDigest {
467					service: ServiceId::MAX,
468					code_hash: Default::default(),
469					payload_hash: Default::default(),
470					accumulate_gas: UnsignedGas::MAX,
471					result: Ok(output(1 + i).into()),
472					refine_load: RefineLoad {
473						gas_used: UnsignedGas::MAX,
474						imports: u16::MAX,
475						extrinsic_count: u16::MAX,
476						extrinsic_size: u32::MAX,
477						exports: u16::MAX,
478					},
479				})
480				.try_collect()
481				.expect("max workitem should be this vec bound"),
482			auth_gas_used: UnsignedGas::MAX,
483		}
484	}
485}
486
487#[cfg(test)]
488mod tests {
489	use super::*;
490	use jam_types::{MAX_PREIMAGE_BLOB_LEN, MAX_PREIMAGE_LEN, max_work_items};
491
492	#[test]
493	fn report_gas_saturates() {
494		let mut report = tests_utils::largest_report();
495		report.results.truncate(2);
496		report.results[0].accumulate_gas = UnsignedGas::MAX - 2;
497		// Just below the max, the sum is exact.
498		report.results[1].accumulate_gas = 1;
499		assert_eq!(report.gas(), UnsignedGas::MAX - 1);
500		// Saturate it.
501		report.results[1].accumulate_gas = 10000;
502		assert_eq!(report.gas(), UnsignedGas::MAX);
503	}
504
505	#[test]
506	fn max_preimage_blob_len_is_correct() {
507		let preimage = Preimage::new_testing(ServiceId::MAX, vec![123_u8; MAX_PREIMAGE_BLOB_LEN]);
508		let encoded_len = preimage.encode().len();
509		assert_eq!(MAX_PREIMAGE_LEN, encoded_len);
510	}
511
512	#[test]
513	fn preimage_encoded_len_works() {
514		for blob_len in (0..MAX_PREIMAGE_BLOB_LEN).step_by(997 * 64) {
515			let preimage = Preimage::new_testing(ServiceId::MAX, vec![123_u8; blob_len]);
516			let expected_len = Preimage::encoded_len(blob_len);
517			let actual_len = preimage.encode().len();
518			assert_eq!(expected_len, actual_len);
519		}
520	}
521
522	#[test]
523	fn report_max_encoded_len() {
524		let largest_report = super::tests_utils::largest_report();
525		assert_eq!(largest_report.package_spec.encoded_size(), WorkPackageSpec::max_encoded_len());
526
527		let largest_report_size = largest_report.encoded_size();
528
529		// WorkReport::max_encoded_len() assumes each "output" Vec can be the maximum length when
530		// calculating how many bytes are needed for encoding Vec lengths in the worst case. The
531		// Vecs cannot all be the maximum length though due to the shared nature of
532		// max_report_elective_data(). To account for this discrepancy, we allow
533		// WorkReport::max_encoded_len() to overshoot largest_report_size by 1 for each such Vec.
534		let max = WorkReport::max_encoded_len();
535		assert!(largest_report_size <= max);
536		let max_outputs = 1 + max_work_items(); // AuthTrace + WorkOutputs
537		assert!((largest_report_size + max_outputs) >= max);
538	}
539}