use crate::offchain::storage::StorageValueRef;
use crate::traits::AtLeast32BitUnsigned;
use codec::{Codec, Decode, Encode};
use sp_core::offchain::{Duration, Timestamp};
use sp_io::offchain;
const STORAGE_LOCK_DEFAULT_EXPIRY_DURATION: Duration = Duration::from_millis(20_000);
const STORAGE_LOCK_DEFAULT_EXPIRY_BLOCKS: u32 = 4;
const STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MIN: Duration = Duration::from_millis(100);
const STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MAX: Duration = Duration::from_millis(10);
pub trait Lockable: Sized {
type Deadline: Sized + Codec + Clone;
fn deadline(&self) -> Self::Deadline;
fn has_expired(deadline: &Self::Deadline) -> bool;
fn snooze(_deadline: &Self::Deadline) {
sp_io::offchain::sleep_until(
offchain::timestamp().add(STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MAX),
);
}
}
#[derive(Encode, Decode)]
pub struct Time {
expiration_duration: Duration,
}
impl Default for Time {
fn default() -> Self {
Self {
expiration_duration: STORAGE_LOCK_DEFAULT_EXPIRY_DURATION,
}
}
}
impl Lockable for Time {
type Deadline = Timestamp;
fn deadline(&self) -> Self::Deadline {
offchain::timestamp().add(self.expiration_duration)
}
fn has_expired(deadline: &Self::Deadline) -> bool {
offchain::timestamp() > *deadline
}
fn snooze(deadline: &Self::Deadline) {
let now = offchain::timestamp();
let remainder: Duration = now.diff(&deadline);
use core::cmp::{max, min};
let snooze = max(
min(remainder, STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MAX),
STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MIN,
);
sp_io::offchain::sleep_until(now.add(snooze));
}
}
#[derive(Encode, Decode, Eq, PartialEq)]
pub struct BlockAndTimeDeadline<B: BlockNumberProvider> {
pub block_number: <B as BlockNumberProvider>::BlockNumber,
pub timestamp: Timestamp,
}
impl<B: BlockNumberProvider> Clone for BlockAndTimeDeadline<B> {
fn clone(&self) -> Self {
Self {
block_number: self.block_number.clone(),
timestamp: self.timestamp.clone(),
}
}
}
impl<B: BlockNumberProvider> Default for BlockAndTimeDeadline<B> {
fn default() -> Self {
Self {
block_number: B::current_block_number() + STORAGE_LOCK_DEFAULT_EXPIRY_BLOCKS.into(),
timestamp: offchain::timestamp().add(STORAGE_LOCK_DEFAULT_EXPIRY_DURATION),
}
}
}
pub struct BlockAndTime<B: BlockNumberProvider> {
expiration_block_number_offset: u32,
expiration_duration: Duration,
_phantom: core::marker::PhantomData<B>,
}
impl<B: BlockNumberProvider> Default for BlockAndTime<B> {
fn default() -> Self {
Self {
expiration_block_number_offset: STORAGE_LOCK_DEFAULT_EXPIRY_BLOCKS,
expiration_duration: STORAGE_LOCK_DEFAULT_EXPIRY_DURATION,
_phantom: core::marker::PhantomData::<B>,
}
}
}
impl<B: BlockNumberProvider> Clone for BlockAndTime<B> {
fn clone(&self) -> Self {
Self {
expiration_block_number_offset: self.expiration_block_number_offset.clone(),
expiration_duration: self.expiration_duration,
_phantom: core::marker::PhantomData::<B>,
}
}
}
impl<B: BlockNumberProvider> Lockable for BlockAndTime<B> {
type Deadline = BlockAndTimeDeadline<B>;
fn deadline(&self) -> Self::Deadline {
let block_number = <B as BlockNumberProvider>::current_block_number()
+ self.expiration_block_number_offset.into();
BlockAndTimeDeadline {
timestamp: offchain::timestamp().add(self.expiration_duration),
block_number,
}
}
fn has_expired(deadline: &Self::Deadline) -> bool {
offchain::timestamp() > deadline.timestamp
&& <B as BlockNumberProvider>::current_block_number() > deadline.block_number
}
fn snooze(deadline: &Self::Deadline) {
let now = offchain::timestamp();
let remainder: Duration = now.diff(&(deadline.timestamp));
use core::cmp::{max, min};
let snooze = max(
min(remainder, STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MAX),
STORAGE_LOCK_PER_CHECK_ITERATION_SNOOZE_MIN,
);
sp_io::offchain::sleep_until(now.add(snooze));
}
}
pub struct StorageLock<'a, L = Time> {
value_ref: StorageValueRef<'a>,
lockable: L,
}
impl<'a, L: Lockable + Default> StorageLock<'a, L> {
pub fn new(key: &'a [u8]) -> Self {
Self::with_lockable(key, Default::default())
}
}
impl<'a, L: Lockable> StorageLock<'a, L> {
pub fn with_lockable(key: &'a [u8], lockable: L) -> Self {
Self {
value_ref: StorageValueRef::<'a>::persistent(key),
lockable,
}
}
fn extend_active_lock(&mut self) -> Result<<L as Lockable>::Deadline, ()> {
let res = self.value_ref.mutate(|s: Option<Option<L::Deadline>>| -> Result<<L as Lockable>::Deadline, ()> {
match s {
Some(Some(deadline)) if !<L as Lockable>::has_expired(&deadline) =>
Ok(self.lockable.deadline()),
_ => Err(()),
}
});
match res {
Ok(Ok(deadline)) => Ok(deadline),
Ok(Err(_)) => Err(()),
Err(e) => Err(e),
}
}
fn try_lock_inner(
&mut self,
new_deadline: L::Deadline,
) -> Result<(), <L as Lockable>::Deadline> {
let res = self.value_ref.mutate(
|s: Option<Option<L::Deadline>>|
-> Result<<L as Lockable>::Deadline, <L as Lockable>::Deadline> {
match s {
None => Ok(new_deadline),
Some(None) => Ok(new_deadline),
Some(Some(deadline)) if <L as Lockable>::has_expired(&deadline) =>
Ok(new_deadline),
Some(Some(deadline)) => Err(deadline),
}
},
);
match res {
Ok(Ok(_)) => Ok(()),
Ok(Err(deadline)) => Err(deadline),
Err(e) => Err(e),
}
}
pub fn try_lock(&mut self) -> Result<StorageLockGuard<'a, '_, L>, <L as Lockable>::Deadline> {
self.try_lock_inner(self.lockable.deadline())?;
Ok(StorageLockGuard::<'a, '_> { lock: Some(self) })
}
pub fn lock(&mut self) -> StorageLockGuard<'a, '_, L> {
while let Err(deadline) = self.try_lock_inner(self.lockable.deadline()) {
L::snooze(&deadline);
}
StorageLockGuard::<'a, '_, L> { lock: Some(self) }
}
fn unlock(&mut self) {
self.value_ref.clear();
}
}
pub struct StorageLockGuard<'a, 'b, L: Lockable> {
lock: Option<&'b mut StorageLock<'a, L>>,
}
impl<'a, 'b, L: Lockable> StorageLockGuard<'a, 'b, L> {
pub fn forget(mut self) {
let _ = self.lock.take();
}
pub fn extend_lock(&mut self) -> Result<<L as Lockable>::Deadline, ()> {
if let Some(ref mut lock) = self.lock {
lock.extend_active_lock()
} else {
Err(())
}
}
}
impl<'a, 'b, L: Lockable> Drop for StorageLockGuard<'a, 'b, L> {
fn drop(&mut self) {
if let Some(lock) = self.lock.take() {
lock.unlock();
}
}
}
impl<'a> StorageLock<'a, Time> {
pub fn with_deadline(key: &'a [u8], expiration_duration: Duration) -> Self {
Self {
value_ref: StorageValueRef::<'a>::persistent(key),
lockable: Time {
expiration_duration: expiration_duration,
},
}
}
}
impl<'a, B> StorageLock<'a, BlockAndTime<B>>
where
B: BlockNumberProvider,
{
pub fn with_block_and_time_deadline(
key: &'a [u8],
expiration_block_number_offset: u32,
expiration_duration: Duration,
) -> Self {
Self {
value_ref: StorageValueRef::<'a>::persistent(key),
lockable: BlockAndTime::<B> {
expiration_block_number_offset,
expiration_duration,
_phantom: core::marker::PhantomData,
},
}
}
pub fn with_block_deadline(key: &'a [u8], expiration_block_number_offset: u32) -> Self {
Self {
value_ref: StorageValueRef::<'a>::persistent(key),
lockable: BlockAndTime::<B> {
expiration_block_number_offset,
expiration_duration: STORAGE_LOCK_DEFAULT_EXPIRY_DURATION,
_phantom: core::marker::PhantomData,
},
}
}
}
pub trait BlockNumberProvider {
type BlockNumber: Codec + Clone + Ord + Eq + AtLeast32BitUnsigned;
fn current_block_number() -> Self::BlockNumber;
}
#[cfg(test)]
mod tests {
use super::*;
use sp_core::offchain::{testing, OffchainExt};
use sp_io::TestExternalities;
const VAL_1: u32 = 0u32;
const VAL_2: u32 = 0xFFFF_FFFFu32;
#[test]
fn storage_lock_write_unlock_lock_read_unlock() {
let (offchain, state) = testing::TestOffchainExt::new();
let mut t = TestExternalities::default();
t.register_extension(OffchainExt::new(offchain));
t.execute_with(|| {
let mut lock = StorageLock::<'_, Time>::new(b"lock_1");
let val = StorageValueRef::persistent(b"protected_value");
{
let _guard = lock.lock();
val.set(&VAL_1);
assert_eq!(val.get::<u32>(), Some(Some(VAL_1)));
}
{
let _guard = lock.lock();
val.set(&VAL_2);
assert_eq!(val.get::<u32>(), Some(Some(VAL_2)));
}
});
assert_eq!(state.read().persistent_storage.get(b"lock_1"), None);
}
#[test]
fn storage_lock_and_forget() {
let (offchain, state) = testing::TestOffchainExt::new();
let mut t = TestExternalities::default();
t.register_extension(OffchainExt::new(offchain));
t.execute_with(|| {
let mut lock = StorageLock::<'_, Time>::new(b"lock_2");
let val = StorageValueRef::persistent(b"protected_value");
let guard = lock.lock();
val.set(&VAL_1);
assert_eq!(val.get::<u32>(), Some(Some(VAL_1)));
guard.forget();
});
let opt = state.read().persistent_storage.get(b"lock_2");
assert!(opt.is_some());
}
#[test]
fn storage_lock_and_let_expire_and_lock_again() {
let (offchain, state) = testing::TestOffchainExt::new();
let mut t = TestExternalities::default();
t.register_extension(OffchainExt::new(offchain));
t.execute_with(|| {
let sleep_until = offchain::timestamp().add(Duration::from_millis(500));
let lock_expiration = Duration::from_millis(200);
let mut lock = StorageLock::<'_, Time>::with_deadline(b"lock_3", lock_expiration);
{
let guard = lock.lock();
guard.forget();
}
offchain::sleep_until(sleep_until);
let mut lock = StorageLock::<'_, Time>::new(b"lock_3");
let res = lock.try_lock();
assert!(res.is_ok());
let guard = res.unwrap();
guard.forget();
});
let opt = state.read().persistent_storage.get(b"lock_3");
assert!(opt.is_some());
}
#[test]
fn extend_active_lock() {
let (offchain, state) = testing::TestOffchainExt::new();
let mut t = TestExternalities::default();
t.register_extension(OffchainExt::new(offchain));
t.execute_with(|| {
let lock_expiration = Duration::from_millis(300);
let mut lock = StorageLock::<'_, Time>::with_deadline(b"lock_4", lock_expiration);
let mut guard = lock.lock();
offchain::sleep_until(offchain::timestamp().add(Duration::from_millis(200)));
assert_eq!(guard.extend_lock().is_ok(), true);
offchain::sleep_until(offchain::timestamp().add(Duration::from_millis(200)));
let mut lock = StorageLock::<'_, Time>::with_deadline(b"lock_4", lock_expiration);
let res = lock.try_lock();
assert_eq!(res.is_ok(), false);
offchain::sleep_until(offchain::timestamp().add(Duration::from_millis(200)));
assert_eq!(guard.extend_lock().is_ok(), false);
guard.forget();
let mut lock = StorageLock::<'_, Time>::with_deadline(b"lock_4", lock_expiration);
let res = lock.try_lock();
assert!(res.is_ok());
let guard = res.unwrap();
guard.forget();
});
let opt = state.read().persistent_storage.get(b"lock_4");
assert_eq!(opt.unwrap(), vec![132_u8, 3u8, 0, 0, 0, 0, 0, 0]);
}
}