referrerpolicy=no-referrer-when-downgrade

sp_trie/
proof_size_extension.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Externalities extension that provides access to the current proof size
19//! of the underlying recorder.
20
21use parking_lot::Mutex;
22
23use crate::ProofSizeProvider;
24use std::{collections::VecDeque, sync::Arc};
25
26sp_externalities::decl_extension! {
27	/// The proof size extension to fetch the current storage proof size
28	/// in externalities.
29	pub struct ProofSizeExt(Box<dyn ProofSizeProvider + 'static + Sync + Send>);
30
31	impl ProofSizeExt {
32		fn start_transaction(&mut self, ty: sp_externalities::TransactionType) {
33			self.0.start_transaction(ty.is_host());
34		}
35
36		fn rollback_transaction(&mut self, ty: sp_externalities::TransactionType) {
37			self.0.rollback_transaction(ty.is_host());
38		}
39
40		fn commit_transaction(&mut self, ty: sp_externalities::TransactionType) {
41			self.0.commit_transaction(ty.is_host());
42		}
43	}
44}
45
46impl ProofSizeExt {
47	/// Creates a new instance of [`ProofSizeExt`].
48	pub fn new<T: ProofSizeProvider + Sync + Send + 'static>(recorder: T) -> Self {
49		ProofSizeExt(Box::new(recorder))
50	}
51
52	/// Returns the storage proof size.
53	pub fn storage_proof_size(&self) -> u64 {
54		self.0.estimate_encoded_size() as _
55	}
56}
57
58/// Proof size estimations as recorded by [`RecordingProofSizeProvider`].
59///
60/// Each item is the estimated proof size as observed when calling
61/// [`ProofSizeProvider::estimate_encoded_size`]. The items are ordered by their observation and
62/// need to be replayed in the exact same order.
63pub struct RecordedProofSizeEstimations(pub VecDeque<usize>);
64
65impl From<Vec<u32>> for RecordedProofSizeEstimations {
66	fn from(recordings: Vec<u32>) -> Self {
67		Self(recordings.into_iter().map(|x| x as usize).collect())
68	}
69}
70
71/// Inner structure of [`RecordingProofSizeProvider`].
72struct RecordingProofSizeProviderInner {
73	inner: Box<dyn ProofSizeProvider + Send + Sync>,
74	/// Stores the observed proof estimations (in order of observation) per transaction.
75	///
76	/// Last element of the outer vector is the active transaction.
77	proof_size_estimations: Vec<Vec<usize>>,
78}
79
80/// An implementation of [`ProofSizeProvider`] that records the return value of the calls to
81/// [`ProofSizeProvider::estimate_encoded_size`].
82///
83/// Wraps an inner [`ProofSizeProvider`] that is used to get the actual encoded size estimations.
84/// Each estimation is recorded in the order it was observed.
85#[derive(Clone)]
86pub struct RecordingProofSizeProvider {
87	inner: Arc<Mutex<RecordingProofSizeProviderInner>>,
88}
89
90impl RecordingProofSizeProvider {
91	/// Creates a new instance of [`RecordingProofSizeProvider`].
92	pub fn new<T: ProofSizeProvider + Sync + Send + 'static>(recorder: T) -> Self {
93		Self {
94			inner: Arc::new(Mutex::new(RecordingProofSizeProviderInner {
95				inner: Box::new(recorder),
96				// Init the always existing transaction.
97				proof_size_estimations: vec![Vec::new()],
98			})),
99		}
100	}
101
102	/// Returns the recorded estimations returned by each call to
103	/// [`Self::estimate_encoded_size`].
104	pub fn recorded_estimations(&self) -> Vec<usize> {
105		self.inner.lock().proof_size_estimations.iter().flatten().copied().collect()
106	}
107}
108
109impl ProofSizeProvider for RecordingProofSizeProvider {
110	fn estimate_encoded_size(&self) -> usize {
111		let mut inner = self.inner.lock();
112
113		let estimation = inner.inner.estimate_encoded_size();
114
115		inner
116			.proof_size_estimations
117			.last_mut()
118			.expect("There is always at least one transaction open; qed")
119			.push(estimation);
120
121		estimation
122	}
123
124	fn start_transaction(&mut self, is_host: bool) {
125		// We don't care about runtime transactions, because they are part of the consensus critical
126		// path, that will always deterministically call this code.
127		//
128		// For example a runtime execution is creating 10 runtime transaction and calling in every
129		// transaction the proof size estimation host function and 8 of these transactions are
130		// rolled back. We need to keep all the 10 estimations. When the runtime execution is
131		// replayed (by e.g. importing a block), we will deterministically again create 10 runtime
132		// executions and roll back 8. However, in between we require all 10 estimations as
133		// otherwise the execution would not be deterministically anymore.
134		//
135		// A host transaction is only rolled back while for example building a block and an
136		// extrinsic failed in the early checks in the runtime. In this case, the extrinsic will
137		// also never appear in a block and thus, will not need to be replayed later on.
138		if is_host {
139			self.inner.lock().proof_size_estimations.push(Default::default());
140		}
141	}
142
143	fn rollback_transaction(&mut self, is_host: bool) {
144		let mut inner = self.inner.lock();
145
146		// The host side transaction needs to be reverted, because this is only done when an
147		// entire execution is rolled back. So, the execution will never be part of the consensus
148		// critical path.
149		if is_host && inner.proof_size_estimations.len() > 1 {
150			inner.proof_size_estimations.pop();
151		}
152	}
153
154	fn commit_transaction(&mut self, is_host: bool) {
155		let mut inner = self.inner.lock();
156
157		if is_host && inner.proof_size_estimations.len() > 1 {
158			let last = inner
159				.proof_size_estimations
160				.pop()
161				.expect("There are more than one element in the vector; qed");
162
163			inner
164				.proof_size_estimations
165				.last_mut()
166				.expect("There are more than one element in the vector; qed")
167				.extend(last);
168		}
169	}
170}
171
172/// An implementation of [`ProofSizeProvider`] that replays estimations recorded by
173/// [`RecordingProofSizeProvider`].
174///
175/// The recorded estimations are removed as they are required by calls to
176/// [`Self::estimate_encoded_size`]. Will return `0` when all estimations are consumed.
177pub struct ReplayProofSizeProvider(Arc<Mutex<RecordedProofSizeEstimations>>);
178
179impl ReplayProofSizeProvider {
180	/// Creates a new instance from the given [`RecordedProofSizeEstimations`].
181	pub fn from_recorded(recorded: RecordedProofSizeEstimations) -> Self {
182		Self(Arc::new(Mutex::new(recorded)))
183	}
184}
185
186impl From<RecordedProofSizeEstimations> for ReplayProofSizeProvider {
187	fn from(value: RecordedProofSizeEstimations) -> Self {
188		Self::from_recorded(value)
189	}
190}
191
192impl ProofSizeProvider for ReplayProofSizeProvider {
193	fn estimate_encoded_size(&self) -> usize {
194		self.0.lock().0.pop_front().unwrap_or_default()
195	}
196}
197
198#[cfg(test)]
199mod tests {
200	use super::*;
201	use std::sync::atomic::{AtomicUsize, Ordering};
202
203	// Mock ProofSizeProvider for testing
204	#[derive(Clone)]
205	struct MockProofSizeProvider {
206		size: Arc<AtomicUsize>,
207	}
208
209	impl MockProofSizeProvider {
210		fn new(initial_size: usize) -> Self {
211			Self { size: Arc::new(AtomicUsize::new(initial_size)) }
212		}
213
214		fn set_size(&self, new_size: usize) {
215			self.size.store(new_size, Ordering::Relaxed);
216		}
217	}
218
219	impl ProofSizeProvider for MockProofSizeProvider {
220		fn estimate_encoded_size(&self) -> usize {
221			self.size.load(Ordering::Relaxed)
222		}
223
224		fn start_transaction(&mut self, _is_host: bool) {}
225		fn rollback_transaction(&mut self, _is_host: bool) {}
226		fn commit_transaction(&mut self, _is_host: bool) {}
227	}
228
229	#[test]
230	fn recording_proof_size_provider_basic_functionality() {
231		let mock = MockProofSizeProvider::new(100);
232		let tracker = RecordingProofSizeProvider::new(mock.clone());
233
234		// Initial state - no estimations recorded yet
235		assert_eq!(tracker.recorded_estimations(), Vec::<usize>::new());
236
237		// Call estimate_encoded_size and verify it's recorded
238		let size = tracker.estimate_encoded_size();
239		assert_eq!(size, 100);
240		assert_eq!(tracker.recorded_estimations(), vec![100]);
241
242		// Change the mock size and call again
243		mock.set_size(200);
244		let size = tracker.estimate_encoded_size();
245		assert_eq!(size, 200);
246		assert_eq!(tracker.recorded_estimations(), vec![100, 200]);
247
248		// Multiple calls with same size
249		let size = tracker.estimate_encoded_size();
250		assert_eq!(size, 200);
251		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 200]);
252	}
253
254	#[test]
255	fn recording_proof_size_provider_host_transactions() {
256		let mock = MockProofSizeProvider::new(100);
257		let mut tracker = RecordingProofSizeProvider::new(mock.clone());
258
259		// Record some estimations in the initial transaction
260		tracker.estimate_encoded_size();
261		tracker.estimate_encoded_size();
262		assert_eq!(tracker.recorded_estimations(), vec![100, 100]);
263
264		// Start a host transaction
265		tracker.start_transaction(true);
266		mock.set_size(200);
267		tracker.estimate_encoded_size();
268
269		// Should have 3 estimations total
270		assert_eq!(tracker.recorded_estimations(), vec![100, 100, 200]);
271
272		// Commit the host transaction
273		tracker.commit_transaction(true);
274
275		// All estimations should still be there
276		assert_eq!(tracker.recorded_estimations(), vec![100, 100, 200]);
277
278		// Add more estimations
279		mock.set_size(300);
280		tracker.estimate_encoded_size();
281		assert_eq!(tracker.recorded_estimations(), vec![100, 100, 200, 300]);
282	}
283
284	#[test]
285	fn recording_proof_size_provider_host_transaction_rollback() {
286		let mock = MockProofSizeProvider::new(100);
287		let mut tracker = RecordingProofSizeProvider::new(mock.clone());
288
289		// Record some estimations in the initial transaction
290		tracker.estimate_encoded_size();
291		assert_eq!(tracker.recorded_estimations(), vec![100]);
292
293		// Start a host transaction
294		tracker.start_transaction(true);
295		mock.set_size(200);
296		tracker.estimate_encoded_size();
297		tracker.estimate_encoded_size();
298
299		// Should have 3 estimations total
300		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 200]);
301
302		// Rollback the host transaction
303		tracker.rollback_transaction(true);
304
305		// Should only have the original estimation
306		assert_eq!(tracker.recorded_estimations(), vec![100]);
307	}
308
309	#[test]
310	fn recording_proof_size_provider_runtime_transactions_ignored() {
311		let mock = MockProofSizeProvider::new(100);
312		let mut tracker = RecordingProofSizeProvider::new(mock.clone());
313
314		// Record initial estimation
315		tracker.estimate_encoded_size();
316		assert_eq!(tracker.recorded_estimations(), vec![100]);
317
318		// Start a runtime transaction (is_host = false)
319		tracker.start_transaction(false);
320		mock.set_size(200);
321		tracker.estimate_encoded_size();
322
323		// Should have both estimations
324		assert_eq!(tracker.recorded_estimations(), vec![100, 200]);
325
326		// Commit runtime transaction - should not affect recording
327		tracker.commit_transaction(false);
328		assert_eq!(tracker.recorded_estimations(), vec![100, 200]);
329
330		// Rollback runtime transaction - should not affect recording
331		tracker.rollback_transaction(false);
332		assert_eq!(tracker.recorded_estimations(), vec![100, 200]);
333	}
334
335	#[test]
336	fn recording_proof_size_provider_nested_host_transactions() {
337		let mock = MockProofSizeProvider::new(100);
338		let mut tracker = RecordingProofSizeProvider::new(mock.clone());
339
340		// Initial estimation
341		tracker.estimate_encoded_size();
342		assert_eq!(tracker.recorded_estimations(), vec![100]);
343
344		// Start first host transaction
345		tracker.start_transaction(true);
346		mock.set_size(200);
347		tracker.estimate_encoded_size();
348
349		// Start nested host transaction
350		tracker.start_transaction(true);
351		mock.set_size(300);
352		tracker.estimate_encoded_size();
353
354		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 300]);
355
356		// Commit nested transaction
357		tracker.commit_transaction(true);
358		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 300]);
359
360		// Commit outer transaction
361		tracker.commit_transaction(true);
362		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 300]);
363	}
364
365	#[test]
366	fn recording_proof_size_provider_nested_host_transaction_rollback() {
367		let mock = MockProofSizeProvider::new(100);
368		let mut tracker = RecordingProofSizeProvider::new(mock.clone());
369
370		// Initial estimation
371		tracker.estimate_encoded_size();
372
373		// Start first host transaction
374		tracker.start_transaction(true);
375		mock.set_size(200);
376		tracker.estimate_encoded_size();
377
378		// Start nested host transaction
379		tracker.start_transaction(true);
380		mock.set_size(300);
381		tracker.estimate_encoded_size();
382
383		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 300]);
384
385		// Rollback nested transaction
386		tracker.rollback_transaction(true);
387		assert_eq!(tracker.recorded_estimations(), vec![100, 200]);
388
389		// Rollback outer transaction
390		tracker.rollback_transaction(true);
391		assert_eq!(tracker.recorded_estimations(), vec![100]);
392	}
393
394	#[test]
395	fn recording_proof_size_provider_rollback_on_base_transaction_does_nothing() {
396		let mock = MockProofSizeProvider::new(100);
397		let mut tracker = RecordingProofSizeProvider::new(mock.clone());
398
399		// Record some estimations
400		tracker.estimate_encoded_size();
401		tracker.estimate_encoded_size();
402		assert_eq!(tracker.recorded_estimations(), vec![100, 100]);
403
404		// Try to rollback the base transaction - should do nothing
405		tracker.rollback_transaction(true);
406		assert_eq!(tracker.recorded_estimations(), vec![100, 100]);
407	}
408
409	#[test]
410	fn recorded_proof_size_estimations_struct() {
411		let estimations = vec![100, 200, 300];
412		let recorded = RecordedProofSizeEstimations(estimations.into());
413		let expected: VecDeque<usize> = vec![100, 200, 300].into();
414		assert_eq!(recorded.0, expected);
415	}
416
417	#[test]
418	fn replay_proof_size_provider_basic_functionality() {
419		let estimations = vec![100, 200, 300, 150];
420		let recorded = RecordedProofSizeEstimations(estimations.into());
421		let replay = ReplayProofSizeProvider::from_recorded(recorded);
422
423		// Should replay estimations in order
424		assert_eq!(replay.estimate_encoded_size(), 100);
425		assert_eq!(replay.estimate_encoded_size(), 200);
426		assert_eq!(replay.estimate_encoded_size(), 300);
427		assert_eq!(replay.estimate_encoded_size(), 150);
428	}
429
430	#[test]
431	fn replay_proof_size_provider_exhausted_returns_zero() {
432		let estimations = vec![100, 200];
433		let recorded = RecordedProofSizeEstimations(estimations.into());
434		let replay = ReplayProofSizeProvider::from_recorded(recorded);
435
436		// Consume all estimations
437		assert_eq!(replay.estimate_encoded_size(), 100);
438		assert_eq!(replay.estimate_encoded_size(), 200);
439
440		// Should return 0 when exhausted
441		assert_eq!(replay.estimate_encoded_size(), 0);
442		assert_eq!(replay.estimate_encoded_size(), 0);
443	}
444
445	#[test]
446	fn replay_proof_size_provider_empty_returns_zero() {
447		let recorded = RecordedProofSizeEstimations(VecDeque::new());
448		let replay = ReplayProofSizeProvider::from_recorded(recorded);
449
450		// Should return 0 for empty estimations
451		assert_eq!(replay.estimate_encoded_size(), 0);
452		assert_eq!(replay.estimate_encoded_size(), 0);
453	}
454
455	#[test]
456	fn replay_proof_size_provider_from_trait() {
457		let estimations = vec![42, 84];
458		let recorded = RecordedProofSizeEstimations(estimations.into());
459		let replay: ReplayProofSizeProvider = recorded.into();
460
461		assert_eq!(replay.estimate_encoded_size(), 42);
462		assert_eq!(replay.estimate_encoded_size(), 84);
463		assert_eq!(replay.estimate_encoded_size(), 0);
464	}
465
466	#[test]
467	fn record_and_replay_integration() {
468		let mock = MockProofSizeProvider::new(100);
469		let recorder = RecordingProofSizeProvider::new(mock.clone());
470
471		// Record some estimations
472		recorder.estimate_encoded_size();
473		mock.set_size(200);
474		recorder.estimate_encoded_size();
475		mock.set_size(300);
476		recorder.estimate_encoded_size();
477
478		// Get recorded estimations
479		let recorded_estimations = recorder.recorded_estimations();
480		assert_eq!(recorded_estimations, vec![100, 200, 300]);
481
482		// Create replay provider from recorded estimations
483		let recorded = RecordedProofSizeEstimations(recorded_estimations.into());
484		let replay = ReplayProofSizeProvider::from_recorded(recorded);
485
486		// Replay should return the same sequence
487		assert_eq!(replay.estimate_encoded_size(), 100);
488		assert_eq!(replay.estimate_encoded_size(), 200);
489		assert_eq!(replay.estimate_encoded_size(), 300);
490		assert_eq!(replay.estimate_encoded_size(), 0);
491	}
492
493	#[test]
494	fn replay_proof_size_provider_single_value() {
495		let estimations = vec![42];
496		let recorded = RecordedProofSizeEstimations(estimations.into());
497		let replay = ReplayProofSizeProvider::from_recorded(recorded);
498
499		// Should return the single value then default to 0
500		assert_eq!(replay.estimate_encoded_size(), 42);
501		assert_eq!(replay.estimate_encoded_size(), 0);
502	}
503}