Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Dynamic trait based contract calling #631

Open
Robbepop opened this issue Jan 13, 2021 · 6 comments
Open

[Feature] Dynamic trait based contract calling #631

Robbepop opened this issue Jan 13, 2021 · 6 comments
Assignees
Labels
A-ink_lang [ink_lang] Work item B-design Designing a new component, interface or functionality. B-enhancement New feature or request B-research Research task that has open questions that need to be resolved. E-mentor-available A mentor for this issue is available

Comments

@Robbepop
Copy link
Collaborator

Robbepop commented Jan 13, 2021

Since ink! 3.0-rc1 it is possible to define special trait definitions for ink! smart contract using the #[ink::trait_definition] proc. macro.

Motivation

Defining and implementing such a trait works as expected with regard to some technical limitations.
Smart contracts today can some limited use of traits by calling or instantiating a contract using just the trait it implements.

However, this process is static and not dynamic, meaning that it is currently not possible to store a contract, e.g. in a ink_storage::Lazy<dyn MyContractTrait> or having an input parameter to a contract constructor or message with contract: &dyn MyContractTrait and be able to call constructors and messages defined in the trait dynamically on the provided contract instance.

This feature is critical to making trait definitions and implementation actually an integrated user experience for ink! smart contracts.

Syntax

There are several different ways in which a smart contract might want to interact with another smart contract dynamically.

As Message Parameter

It should be possible to declare a smart contract message or constructor with an input argument with contract: &[mut] dyn MyContractTrait where MyContractTrait is a trait definition that was defined using ink!'s #[ink::trait_definition] and contract is an instance of a smart contract (basically its AccountId) that implements this exact trait.

Then the ink! constructor or message receiving this argument can call any messages defined on the trait definition using the given contract instance. Note that it would not be possible to call trait definition constructors on contract since it represents an already instantiated contract.
Also we have to differentiate between contract: &dyn MyContractTrait and &mut dyn MyContractTrait where only the latter allows to call &mut self defined ink! messages.

It would be possible to syntactically detect usages of &dyn Trait in the input parameters of an ink! smart contract message or constructor and convert the type into something that is actually usable as input parameter, e.g. implements scale::Codec and has an AccountId field for the indirection etc. However, with general ink! design we try to be very explicit about user intentions which is why it might be preferable to start with an ink! annotation again and see how far we can get with it.

The proposed ink! annotation could be something like #[ink(dyn)] or #[ink(trait)].

Example

In the following examples we took the #[ink(trait)] as proposed ink! annotation. Note that this design is not final.

#[ink(message)]
pub fn call_back(&self, #[ink(trait)] callback: &dyn ContractCallback) {
    if self.condition() {
        callback.do_something();
    }
}

Or ...

#[ink(message)]
pub fn call_back_mut(&self, #[ink(trait)] callback: &mut dyn ContractCallback) {
    if self.condition() {
        callback.mutate_something();
    }
}

Where do_something is an ink! message defined in the ContractCallback trait definition as &self message and mutate_something is an ink! message defined there as well as &mut self message:

#[ink::trait_definition]
pub trait ContractCallback {
    #[ink(message)]
    fn do_something(&self);
    #[ink(message)]
    fn mutate_something(&mut self);
}

Further complications arise in the case of multiple trait bounds such as &dyn MyContractTraitA + MyContractTraitB + .... If we find out that these cases introduce severe complications we might drop them from the initial support and will try to work on these enhancements on a later point in time.

As Storage Entity

It might also be handy to store references to other smart contracts and being able to call them by traits that they implement.

Given the ContractCallback trait from last section the natural way a Rust programmer would make use an instance implementing this trait is by using the following storage fields and types:

#[ink(storage)]
pub struct MyStorage {
    value: Box<dyn ContractCallback>,
    vec: Vec<Box<dyn ContractCallback>>, // ... or similar
    array: [Box<dyn ContractCallback>] // ... etc
}

So it is all about utilizing Rust's owning smart references which is the Box<T> abstraction.
However, to a normal Rust developer usage of a Box would imply usage of dynamic heap memory allocations which we do not really need for our purposes since the underlying AccountId with which we identify or point to or reference a smart contract through its implemented interface is already serving as a pointer and the smart contract instance itself with its defined trait implementation that is about to be dispatched is already sufficient to perfectly emulate the dynamic dispatch.

So per design we ideally want to convert usages of Box<dyn Trait> into some other static type that acts like a type that implements the trait and therefore can be used interchangeably. This might count for the input parameters described above as well.
On the other handside making it possible to use Box<T> in places that are later completely replaced by some other non-allocating type that is to be used to indirect trait based contract calls (scale::Codec impl and internal AccountId) might confuse users which is why we should actually be more clever about how exactly we determine usage of dynamic trait based indirect calls for contracts.

The current proposal is to again introduce a new ink! annotation #[ink(dyn)] or #[ink(trait)] etc. in order for a user to flag a field of the #[ink(storage)] struct so that the ink! code analysis can recursively step through the syntactic type and replace uses of dyn Trait with whatever utility type is to be used in order to actually call traits implemented by contracts via cross contract calling.

As there might be aliases or custom types that might hide such parameters from the #[ink(storage)] struct definition we need to allow users to apply this new ink! annotation on structs fields, enums variant fields and type alises.

Example

In the following examples we took the #[ink(trait)] as proposed ink! annotation. Note that this design is not final.

#[ink(storage)]
pub struct MyStorage {
    #[ink(trait)]
    single: dyn ContractCallback, // replaces dyn Trait directly
    #[ink(trait)]
    vec: StorageVec<dyn ContractCallback>, // replaces dyn Trait in the generic argument of the StorageVec
    #[ink(trait)]
    array: [dyn ContractCallback; 10], // replaces dyn Trait of the array argument,
    #[ink(trait)]
    aliased: Aliased,
}

#[ink(trait)]
type Aliased = dyn ContractCallback; // replaces dyn Trait directly

pub struct MyStruct {
    #[ink(trait)]
    single: dyn ContractCallback, // replaces dyn Trait directly
}

pub enum MyEnum {
    A(#[ink(trait)] dyn ContractCallback), // replaces dyn Trait directly
    B,
}

As Dynamic Let-Binding

TODO

@Robbepop Robbepop added A-ink_lang [ink_lang] Work item B-design Designing a new component, interface or functionality. B-enhancement New feature or request B-research Research task that has open questions that need to be resolved. labels Jan 13, 2021
@Robbepop
Copy link
Collaborator Author

Robbepop commented Jan 25, 2021

To help clear out some design requirements I want to demonstrate some "optimal" goals of this implementation proposal.

This is just to show certain ways in which this feature is going to be used.
In the following we are looking at certain ways how to use the following contract using dynamic contract dispatch.

#[ink::trait_definition]
pub trait Callback {
    #[ink(message)]
    fn callback(&self, value: i32) -> bool;
}

#[ink::contract]
pub mod bc {
    #[ink::storage]
    pub struct BackCaller {
        counter: i32,
        #[ink(dyn)]
        cb: dyn Callback,
    }

    impl BackCaller {
        /// Constructs a new back caller with the given callback.
        #[ink(constructor)]
        pub fn new(#[ink(dyn)] cb: &dyn Callback) -> Self {
            Self { counter: 0, cb }
        }

        /// Calls the stored callback with the current value and either bump or reset it.
        #[ink(message)]
        pub fn bump(&mut self) -> bool {
            let reset = self.cb(self.counter);
            if reset {
                self.v = 0;
            } else {
                self.counter += 1;
            }
        }

        /// Returns the current value of the counter.
        #[ink(message)]
        pub fn get(&self) -> i32 {
            self.counter
        }
    }
}

Note: The above syntax is not final and may change significantly over the course of design and implementation.

Example: Use BackCaller As Dependency

TODO

Example: Use BackCaller Standalone

TODO

Idea for Mapping from Trait to Concrete Type

One of the biggest problems with the design proposed by this feature is that it is really hard to have a semantic level (Rust type system) way to map from a given Rust trait definition (e.g. in &dyn MyTrait) to a concrete type that we know to implement the MyTrait trait and that has some other important properties such as scale::Decode::decode impl equal to that of AccountId.

We could introduce a known type in ink_env, e.g. called ink_env::TraitConcretizer. Whenever we have an #[ink::trait_definition] we implement this trait for the ink_env::TraitConcretizer with some dummy assoc types and implementations that make no sense and yield link errors when actually used with one expection. The expection is the introduction of a new assoc type for every ink! trait definition, let's call it CallForwarder. This CallForwarder assoc. type in conjunction with our known-in-advance ink_env::TraitConcretizer for which we know that it always implements all available ink! trait definitions we can have a mapping from out ink! trait definition to its concrete CallForwarder type by stating:

<ink_env::TraitConcretizer as MyTrait>::CallForwarder

Where we know that this CallForwarder also implements MyTrait, and has a scale::Decode impl equal to <AccountId as scale::Decode>::decode.

@virgil2019
Copy link

So can I get a wrapped instance for a deployed contract, like 'from_account_id'?

@xgreenx
Copy link
Collaborator

xgreenx commented May 2, 2022

So can I get a wrapped instance for a deployed contract, like 'from_account_id'?

It is not implemented right now in ink!. If the contract that you want to work with is implementing a trait, then you can use wrapper feature from OpenBrush. You can use any AccountId to send a cross-contract call, it is the first argument of the method.

@sebastienpattyn93
Copy link

@cmichi is this still something that will be added in ink!4.0 ?

@cmichi
Copy link
Collaborator

cmichi commented Feb 17, 2023

@sebastienpattyn93 Sorry for only replying now, your message disappeared in a flood of GitHub notifications.

We didn't manage to include it for 4.0, but I just had a conversation with @xgreenx about implementing it in the short term.

@xgreenx
Copy link
Collaborator

xgreenx commented Mar 19, 2023

We added basic support for dyn Trait in the #1673. It uses ideas described above, but instead of the procedure macro, we used macro_rules. It was not a breaking change and easy to implement, but the issue is still actual because we want to make traits more "Rust" without generating associated types.

The current codegen adds __ink_TraitInfo, Env and Output{}(for each method and changes the output type as Self::Output{}) associated types that don't allow to use of traits in the expected way. Because of it, we can't use the dyn Trait syntax or have default implementation in the methods.

It is not a problem to remove __ink_TraitInfo and Env, because we can painlessly do that. But {}Output is a huge problem.

The primary purpose why we have an {}Output is for the CallBuilder. We use it to return a CallBuilder for cases when the user wants to specify flags or conditions for the cross-contract call manually.

It allows us to write ContractRef::call().flip().transferred_value(0).invoke(). The flip() returns the CallBuilder and we can do .transferred_value(0).invoke(). It is possible to allow that without {}Output, by changing the order and introducing a new API: The user should specify the flags/conditions first and call the method after like ContractRef::call().transferred_value(0).flip(). But with syntax like this, we can't handle the result of the execution from invoke()(ink::env::Error).

But we also can solve it and provide a new syntax to use CallBuilder and be able to handle the result of the invoke function. If the user wants to do a cross-contract call he can use the call_builder!(contract_ref.flip()) macro that returns the CallBuilder type with an already set selector, input, and output type.

// `Erc20` is trait defined via `#[ink::trait_definition]` and `account_id` is variable of `AccountId` type
let contract_ref: contract_ref!(Erc20) =  account_id.into();
let builder = call_builder!(contract_ref.transfer(20, to));
// or
// let builder = call_builder!(<_ as Erc20>::transfer(&mut contract_ref, 20, to));
builder.transferred_value(0).invoke();

The CallBuilder is a rare use case, but we must support it. If the syntax above for this case is okay for us, then we can go with this approach. The usage of the trait without a call builder will have the same API and will use a pure Rust trait. With pure rust trait, we can use dyn Trait from the box and use this trait not only in the #[ink::contract] context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ink_lang [ink_lang] Work item B-design Designing a new component, interface or functionality. B-enhancement New feature or request B-research Research task that has open questions that need to be resolved. E-mentor-available A mentor for this issue is available
Projects
None yet
Development

No branches or pull requests

5 participants