Developers Home»How-to Guide»Making Transactions

Making Transactions

Goal

Learn how to save data that has been handled by an off-chain worker using signed and unsigned transactions. To do so, a transaction must be sent on-chain from off-chain workers.

Overview

You cannot save data processed by off-chain workers directly to on-chain storage. To store any data from an off-chain worker on-chain, you must create a transaction that sends the data from the off-chain worker to the on-chain storage system. You can create transactions that send data from off-chain workers to on-chain storage as signed transactions or unsigned transactions depending on how you want the transaction calling account to be handled. For example:

  • Use signed transactions if you want to record the associated transaction caller and deduct the transaction fee from the caller account.
  • Use unsigned transactions if you DO NOT want to record the associated transaction caller.
  • Use unsigned transactions with signed payload if you want to record the associated transaction caller, but do not want the caller be responsible for the transaction fee payment.

Sending signed transactions

  1. In your pallet, call the hook for off-chain workers as follows:

    #[pallet::hooks]
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
      /// Offchain Worker entry point.
      ///
      /// By implementing `fn offchain_worker` you declare a new offchain worker.
      /// This function will be called when the node is fully synced and a new best block is
      /// successfully imported.
      /// Note that it's not guaranteed for offchain workers to run on EVERY block, there might
      /// be cases where some blocks are skipped, or for some the worker runs twice (re-orgs),
      /// so the code should be able to handle that.
      fn offchain_worker(block_number: T::BlockNumber) {
        log::info!("Hello from pallet-ocw.");
        // The entry point of your code called by off-chain worker
      }
      // ...
    }
    
  2. Add the CreateSignedTransaction trait to the Config trait for your pallet. For example, your pallet Config trait should look similar to this:

    /// This pallet's configuration trait
    #[pallet::config]
    pub trait Config: CreateSignedTransaction<Call<Self>> + frame_system::Config {
      // ...
    }
    
  3. Add a crypto module with an sr25519 signature key to ensure that your pallet owns an account that can be used for signing transactions.

    use sp_core::{crypto::KeyTypeId};
    
    // ...
    
    pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"demo");
    
    // ...
    
    pub mod crypto {
      use super::KEY_TYPE;
      use sp_core::sr25519::Signature as Sr25519Signature;
      use sp_runtime::{
        app_crypto::{app_crypto, sr25519},
        traits::Verify, MultiSignature, MultiSigner
      };
      app_crypto!(sr25519, KEY_TYPE);
    
      pub struct TestAuthId;
    
      // implemented for runtime
      impl frame_system::offchain::AppCrypto<MultiSigner, MultiSignature> for TestAuthId {
        type RuntimeAppPublic = Public;
        type GenericSignature = sp_core::sr25519::Signature;
        type GenericPublic = sp_core::sr25519::Public;
      }
    }
    

    The app_crypto macro declares an account with an sr25519 signature that is identified by KEY_TYPE. Note that this doesn't create a new account. The macro simply declares that a crypto account is available for this pallet. You will need to initialize this account in the next step.

  4. Initialize a signing account for sending a signed transaction to on-chain storage.

    fn offchain_worker(block_number: T::BlockNumber) {
      let signer = Signer::<T, T::AuthorityId>::all_accounts();
    
      // ...
    }
    

    Call Signer<T, C>::all_accounts() to retrieve all signers this pallet owned. You will later (in step #9) inject one account into this pallet for this retrieval.

  5. Use send_signed_transaction() to send an extrinsic call:

    fn offchain_worker(block_number: T::BlockNumber) {
      let signer = Signer::<T, T::AuthorityId>::all_accounts();
    
      // Using `send_signed_transaction` associated type we create and submit a transaction
      // representing the call we've just created.
      // `send_signed_transaction()` return type is `Option<(Account<T>, Result<(), ()>)>`. It is:
      //   - `None`: no account is available for sending transaction
      //   - `Some((account, Ok(())))`: transaction is successfully sent
      //   - `Some((account, Err(())))`: error occurred when sending the transaction
      let results = signer.send_signed_transaction(|_account| {
        Call::on_chain_call { key: val }
      });
    
      // ...
    }
    
  6. Check if the transaction is successfully submitted on-chain and perform proper error handling by checking the returned results.

    fn offchain_worker(block_number: T::BlockNumber) {
      // ...
    
      for (acc, res) in &results {
        match res {
          Ok(()) => log::info!("[{:?}]: submit transaction success.", acc.id),
          Err(e) => log::error!("[{:?}]: submit transaction failure. Reason: {:?}", acc.id, e),
        }
      }
    
      Ok(())
    }
    
  7. Implement the CreateSignedTransaction trait in the runtime.

    Because you configured the Config trait for this pallet to implement the CreateSignedTransaction trait, you also need to implement that trait for the runtime.

    By looking at CreateSignedTransaction Rust docs, you can see that you only need to implement the function create_transaction() for the runtime. In runtime/src/lib.rs:

    impl<LocalCall> frame_system::offchain::CreateSignedTransaction<LocalCall> for Runtime
    where
      Call: From<LocalCall>,
    {
      fn create_transaction<C: frame_system::offchain::AppCrypto<Self::Public, Self::Signature>>(
        call: Call,
        public: <Signature as sp_runtime::traits::Verify>::Signer,
        account: AccountId,
        index: Index,
      ) -> Option<(Call, <UncheckedExtrinsic as sp_runtime::traits::Extrinsic>::SignaturePayload)> {
        let period = BlockHashCount::get() as u64;
        let current_block = System::block_number()
          .saturated_into::<u64>()
          .saturating_sub(1);
        let tip = 0;
        let extra: SignedExtra = (
          frame_system::CheckSpecVersion::<Runtime>::new(),
          frame_system::CheckTxVersion::<Runtime>::new(),
          frame_system::CheckGenesis::<Runtime>::new(),
          frame_system::CheckEra::<Runtime>::from(generic::Era::mortal(period, current_block)),
          frame_system::CheckNonce::<Runtime>::from(index),
          frame_system::CheckWeight::<Runtime>::new(),
          pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(tip),
        );
    
        let raw_payload = SignedPayload::new(call, extra)
          .map_err(|e| {
            log::warn!("Unable to create signed payload: {:?}", e);
          })
          .ok()?;
        let signature = raw_payload.using_encoded(|payload| C::sign(payload, public))?;
        let address = account;
        let (call, extra, _) = raw_payload.deconstruct();
        Some((call, (sp_runtime::MultiAddress::Id(address), signature.into(), extra)))
      }
    }
    

    The above code seems long, but what it tries to do is really:

    • Create and prepare extra of SignedExtra type, and put various checkers in-place.
    • Create a raw payload based on the passed in call and extra.
    • Sign the raw payload with the account public key.
    • Finally, bundle all data up and return a tuple of the call, the caller, its signature, and any signed extension data.

    You can see a full example of the code in the Substrate code base.

  8. Implement SigningTypes and SendTransactionTypes in the runtime to support submitting transactions, whether they are signed or unsigned.

    impl frame_system::offchain::SigningTypes for Runtime {
      type Public = <Signature as sp_runtime::traits::Verify>::Signer;
      type Signature = Signature;
    }
    
    impl<C> frame_system::offchain::SendTransactionTypes<C> for Runtime
    where
      Call: From<C>,
    {
      type OverarchingCall = Call;
      type Extrinsic = UncheckedExtrinsic;
    }
    

    You can see an example of this implementation in the Substrate code base.

  9. Inject an account for this pallet to own. In a development environment (node running with --dev flag), this account key is inserted in the node/src/service.rs file as follows:

    pub fn new_partial(config: &Configuration) -> Result <SomeStruct, SomeError> {
    
      //...
    
      if config.offchain_worker.enabled {
        // Initialize seed for signing transaction using off-chain workers. This is a convenience
        // so learners can see the transactions submitted simply running the node.
        // Typically these keys should be inserted with RPC calls to `author_insertKey`.
        sp_keystore::SyncCryptoStore::sr25519_generate_new(
          &*keystore,
          node_template_runtime::pallet_your_ocw_pallet::KEY_TYPE,
          Some("//Alice"),
        ).expect("Creating key with account Alice should succeed.");
      }
    }
    

    Refer to this file for a working example. This example adds the key for the Alice account to the key store identified by the pallet-defined KEY_TYPE. In production, one or more accounts are injected via chain spec configuration.

    Now, your pallet is ready to send signed transactions on-chain from off-chain workers.

Sending unsigned transactions

By default, all unsigned transactions are rejected in Substrate. To enable Substrate to accept certain unsigned transactions, you must implement the ValidateUnsigned trait for the pallet.

  1. Open the src/lib.rs file for your pallet in a text editor.

    #[pallet::validate_unsigned]
    impl<T: Config> ValidateUnsigned for Pallet<T> {
      type Call = Call<T>;
    
      /// Validate unsigned call to this module.
      ///
      /// By default unsigned transactions are disallowed, but implementing the validator
      /// here we make sure that some particular calls (the ones produced by offchain worker)
      /// are being whitelisted and marked as valid.
      fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
        //...
      }
    }
    

    Call the validate_unsigned pallet macro, then implement the trait as follows:

    fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
      let valid_tx = |provide| ValidTransaction::with_tag_prefix("my-pallet")
        .priority(UNSIGNED_TXS_PRIORITY) // please define `UNSIGNED_TXS_PRIORITY` before this line
        .and_provides([&provide])
        .longevity(3)
        .propagate(true)
        .build();
      // ...
    }
    

    Next, check the calling extrinsics to determine if the call is allowed. Return ValidTransaction if the call is allowed or return TransactionValidityError if the call is not allowed.

    fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity {
      // ...
      match call {
        Call::extrinsic1 { key: value } => valid_tx(b"extrinsic1".to_vec()),
        _ => InvalidTransaction::Call.into(),
      }
    }
    

    In this example, users can call the on-chain extrinsic1 function without a signature, but not any other extrinsics.

    To see a full example of how ValidateUnsigned is implemented in a pallet, refer to pallet-example-offchain-worker in Substrate.

  2. In the off-chain worker function, you can send unsigned transactions as follows:

    #[pallet::hooks]
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
      /// Offchain Worker entry point.
      fn offchain_worker(block_number: T::BlockNumber) {
        let value: u64 = 10;
        // This is your call to on-chain extrinsic together with any necessary parameters.
        let call = Call::unsigned_extrinsic1 { key: value };
    
        // `submit_unsigned_transaction` returns a type of `Result<(), ()>`
        //   ref: https://paritytech.github.io/substrate/latest/frame_system/offchain/struct.SubmitTransaction.html
        SubmitTransaction::<T, Call<T>>::submit_unsigned_transaction(call.into())
          .map_err(|_| {
            log::error!("Failed in offchain_unsigned_tx");
          });
      }
    }
    

    This code prepares the call in the let call = ... line, submits the transaction using SubmitTransaction::submit_unsigned_transaction, and performs any necessary error handling in the callback function passed in.

  3. Enable the ValidateUnsigned trait for the pallet in the runtime by adding the ValidateUnsigned type to the construct_runtime macro.

    For example:

    construct_runtime!(
      pub enum Runtime where
        Block = Block,
        NodeBlock = opaque::Block,
        UncheckedExtrinsic = UncheckedExtrinsic
      {
        // ...
        OcwPallet: pallet_ocw::{Pallet, Call, Storage, Event<T>, ValidateUnsigned},
      }
    );
    
  4. Implement the SendTransactionTypes trait for the runtime as described in sending signed transactions.

You can see a full example in pallet-example-offchain-worker in Substrate code base.

Sending unsigned transactions with signed payloads

Sending unsigned transactions with signed payloads is similar to sending unsigned transactions. You need to:

  • Implement the ValidateUnsigned trait for the pallet.
  • Add the ValidateUnsigned type to the runtime when using this pallet.
  • Prepare the data structure to be signed—the signed payload—by implementing the SignedPayload trait.
  • Send the transaction with the signed payload.

You can refer to the section on sending unsigned transactions for more information about implementing the ValidateUnsigned trait and adding the ValidateUnsigned type to the runtime. The differences between sending unsigned transactions and sending unsigned transactions with signed payload are illustrated in the following code examples.

  1. To make your data structure signable, implement the SignedPayload trait. For example:

    #[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, scale_info::TypeInfo)]
    pub struct Payload<Public> {
      number: u64,
      public: Public,
    }
    
    impl<T: SigningTypes> SignedPayload<T> for Payload<T::Public> {
      fn public(&self) -> T::Public {
        self.public.clone()
      }
    }
    

    You can also see an example here.

  2. In your pallet's offchain_worker function, call the signer, then the function to send the transaction:

    #[pallet::hooks]
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
      /// Offchain Worker entry point.
      fn offchain_worker(block_number: T::BlockNumber) {
        let value: u64 = 10;
    
        // Retrieve the signer to sign the payload
        let signer = Signer::<T, T::AuthorityId>::any_account();
    
        // `send_unsigned_transaction` is returning a type of `Option<(Account<T>, Result<(), ()>)>`.
        //   The returned result means:
        //   - `None`: no account is available for sending transaction
        //   - `Some((account, Ok(())))`: transaction is successfully sent
        //   - `Some((account, Err(())))`: error occurred when sending the transaction
        if let Some((_, res)) = signer.send_unsigned_transaction(
          // this line is to prepare and return payload
          |acct| Payload { number, public: acct.public.clone() },
          |payload, signature| Call::some_extrinsics { payload, signature },
        ) {
          match res {
            Ok(()) => log::info!("unsigned tx with signed payload successfully sent.");
            Err(()) => log::error!("sending unsigned tx with signed payload failed.");
          };
        } else {
          // The case of `None`: no account is available for sending
          log::error!("No local account available");
        }
      }
    }
    

    This code retrieves the signer then calls send_unsigned_transaction() with two function closures. The first function closure returns the payload to be used. The second function closure returns the on-chain call with payload and signature passed in. This call returns an Option<(Account<T>, Result<(), ()>)> result type to allow for the following results:

    • None if no account is available for sending the transaction.
    • Some((account, Ok(()))) if the transaction is successfully sent.
    • Some((account, Err(()))) if an error occurs when sending the transaction.
  3. For a more complex implementation of ValidateUnsigned, check whether a provided signature matches the public key used to sign the payload:

    #[pallet::validate_unsigned]
    impl<T: Config> ValidateUnsigned for Pallet<T> {
      type Call = Call<T>;
    
      fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {
        let valid_tx = |provide| ValidTransaction::with_tag_prefix("ocw-demo")
          .priority(UNSIGNED_TXS_PRIORITY)
          .and_provides([&provide])
          .longevity(3)
          .propagate(true)
          .build();
    
        match call {
          Call::unsigned_extrinsic_with_signed_payload {
            ref payload,
            ref signature
          } => {
            if !SignedPayload::<T>::verify::<T::AuthorityId>(payload, signature.clone()) {
              return InvalidTransaction::BadProof.into();
            }
            valid_tx(b"unsigned_extrinsic_with_signed_payload".to_vec())
          },
          _ => InvalidTransaction::Call.into(),
        }
      }
    }
    

    This example uses SignedPayload to verify that the public key in the payload has the same signature as the one provided.

Refer to the off-chain function call and the implementation of ValidateUnsigned for a working example of the above.

You have now seen how you can use off-chain workers to send data for on-chain storage using:

  • Signed transactions
  • Unsigned transactions
  • Unsigned transactions with signed payload

Examples

Last edit: on

Run into problems?
Let us Know