3

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?

1 Answer 1

0

if transaction fails, it seems like it leaves garbage in runtime, due events/storage modifications are not reverted.

This is incorrect. Like any extrinsic the default is that state changes will only persist if it doesn't return an error.

The SignedExtension's validation don't include the full logic of an extrinsic (see first answer extrinsic validation lifecycle). More specifically, for your 'unsigned' case, only the check_weight has a validate_unsigned implementation and the rest return the default (Ok).

SignedExtensions are called from the transaction pool to initially validate a transaction and when applying the transactions from a block (pre_dispatch is called which default implementation is that it calls validate/validate_unsigned). The weight for SignedExtensions is handled through ExtrinsicBaseWeight.

So to your example, prevalidate_something will only be executed when the extrinsic is applied from a block.

6
  • Hmm, are you sure about that? I have added debug logs to print source of the transaction each time a validation is triggered and I see that it is always called twice, first time with TransactionSource::Local/External and second time with InBlock: 2023-08-31 09:30:21.032 WARN offchain-worker pallet_template::pallet: Received Local transaction 2023-08-31 09:30:24.001 WARN tokio-runtime-worker pallet_template::pallet: Recieved InBlock transaction
    – Klapeyron
    Aug 31 at 7:31
  • Validation of the extrinsic's logic or SignedExtension logic? Aug 31 at 8:40
  • That it will be executed only when extrinsic is applied from a block
    – Klapeyron
    Sep 1 at 11:00
  • Can you make an edit to your question and be specific about your question, it is hard to follow you. Sep 1 at 12:20
  • Sure, I have added TLDR section
    – Klapeyron
    Sep 4 at 12:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.