⛓️#[ink::chain_extension]

Na configuração padrão do contracts-pallet, um contrato inteligente só pode interagir com a runtime por meio de sua interface básica bem definida. Essa API já permite uma variedade de interações entre o contracts-pallet e o contrato inteligente em execução. Por exemplo, é possível chamar e instanciar outros contratos inteligentes na mesma cadeia, emitir eventos, consultar informações de contexto ou executar procedimentos de hash criptográfico incorporados.

Se esse conjunto básico de recursos não for suficiente para atender às necessidades de uma determinada blockchain construída com Substrate, é possível estender facilmente essa API usando o recurso chamado chain extension.

Com as chain extensions, você pode expor partes da lógica da sua runtime aos desenvolvedores de contratos inteligentes.

NOTA: O repositório do ink! contém o exemplo rand-extension. Este é um exemplo completo de uma chain extension implementada tanto em ink! quanto no Substrate.

Estrutura

A interface consiste em um código de erro que indica erros leves, bem como a definição de alguns métodos de chain extension.

A estrutura geral segue a de uma simples definição de trait em Rust. O código de erro é definido como uma definição de tipo associado da definição do trait. Os métodos são definidos como métodos associados do trait sem implementação.

Os métodos de chain extension não devem ter um receptor self, como &self ou &mut self, e devem ter entradas e saída que implementam o codec SCALE. O valor de retorno segue regras específicas que podem ser alteradas usando o atributo handle_status e a alternância entre tipos Result e Non-Result, que são descritos com mais detalhes abaixo.

Uso

Normalmente, a definição de chain extension usando essa macro é fornecida pelo autor da chain extension em uma crate separada. Contratos inteligentes ink! que utilizam essa chain extension simplesmente dependem dessa crate e usam sua definição de ambiente associada para utilizar os métodos fornecidos pela chain extension.

Atributos

Existem dois atributos diferentes com os quais os métodos de chain extension podem ser marcados:

Atributo
Obrigatório
Valor Padrão
Description

ink(extension = N: u32)

Yes

-

Determines the unique function ID of the chain extension method.

ink(handle_status = flag: bool)

Optional

true

Assumes that the returned status code of the chain extension method always indicates success and therefore always loads and decodes the output buffer of the call.

Como acontece com todos os atributos do ink!, vários deles podem aparecer em uma lista contígua:

type Access = i32;

#[ink::chain_extension]
pub trait MyChainExtension {
    type ErrorCode = i32;

    #[ink(extension = 5, handle_status = false)]
    fn key_access_for_account(key: &[u8], account: &[u8]) -> Access;
}

...ou como vários atributos individuais do ink! aplicados ao mesmo item:

type Access = i32;

#[ink::chain_extension]
pub trait MyChainExtension {
  type ErrorCode = i32;

  #[ink(extension = 5)]
  #[ink(handle_status = false)]
  fn key_access_for_account(key: &[u8], account: &[u8]) -> Access;
}

Detalhes: handle_status

Valor padrão: true

Por padrão, todos os métodos de chain extension devem retornar um Result<T, E> onde E: From<Self::ErrorCode>. O Self::ErrorCode representa o código de erro da chain extension. Isso significa que um contrato inteligente que chama um método de chain extension primeiro consulta o código de status retornado pelo método de chain extension e só carrega e decodifica a saída se o código de status indicar uma chamada bem-sucedida. Essa abordagem foi escolhida por ser mais eficiente quando nenhuma saída além do código de erro é necessária para uma chamada de chain extension. Ao projetar uma chain extension, tente utilizar o código de erro para retornar erros e use o buffer de saída apenas para informações que não se encaixam em um único valor u32.

Um método de chain extension que é marcado com handle_status = false assume que o código de erro retornado sempre indicará sucesso. Portanto, ele sempre carregará e decodificará o buffer de saída e perderá a restrição E: From<Self::ErrorCode para a chamada.

Observe que se um método de chain extension não retornar Result<T, E> onde E: From<Self::ErrorCode>, mas handle_status = true, ele ainda retornará um valor do tipo Result<T, Self::ErrorCode>.

Uso: handle_status + Tipo de retornoResult<T, E>

Utilize tanto handle_status = false quanto um tipo de retorno non-Result para o mesmo método de chain extension se uma chamada a ele nunca pode falhar e nunca retorna um tipo Result.

Combinações

Devido à possibilidade de marcar um método de chain extension comhandle_status e a escolha entre (1) retorna apenas Result<T, E> ou (2) retornar apenas T, existem 4 casos diferentes com semânticas ligeiramente distintas:

handle_status

Returns Result<T, E>

Effects

true

true

O método de extensão de cadeia é obrigado a retornar um valor do tipoResult<T, E> onde E: From<Self::ErrorCode>. Uma chamada sempre verificará se o código de status retornado indica sucesso e somente então carregará e decodificará o valor no buffer de saída.

true

false

O método de extensão de cadeia pode retornar qualquer tipo non-Result. Uma chamada sempre verificará se o código de status retornado indica sucesso e somente então carregará e decodificará o valor no buffer de saída. O tipo de retorno real do método de extensão de cadeia ainda é Result<T, Self::ErrorCode> quando o método de extensão de cadeia foi definido para retornar um valor do tipo T.

false

true

O método de extensão de cadeia deve retornar um valor do tipo Result<T, E>. Uma chamada sempre assume que o código de status retornado indica sucesso e, portanto, sempre carrega e decodifica o buffer de saída diretamente.

false

false

O método de extensão de cadeia pode retornar qualquer tipo non-Result. Uma chamada sempre assume que o código de status retornado indica sucesso e, portanto, sempre carrega e decodifica diretamente o buffer de saída.

Código de Erro

Cada chain extension define exatamente um ErrorCode usando a seguinte sintaxe:

#[ink::chain_extension]
pub trait MyChainExtension {
    type ErrorCode = MyErrorCode;

    // more definitions ...
}

The defined ErrorCode must implement FromStatusCode which should be implemented as a more or less trivial conversion from the u32 status code to a Result<(), Self::ErrorCode>. The Ok(()) value indicates that the call to the chain extension method was successful.

By convention an error code of 0 represents success. However, chain extension authors may use whatever suits their needs.

Example: Definition

In the below example a chain extension is defined that allows its users to read and write from and to the runtime storage using access privileges:

/// Custom chain extension to read to and write from the runtime.
#[ink::chain_extension]
pub trait RuntimeReadWrite {
    type ErrorCode = ReadWriteErrorCode;

    /// Reads from runtime storage.
    ///
    /// # Note
    ///
    /// Actually returns a value of type `Result<Vec<u8>, Self::ErrorCode>`.
    /// #[ink(extension = 1, returns_result = false)]
    /// fn read(key: &[u8]) -> Vec<u8>;
    ///
    /// Reads from runtime storage.
    ///
    /// Returns the number of bytes read and up to 32 bytes of the
    /// read value. Unused bytes in the output are set to 0.
    ///
    /// # Errors
    ///
    /// If the runtime storage cell stores a value that requires more than
    /// 32 bytes.
    ///
    /// # Note
    ///
    /// This requires `ReadWriteError` to implement `From<ReadWriteErrorCode>`
    /// and may potentially return any `Self::ErrorCode` through its return value.
    #[ink(extension = 2)]
    fn read_small(key: &[u8]) -> Result<(u32, [u8; 32]), ReadWriteError>;

    /// Writes into runtime storage.
    ///
    /// # Note
    ///
    /// Actually returns a value of type `Result<(), Self::ErrorCode>`.
    #[ink(extension = 3)]
    fn write(key: &[u8], value: &[u8]);

    /// Returns the access allowed for the key for the caller.
    ///
    /// # Note
    ///
    /// Assumes to never fail the call and therefore always returns `Option<Access>`.
    #[ink(extension = 4, handle_status = false)]
    fn access(key: &[u8]) -> Option<Access>;

    /// Unlocks previously acquired permission to access key.
    ///
    /// # Errors
    ///
    /// If the permission was not granted.
    ///
    /// # Note
    ///
    /// Assumes the call to never fail and therefore does _NOT_ require `UnlockAccessError`
    /// to implement `From<Self::ErrorCode>` as in the `read_small` method above.
    #[ink(extension = 5, handle_status = false)]
    fn unlock_access(key: &[u8], access: Access) -> Result<(), UnlockAccessError>;
}

#[derive(scale::Encode, scale::Decode, scale_info::TypeInfo)]
pub enum ReadWriteErrorCode {
  InvalidKey,
  CannotWriteToKey,
  CannotReadFromKey,
}

#[derive(scale::Encode, scale::Decode, scale_info::TypeInfo)]
pub enum ReadWriteError {
  ErrorCode(ReadWriteErrorCode),
  BufferTooSmall { required_bytes: u32 },
}

impl From<ReadWriteErrorCode> for ReadWriteError {
  fn from(error_code: ReadWriteErrorCode) -> Self {
    Self::ErrorCode(error_code)
  }
}

impl From<scale::Error> for ReadWriteError {
  fn from(_: scale::Error) -> Self {
    panic!("encountered unexpected invalid SCALE encoding")
  }
}

#[derive(scale::Encode, scale::Decode, scale_info::TypeInfo)]
pub struct UnlockAccessError {
  reason: String,
}

impl From<scale::Error> for UnlockAccessError {
  fn from(_: scale::Error) -> Self {
    panic!("encountered unexpected invalid SCALE encoding")
  }
}

#[derive(scale::Encode, scale::Decode, scale_info::TypeInfo)]
pub enum Access {
  ReadWrite,
  ReadOnly,
  WriteOnly,
}

impl ink::env::chain_extension::FromStatusCode for ReadWriteErrorCode {
  fn from_status_code(status_code: u32) -> Result<(), Self> {
    match status_code {
      0 => Ok(()),
      1 => Err(Self::InvalidKey),
      2 => Err(Self::CannotWriteToKey),
      3 => Err(Self::CannotReadFromKey),
      _ => panic!("encountered unknown status code"),
    }
  }
}

All the error types and other utility types used in the chain extension definition above are often required to implement various traits such as SCALE's Encode and Decode as well as scale-info's TypeInfo trait.

A full example of the above chain extension definition can be seen here.

Example: Environment

In order to allow ink! smart contracts to use the above defined chain extension it needs to be integrated into an Environment definition as shown below:

type RuntimeReadWrite = i32;

use ink::env::{Environment, DefaultEnvironment};

pub enum CustomEnvironment {}

impl Environment for CustomEnvironment {
    const MAX_EVENT_TOPICS: usize =
        <DefaultEnvironment as Environment>::MAX_EVENT_TOPICS;

    type AccountId = <DefaultEnvironment as Environment>::AccountId;
    type Balance = <DefaultEnvironment as Environment>::Balance;
    type Hash = <DefaultEnvironment as Environment>::Hash;
    type BlockNumber = <DefaultEnvironment as Environment>::BlockNumber;
    type Timestamp = <DefaultEnvironment as Environment>::Timestamp;

    type ChainExtension = RuntimeReadWrite;
}

Above we defined the CustomEnvironment which defaults to ink!'s DefaultEnvironment for all constants and types but the ChainExtension type which is assigned to our newly defined chain extension.

Example: Usage

An ink! smart contract can use the above defined chain extension through the Environment definition defined in the last example section using the env macro parameter as shown below.

Note that chain extension methods are accessible through Self::extension() or self.extension(). For example as in Self::extension().read(..) or self.extension().read(..).

#[ink::contract(env = CustomEnvironment)]
mod read_writer {

    #[ink(storage)]
    pub struct ReadWriter {}

    impl ReadWriter {
        #[ink(constructor)]
        pub fn new() -> Self { Self {} }

        #[ink(message)]
        pub fn read(&self, key: Vec<u8>) -> Result<Vec<u8>, ReadWriteErrorCode> {
            self.env()
                .extension()
                .read(&key)
        }

        #[ink(message)]
        pub fn read_small(&self, key: Vec<u8>) -> Result<(u32, [u8; 32]), ReadWriteError> {
            self.env()
                .extension()
                .read_small(&key)
        }

        #[ink(message)]
        pub fn write(
            &self,
            key: Vec<u8>,
            value: Vec<u8>,
        ) -> Result<(), ReadWriteErrorCode> {
            self.env()
                .extension()
                .write(&key, &value)
        }

        #[ink(message)]
        pub fn access(&self, key: Vec<u8>) -> Option<Access> {
            self.env()
                .extension()
                .access(&key)
        }

        #[ink(message)]
        pub fn unlock_access(&self, key: Vec<u8>, access: Access) -> Result<(), UnlockAccessError> {
            self.env()
                .extension()
                .unlock_access(&key, access)
        }
    }

    /// Custom chain extension to read to and write from the runtime.
    #[ink::chain_extension]
    pub trait RuntimeReadWrite {
          type ErrorCode = ReadWriteErrorCode;
          #[ink(extension = 1)]
          fn read(key: &[u8]) -> Vec<u8>;
          #[ink(extension = 2)]
          fn read_small(key: &[u8]) -> Result<(u32, [u8; 32]), ReadWriteError>;
          #[ink(extension = 3)]
          fn write(key: &[u8], value: &[u8]);
          #[ink(extension = 4, handle_status = false)]
          fn access(key: &[u8]) -> Option<Access>;
          #[ink(extension = 5, handle_status = false)]
          fn unlock_access(key: &[u8], access: Access) -> Result<(), UnlockAccessError>;
    }

    #[derive(scale::Encode, scale::Decode, scale_info::TypeInfo)]
    pub enum ReadWriteErrorCode {
          InvalidKey,
          CannotWriteToKey,
          CannotReadFromKey,
    }

    #[derive(scale::Encode, scale::Decode, scale_info::TypeInfo)]
    pub enum ReadWriteError {
          ErrorCode(ReadWriteErrorCode),
          BufferTooSmall { required_bytes: u32 },
    }
    impl From<ReadWriteErrorCode> for ReadWriteError {
         fn from(error_code: ReadWriteErrorCode) -> Self {
             Self::ErrorCode(error_code)
         }
    }
    impl From<scale::Error> for ReadWriteError {
         fn from(_: scale::Error) -> Self {
             panic!("encountered unexpected invalid SCALE encoding")
         }
    }

    #[derive(scale::Encode, scale::Decode, scale_info::TypeInfo)]
    pub struct UnlockAccessError {
         reason: String,
    }
    impl From<scale::Error> for UnlockAccessError {
         fn from(_: scale::Error) -> Self {
             panic!("encountered unexpected invalid SCALE encoding")
         }
    }
    #[derive(scale::Encode, scale::Decode, scale_info::TypeInfo)]
    pub enum Access {
         ReadWrite,
         ReadOnly,
         WriteOnly,
    }
    impl ink::env::chain_extension::FromStatusCode for ReadWriteErrorCode {
         fn from_status_code(status_code: u32) -> Result<(), Self> {
             match status_code {
                 0 => Ok(()),
                 1 => Err(Self::InvalidKey),
                 2 => Err(Self::CannotWriteToKey),
                 3 => Err(Self::CannotReadFromKey),
                 _ => panic!("encountered unknown status code"),
             }
         }
    }
    pub enum CustomEnvironment {}
    impl ink::env::Environment for CustomEnvironment {
         const MAX_EVENT_TOPICS: usize =
             <ink::env::DefaultEnvironment as ink::env::Environment>::MAX_EVENT_TOPICS;

         type AccountId = <ink::env::DefaultEnvironment as ink::env::Environment>::AccountId;
         type Balance = <ink::env::DefaultEnvironment as ink::env::Environment>::Balance;
         type Hash = <ink::env::DefaultEnvironment as ink::env::Environment>::Hash;
         type BlockNumber = <ink::env::DefaultEnvironment as ink::env::Environment>::BlockNumber;
         type Timestamp = <ink::env::DefaultEnvironment as ink::env::Environment>::Timestamp;

         type ChainExtension = RuntimeReadWrite;
    }
}

Technical Limitations

  • Due to technical limitations it is not possible to refer to the ErrorCode associated type using Self::ErrorCode anywhere within the chain extension and its defined methods. Instead chain extension authors should directly use the error code type when required. This limitation might be lifted in future versions of ink!.

  • It is not possible to declare other chain extension traits as super traits or super chain extensions of another.

Last updated

Was this helpful?