-
Notifications
You must be signed in to change notification settings - Fork 432
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
Shared external event definitions #1243
Conversation
…-events # Conflicts: # crates/lang/ir/src/ir/item/event.rs
Fixing that issue is part of our Grant. If we can agree on the implementation the Superoclony will handle the implementation=) In the comment we highlighted two problems that we need to solve.
A created issue during retreat solves that very well. The name of the events enum + the name of the variant defines a unique identifier of the event.
The approach with explicit aliases duplicates the code. But it still is better than before(you don't need to define all events in the contract's body). // import event as a type alias and mark as an ink(event)
// Possibly could do this as a `use` instead or in addition.
#[ink(event)]
type Event0 = super::Event0; In the comment, we suggested another approach with lazy loading of all events from the crates. This repository contains an example of how we can use the |
Yes that would indeed allow us to generate a useful event identifier for the contract. However it is worth noting that my current solution would also allow that since the codegen is already generating a top level mod shared {
#[ink::event_definition]
pub struct SharedEvent;
} #[ink(event)]
struct EventLocal;
#[ink(event)]
type EventB = shared::SharedEvent; generates approximately... enum __ink_EventBase {
EventLocal(EventLocal),
SharedEvent(SharedEvent),
} So, regardless of whether we implement #1222, this solution is available to us.
By code duplication do you mean the fact that it is required to annotate the imported shared event with
I'm not sure about this approach, it is a big change to the metadata generation philosophy and introduces some extra complexity.
Currently the metadata lists all possible events defined in a contract, and with this approach the metadata would possibly contain events that are not used by the contract at all. IMO it is preferable that the metadata should be an accurate representation as to the API of the contract. Overall the philosophy I have approached my solution is: how can we introduce shared events into the existing model of event definitions, not aiming for a complete overhaul. It should make it possible to release this as a Not to say that the event codegen doesn't need some attention and thought and possibly a complete overhaul, especially optimizing codesize of topics etc. It's just for this I want to focus on only adding shared events into the existing framework. |
I guess you meant: enum __ink_EventBase {
EventLocal(EventLocal),
EventB(shared::SharedEvent),
} What benefits do we have in grouping all events into one Do we need really combine all events into one enum if we can refactor the events collection and in that case, each event definition can be independent? We hoped that resolving the #809 also will resolve the problem with event identifiers. So it is why #1222 looks very promising. Yes, it breaks the previous API but it allows contracts to be production-ready. Because all contracts can emit the same events(the name of the contract will not affect the identifier). mod events {
enum PSP22 {
Transfer(AccountId, AccountId, Balance),
Approve(AccountId, AccountId, Balance),
...
}
} That kind of event can be standardized and everyone can emit it. We can extract that to a separate issue but I think all of it is related to each other and should be solved during one breaking change refactoring=)
Yes!=) Now you want to combine all parts together to create a final contract. #[ink(storage)]
pub struct ComplexContract {
access: AccessControlData,
psp22: PSP22Data,
lending: LendingData,
router: RouterData,
}
impl AccessControl for ComplexContract {}
impl PSP22 for ComplexContract {}
impl Lending for ComplexContract {}
impl Router for ComplexContract {}
#[ink(event)]
type AccessControlEventChangeRole = access_contol::ChangeRole;
#[ink(event)]
type AccessControlEventRoleGranted = access_contol::RoleGranted;
#[ink(event)]
type AccessControlEventRoleRevoked = access_contol::RoleRevoked;
#[ink(event)]
type AccessControlEventCucumber = access_contol::Cucumber;
#[ink(event)]
type PSP22EventTransfer = psp22::Tranfer;
#[ink(event)]
type PSP22EventApprove = psp22::Approve;
#[ink(event)]
type PSP22EventHello = psp22::Hello;
#[ink(event)]
type PSP22EventWorld = psp22::World;
#[ink(event)]
type LendingEventBorrow = lending::Borrow;
#[ink(event)]
type LendingEventDeposit = lending::Deposit;
#[ink(event)]
type LendingEventLiquidated = lending::Liquidated;
#[ink(event)]
type LendingEventOracleChange = lending::OracleChange;
#[ink(event)]
type RouterEventCreated = router::Created;
#[ink(event)]
type RouterEventNewProposal = router::NewProposal;
#[ink(event)]
type RouterEventNewLending = router::NewLending;
#[ink(event)]
type RouterEventSwap = router::Swap; And you scrolled here=D It is really a lot of boilerplate code and you should create a unique name for each event. Overwise it will conflict with other evens like
The philosophy changed only a little bit. Instead of one place to collect metadata, we will collect it from each crate and the final metadata will be generated in the same way. In simple form: we have a static array with events. During a load of each crate, we add events to that array. When all crates are loaded it runs
Better to have several useless events than to forget to add some events at all=) If all events are collected automatically, it doesn't add any problem. But if we missed some events it can cause some issues. With the introduction of upgradable contracts, we can't have accurate representation at all=) Because any contract can be upgradable or be Another plus for collecting all events(Example from our integration testing): BTW, we also can use P. S. Sorry for long comment=D |
Okay I had not considered this scenario, my assumption was that the contract implementation itself would always raise the events, therefore it would have to import them either with a Your examples got me thinking, that shared events are tied to their corresponding traits, since they are effectively part of the contract's interface. i.e. any UI for ERCX contracts requires both standard messages and events. Indeed if you look at the standards e.g. https://eips.ethereum.org/EIPS/eip-1155 they include the event definitions. So we could have something like: #[ink::interface]
pub mod access_control {
#[ink(trait_definition)]
pub trait AccessControl {
// messages
}
#[ink(event)]
pub enum AccessControlEvent {
ChangeRole,
RoleGranted,
RoleRevoked,
Cucumber,
}
} So now the trait and the events are tied together, we could even use the
It is changing from explicit "pulling" to implicit "pushing" of event metadata via global state, so we need to be sure it is the correct thing to do, not just the easiest way to solve the problem. |
Yep, that idea was a reason to create that issue=)
Yep, we also come up with that solution and it already fits all requirements. The trait can have some hidden That solution has 3 cons:
You are right, it changes it in that way=) The main pros:
cons:
I hope that we will find and select the best approach. I'm describing only the ideas and thoughts of our team and the use cases that we faced. |
Will respond properly tomorrow but regarding:
I did some googling about the state of that in Solidity, and looks like that is not currently supported but this PR looks like it might be a solution ethereum/solidity#10996? |
The best solution is to include the event if it was somewhere emitted. But ink! is not its own language, so it is hard to identify that event was emitted. Yesterday I tried to play with rust ABI and linking attributes and with |
crates/primitives/src/event.rs
Outdated
event_variant: &'static str, | ||
) -> [u8; 32] { | ||
let p = xxh3_128(path.as_bytes()).to_be_bytes(); | ||
// todo: add fields to signature? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The anchor for discussing prefixes and topics(I use it only as a reference).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you mentioned in our chat, we don't need this const fn
if we are just using Enum::Variant
, we can do the hashing in the proc_macro itself. Currently I need this because I am using module_path!
, but if we can do without this then we can choose our hashing algorithm.
The question remains then whether it is sufficient to use Enum::Variant
or do we add the field names too e.g. Enum::Variant { field_1, field_2, field_3 }
similar to Ethereum (except without type names).
With our without the field names, need to consider the impact of signature topic collisions.
quote_spanned!(span => | ||
.push_topic::<::ink::env::topics::PrefixedValue<#field_type>>( | ||
&::ink::env::topics::PrefixedValue { | ||
// todo: figure out whether we even need to include a prefix here? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With #1533, tests fail because of the same topics.
The caller
and from
accounts are the same. It is clearly a problem with the test because the caller and from
should be different, but it raises discussion about this part.
In the ethereum, the first topic is almost always a signature of the event(except manual LOG*
opcode). And you highlighted it here. So the first topic is affected by the event's name and used types. But other topics are not prefixed and contain raw/hashed(if the value is more than 32 bytes) values.
I think we need to follow the same pattern. The first topic helps to subscribe to a specific event of contracts. With raw/hashed values, we can do searches across all events, only knowing value without knowing the event structure. It is also very helpful.
Prefixing each topic with the field's name allows us not to mix different events or fields with the same type. And for that, we need to know the definition of the event to do searches. But, we can achieve the same by filtering supported by ethereum nodes(we can do the same). We can use the event's signature if we need only specific events. If we need a specific field, we can use the (null, null, 0x...)
format where we want the third topic.
So comparing those two options, the Ethereum approach looks more flexible and performant(we don't need to hash prefixes and values each time).
It seems you follow the same thoughts(based on your comment), so I only highlighted conclusions here(if not, let's discuss =) ).
I also created a discussion here because following the Ethereum approach creates several issues we need to solve:
-
How do we want to calculate a unique signature of the event? It should be documented to be clear for developers of tools. We discussed it in this PR before but didn't finalize it. My approach was to use the
hash({Name of the enum}::{Name of the variant})
orhash({Name of the enum}::{Name of the variant}::{Number of fields})
. It is a bad idea to use types names because, in Rust, we can create a wrapper that behaves as an original type but has a new definition=( We can use only names, as we do for traits. The number of fields is only an idea that we can discuss or maybe that can give you more ideas=) Also we need to define which hash algorithm we want to use. Because we can calculate the hash during event definition, we are not limited. -
How do we want to process two same topics? Based on the Ethereum approach, we need to allow them, but on the
contract-pallet
side, we don't let it(and it is an error that I got in the tests).
- Right now, we sort topics, but it breaks the filtering strategy because the expected topic can be in an unexpected position.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are on the same page regarding the event topics, I am also leaning towards using non-prefixed field topics. Clients would then need to filter on both the signature topic and the field topic if they wanted only a specific field. It also opens up the possibility to susbscribe to all events with a topic of a given account id, which would be very useful for explorers.
* Update README.md (#1521) * Fixed compilation errors * Make CI happy * Make CI happy * Return clippy changes back to minimize conflicts Co-authored-by: German <[email protected]>
Superseded by #1827 |
WIP Closes #809
events must now be defined as an
enum
annotated with#[ink::event_definition]
events can now be defined outside the
#[ink::contract]
definition and can be shared across contractscontracts can emit externally defined events.
contracts can still define an event inline, but this can now be referenced and used by other contracts.
contracts can now define multiple inline event enums (though probably should not be encouraged)
each variant has its own signature topic which should be calculated at compile time
field topics will no longer have a prefix but will be just the raw value, see Shared external event definitions #1243 (comment)
Option
types, possibly not write out the topic at all in case ofNone
. Don't want a bunch of0x0000..
topics for e.g.Option<AccountId>
resolve how to deal with compile time enforcement of the environments
MAX_EVENT_TOPICS
.