Skip to content

Commit

Permalink
Custom signature topic (#2031)
Browse files Browse the repository at this point in the history
* basic setup

* successful parsing of args

* working specification

* fix tests

* add changelog

* fmt

* add signature_topic macro

* simpligy argument parsing

* add tests

* remove links

* use const array

* remove hex crate

* Refactor. Remove signature topic macro in favor of local custom ink attr in the derive

* minor fixes

* remove trailing code

* fix UI tests

* add UI tests

* extra UI tests and improved error reporting

* uncomment tests

* rename function
  • Loading branch information
Gherman authored Jan 11, 2024
1 parent 3e5ea12 commit bc6f5a9
Show file tree
Hide file tree
Showing 30 changed files with 764 additions and 107 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Custom signature topic in Events - #[2031](https://github.com/paritytech/ink/pull/2031)
- Linter: `non_fallible_api` lint - [#2004](https://github.com/paritytech/ink/pull/2004)

### Fixed
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ In a module annotated with `#[ink::contract]` these attributes are available:
| `#[ink(constructor)]` | Applicable to method. | Flags a method for the ink! storage struct as constructor making it available to the API for instantiating the contract. |
| `#[ink(event)]` | On `struct` definitions. | Defines an ink! event. A contract can define multiple such ink! events. |
| `#[ink(anonymous)]` | Applicable to ink! events. | Tells the ink! codegen to treat the ink! event as anonymous which omits the event signature as topic upon emitting. Very similar to anonymous events in Solidity. |
| `#[ink(signature_topic = _)]` | Applicable to ink! events. | Specifies custom signature topic of the event that allows to use manually specify shared event definition. |
| `#[ink(topic)]` | Applicable on ink! event field. | Tells the ink! codegen to provide a topic hash for the given field. Every ink! event can only have a limited number of such topic fields. Similar semantics as to indexed event arguments in Solidity. |
| `#[ink(payable)]` | Applicable to ink! messages. | Allows receiving value as part of the call of the ink! message. ink! constructors are implicitly payable. |
| `#[ink(selector = S:u32)]` | Applicable to ink! messages and ink! constructors. | Specifies a concrete dispatch selector for the flagged entity. This allows a contract author to precisely control the selectors of their APIs making it possible to rename their API without breakage. |
Expand Down
2 changes: 1 addition & 1 deletion crates/engine/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ fn events() {
let event = events.next().expect("event must exist");
assert_eq!(event.topics.len(), 2);
assert_eq!(
event.topics.get(0).expect("first topic must exist"),
event.topics.first().expect("first topic must exist"),
&topic1
);
assert_eq!(
Expand Down
6 changes: 4 additions & 2 deletions crates/env/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,11 @@ pub trait Event: scale::Encode {

/// The unique signature topic of the event. `None` for anonymous events.
///
/// It can be automatically calculated or manually specified.
///
/// Usually this is calculated using the `#[derive(ink::Event)]` derive, which by
/// default calculates this as `blake2b("Event(field1_type,field2_type)")`
const SIGNATURE_TOPIC: Option<[u8; 32]>;
/// default calculates this as `blake2b("Event(field1_type,field2_type)"`
const SIGNATURE_TOPIC: core::option::Option<[u8; 32]>;

/// Guides event topic serialization using the given topics builder.
fn topics<E, B>(
Expand Down
13 changes: 11 additions & 2 deletions crates/ink/codegen/src/generator/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,36 @@
use crate::GenerateCode;
use derive_more::From;
use proc_macro2::TokenStream as TokenStream2;
use syn::spanned::Spanned;

/// Generates code for the storage item.
/// Generates code for the event item.
#[derive(From, Copy, Clone)]
pub struct Event<'a> {
/// The storage item to generate code for.
item: &'a ir::Event,
}

impl GenerateCode for Event<'_> {
/// Generates ink! storage item code.
/// Generates ink! event item code.
fn generate_code(&self) -> TokenStream2 {
let item = self.item.item();
let anonymous = self
.item
.anonymous()
.then(|| quote::quote! { #[ink(anonymous)] });
let signature_topic = self
.item
.signature_topic_hex()
.map(|hex_s| quote::quote! { #[ink(signature_topic = #hex_s)] });
let cfg_attrs = self.item.get_cfg_attrs(item.span());

quote::quote! (
#( #cfg_attrs )*
#[cfg_attr(feature = "std", derive(::ink::EventMetadata))]
#[derive(::ink::Event)]
#[::ink::scale_derive(Encode, Decode)]
#anonymous
#signature_topic
#item
)
}
Expand Down
1 change: 1 addition & 0 deletions crates/ink/ir/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ proc-macro2 = { workspace = true }
itertools = { workspace = true }
either = { workspace = true }
blake2 = { workspace = true }
impl-serde = { workspace = true }
ink_prelude = { workspace = true }

[features]
Expand Down
71 changes: 57 additions & 14 deletions crates/ink/ir/src/ir/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ impl IsDocAttribute for syn::Attribute {

fn extract_docs(&self) -> Option<String> {
if !self.is_doc_attribute() {
return None
return None;
}
match &self.meta {
syn::Meta::NameValue(nv) => {
if let syn::Expr::Lit(l) = &nv.value {
if let syn::Lit::Str(s) = &l.lit {
return Some(s.value())
return Some(s.value());
}
}
}
Expand Down Expand Up @@ -172,7 +172,7 @@ impl InkAttribute {
return Err(format_err!(
self.span(),
"unexpected first ink! attribute argument",
))
));
}
Ok(())
}
Expand Down Expand Up @@ -200,7 +200,7 @@ impl InkAttribute {
.into_combine(format_err!(
seen.span(),
"first equal ink! attribute argument here"
)))
)));
}
if let Some(seen) = seen2.get(&arg.kind().kind()) {
return Err(format_err!(
Expand All @@ -210,7 +210,7 @@ impl InkAttribute {
.into_combine(format_err!(
*seen,
"first equal ink! attribute argument with equal kind here"
)))
)));
}
seen.insert(arg);
seen2.insert(arg.kind().kind(), arg.span());
Expand Down Expand Up @@ -242,7 +242,7 @@ impl InkAttribute {
return Err(format_err!(
Span::call_site(),
"encountered unexpected empty expanded ink! attribute arguments",
))
));
}
Self::ensure_no_duplicate_args(&args)?;
Ok(Self { args })
Expand All @@ -268,7 +268,7 @@ impl InkAttribute {
pub fn namespace(&self) -> Option<ir::Namespace> {
self.args().find_map(|arg| {
if let ir::AttributeArg::Namespace(namespace) = arg.kind() {
return Some(namespace.clone())
return Some(namespace.clone());
}
None
})
Expand All @@ -278,7 +278,17 @@ impl InkAttribute {
pub fn selector(&self) -> Option<SelectorOrWildcard> {
self.args().find_map(|arg| {
if let ir::AttributeArg::Selector(selector) = arg.kind() {
return Some(*selector)
return Some(*selector);
}
None
})
}

/// Returns the signature topic of the ink! attribute if any.
pub fn signature_topic_hex(&self) -> Option<String> {
self.args().find_map(|arg| {
if let ir::AttributeArg::SignatureTopic(hash) = arg.kind() {
return Some(hash.clone());
}
None
})
Expand Down Expand Up @@ -363,6 +373,9 @@ pub enum AttributeArgKind {
/// `#[ink(selector = _)]`
/// `#[ink(selector = 0xDEADBEEF)]`
Selector,
/// `#[ink(signature_topic =
/// "325c98ff66bd0d9d1c10789ae1f2a17bdfb2dcf6aa3d8092669afafdef1cb72d")]`
SignatureTopicArg,
/// `#[ink(function = N: u16)]`
Function,
/// `#[ink(namespace = "my_namespace")]`
Expand Down Expand Up @@ -418,6 +431,9 @@ pub enum AttributeArg {
/// - `#[ink(selector = _)]` Applied on ink! messages to define a fallback messages
/// that is invoked if no other ink! message matches a given selector.
Selector(SelectorOrWildcard),
/// `#[ink(signature_topic =
/// "325c98ff66bd0d9d1c10789ae1f2a17bdfb2dcf6aa3d8092669afafdef1cb72d")]`
SignatureTopic(String),
/// `#[ink(namespace = "my_namespace")]`
///
/// Applied on ink! trait implementation blocks to disambiguate other trait
Expand Down Expand Up @@ -462,6 +478,9 @@ impl core::fmt::Display for AttributeArgKind {
Self::Selector => {
write!(f, "selector = S:[u8; 4] || _")
}
Self::SignatureTopicArg => {
write!(f, "signature_topic = S:[u8; 32]")
}
Self::Function => {
write!(f, "function = N:u16)")
}
Expand All @@ -486,6 +505,7 @@ impl AttributeArg {
Self::Constructor => AttributeArgKind::Constructor,
Self::Payable => AttributeArgKind::Payable,
Self::Selector(_) => AttributeArgKind::Selector,
Self::SignatureTopic(_) => AttributeArgKind::SignatureTopicArg,
Self::Function(_) => AttributeArgKind::Function,
Self::Namespace(_) => AttributeArgKind::Namespace,
Self::Implementation => AttributeArgKind::Implementation,
Expand All @@ -505,6 +525,9 @@ impl core::fmt::Display for AttributeArg {
Self::Constructor => write!(f, "constructor"),
Self::Payable => write!(f, "payable"),
Self::Selector(selector) => core::fmt::Display::fmt(&selector, f),
Self::SignatureTopic(hash) => {
write!(f, "signature_topic = {:?}", hash)
}
Self::Function(function) => {
write!(f, "function = {:?}", function.into_u16())
}
Expand Down Expand Up @@ -778,7 +801,7 @@ where
{
let (ink_attrs, rust_attrs) = ir::partition_attributes(attrs)?;
if ink_attrs.is_empty() {
return Ok((None, rust_attrs))
return Ok((None, rust_attrs));
}
let normalized = ir::InkAttribute::from_expanded(ink_attrs).map_err(|err| {
err.into_combine(format_err!(parent_span, "at this invocation",))
Expand Down Expand Up @@ -807,7 +830,7 @@ impl Attribute {
attr.span(),
"encountered duplicate ink! attribute"
)
.into_combine(format_err!(seen.span(), "first ink! attribute here")))
.into_combine(format_err!(seen.span(), "first ink! attribute here")));
}
seen.insert(attr);
}
Expand All @@ -820,7 +843,7 @@ impl TryFrom<syn::Attribute> for Attribute {

fn try_from(attr: syn::Attribute) -> Result<Self, Self::Error> {
if attr.path().is_ident("ink") {
return <InkAttribute as TryFrom<_>>::try_from(attr).map(Into::into)
return <InkAttribute as TryFrom<_>>::try_from(attr).map(Into::into);
}
Ok(Attribute::Other(attr))
}
Expand All @@ -837,7 +860,7 @@ impl TryFrom<syn::Attribute> for InkAttribute {

fn try_from(attr: syn::Attribute) -> Result<Self, Self::Error> {
if !attr.path().is_ident("ink") {
return Err(format_err_spanned!(attr, "unexpected non-ink! attribute"))
return Err(format_err_spanned!(attr, "unexpected non-ink! attribute"));
}

let args: Vec<_> = attr
Expand All @@ -850,7 +873,7 @@ impl TryFrom<syn::Attribute> for InkAttribute {
return Err(format_err_spanned!(
attr,
"encountered unsupported empty ink! attribute"
))
));
}
Ok(InkAttribute { args })
}
Expand Down Expand Up @@ -898,7 +921,7 @@ impl InkAttribute {
}
}
if let Some(err) = err {
return Err(err)
return Err(err);
}
Ok(())
}
Expand All @@ -925,6 +948,16 @@ impl Parse for AttributeFrag {
Namespace::try_from(&name_value.value)
.map(AttributeArg::Namespace)
}
"signature_topic" => {
if let Some(hash) = name_value.value.as_string() {
Ok(AttributeArg::SignatureTopic(hash))
} else {
Err(format_err_spanned!(
name_value.value,
"expected String type for `S` in #[ink(signature_topic = S)]",
))
}
}
"function" => {
if let Some(lit_int) = name_value.value.as_lit_int() {
let id = lit_int.base10_parse::<u16>()
Expand Down Expand Up @@ -1508,4 +1541,14 @@ mod tests {
Err("encountered duplicate ink! attribute"),
)
}
#[test]
fn signature_topic_works() {
let s = "11".repeat(32);
assert_attribute_try_from(
syn::parse_quote! {
#[ink(signature_topic = #s)]
},
Ok(test::Attribute::Ink(vec![AttributeArg::SignatureTopic(s)])),
);
}
}
41 changes: 37 additions & 4 deletions crates/ink/ir/src/ir/event/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,21 @@ pub struct EventConfig {
/// If set to `true`, **no** signature topic is generated or emitted for this event.,
/// This is the default value.
anonymous: bool,

/// Manually specified signature topic hash.
signature_topic_hex: Option<String>,
}

impl TryFrom<ast::AttributeArgs> for EventConfig {
type Error = syn::Error;

fn try_from(args: ast::AttributeArgs) -> Result<Self, Self::Error> {
let mut anonymous: Option<syn::LitBool> = None;
let mut signature_topic: Option<syn::LitStr> = None;
for arg in args.into_iter() {
if arg.name.is_ident("anonymous") {
if let Some(lit_bool) = anonymous {
return Err(duplicate_config_err(lit_bool, arg, "anonymous", "event"))
return Err(duplicate_config_err(lit_bool, arg, "anonymous", "event"));
}
if let ast::MetaValue::Lit(syn::Lit::Bool(lit_bool)) = &arg.value {
anonymous = Some(lit_bool.clone())
Expand All @@ -44,27 +48,56 @@ impl TryFrom<ast::AttributeArgs> for EventConfig {
"expected a bool literal for `anonymous` ink! event item configuration argument",
));
}
} else if arg.name.is_ident("signature_topic") {
if anonymous.is_some() {
return Err(format_err_spanned!(
arg,
"cannot specify `signature_topic` with `anonymous` in ink! event item configuration argument",
));
}

if let Some(lit_str) = signature_topic {
return Err(duplicate_config_err(lit_str, arg, "anonymous", "event"));
}
if let ast::MetaValue::Lit(syn::Lit::Str(lis_str)) = &arg.value {
signature_topic = Some(lis_str.clone())
} else {
return Err(format_err_spanned!(
arg,
"expected a bool literal for `anonymous` ink! event item configuration argument",
));
}
} else {
return Err(format_err_spanned!(
arg,
"encountered unknown or unsupported ink! storage item configuration argument",
"encountered unknown or unsupported ink! event item configuration argument",
));
}
}

Ok(EventConfig::new(
anonymous.map(|lit_bool| lit_bool.value).unwrap_or(false),
signature_topic.map(|lit_str| lit_str.value()),
))
}
}

impl EventConfig {
/// Construct a new [`EventConfig`].
pub fn new(anonymous: bool) -> Self {
Self { anonymous }
pub fn new(anonymous: bool, signature_topic_hex: Option<String>) -> Self {
Self {
anonymous,
signature_topic_hex,
}
}

/// Returns the anonymous configuration argument.
pub fn anonymous(&self) -> bool {
self.anonymous
}

/// Returns the manually specified signature topic.
pub fn signature_topic_hex(&self) -> Option<&str> {
self.signature_topic_hex.as_deref()
}
}
Loading

0 comments on commit bc6f5a9

Please sign in to comment.