DEV Community 👩‍💻👨‍💻

Cover image for Substrate Pallet Code Analysis: Sudo
SoYounJeong
SoYounJeong

Posted on

Substrate Pallet Code Analysis: Sudo

Overview

In substrate-based chain, there is a god-like account who can call a privileged function which is called sudo account. Sudo pallet composed of that function what sudo account can do. There should be only one sudo account for the Runtime, which is called 'sudo-key'. Before launching public blockchain network, having sudo account could be useful but after launching, most of the Parachain projects tend to get rid of this Sudo pallet for safety.

Let's look at how this pallet is implemented.


Source Code Analysis

Storage

There is only one storage for this pallet. Storage type is StorageValue to store only one sudo key account for Runtime.

#[pallet::storage]
#[pallet::getter(fn key)]
pub(super) type Key<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;
Enter fullscreen mode Exit fullscreen mode

Dispatch Call

sudo

Dispatch a call with Root origin.

Params

  • origin: who calls this function
  • call: Some Call type like dispatch calls. When building runtime, Compiler collects all the Call type into one single Call Enum type by #[pallet::call] macro. Code below is how Substrate deals with Call data type.

In Runtime,

pub enum Call {

    System( <Pallet<Runtime> as Callable<Runtime>>::Call ),
    Balances( <Pallet<Runtime> as Callable<Runtime>>::Call ),
    Staking( <Pallet<Runtime> as Callable<Runtime>>::Call ),
    ...
}

Enter fullscreen mode Exit fullscreen mode

For example, in Staking Pallet, any dispatch call is defined in Call enum type

pub enum Call {
   ...
   Staking(pallet_staking::Call::<Runtime>)
   ...
}

pub enum Call {
    bond { controller, value, payee },
    nominate { targets }
    ....
}
Enter fullscreen mode Exit fullscreen mode

Analysis

  1. Check whether origin is signed.
  2. Check whether caller is 'sudo-key account'
  3. dispatch_bypass_filter: Skip checking origin filter in dispatch call.
  4. Emit 'Sudid' event
  5. Sudo account doesn't pay fee
pub fn sudo(
    origin: OriginFor<T>,
    call: Box<<T as Config>::Call>,
) -> DispatchResultWithPostInfo {

    // 1
    let sender = ensure_signed(origin)?;

    // 2
    ensure!(Self::key().map_or(false, |k| sender == k), Error::<T>::RequireSudo);

    // 3 
    let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into());

    // 4
    Self::deposit_event(Event::Sudid { sudo_result: res.map(|_| ()).map_err(|e| e.error) });

    // 5 
    Ok(Pays::No.into())
}
Enter fullscreen mode Exit fullscreen mode

sudo_unchecked_weight

The logic of this method is same with _sudo but this time, Sudo account can control the Weight of the call. As we can see the parameter we give as _weight is used in the weight calculation in #[pallet::weight((*_weight, call.get_dispatch_info().class))] macro._

Params

  • _weight: Amount of weight sudo want to control
#[pallet::weight((*_weight, call.get_dispatch_info().class))]
pub fn sudo_unchecked_weight(
    origin: OriginFor<T>,
    call: Box<<T as Config>::Call>,
    _weight: Weight,
) -> DispatchResultWithPostInfo {

    // Same logic with sudo
}
Enter fullscreen mode Exit fullscreen mode

set_key

Set sudo key with different account

Params

  • new: New sudo key account. Type is Source type of StaticLookup trait. This type is for converting any source type into AccountId. Let's take a closer look!

First of all, StaticLookup is defined as

  1. type Source: any type that we want to look
  2. type Target: Type that we want to covert
  3. fn lookup: Convert Source type into Target type
  4. fn unlookup: Convert Target type into Source type
pub trait StaticLookup {
    // 1
    pub type Source;
    // 2 
    pub type Target;
    // 3 
    fn lookup(s: Self::Source) -> Result<Self::Target, LookupError>
    // 4
    fn unlookup(t: Self::Target) -> Self::Source;
}
Enter fullscreen mode Exit fullscreen mode

In frame system pallet, Target is defined with AccountId type, which means any Source type that we want to look can be converted into AccountId type when we call lookup method.

pub trait Config {
    ...

    type lookup: StaticLookup<Target = AccountId>

    ...
}
Enter fullscreen mode Exit fullscreen mode

In Runtime, when we look at how frame_system's config is implemented for Runtime, pallet_indices is defined as lookup, which means pallet_indices must implement StaticLookup trait.

impl frame_system::Config for Runtime {

    ...

    type lookup = Indices(pallet_indices)

    ...
}
Enter fullscreen mode Exit fullscreen mode

Inside pallet_indices

  1. impl StaticLookup for Pallet
  2. MultiAddress: Type of address formats
  3. fn lookup_address(): Returns address if exists depending on the formats of address. Here, type of address format we are looking for is Account Id and AccountIndex.
// 1
impl<T: Config> StaticLookup for Pallet<T> {
    type Source = MultiAddress<T::AccountId, T::AccountIndex>,
    type Target = T::AccountId,

    fn lookup(a: Self::Source) -> Result<Self::Target, LookupError> {
        Self::lookup_address(a).ok_or(LookupError);
    }

    fn unlookup(a: Self::Target) -> Self::Source {
       Multiaddress::Id(a)
    } 
}

// 2
pub enum MultiAddress<AccountId, AccountIndex> {
    Id(AccountId), // public address
    Index(AccountIndex), // Account index for database
    ...
}

// 3
impl<T: Config> Pallet<T> {

    fn lookup_index(index: T::AccountIndex) -> Option<T::AccountId> {
        // where Accounts is a storage that maps _AccountIndex_ to (AccountId, Balance)
        Accounts::<T>::get(index).map(|x| x.0)

    }  

    fn lookup_address(a: Multiaddress<T::AccountId, T::AccountIndex> -> Option<T::AccountId> {
         match a {
              Multiaddress::Id(account) => Some(account)
              Multiaddress::Index(index) => Self::lookup_index(index)
         }
    }
}

#[pallet::storage]
pub type Accounts<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountIndex, (T::AccountId, BalanceOf<T>, bool)>;
Enter fullscreen mode Exit fullscreen mode

Analysis

  1. Check whether origin is signed.
  2. Check whether caller is 'sudo-key account'
  3. Convert MultiAddress type into AccountId,
  4. Emit 'KeyChanged' event
  5. Put new sudo-key into Key storage
  6. Sudo doesn't pay a fee
#[pallet::weight(0)]
pub fn set_key(
    origin: OriginFor<T>,
    new: <T::Lookup as StaticLookup>::Source,
) -> DispatchResultWithPostInfo {

    // 1
    let sender = ensure_signed(origin)?;

    // 2
    ensure!(Self::key().map_or(false, |k| sender == k), Error::<T>::RequireSudo);

    // 3
    let new = T::Lookup::lookup(new)?;

    // 4
    Self::deposit_event(Event::KeyChanged { old_sudoer: Key::<T>::get() });

    // 5
    Key::<T>::put(&new);

    // 6
    Ok(Pays::No.into())
}
Enter fullscreen mode Exit fullscreen mode

sudo_as

Sudo account calls and dispatches a call with a signed account.
Make signed account as sudo account which pays no fee
Params

  • who: The one who calls the dispatch call
pub fn sudo_as(
    origin: OriginFor<T>,
    who: <T::Lookup as StaticLookup>::Source,
    call: Box<<T as Config>::Call>,
) -> DispatchResultWithPostInfo {

    let sender = ensure_signed(origin)?;
    ensure!(Self::key().map_or(false, |k| sender == k), Error::<T>::RequireSudo);

    let who = T::Lookup::lookup(who)?;

    let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Signed(who).into());

    Self::deposit_event(Event::SudoAsDone {
                sudo_result: res.map(|_| ()).map_err(|e| e.error),
            });

    Ok(Pays::No.into())
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Sudo is special feature that only exists in Substrate based chain which can do everything and it is not preferred to have in public blockchain.

Feel free to leave a comment if you have some questions.


Reference

Sudo
Frame System
Indices

Top comments (0)

Take a look at this:

Settings

Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. 🛠