TLDR: How to implement a substrate unsigned call, which performs storage modifications and emits events during it's validation? If transaction fails, it seems like it leaves garbage in runtime, due events/storage modifications are not reverted (see docs below).
Let's say there is an extrinsic, which is an unsigned transaction with following body:
#[pallet::call_index(1)]
#[pallet::weight(42)]
#[frame_support::transactional]
pub fn extrinsic(
origin: OriginFor<T>,
..
) -> DispatchResultWithPostInfo {
let _ = ensure_none(origin)?;
let prevalidate_data = Self::prevalidate_something(..)?;
Self::do_something(..);
// Adjust weight to performed computations
let weight = Self::calc_weight(&prevalidate_data);
Ok(PostDispatchInfo {
actual_weight: Some(weight),
pays_fee: Pays::No,
})
}
Extrinsic basically calls prevalidate_something
function to perform all operations that may potentially fail (it emits events and modifies storage as well and during execution it may fail). If prevalidate_something
succedes, then do_something
is called, which performs operations that do not fail and in the end weight is calculated based on prevalidate_data
.
As long as it is unsigned transaction, it is validated in ValidateUnsigned::validate_unsigned
trait. According to documentation, this function can be called multiple times, the same thing applies to ValidateUnsigned::pre_dispatch
. But if validate_unsigned
will be called just before right dispatch (with TransactionSource::InBlock as argument), all operations performed by prevalidate_something
will be persisted, even if it will fail:
/// Validate the call right before dispatch.
///
/// This method should be used to prevent transactions already in the pool
/// (i.e. passing [`validate_unsigned`](Self::validate_unsigned)) from being included in blocks
/// in case they became invalid since being added to the pool.
///
/// By default it's a good idea to call [`validate_unsigned`](Self::validate_unsigned) from
/// within this function again to make sure we never include an invalid transaction. Otherwise
/// the implementation of the call or this method will need to provide proper validation to
/// ensure that the transaction is valid.
///
/// Changes made to storage *WILL* be persisted if the call returns `Ok`.
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
Self::validate_unsigned(TransactionSource::InBlock, call)
.map(|_| ())
.map_err(Into::into)
}
In theory, we can protect it by moving call of prevalidate_something
to validate_unsigned
and wrap it in additional transactional layer, so all operations will be reverted once it will fail:
let prevalidate_data = frame_support::storage::with_storage_layer(|| {
Pallet::<T>::prevalidate_something(
..
)
}).map_err(|err| InvalidTransaction::Custom(err.into()))?;
// We can use prevalidate_data to define priority as well
ValidTransaction::with_tag_prefix("Prefix")
.priority(42)
.propagate(!matches!(source, TransactionSource::InBlock))
.build();
But how to get prevalidate_data
for weight calculation, in case that it will be not called inside extrinsic, but in validate_unsigned
?
In theory, we can call prevalidate_something
in validate_unsigned
wrapped in with_transaction and always return TransactionOutcome::Rollback
and call prevalidate_something
in extrinsic as usuall, but it causes that this computations are performed twice for InBlock
case.
Maybe adding StorageValue like below will solve the issue, so in validate_unsigned
weight will be calculated based on prevalidate_data
and later read in the dispatch of extrinsic?
// Last weight of the transaction
#[pallet::storage]
pub type LastWeight<T: Config> = StorageValue<_, Weight, ValueQuery>;
let prevalidate_data = frame_support::storage::with_storage_layer(|| {
Pallet::<T>::prevalidate_something(
..
)
}).map_err(|err| InvalidTransaction::Custom(err.into()))?;
let weight = Pallet::<T>::calc_weight(&prevalidate_data);
LastWeight::<T>::mutate(|last_weight| {
*last_weight = weight;
});
// We can use prevalidate_data to define priority as well
ValidTransaction::with_tag_prefix("Prefix")
.priority(42)
.propagate(!matches!(source, TransactionSource::InBlock))
.build();
- Is there some possible race condition within this approach?
- Can we assume, that right after
InBlock
dispatch, no other extrinsic will be set between? - Do you have some suggestions, how to handle an extrinsic, which has a prevalidate function that emits events and modifies storage?