subsoil 0.2.0

Soil primitives foundation crate
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
// This file is part of Soil.

// Copyright (C) Soil contributors.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later WITH Classpath-exception-2.0

//! Externalities extension that provides access to the current proof size
//! of the underlying recorder.

use parking_lot::Mutex;

use super::ProofSizeProvider;
use std::{collections::VecDeque, sync::Arc};

crate::decl_extension! {
	/// The proof size extension to fetch the current storage proof size
	/// in externalities.
	pub struct ProofSizeExt(Box<dyn ProofSizeProvider + 'static + Sync + Send>);

	impl ProofSizeExt {
		fn start_transaction(&mut self, ty: crate::externalities::TransactionType) {
			self.0.start_transaction(ty.is_host());
		}

		fn rollback_transaction(&mut self, ty: crate::externalities::TransactionType) {
			self.0.rollback_transaction(ty.is_host());
		}

		fn commit_transaction(&mut self, ty: crate::externalities::TransactionType) {
			self.0.commit_transaction(ty.is_host());
		}
	}
}

impl ProofSizeExt {
	/// Creates a new instance of [`ProofSizeExt`].
	pub fn new<T: ProofSizeProvider + Sync + Send + 'static>(recorder: T) -> Self {
		ProofSizeExt(Box::new(recorder))
	}

	/// Returns the storage proof size.
	pub fn storage_proof_size(&self) -> u64 {
		self.0.estimate_encoded_size() as _
	}
}

/// Proof size estimations as recorded by [`RecordingProofSizeProvider`].
///
/// Each item is the estimated proof size as observed when calling
/// [`ProofSizeProvider::estimate_encoded_size`]. The items are ordered by their observation and
/// need to be replayed in the exact same order.
pub struct RecordedProofSizeEstimations(pub VecDeque<usize>);

/// Inner structure of [`RecordingProofSizeProvider`].
struct RecordingProofSizeProviderInner {
	inner: Box<dyn ProofSizeProvider + Send + Sync>,
	/// Stores the observed proof estimations (in order of observation) per transaction.
	///
	/// Last element of the outer vector is the active transaction.
	proof_size_estimations: Vec<Vec<usize>>,
}

/// An implementation of [`ProofSizeProvider`] that records the return value of the calls to
/// [`ProofSizeProvider::estimate_encoded_size`].
///
/// Wraps an inner [`ProofSizeProvider`] that is used to get the actual encoded size estimations.
/// Each estimation is recorded in the order it was observed.
#[derive(Clone)]
pub struct RecordingProofSizeProvider {
	inner: Arc<Mutex<RecordingProofSizeProviderInner>>,
}

impl RecordingProofSizeProvider {
	/// Creates a new instance of [`RecordingProofSizeProvider`].
	pub fn new<T: ProofSizeProvider + Sync + Send + 'static>(recorder: T) -> Self {
		Self {
			inner: Arc::new(Mutex::new(RecordingProofSizeProviderInner {
				inner: Box::new(recorder),
				// Init the always existing transaction.
				proof_size_estimations: vec![Vec::new()],
			})),
		}
	}

	/// Returns the recorded estimations returned by each call to
	/// [`Self::estimate_encoded_size`].
	pub fn recorded_estimations(&self) -> Vec<usize> {
		self.inner.lock().proof_size_estimations.iter().flatten().copied().collect()
	}
}

impl ProofSizeProvider for RecordingProofSizeProvider {
	fn estimate_encoded_size(&self) -> usize {
		let mut inner = self.inner.lock();

		let estimation = inner.inner.estimate_encoded_size();

		inner
			.proof_size_estimations
			.last_mut()
			.expect("There is always at least one transaction open; qed")
			.push(estimation);

		estimation
	}

	fn start_transaction(&mut self, is_host: bool) {
		// We don't care about runtime transactions, because they are part of the consensus critical
		// path, that will always deterministically call this code.
		//
		// For example a runtime execution is creating 10 runtime transaction and calling in every
		// transaction the proof size estimation host function and 8 of these transactions are
		// rolled back. We need to keep all the 10 estimations. When the runtime execution is
		// replayed (by e.g. importing a block), we will deterministically again create 10 runtime
		// executions and roll back 8. However, in between we require all 10 estimations as
		// otherwise the execution would not be deterministically anymore.
		//
		// A host transaction is only rolled back while for example building a block and an
		// extrinsic failed in the early checks in the runtime. In this case, the extrinsic will
		// also never appear in a block and thus, will not need to be replayed later on.
		if is_host {
			self.inner.lock().proof_size_estimations.push(Default::default());
		}
	}

	fn rollback_transaction(&mut self, is_host: bool) {
		let mut inner = self.inner.lock();

		// The host side transaction needs to be reverted, because this is only done when an
		// entire execution is rolled back. So, the execution will never be part of the consensus
		// critical path.
		if is_host && inner.proof_size_estimations.len() > 1 {
			inner.proof_size_estimations.pop();
		}
	}

	fn commit_transaction(&mut self, is_host: bool) {
		let mut inner = self.inner.lock();

		if is_host && inner.proof_size_estimations.len() > 1 {
			let last = inner
				.proof_size_estimations
				.pop()
				.expect("There are more than one element in the vector; qed");

			inner
				.proof_size_estimations
				.last_mut()
				.expect("There are more than one element in the vector; qed")
				.extend(last);
		}
	}
}

/// An implementation of [`ProofSizeProvider`] that replays estimations recorded by
/// [`RecordingProofSizeProvider`].
///
/// The recorded estimations are removed as they are required by calls to
/// [`Self::estimate_encoded_size`]. Will return `0` when all estimations are consumed.
pub struct ReplayProofSizeProvider(Arc<Mutex<RecordedProofSizeEstimations>>);

impl ReplayProofSizeProvider {
	/// Creates a new instance from the given [`RecordedProofSizeEstimations`].
	pub fn from_recorded(recorded: RecordedProofSizeEstimations) -> Self {
		Self(Arc::new(Mutex::new(recorded)))
	}
}

impl From<RecordedProofSizeEstimations> for ReplayProofSizeProvider {
	fn from(value: RecordedProofSizeEstimations) -> Self {
		Self::from_recorded(value)
	}
}

impl ProofSizeProvider for ReplayProofSizeProvider {
	fn estimate_encoded_size(&self) -> usize {
		self.0.lock().0.pop_front().unwrap_or_default()
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use std::sync::atomic::{AtomicUsize, Ordering};

	// Mock ProofSizeProvider for testing
	#[derive(Clone)]
	struct MockProofSizeProvider {
		size: Arc<AtomicUsize>,
	}

	impl MockProofSizeProvider {
		fn new(initial_size: usize) -> Self {
			Self { size: Arc::new(AtomicUsize::new(initial_size)) }
		}

		fn set_size(&self, new_size: usize) {
			self.size.store(new_size, Ordering::Relaxed);
		}
	}

	impl ProofSizeProvider for MockProofSizeProvider {
		fn estimate_encoded_size(&self) -> usize {
			self.size.load(Ordering::Relaxed)
		}

		fn start_transaction(&mut self, _is_host: bool) {}
		fn rollback_transaction(&mut self, _is_host: bool) {}
		fn commit_transaction(&mut self, _is_host: bool) {}
	}

	#[test]
	fn recording_proof_size_provider_basic_functionality() {
		let mock = MockProofSizeProvider::new(100);
		let tracker = RecordingProofSizeProvider::new(mock.clone());

		// Initial state - no estimations recorded yet
		assert_eq!(tracker.recorded_estimations(), Vec::<usize>::new());

		// Call estimate_encoded_size and verify it's recorded
		let size = tracker.estimate_encoded_size();
		assert_eq!(size, 100);
		assert_eq!(tracker.recorded_estimations(), vec![100]);

		// Change the mock size and call again
		mock.set_size(200);
		let size = tracker.estimate_encoded_size();
		assert_eq!(size, 200);
		assert_eq!(tracker.recorded_estimations(), vec![100, 200]);

		// Multiple calls with same size
		let size = tracker.estimate_encoded_size();
		assert_eq!(size, 200);
		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 200]);
	}

	#[test]
	fn recording_proof_size_provider_host_transactions() {
		let mock = MockProofSizeProvider::new(100);
		let mut tracker = RecordingProofSizeProvider::new(mock.clone());

		// Record some estimations in the initial transaction
		tracker.estimate_encoded_size();
		tracker.estimate_encoded_size();
		assert_eq!(tracker.recorded_estimations(), vec![100, 100]);

		// Start a host transaction
		tracker.start_transaction(true);
		mock.set_size(200);
		tracker.estimate_encoded_size();

		// Should have 3 estimations total
		assert_eq!(tracker.recorded_estimations(), vec![100, 100, 200]);

		// Commit the host transaction
		tracker.commit_transaction(true);

		// All estimations should still be there
		assert_eq!(tracker.recorded_estimations(), vec![100, 100, 200]);

		// Add more estimations
		mock.set_size(300);
		tracker.estimate_encoded_size();
		assert_eq!(tracker.recorded_estimations(), vec![100, 100, 200, 300]);
	}

	#[test]
	fn recording_proof_size_provider_host_transaction_rollback() {
		let mock = MockProofSizeProvider::new(100);
		let mut tracker = RecordingProofSizeProvider::new(mock.clone());

		// Record some estimations in the initial transaction
		tracker.estimate_encoded_size();
		assert_eq!(tracker.recorded_estimations(), vec![100]);

		// Start a host transaction
		tracker.start_transaction(true);
		mock.set_size(200);
		tracker.estimate_encoded_size();
		tracker.estimate_encoded_size();

		// Should have 3 estimations total
		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 200]);

		// Rollback the host transaction
		tracker.rollback_transaction(true);

		// Should only have the original estimation
		assert_eq!(tracker.recorded_estimations(), vec![100]);
	}

	#[test]
	fn recording_proof_size_provider_runtime_transactions_ignored() {
		let mock = MockProofSizeProvider::new(100);
		let mut tracker = RecordingProofSizeProvider::new(mock.clone());

		// Record initial estimation
		tracker.estimate_encoded_size();
		assert_eq!(tracker.recorded_estimations(), vec![100]);

		// Start a runtime transaction (is_host = false)
		tracker.start_transaction(false);
		mock.set_size(200);
		tracker.estimate_encoded_size();

		// Should have both estimations
		assert_eq!(tracker.recorded_estimations(), vec![100, 200]);

		// Commit runtime transaction - should not affect recording
		tracker.commit_transaction(false);
		assert_eq!(tracker.recorded_estimations(), vec![100, 200]);

		// Rollback runtime transaction - should not affect recording
		tracker.rollback_transaction(false);
		assert_eq!(tracker.recorded_estimations(), vec![100, 200]);
	}

	#[test]
	fn recording_proof_size_provider_nested_host_transactions() {
		let mock = MockProofSizeProvider::new(100);
		let mut tracker = RecordingProofSizeProvider::new(mock.clone());

		// Initial estimation
		tracker.estimate_encoded_size();
		assert_eq!(tracker.recorded_estimations(), vec![100]);

		// Start first host transaction
		tracker.start_transaction(true);
		mock.set_size(200);
		tracker.estimate_encoded_size();

		// Start nested host transaction
		tracker.start_transaction(true);
		mock.set_size(300);
		tracker.estimate_encoded_size();

		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 300]);

		// Commit nested transaction
		tracker.commit_transaction(true);
		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 300]);

		// Commit outer transaction
		tracker.commit_transaction(true);
		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 300]);
	}

	#[test]
	fn recording_proof_size_provider_nested_host_transaction_rollback() {
		let mock = MockProofSizeProvider::new(100);
		let mut tracker = RecordingProofSizeProvider::new(mock.clone());

		// Initial estimation
		tracker.estimate_encoded_size();

		// Start first host transaction
		tracker.start_transaction(true);
		mock.set_size(200);
		tracker.estimate_encoded_size();

		// Start nested host transaction
		tracker.start_transaction(true);
		mock.set_size(300);
		tracker.estimate_encoded_size();

		assert_eq!(tracker.recorded_estimations(), vec![100, 200, 300]);

		// Rollback nested transaction
		tracker.rollback_transaction(true);
		assert_eq!(tracker.recorded_estimations(), vec![100, 200]);

		// Rollback outer transaction
		tracker.rollback_transaction(true);
		assert_eq!(tracker.recorded_estimations(), vec![100]);
	}

	#[test]
	fn recording_proof_size_provider_rollback_on_base_transaction_does_nothing() {
		let mock = MockProofSizeProvider::new(100);
		let mut tracker = RecordingProofSizeProvider::new(mock.clone());

		// Record some estimations
		tracker.estimate_encoded_size();
		tracker.estimate_encoded_size();
		assert_eq!(tracker.recorded_estimations(), vec![100, 100]);

		// Try to rollback the base transaction - should do nothing
		tracker.rollback_transaction(true);
		assert_eq!(tracker.recorded_estimations(), vec![100, 100]);
	}

	#[test]
	fn recorded_proof_size_estimations_struct() {
		let estimations = vec![100, 200, 300];
		let recorded = RecordedProofSizeEstimations(estimations.into());
		let expected: VecDeque<usize> = vec![100, 200, 300].into();
		assert_eq!(recorded.0, expected);
	}

	#[test]
	fn replay_proof_size_provider_basic_functionality() {
		let estimations = vec![100, 200, 300, 150];
		let recorded = RecordedProofSizeEstimations(estimations.into());
		let replay = ReplayProofSizeProvider::from_recorded(recorded);

		// Should replay estimations in order
		assert_eq!(replay.estimate_encoded_size(), 100);
		assert_eq!(replay.estimate_encoded_size(), 200);
		assert_eq!(replay.estimate_encoded_size(), 300);
		assert_eq!(replay.estimate_encoded_size(), 150);
	}

	#[test]
	fn replay_proof_size_provider_exhausted_returns_zero() {
		let estimations = vec![100, 200];
		let recorded = RecordedProofSizeEstimations(estimations.into());
		let replay = ReplayProofSizeProvider::from_recorded(recorded);

		// Consume all estimations
		assert_eq!(replay.estimate_encoded_size(), 100);
		assert_eq!(replay.estimate_encoded_size(), 200);

		// Should return 0 when exhausted
		assert_eq!(replay.estimate_encoded_size(), 0);
		assert_eq!(replay.estimate_encoded_size(), 0);
	}

	#[test]
	fn replay_proof_size_provider_empty_returns_zero() {
		let recorded = RecordedProofSizeEstimations(VecDeque::new());
		let replay = ReplayProofSizeProvider::from_recorded(recorded);

		// Should return 0 for empty estimations
		assert_eq!(replay.estimate_encoded_size(), 0);
		assert_eq!(replay.estimate_encoded_size(), 0);
	}

	#[test]
	fn replay_proof_size_provider_from_trait() {
		let estimations = vec![42, 84];
		let recorded = RecordedProofSizeEstimations(estimations.into());
		let replay: ReplayProofSizeProvider = recorded.into();

		assert_eq!(replay.estimate_encoded_size(), 42);
		assert_eq!(replay.estimate_encoded_size(), 84);
		assert_eq!(replay.estimate_encoded_size(), 0);
	}

	#[test]
	fn record_and_replay_integration() {
		let mock = MockProofSizeProvider::new(100);
		let recorder = RecordingProofSizeProvider::new(mock.clone());

		// Record some estimations
		recorder.estimate_encoded_size();
		mock.set_size(200);
		recorder.estimate_encoded_size();
		mock.set_size(300);
		recorder.estimate_encoded_size();

		// Get recorded estimations
		let recorded_estimations = recorder.recorded_estimations();
		assert_eq!(recorded_estimations, vec![100, 200, 300]);

		// Create replay provider from recorded estimations
		let recorded = RecordedProofSizeEstimations(recorded_estimations.into());
		let replay = ReplayProofSizeProvider::from_recorded(recorded);

		// Replay should return the same sequence
		assert_eq!(replay.estimate_encoded_size(), 100);
		assert_eq!(replay.estimate_encoded_size(), 200);
		assert_eq!(replay.estimate_encoded_size(), 300);
		assert_eq!(replay.estimate_encoded_size(), 0);
	}

	#[test]
	fn replay_proof_size_provider_single_value() {
		let estimations = vec![42];
		let recorded = RecordedProofSizeEstimations(estimations.into());
		let replay = ReplayProofSizeProvider::from_recorded(recorded);

		// Should return the single value then default to 0
		assert_eq!(replay.estimate_encoded_size(), 42);
		assert_eq!(replay.estimate_encoded_size(), 0);
	}
}