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

Support multiple chain extensions #1958

Merged
merged 9 commits into from Nov 28, 2023
Merged

Conversation

xgreenx
Copy link
Collaborator

@xgreenx xgreenx commented Oct 28, 2023

Summary

Closes #1747

  • Does it introduce breaking changes? - Yes
  • Is it dependent on the specific version of cargo-contract or pallet-contracts? - No

This change closes #1747 by adding an example of how to use multiple chain extensions. It contains:

  • New example of how to use multiple chain extensions in one contract.
  • Affects the usage of the #[ink::chain_extension] macro and the definition of the chain extension.

Description

This pull request adds a new combined_extension example that shows how to use rand_extension::FetchRandom and psp22_extension::Psp22Extension together inside of one contract. This kind of functionality is already available in the current version of the ink. You only need to define a new chain extension that uses other chain extensions - combined chain extension. It can be done by implementing the ink::ChainExtensionInstance trait for any type.

#[ink::scale_derive(TypeInfo)]
pub struct CombinedChainExtension;

pub struct CombinedInstance {
    pub psp22: <Psp22Extension as ChainExtensionInstance>::Instance,
    pub rand: <FetchRandom as ChainExtensionInstance>::Instance,
}

impl ChainExtensionInstance for CombinedChainExtension {
    type Instance = CombinedInstance;

    fn instantiate() -> Self::Instance {
        CombinedInstance {
            psp22: <Psp22Extension as ChainExtensionInstance>::instantiate(),
            rand: <FetchRandom as ChainExtensionInstance>::instantiate(),
        }
    }
}

But the current chains extension works only with u32 - a combination of the extension id u16 and function id u16.

I've split them explicitly. It brings a new breaking change API that requires defining the extension id during the definition of the chain extension - ink::chain_extension(extension = u16). Plus, the attribute #[ink(extension = u32)] was replaced with #[ink(function = u16)].

image

Checklist before requesting a review

  • My code follows the style guidelines of this project
  • I have added an entry to CHANGELOG.md
  • I have commented my code, particularly in hard-to-understand areas
  • I have added tests that prove my fix is effective or that my feature works
  • Any dependent changes have been merged and published in downstream modules

Follow-up

After merging this pull request, we need to update documentation here.

@xgreenx xgreenx self-assigned this Oct 28, 2023
@xgreenx xgreenx changed the title Combined chain extension example Support multiple chain extensions Oct 28, 2023
@paritytech-cicd-pr
Copy link

paritytech-cicd-pr commented Oct 28, 2023

🦑 📈 ink! Example Contracts ‒ Changes Report 📉 🦑

These are the results when building the integration-tests/* contracts from this branch with cargo-contract 4.0.0-alpha-f7dd92d and comparing them to ink! master:

Contract Upstream Size (kB) PR Size (kB) Diff (kB) Diff (%) Change
basic-contract-caller 2.967 2.967 0 0
basic-contract-caller/other-contract 1.337 1.337 0 0
call-builder-return-value 8.735 8.735 0 0
call-runtime 1.769 1.769 0 0
conditional-compilation 1.209 1.209 0 0
contract-storage 7.171 7.171 0 0
contract-terminate 1.092 1.092 0 0
contract-transfer 1.444 1.444 0 0
custom-allocator 7.428 7.428 0 0
dns 7.249 7.249 0 0
e2e-call-runtime 1.058 1.058 0 0
e2e-runtime-only-backend 1.635 1.635 0 0
erc1155 13.962 13.962 0 0
erc20 6.687 6.687 0 0
erc721 9.64 9.64 0 0
events 4.763 4.763 0 0
flipper 1.393 1.393 0 0
incrementer 1.221 1.221 0 0
lang-err-integration-tests/call-builder-delegate 2.317 2.317 0 0
lang-err-integration-tests/call-builder 4.847 4.847 0 0
lang-err-integration-tests/constructors-return-value 1.773 1.773 0 0
lang-err-integration-tests/contract-ref 4.328 4.328 0 0
lang-err-integration-tests/integration-flipper 1.571 1.571 0 0
mapping-integration-tests 7.685 7.685 0 0
mother 9.508 9.508 0 0
multi-contract-caller 5.924 5.924 0 0
multi-contract-caller/accumulator 1.095 1.095 0 0
multi-contract-caller/adder 1.669 1.669 0 0
multi-contract-caller/subber 1.689 1.689 0 0
multisig 21.471 21.471 0 0
payment-channel 5.501 5.501 0 0
sr25519-verification 0.865 0.865 0 0
static-buffer 1.405 1.405 0 0
trait-dyn-cross-contract-calls 2.466 2.466 0 0
trait-dyn-cross-contract-calls/contracts/incrementer 1.305 1.305 0 0
trait-erc20 7.063 7.063 0 0
trait-flipper 1.209 1.209 0 0
trait-incrementer 1.37 1.37 0 0
upgradeable-contracts/delegator 2.908 2.908 0 0
upgradeable-contracts/delegator/delegatee 1.369 1.369 0 0
upgradeable-contracts/set-code-hash 1.464 1.464 0 0
upgradeable-contracts/set-code-hash/updated-incrementer 1.443 1.443 0 0
wildcard-selector 2.622 2.622 0 0

Link to the run | Last update: Wed Nov 8 10:06:45 CET 2023

@codecov-commenter
Copy link

codecov-commenter commented Oct 28, 2023

Codecov Report

Attention: 22 lines in your changes are missing coverage. Please review.

Comparison is base (f7e831d) 53.30% compared to head (746e4d3) 53.40%.

Files Patch % Lines
crates/ink/ir/src/ir/chain_extension.rs 79.06% 9 Missing ⚠️
crates/env/src/chain_extension.rs 0.00% 5 Missing ⚠️
crates/engine/src/chain_extension.rs 57.14% 3 Missing ⚠️
crates/ink/ir/src/ir/attrs.rs 62.50% 3 Missing ⚠️
crates/env/src/engine/off_chain/impls.rs 0.00% 1 Missing ⚠️
...rates/ink/codegen/src/generator/chain_extension.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1958      +/-   ##
==========================================
+ Coverage   53.30%   53.40%   +0.09%     
==========================================
  Files         220      220              
  Lines        6845     6879      +34     
==========================================
+ Hits         3649     3674      +25     
- Misses       3196     3205       +9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@SkymanOne SkymanOne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good start! Although I have some comments on the API design choices.

Comment on lines 29 to 49
pub struct CombinedInstance {
/// The instance of the [`Psp22Extension`] chain extension.
///
/// It provides you access to `PSP22` functionality.
pub psp22: <Psp22Extension as ChainExtensionInstance>::Instance,
/// The instance of the [`FetchRandom`] chain extension.
///
/// It provides you access to randomness functionality.
pub rand: <FetchRandom as ChainExtensionInstance>::Instance,
}

impl ChainExtensionInstance for CombinedChainExtension {
type Instance = CombinedInstance;

fn instantiate() -> Self::Instance {
CombinedInstance {
psp22: <Psp22Extension as ChainExtensionInstance>::instantiate(),
rand: <FetchRandom as ChainExtensionInstance>::instantiate(),
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instantiate() method can be auto generated by macro since there is no custom logic to be implemented on the developer side. You already provide each extension type as <Psp22Extension as ChainExtensionInstance>::Instance. You can just automatically generate implementation through Derive macro.

Suggested change
pub struct CombinedInstance {
/// The instance of the [`Psp22Extension`] chain extension.
///
/// It provides you access to `PSP22` functionality.
pub psp22: <Psp22Extension as ChainExtensionInstance>::Instance,
/// The instance of the [`FetchRandom`] chain extension.
///
/// It provides you access to randomness functionality.
pub rand: <FetchRandom as ChainExtensionInstance>::Instance,
}
impl ChainExtensionInstance for CombinedChainExtension {
type Instance = CombinedInstance;
fn instantiate() -> Self::Instance {
CombinedInstance {
psp22: <Psp22Extension as ChainExtensionInstance>::instantiate(),
rand: <FetchRandom as ChainExtensionInstance>::instantiate(),
}
}
}
#[derive(CombinedChainExtension)]
pub struct CombinedInstance {
/// The instance of the [`Psp22Extension`] chain extension.
///
/// It provides you access to `PSP22` functionality.
pub psp22: <Psp22Extension as ChainExtensionInstance>::Instance,
/// The instance of the [`FetchRandom`] chain extension.
///
/// It provides you access to randomness functionality.
pub rand: <FetchRandom as ChainExtensionInstance>::Instance,
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The derive macro can't modify the field's type. It leads to a strange API where the user still should use <Psp22Extension as ChainExtensionInstance>::Instance, but implementation will be generated automatically. So, I decided to go with the macro_rule approach. You can define the structure as usual structure, where each field is a chain extension, and it will produce the implementation for you.

ink::combine_extensions! {
    /// This extension combines the [`FetchRandom`] and [`Psp22Extension`] extensions.
    /// It is possible to combine any number of extensions in this way.
    ///
    /// This structure is an instance that is returned by the `self.env().extension()` call.
    pub struct CombinedChainExtension {
        /// The instance of the [`Psp22Extension`] chain extension.
        ///
        /// It provides you access to `PSP22` functionality.
        pub psp22: Psp22Extension,
        /// The instance of the [`FetchRandom`] chain extension.
        ///
        /// It provides you access to randomness functionality.
        pub rand: FetchRandom,
    }
}

/// the corresponding sub-extension implementation.
/// It is possible to combine any number of extensions in this way.
#[ink::scale_derive(TypeInfo)]
pub struct CombinedChainExtension;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For similar reason, I am not a fun of having two structs associated with chain extensions. Can't you use CombinedInstance instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The factory type should implement TypeInfo without any information regarding private types generated by the #[ink::chain_extension] macro. For that, we must either manually implement the TypeInfo trait, or have a separate instance type. Another reason is that the type that implements the ink::ChainExtensionInstance trait is a factory. If the factory produces self instance, it leads to incorrect usage of the pattern.

integration-tests/combined-extension/lib.rs Show resolved Hide resolved
///
/// It is a combination of the [`ExtensionId`] and [`FunctionId`].
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GlobalMethodId {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your example you access the method by specifying a chain extension instance first: self.env().extension().rand.fetch_random([0; 32]

Why do you need that if you can identify each method uniquely? Why can't you collect all chain extension functions into a single "list" to make them accessible together?

e.g.
self.env().extensions().fetch_random([0; 32]

and then dispatch the call to the associated chain extension since you can derive this info from the function id

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have the method with the same name in each extension, it is impossible to identify which instance we need to call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, you just define them with different names? CE function names can be anything since they are defined by the SC developer.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't work if you want to use extensions defined in other crates.

Copy link

🦑 📈 ink! Example Contracts ‒ Changes Report 📉 🦑

These are the results when building the integration-tests/* contracts from this branch with cargo-contract and comparing them to ink! master:

Contract Upstream Size (kB) PR Size (kB) Diff (kB) Diff (%) Change
basic-contract-caller 2.967 2.967 0 0
basic-contract-caller/other-contract 1.337 1.337 0 0
call-builder-return-value 8.735 8.735 0 0
call-runtime 1.769 1.769 0 0
conditional-compilation 1.209 1.209 0 0
contract-storage 7.171 7.171 0 0
contract-terminate 1.092 1.092 0 0
contract-transfer 1.444 1.444 0 0
custom-allocator 7.428 7.428 0 0
dns 7.249 7.249 0 0
e2e-call-runtime 1.058 1.058 0 0
e2e-runtime-only-backend 1.635 1.635 0 0
erc1155 13.962 13.968 0.006 0.0429738 📈
erc20 6.687 6.687 0 0
erc721 9.64 9.64 0 0
events 4.763 4.763 0 0
flipper 1.393 1.393 0 0
incrementer 1.221 1.221 0 0
lang-err-integration-tests/call-builder-delegate 2.317 2.317 0 0
lang-err-integration-tests/call-builder 4.847 4.847 0 0
lang-err-integration-tests/constructors-return-value 1.773 1.773 0 0
lang-err-integration-tests/contract-ref 4.328 4.328 0 0
lang-err-integration-tests/integration-flipper 1.571 1.571 0 0
mapping-integration-tests 7.685 7.685 0 0
mother 9.508 9.508 0 0
multi-contract-caller 5.924 5.924 0 0
multi-contract-caller/accumulator 1.095 1.095 0 0
multi-contract-caller/adder 1.669 1.669 0 0
multi-contract-caller/subber 1.689 1.689 0 0
multisig 21.471 21.471 0 0
payment-channel 5.501 5.501 0 0
sr25519-verification 0.865 0.865 0 0
static-buffer 1.405 1.405 0 0
trait-dyn-cross-contract-calls 2.466 2.466 0 0
trait-dyn-cross-contract-calls/contracts/incrementer 1.305 1.305 0 0
trait-erc20 7.063 7.063 0 0
trait-flipper 1.209 1.209 0 0
trait-incrementer 1.37 1.37 0 0
upgradeable-contracts/delegator 2.908 2.908 0 0
upgradeable-contracts/delegator/delegatee 1.369 1.369 0 0
upgradeable-contracts/set-code-hash 1.464 1.464 0 0
upgradeable-contracts/set-code-hash/updated-incrementer 1.443 1.443 0 0
wildcard-selector 2.622 2.622 0 0

Link to the run | Last update: Tue Nov 28 14:11:48 CET 2023

Copy link
Collaborator

@ascjones ascjones left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

}
}

/// The unique ID of a chain extension method across all chain extensions.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The uniqueness of the id doesn't seem to be guaranteed anywhere, so we're just relying on the contract author to check for colliding ids?

Tricky to do statically, though could possibly check for duplicates during the metadata pass.

Anyway it may not be important, since if by chance (unlikely?) there are colliding ids that will be caught very quickly. when testing.

@xgreenx xgreenx merged commit 27407b5 into master Nov 28, 2023
22 checks passed
@xgreenx xgreenx deleted the feature/muli-chain-extension branch November 28, 2023 14:57
@SkymanOne SkymanOne mentioned this pull request Nov 30, 2023
@SkymanOne SkymanOne mentioned this pull request Mar 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support multiple chain extensions
5 participants