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