Эх сурвалжийг харах

Substrate: Fix events with topics (#1064)

Signed-off-by: xermicus <cyrill@parity.io>
Signed-off-by: Sean Young <sean@mess.org>
Co-authored-by: Sean Young <sean@mess.org>
Cyrill Leutwiler 3 жил өмнө
parent
commit
cfbc7a9f6b

+ 1 - 1
Cargo.toml

@@ -54,13 +54,13 @@ bitflags = "1.3"
 anchor-syn = { version = "0.25", features = ["idl"] }
 convert_case = "0.6"
 parse-display = "0.6.0"
+parity-scale-codec = "3.1"
 ink = "4.0.0-beta"
 scale-info = "2.3"
 
 
 [dev-dependencies]
 num-derive = "0.3"
-parity-scale-codec = "3.1"
 ethabi = "17.0"
 wasmi = "0.11"
 # rand version 0.7 is needed for ed25519_dalek::keypair::generate, used in solana_tests/signature_verify.rs

+ 17 - 18
integration/substrate/events.sol

@@ -1,23 +1,22 @@
-contract events {
-	/// Ladida tada
-	event foo1(
-		int64 id,
-		string s
-	);
+contract Events {
+    /// Ladida tada
+    event foo1(int64 id, string s);
 
-	/// @title Event Foo2
-	/// @notice Just a test
-	/// @author them is me
-	event foo2(
-		int64 id,
-		string s2,
-		address a
-	);
+    /// @title Event Foo2
+    /// @notice Just a test
+    /// @author them is me
+    event foo2(int64 id, string s2, address a);
 
+    event ThisEventTopicShouldGetHashed(address indexed caller);
+    event Event(bool indexed something);
 
-	function emit_event() public {
-		emit foo1(254, "hello there");
+    function emit_event() public {
+        emit foo1(254, "hello there");
 
-		emit foo2(type(int64).max, "minor", address(this));
-	}
+        emit foo2(type(int64).max, "minor", address(this));
+
+        emit ThisEventTopicShouldGetHashed(msg.sender);
+
+        emit Event(true);
+    }
 }

+ 53 - 8
integration/substrate/events.spec.ts

@@ -4,7 +4,7 @@ import { ContractPromise } from '@polkadot/api-contract';
 import { ApiPromise } from '@polkadot/api';
 import { DecodedEvent } from '@polkadot/api-contract/types';
 
-describe('Deploy events contract and test', () => {
+describe('Deploy events contract and test event data, docs and topics', () => {
     let conn: ApiPromise;
 
     before(async function () {
@@ -20,18 +20,13 @@ describe('Deploy events contract and test', () => {
 
         const alice = aliceKeypair();
 
-        // call the constructors
-        let deploy_contract = await deploy(conn, alice, 'events.contract', BigInt(0));
-
+        let deploy_contract = await deploy(conn, alice, 'Events.contract', BigInt(0));
         let contract = new ContractPromise(conn, deploy_contract.abi, deploy_contract.address);
-
         let tx = contract.tx.emitEvent({ gasLimit });
-
         let res0: any = await transaction(tx, alice);
-
         let events: DecodedEvent[] = res0.contractEvents;
 
-        expect(events.length).toEqual(2);
+        expect(events.length).toEqual(4);
 
         expect(events[0].event.identifier).toBe("foo1");
         expect(events[0].event.docs).toEqual(["Ladida tada\n\n"]);
@@ -40,5 +35,55 @@ describe('Deploy events contract and test', () => {
         expect(events[1].event.identifier).toBe("foo2");
         expect(events[1].event.docs).toEqual(["Event Foo2\n\nJust a test\n\nAuthor: them is me"]);
         expect(events[1].args.map(a => a.toJSON())).toEqual(["0x7fffffffffffffff", "minor", deploy_contract.address.toString()]);
+
+        expect(events[2].event.identifier).toBe("ThisEventTopicShouldGetHashed");
+        expect(events[2].args.map(a => a.toJSON())).toEqual([alice.address]);
+
+        // In ink! the 3rd event does look like this:
+        //
+        //  #[ink(event)]
+        //  pub struct ThisEventTopicShouldGetHashed {
+        //      #[ink(topic)]
+        //      caller: AccountId,
+        //  }
+        //
+        // It yields the following event topics:
+        //
+        //  topics: [
+        //      0x5dde952854d38c37cff349bfc574a48a831de385b82457a5c25d9d39c220f3a7
+        //      0xa5af79de4a26a64813f980ffbb64ac0d7c278f67b17721423daed26ec5d3fe51
+        //  ]
+        //
+        // So we expect our solidity contract to produce the exact same topics:
+
+        let hashed_event_topics = await conn.query.system.eventTopics("0x5dde952854d38c37cff349bfc574a48a831de385b82457a5c25d9d39c220f3a7");
+        expect(hashed_event_topics.length).toBe(1);
+        let hashed_topics = await conn.query.system.eventTopics("0xa5af79de4a26a64813f980ffbb64ac0d7c278f67b17721423daed26ec5d3fe51");
+        expect(hashed_topics.length).toBe(1);
+
+        expect(events[3].event.identifier).toBe("Event");
+        expect(events[3].args.map(a => a.toJSON())).toEqual([true]);
+
+        // In ink! the 4th event does look like this:
+        //
+        //  #[ink(event)]
+        //  pub struct Event {
+        //      #[ink(topic)]
+        //      something: bool,
+        //  }
+        //
+        // It yields the following event topics:
+        //
+        //  topics: [
+        //      0x004576656e74733a3a4576656e74000000000000000000000000000000000000
+        //      0x604576656e74733a3a4576656e743a3a736f6d657468696e6701000000000000
+        //  ]
+        //
+        // So we expect our solidity contract to produce the exact same topics:
+
+        let unhashed_event_topics = await conn.query.system.eventTopics("0x004576656e74733a3a4576656e74000000000000000000000000000000000000");
+        expect(unhashed_event_topics.length).toBe(1);
+        let unhashed_topics = await conn.query.system.eventTopics("0x604576656e74733a3a4576656e743a3a736f6d657468696e6701000000000000");
+        expect(unhashed_topics.length).toBe(1);
     });
 });

+ 3 - 3
integration/substrate/package.json

@@ -20,9 +20,9 @@
     "typescript": "^4.7"
   },
   "dependencies": {
-    "@polkadot/api": "^9.7",
-    "@polkadot/api-contract": "^9.7",
-    "@polkadot/types": "^9.7",
+    "@polkadot/api": "^9.9",
+    "@polkadot/api-contract": "^9.9",
+    "@polkadot/types": "^9.9",
     "@polkadot/keyring": "^10.1",
     "@polkadot/util-crypto": "^10.1"
   }

+ 141 - 64
src/codegen/events/substrate.rs

@@ -1,12 +1,15 @@
 // SPDX-License-Identifier: Apache-2.0
 
+use std::collections::VecDeque;
+
 use crate::codegen::cfg::{ControlFlowGraph, Instr};
 use crate::codegen::events::EventEmitter;
 use crate::codegen::expression::expression;
 use crate::codegen::vartable::Vartable;
-use crate::codegen::Options;
-use crate::sema::ast;
-use crate::sema::ast::{Function, Namespace, RetrieveType, Type};
+use crate::codegen::{Builtin, Expression, Options};
+use crate::sema::ast::{self, Function, Namespace, RetrieveType, StringLocation, Type};
+use ink::env::hash::{Blake2x256, CryptoHash};
+use parity_scale_codec::Encode;
 use solang_parser::pt;
 
 /// This struct implements the trait 'EventEmitter' in order to handle the emission of events
@@ -18,6 +21,17 @@ pub(super) struct SubstrateEventEmitter<'a> {
     pub(super) event_no: usize,
 }
 
+/// Takes a scale-encoded topic and makes it into a topic hash.
+fn topic_hash(encoded: &[u8]) -> Vec<u8> {
+    let mut buf = [0; 32];
+    if encoded.len() <= 32 {
+        buf[..encoded.len()].copy_from_slice(encoded);
+    } else {
+        <Blake2x256 as CryptoHash>::hash(encoded, &mut buf);
+    };
+    buf.into()
+}
+
 impl EventEmitter for SubstrateEventEmitter<'_> {
     fn emit(
         &self,
@@ -32,68 +46,131 @@ impl EventEmitter for SubstrateEventEmitter<'_> {
         let mut topics = Vec::new();
         let mut topic_tys = Vec::new();
 
-        for (i, arg) in self.args.iter().enumerate() {
-            if self.ns.events[self.event_no].fields[i].indexed {
-                let ty = arg.ty();
-
-                match ty {
-                    Type::String | Type::DynamicBytes => {
-                        let e = expression(
-                            &ast::Expression::Builtin(
-                                pt::Loc::Codegen,
-                                vec![Type::Bytes(32)],
-                                ast::Builtin::Keccak256,
-                                vec![arg.clone()],
-                            ),
-                            cfg,
-                            contract_no,
-                            Some(func),
-                            self.ns,
-                            vartab,
-                            opt,
-                        );
-
-                        topics.push(e);
-                        topic_tys.push(Type::Bytes(32));
-                    }
-                    Type::Struct(_) | Type::Array(..) => {
-                        // We should have an AbiEncodePackedPad
-                        let e = expression(
-                            &ast::Expression::Builtin(
-                                pt::Loc::Codegen,
-                                vec![Type::Bytes(32)],
-                                ast::Builtin::Keccak256,
-                                vec![ast::Expression::Builtin(
-                                    pt::Loc::Codegen,
-                                    vec![Type::DynamicBytes],
-                                    ast::Builtin::AbiEncodePacked,
-                                    vec![arg.clone()],
-                                )],
-                            ),
-                            cfg,
-                            contract_no,
-                            Some(func),
-                            self.ns,
-                            vartab,
-                            opt,
-                        );
-
-                        topics.push(e);
-                        topic_tys.push(Type::Bytes(32));
-                    }
-                    _ => {
-                        let e = expression(arg, cfg, contract_no, Some(func), self.ns, vartab, opt);
-
-                        topics.push(e);
-                        topic_tys.push(ty);
-                    }
-                }
-            } else {
-                let e = expression(arg, cfg, contract_no, Some(func), self.ns, vartab, opt);
-
-                data.push(e);
-                data_tys.push(arg.ty());
+        let loc = pt::Loc::Builtin;
+        let event = &self.ns.events[self.event_no];
+        // For freestanding events the name of the emitting contract is used
+        let contract_name = &self.ns.contracts[event.contract.unwrap_or(contract_no)].name;
+        let hash_len = Box::new(Expression::NumberLiteral(loc, Type::Uint(32), 32.into()));
+
+        // Events that are not anonymous always have themselves as a topic.
+        // This is static and can be calculated at compile time.
+        if !event.anonymous {
+            // First byte is 0 because there is no prefix for the event topic
+            let encoded = format!("\0{}::{}", contract_name, &event.name);
+            topics.push(Expression::AllocDynamicBytes(
+                loc,
+                Type::Slice(Type::Uint(8).into()),
+                hash_len.clone(),
+                Some(topic_hash(encoded.as_bytes())),
+            ));
+            topic_tys.push(Type::DynamicBytes);
+        };
+
+        // Topic prefixes are static and can be calculated at compile time.
+        let mut topic_prefixes: VecDeque<Vec<u8>> = event
+            .fields
+            .iter()
+            .filter(|field| field.indexed)
+            .map(|field| {
+                format!(
+                    "{}::{}::{}",
+                    contract_name,
+                    &event.name,
+                    &field.name_as_str()
+                )
+                .into_bytes()
+                .encode()
+            })
+            .collect();
+
+        for (ast_exp, field) in self.args.iter().zip(event.fields.iter()) {
+            let value_exp = expression(ast_exp, cfg, contract_no, Some(func), self.ns, vartab, opt);
+            let value_var = vartab.temp_anonymous(&value_exp.ty());
+            let value = Expression::Variable(loc, value_exp.ty(), value_var);
+            cfg.add(
+                vartab,
+                Instr::Set {
+                    loc,
+                    res: value_var,
+                    expr: value_exp,
+                },
+            );
+            data_tys.push(value.ty());
+            data.push(value.clone());
+
+            if !field.indexed {
+                continue;
             }
+
+            let encoded = Expression::AbiEncode {
+                loc,
+                tys: vec![value.ty()],
+                packed: vec![],
+                args: vec![value],
+            };
+            let concatenated = Expression::StringConcat(
+                loc,
+                Type::DynamicBytes,
+                StringLocation::CompileTime(topic_prefixes.pop_front().unwrap()),
+                StringLocation::RunTime(encoded.into()),
+            );
+
+            vartab.new_dirty_tracker();
+            let var_buffer = vartab.temp_anonymous(&Type::DynamicBytes);
+            cfg.add(
+                vartab,
+                Instr::Set {
+                    loc,
+                    res: var_buffer,
+                    expr: concatenated,
+                },
+            );
+            let buffer = Expression::Variable(loc, Type::DynamicBytes, var_buffer);
+            let compare = Expression::UnsignedMore(
+                loc,
+                Expression::Builtin(
+                    loc,
+                    vec![Type::Uint(32)],
+                    Builtin::ArrayLength,
+                    vec![buffer.clone()],
+                )
+                .into(),
+                hash_len.clone(),
+            );
+
+            let hash_topic_block = cfg.new_basic_block("hash_topic".into());
+            let done_block = cfg.new_basic_block("done".into());
+            cfg.add(
+                vartab,
+                Instr::BranchCond {
+                    cond: compare,
+                    true_block: hash_topic_block,
+                    false_block: done_block,
+                },
+            );
+
+            cfg.set_basic_block(hash_topic_block);
+            cfg.add(
+                vartab,
+                Instr::WriteBuffer {
+                    buf: buffer.clone(),
+                    offset: Expression::NumberLiteral(loc, Type::Uint(32), 0.into()),
+                    value: Expression::Builtin(
+                        loc,
+                        vec![Type::Bytes(32)],
+                        Builtin::Blake2_256,
+                        vec![buffer.clone()],
+                    ),
+                },
+            );
+            vartab.set_dirty(var_buffer);
+            cfg.add(vartab, Instr::Branch { block: done_block });
+
+            cfg.set_basic_block(done_block);
+            cfg.set_phis(done_block, vartab.pop_dirty_tracker());
+
+            topic_tys.push(Type::DynamicBytes);
+            topics.push(buffer);
         }
 
         cfg.add(

+ 9 - 12
src/emit/substrate/target.rs

@@ -1459,7 +1459,7 @@ impl<'a> TargetRuntime<'a> for SubstrateTarget {
         data: &[BasicValueEnum<'b>],
         data_tys: &[ast::Type],
         topics: &[BasicValueEnum<'b>],
-        topic_tys: &[ast::Type],
+        _topic_tys: &[ast::Type],
         ns: &ast::Namespace,
     ) {
         emit_context!(binary);
@@ -1502,17 +1502,14 @@ impl<'a> TargetRuntime<'a> for SubstrateTarget {
                 ]
             );
 
-            for (i, topic) in topics.iter().enumerate() {
-                let mut data = dest;
-                self.encode_ty(
-                    binary,
-                    ns,
-                    false,
-                    true,
-                    function,
-                    &topic_tys[i],
-                    *topic,
-                    &mut data,
+            for topic in topics.iter() {
+                call!(
+                    "__memcpy",
+                    &[
+                        dest.into(),
+                        binary.vector_bytes(*topic).into(),
+                        binary.vector_len(*topic).into(),
+                    ]
                 );
 
                 dest = unsafe { binary.builder.build_gep(dest, &[i32_const!(32)], "dest") };

+ 6 - 0
src/sema/types.rs

@@ -577,6 +577,12 @@ fn event_decl(
                 loc: name.loc,
             })
         } else {
+            if ns.target.is_substrate() && field.indexed {
+                ns.diagnostics.push(Diagnostic::error(
+                    field.loc,
+                    "indexed event fields must have a name on substrate".into(),
+                ));
+            }
             None
         };
 

+ 10 - 0
tests/contract_testcases/substrate/events/event_decl_11.dot

@@ -0,0 +1,10 @@
+strict digraph "tests/contract_testcases/substrate/events/event_decl_11.sol" {
+	foo [label="name:foo\ncontract: 0\ntests/contract_testcases/substrate/events/event_decl_11.sol:2:11-14\nfield name:x ty:bool indexed:no\nfield name:y ty:uint32 indexed:no\nfield name: ty:address indexed:yes"]
+	contract [label="contract c\ntests/contract_testcases/substrate/events/event_decl_11.sol:1:1-3:2"]
+	diagnostic [label="found abstract contract 'c'\nlevel Debug\ntests/contract_testcases/substrate/events/event_decl_11.sol:1:1-3:2"]
+	diagnostic_6 [label="indexed event fields must have a name on substrate\nlevel Error\ntests/contract_testcases/substrate/events/event_decl_11.sol:2:33-48"]
+	events -> foo
+	contracts -> contract
+	diagnostics -> diagnostic [label="Debug"]
+	diagnostics -> diagnostic_6 [label="Error"]
+}

+ 3 - 0
tests/contract_testcases/substrate/events/event_decl_11.sol

@@ -0,0 +1,3 @@
+abstract contract c {
+    event foo(bool x, uint32 y, address indexed);
+}

+ 160 - 17
tests/substrate_tests/events.rs

@@ -1,16 +1,31 @@
 // SPDX-License-Identifier: Apache-2.0
 
 use crate::build_solidity;
-use parity_scale_codec::{Decode, Encode};
+use ink::env::{
+    hash::{Blake2x256, CryptoHash},
+    topics::PrefixedValue,
+};
+use ink::primitives::AccountId;
+use parity_scale_codec::Encode;
 use solang::{file_resolver::FileResolver, Target};
 use std::ffi::OsStr;
 
+fn topic_hash(encoded: &[u8]) -> Vec<u8> {
+    let mut buf = [0; 32];
+    if encoded.len() <= 32 {
+        buf[..encoded.len()].copy_from_slice(encoded);
+    } else {
+        <Blake2x256 as CryptoHash>::hash(encoded, &mut buf);
+    };
+    buf.into()
+}
+
 #[test]
-fn emit() {
+fn anonymous() {
     let mut runtime = build_solidity(
         r##"
         contract a {
-            event foo(bool) anonymous;
+            event foo(bool b) anonymous;
             function emit_event() public {
                 emit foo(true);
             }
@@ -24,15 +39,21 @@ fn emit() {
     let event = &runtime.events[0];
     assert_eq!(event.topics.len(), 0);
     assert_eq!(event.data, (0u8, true).encode());
+}
 
-    #[derive(Debug, PartialEq, Eq, Encode, Decode)]
-    struct Foo(u8, bool, u32);
+#[test]
+fn emit() {
+    #[derive(Encode)]
+    enum Event {
+        Foo(bool, u32, i64),
+        Bar(u32, u64, String),
+    }
 
     let mut runtime = build_solidity(
         r##"
         contract a {
-            event foo(bool,uint32,int64 indexed);
-            event bar(uint32,uint64,string indexed);
+            event foo(bool,uint32,int64 indexed i);
+            event bar(uint32,uint64,string indexed s);
             function emit_event() public {
                 emit foo(true, 102, 1);
                 emit bar(0xdeadcafe, 102, "foobar");
@@ -45,20 +66,37 @@ fn emit() {
 
     assert_eq!(runtime.events.len(), 2);
     let event = &runtime.events[0];
-    assert_eq!(event.topics.len(), 1);
-    let mut t = [0u8; 32];
-    t[0] = 1;
-
-    assert_eq!(event.topics[0], t);
-    assert_eq!(event.data, Foo(0, true, 102).encode());
+    assert_eq!(event.topics.len(), 2);
+    assert_eq!(event.topics[0], topic_hash(b"\0a::foo")[..]);
+    let topic = PrefixedValue {
+        prefix: b"a::foo::i",
+        value: &1i64,
+    }
+    .encode();
+    assert_eq!(event.topics[1], topic_hash(&topic[..])[..]);
+    assert_eq!(event.data, Event::Foo(true, 102, 1).encode());
 
     let event = &runtime.events[1];
-    assert_eq!(event.topics.len(), 1);
+    assert_eq!(event.topics.len(), 2);
+    println!(
+        "topic hash: {}",
+        std::str::from_utf8(&event.topics[0]).unwrap()
+    );
+    println!(
+        "topic hash: {}",
+        std::str::from_utf8(&event.topics[0]).unwrap()
+    );
+    assert_eq!(event.topics[0], topic_hash(b"\0a::bar")[..]);
+    let topic = PrefixedValue {
+        prefix: b"a::bar::s",
+        value: &String::from("foobar"),
+    }
+    .encode();
+    assert_eq!(event.topics[1].to_vec(), topic_hash(&topic[..]));
     assert_eq!(
-        event.topics[0].to_vec(),
-        hex::decode("38d18acb67d25c8bb9942764b62f18e17054f66a817bd4295423adf9ed98873e").unwrap()
+        event.data,
+        Event::Bar(0xdeadcafe, 102, "foobar".into()).encode()
     );
-    assert_eq!(event.data, (1u8, 0xdeadcafeu32, 102u64).encode());
 }
 
 #[test]
@@ -183,3 +221,108 @@ fn event_imported() {
 
     assert!(!ns.diagnostics.any_errors());
 }
+
+/// FIXME: Use the exact same event structure once the `Option<T>` type is available
+#[test]
+fn erc20_ink_example() {
+    #[derive(Encode)]
+    enum Event {
+        Transfer(AccountId, AccountId, u128),
+    }
+
+    #[derive(Encode)]
+    struct Transfer {
+        from: AccountId,
+        to: AccountId,
+        value: u128,
+    }
+
+    let mut runtime = build_solidity(
+        r##"
+        contract Erc20 {
+            event Transfer(
+                address indexed from,
+                address indexed to,
+                uint128 value
+            );
+        
+            function emit_event(address from, address to, uint128 value) public {
+                emit Transfer(from, to, value);
+            }
+        }"##,
+    );
+    runtime.constructor(0, Vec::new());
+    let from = AccountId::from([1; 32]);
+    let to = AccountId::from([2; 32]);
+    let value = 10;
+    runtime.function("emit_event", Transfer { from, to, value }.encode());
+
+    assert_eq!(runtime.events.len(), 1);
+    let event = &runtime.events[0];
+    assert_eq!(event.data, Event::Transfer(from, to, value).encode());
+
+    assert_eq!(event.topics.len(), 3);
+    assert_eq!(event.topics[0], topic_hash(b"\0Erc20::Transfer")[..]);
+
+    let expected_topic = PrefixedValue {
+        prefix: b"Erc20::Transfer::from",
+        value: &from,
+    };
+    assert_eq!(event.topics[1], topic_hash(&expected_topic.encode())[..]);
+
+    let expected_topic = PrefixedValue {
+        prefix: b"Erc20::Transfer::to",
+        value: &to,
+    };
+    assert_eq!(event.topics[2], topic_hash(&expected_topic.encode())[..]);
+}
+
+#[test]
+fn freestanding() {
+    let mut runtime = build_solidity(
+        r##"
+        event A(bool indexed b);
+        function foo() {
+            emit A(true);
+        }
+        contract a {
+            function emit_event() public {
+                foo();
+            }
+        }"##,
+    );
+
+    runtime.constructor(0, Vec::new());
+    runtime.function("emit_event", Vec::new());
+
+    assert_eq!(runtime.events.len(), 1);
+    let event = &runtime.events[0];
+    assert_eq!(event.data, (0u8, true).encode());
+    assert_eq!(event.topics[0], topic_hash(b"\0a::A")[..]);
+    let expected_topic = PrefixedValue {
+        prefix: b"a::A::b",
+        value: &true,
+    };
+    assert_eq!(event.topics[1], topic_hash(&expected_topic.encode())[..]);
+}
+
+#[test]
+fn different_contract() {
+    let mut runtime = build_solidity(
+        r##"abstract contract A { event X(bool indexed foo); }
+        contract B { function emit_event() public { emit A.X(true); } }"##,
+    );
+
+    runtime.constructor(0, Vec::new());
+    runtime.function("emit_event", Vec::new());
+
+    assert_eq!(runtime.events.len(), 1);
+    let event = &runtime.events[0];
+    assert_eq!(event.data, (0u8, true).encode());
+    assert_eq!(event.topics[0], topic_hash(b"\0A::X")[..]);
+    let expected_topic = PrefixedValue {
+        prefix: b"A::X::foo",
+        value: &true,
+    };
+    assert_eq!(event.topics[1], topic_hash(&expected_topic.encode())[..]);
+}