Quellcode durchsuchen

Solana event support

Depends on https://github.com/solana-labs/solana/pull/19764

Signed-off-by: Sean Young <sean@mess.org>
Sean Young vor 4 Jahren
Ursprung
Commit
fd8aec3146

+ 6 - 2
docs/targets.rst

@@ -36,6 +36,7 @@ ______
 The Solana target requires latest `Solana <https://www.solana.com/>`_ master due to:
 
  - https://github.com/solana-labs/solana/pull/19548
+ - https://github.com/solana-labs/solana/pull/19764
 
 This target is in the early stages right now, however it is under active development.
 All data types are supported, but not all the builtin functions and variables
@@ -72,9 +73,12 @@ arguments, and pass in two accounts to the call, the 2nd being the contract stor
 Once that is done, any function on the contract can be called. To do that, abi encode the function call,
 pass this as input, and provide the two accounts on the call, plus any accounts that may be called.
 
-The return data (i.e. the return values or the revert error) is provided in the program log in base64.
+The return data (i.e. the return values or the revert error) is provided in the
+program log in base64. Any emitted events are written to the program log in base64
+too.
 
-There is `an example of this written in node <https://github.com/hyperledger-labs/solang/tree/main/integration/solana>`_.
+There is `an example of this written in node
+<https://github.com/hyperledger-labs/solang/tree/main/integration/solana>`_.
 
 Hyperledger Burrow (ewasm)
 __________________________

+ 19 - 0
integration/solana/events.sol

@@ -0,0 +1,19 @@
+
+event First(
+	int indexed a,
+	bool b,
+	string c
+);
+
+event Second(
+	int indexed a,
+	bytes4 b,
+	bytes c
+);
+
+contract events {
+	function test() public {
+		emit First(102, true, "foobar");
+		emit Second(500332, "ABCD", hex"CAFE0123");
+	}
+}

+ 25 - 0
integration/solana/events.spec.ts

@@ -0,0 +1,25 @@
+import expect from 'expect';
+import { establishConnection } from './index';
+
+describe('Deploy solang contract and test', () => {
+    it('events', async function () {
+        this.timeout(50000);
+
+        let conn = await establishConnection();
+
+        let event_contract = await conn.loadProgram("bundle.so", "events.abi");
+
+        // call the constructor
+        await event_contract.call_constructor(conn, 'events', []);
+
+        let events = await event_contract.call_function_events(conn, "test", []);
+
+        expect(events[0]["0"]).toEqual("102");
+        expect(events[0]["1"]).toEqual(true);
+        expect(events[0]["2"]).toEqual("foobar");
+
+        expect(events[1]["0"]).toEqual("500332");
+        expect(events[1]["1"]).toEqual("0x41424344");
+        expect(events[1]["2"]).toEqual("0xcafe0123");
+    });
+});

+ 82 - 0
integration/solana/index.ts

@@ -19,6 +19,7 @@ const Web3EthAbi = require('web3-eth-abi');
 
 const default_url: string = "http://localhost:8899";
 const return_data_prefix = 'Program return: ';
+const log_data_prefix = 'Program data: ';
 
 export async function establishConnection(): Promise<TestConnection> {
     let url = process.env.RPC_URL || default_url;
@@ -349,6 +350,87 @@ class Program {
         return Web3EthAbi.decodeParameter('string', encoded.subarray(4).toString('hex'));
     }
 
+    async call_function_events(test: TestConnection, name: string, params: any[], pubkeys: PublicKey[] = [], seeds: any[] = [], signers: Keypair[] = []): Promise<any[]> {
+        let abi: AbiItem[] = JSON.parse(this.abi);
+        let func = abi.find((e: AbiItem) => e.name == name)!;
+
+        const input: string = Web3EthAbi.encodeFunctionCall(func, params);
+        const data = Buffer.concat([
+            this.contractStorageAccount.publicKey.toBuffer(),
+            test.payerAccount.publicKey.toBuffer(),
+            Buffer.from('00000000', 'hex'),
+            this.encode_seeds(seeds),
+            Buffer.from(input.replace('0x', ''), 'hex')
+        ]);
+
+        let debug = 'calling function ' + name + ' [' + params + ']';
+
+        let keys = [];
+
+        seeds.forEach((seed) => {
+            keys.push({ pubkey: seed.address, isSigner: false, isWritable: true });
+        });
+
+        keys.push({ pubkey: this.contractStorageAccount.publicKey, isSigner: false, isWritable: true });
+        keys.push({ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false });
+        keys.push({ pubkey: PublicKey.default, isSigner: false, isWritable: false });
+
+        for (let i = 0; i < pubkeys.length; i++) {
+            // make each 2nd key writable (will be account storage for contract)
+            keys.push({ pubkey: pubkeys[i], isSigner: false, isWritable: (i & 1) == 1 });
+        }
+
+        const instruction = new TransactionInstruction({
+            keys,
+            programId: this.programId,
+            data,
+        });
+
+        signers.unshift(test.payerAccount);
+
+        const { err, logs } = (await test.connection.simulateTransaction(new Transaction().add(instruction),
+            signers)).value;
+
+        if (err) {
+            throw 'error is not falsy';
+        }
+
+        let events = [];
+        let event_abis = new Map();
+
+        for (let index in abi) {
+            let e = abi[index];
+
+            if (e.type == "event") {
+                let signature = Web3EthAbi.encodeEventSignature(e);
+                event_abis.set(signature, e);
+            }
+        }
+
+        for (let message of logs!) {
+            if (message.startsWith(log_data_prefix)) {
+                let fields = message.slice(log_data_prefix.length).split(" ");
+                if (fields.length != 2) {
+                    throw "expecting two fields for return data";
+                }
+                let topic_data = Buffer.from(fields[0], 'base64');
+                let topics = [];
+
+                for (let offset = 0; offset < topic_data.length; offset += 32) {
+                    topics.push('0x' + topic_data.subarray(offset, offset + 32).toString('hex'));
+                }
+
+                let data = '0x' + Buffer.from(fields[1], 'base64').toString('hex');
+
+                let event = event_abis.get(topics[0])!;
+
+                events.push(Web3EthAbi.decodeLog(event.inputs, data, topics.slice(1)));
+            }
+        }
+
+        return events;
+    }
+
     async contract_storage(test: TestConnection, upto: number): Promise<Buffer> {
         const accountInfo = await test.connection.getAccountInfo(this.contractStorageAccount.publicKey);
 

+ 84 - 87
src/codegen/statements.rs

@@ -509,101 +509,98 @@ pub fn statement(
             return_override,
         ),
         Statement::Emit { event_no, args, .. } => {
-            // Solana cannot emit events, and breaks when the emitter calls self.abi_encode()
-            if ns.target != crate::Target::Solana {
-                let event = &ns.events[*event_no];
-                let mut data = Vec::new();
-                let mut data_tys = Vec::new();
-                let mut topics = Vec::new();
-                let mut topic_tys = Vec::new();
-
-                if !event.anonymous && ns.target != crate::Target::Substrate {
-                    let mut hasher = Keccak::v256();
-                    hasher.update(event.signature.as_bytes());
-                    let mut hash = [0u8; 32];
-                    hasher.finalize(&mut hash);
-
-                    topic_tys.push(Type::Bytes(32));
-                    topics.push(Expression::BytesLiteral(
-                        pt::Loc(0, 0, 0),
-                        Type::Bytes(32),
-                        hash.to_vec(),
-                    ));
-                }
-
-                for (i, arg) in args.iter().enumerate() {
-                    if event.fields[i].indexed {
-                        let ty = arg.ty();
+            let event = &ns.events[*event_no];
+            let mut data = Vec::new();
+            let mut data_tys = Vec::new();
+            let mut topics = Vec::new();
+            let mut topic_tys = Vec::new();
+
+            if !event.anonymous && ns.target != crate::Target::Substrate {
+                let mut hasher = Keccak::v256();
+                hasher.update(event.signature.as_bytes());
+                let mut hash = [0u8; 32];
+                hasher.finalize(&mut hash);
+
+                topic_tys.push(Type::Bytes(32));
+                topics.push(Expression::BytesLiteral(
+                    pt::Loc(0, 0, 0),
+                    Type::Bytes(32),
+                    hash.to_vec(),
+                ));
+            }
 
-                        match ty {
-                            Type::String | Type::DynamicBytes => {
-                                let e = expression(
-                                    &Expression::Builtin(
+            for (i, arg) in args.iter().enumerate() {
+                if event.fields[i].indexed {
+                    let ty = arg.ty();
+
+                    match ty {
+                        Type::String | Type::DynamicBytes => {
+                            let e = expression(
+                                &Expression::Builtin(
+                                    pt::Loc(0, 0, 0),
+                                    vec![Type::Bytes(32)],
+                                    Builtin::Keccak256,
+                                    vec![arg.clone()],
+                                ),
+                                cfg,
+                                contract_no,
+                                Some(func),
+                                ns,
+                                vartab,
+                            );
+
+                            topics.push(e);
+                            topic_tys.push(Type::Bytes(32));
+                        }
+                        Type::Struct(_) | Type::Array(..) => {
+                            // We should have an AbiEncodePackedPad
+                            let e = expression(
+                                &Expression::Builtin(
+                                    pt::Loc(0, 0, 0),
+                                    vec![Type::Bytes(32)],
+                                    Builtin::Keccak256,
+                                    vec![Expression::Builtin(
                                         pt::Loc(0, 0, 0),
-                                        vec![Type::Bytes(32)],
-                                        Builtin::Keccak256,
+                                        vec![Type::DynamicBytes],
+                                        Builtin::AbiEncodePacked,
                                         vec![arg.clone()],
-                                    ),
-                                    cfg,
-                                    contract_no,
-                                    Some(func),
-                                    ns,
-                                    vartab,
-                                );
-
-                                topics.push(e);
-                                topic_tys.push(Type::Bytes(32));
-                            }
-                            Type::Struct(_) | Type::Array(..) => {
-                                // We should have an AbiEncodePackedPad
-                                let e = expression(
-                                    &Expression::Builtin(
-                                        pt::Loc(0, 0, 0),
-                                        vec![Type::Bytes(32)],
-                                        Builtin::Keccak256,
-                                        vec![Expression::Builtin(
-                                            pt::Loc(0, 0, 0),
-                                            vec![Type::DynamicBytes],
-                                            Builtin::AbiEncodePacked,
-                                            vec![arg.clone()],
-                                        )],
-                                    ),
-                                    cfg,
-                                    contract_no,
-                                    Some(func),
-                                    ns,
-                                    vartab,
-                                );
-
-                                topics.push(e);
-                                topic_tys.push(Type::Bytes(32));
-                            }
-                            _ => {
-                                let e = expression(arg, cfg, contract_no, Some(func), ns, vartab);
-
-                                topics.push(e);
-                                topic_tys.push(ty);
-                            }
+                                    )],
+                                ),
+                                cfg,
+                                contract_no,
+                                Some(func),
+                                ns,
+                                vartab,
+                            );
+
+                            topics.push(e);
+                            topic_tys.push(Type::Bytes(32));
                         }
-                    } else {
-                        let e = expression(arg, cfg, contract_no, Some(func), ns, vartab);
+                        _ => {
+                            let e = expression(arg, cfg, contract_no, Some(func), ns, vartab);
 
-                        data.push(e);
-                        data_tys.push(arg.ty());
+                            topics.push(e);
+                            topic_tys.push(ty);
+                        }
                     }
-                }
+                } else {
+                    let e = expression(arg, cfg, contract_no, Some(func), ns, vartab);
 
-                cfg.add(
-                    vartab,
-                    Instr::EmitEvent {
-                        event_no: *event_no,
-                        data,
-                        data_tys,
-                        topics,
-                        topic_tys,
-                    },
-                );
+                    data.push(e);
+                    data_tys.push(arg.ty());
+                }
             }
+
+            cfg.add(
+                vartab,
+                Instr::EmitEvent {
+                    event_no: *event_no,
+                    data,
+                    data_tys,
+                    topics,
+                    topic_tys,
+                },
+            );
         }
         Statement::Underscore(_) => {
             cfg.add(

+ 106 - 8
src/emit/solana.rs

@@ -261,6 +261,22 @@ impl SolanaTarget {
         function
             .as_global_value()
             .set_unnamed_address(UnnamedAddress::Local);
+
+        let fields = binary.context.opaque_struct_type("SolLogDataField");
+
+        fields.set_body(&[u8_ptr.into(), u64_ty.into()], false);
+
+        let function = binary.module.add_function(
+            "sol_log_data",
+            void_ty.fn_type(
+                &[fields.ptr_type(AddressSpace::Generic).into(), u64_ty.into()],
+                false,
+            ),
+            None,
+        );
+        function
+            .as_global_value()
+            .set_unnamed_address(UnnamedAddress::Local);
     }
 
     /// Returns the SolAccountInfo of the executing binary
@@ -2920,17 +2936,99 @@ impl<'a> TargetRuntime<'a> for SolanaTarget {
     /// Emit event
     fn emit_event<'b>(
         &self,
-        _binary: &Binary<'b>,
+        binary: &Binary<'b>,
         _contract: &ast::Contract,
-        _function: FunctionValue<'b>,
+        function: FunctionValue<'b>,
         _event_no: usize,
-        _data: &[BasicValueEnum<'b>],
-        _data_tys: &[ast::Type],
-        _topics: &[BasicValueEnum<'b>],
-        _topic_tys: &[ast::Type],
-        _ns: &ast::Namespace,
+        data: &[BasicValueEnum<'b>],
+        data_tys: &[ast::Type],
+        topics: &[BasicValueEnum<'b>],
+        topic_tys: &[ast::Type],
+        ns: &ast::Namespace,
     ) {
-        // Solana does not implement events, ignore for now
+        let fields = binary.build_array_alloca(
+            function,
+            binary.module.get_struct_type("SolLogDataField").unwrap(),
+            binary.context.i32_type().const_int(2, false),
+            "fields",
+        );
+
+        let (topic_ptr, topic_len) =
+            self.abi_encode(binary, None, false, function, topics, topic_tys, ns);
+
+        let field_data = unsafe {
+            binary.builder.build_gep(
+                fields,
+                &[
+                    binary.context.i32_type().const_zero(),
+                    binary.context.i32_type().const_zero(),
+                ],
+                "field_data",
+            )
+        };
+
+        binary.builder.build_store(field_data, topic_ptr);
+
+        let field_len = unsafe {
+            binary.builder.build_gep(
+                fields,
+                &[
+                    binary.context.i32_type().const_zero(),
+                    binary.context.i32_type().const_int(1, false),
+                ],
+                "field_len",
+            )
+        };
+
+        binary.builder.build_store(
+            field_len,
+            binary
+                .builder
+                .build_int_z_extend(topic_len, binary.context.i64_type(), "topic_len64"),
+        );
+
+        let (data_ptr, data_len) =
+            self.abi_encode(binary, None, false, function, data, data_tys, ns);
+
+        let field_data = unsafe {
+            binary.builder.build_gep(
+                fields,
+                &[
+                    binary.context.i32_type().const_int(1, false),
+                    binary.context.i32_type().const_zero(),
+                ],
+                "field_data",
+            )
+        };
+
+        binary.builder.build_store(field_data, data_ptr);
+
+        let field_len = unsafe {
+            binary.builder.build_gep(
+                fields,
+                &[
+                    binary.context.i32_type().const_int(1, false),
+                    binary.context.i32_type().const_int(1, false),
+                ],
+                "field_len",
+            )
+        };
+
+        binary.builder.build_store(
+            field_len,
+            binary
+                .builder
+                .build_int_z_extend(data_len, binary.context.i64_type(), "data_len64"),
+        );
+
+        binary.builder.build_call(
+            binary.module.get_function("sol_log_data").unwrap(),
+            &[
+                fields.into(),
+                binary.context.i64_type().const_int(2, false).into(),
+            ],
+            "",
+        );
     }
 
     /// builtin expressions

+ 86 - 5
tests/solana.rs

@@ -3,7 +3,7 @@ mod solana_helpers;
 use base58::{FromBase58, ToBase58};
 use byteorder::{ByteOrder, LittleEndian, WriteBytesExt};
 use ed25519_dalek::{ed25519::signature::Signature, Verifier};
-use ethabi::Token;
+use ethabi::{RawLog, Token};
 use libc::c_char;
 use rand::Rng;
 use serde::{Deserialize, Serialize};
@@ -58,7 +58,8 @@ struct VirtualMachine {
     account_data: HashMap<Account, AccountState>,
     programs: Vec<Contract>,
     stack: Vec<Contract>,
-    printbuf: String,
+    logs: String,
+    events: Vec<Vec<Vec<u8>>>,
     return_data: Option<(Account, Vec<u8>)>,
 }
 
@@ -207,7 +208,8 @@ fn build_solidity(src: &str) -> VirtualMachine {
         account_data,
         programs,
         stack: vec![cur],
-        printbuf: String::new(),
+        logs: String::new(),
+        events: Vec::new(),
         return_data: None,
     }
 }
@@ -376,7 +378,7 @@ impl<'a> SyscallObject<UserError> for SolLog<'a> {
             .unwrap();
             println!("log: {}", message);
             if let Ok(mut context) = self.context.try_borrow_mut() {
-                context.printbuf.push_str(message);
+                context.logs.push_str(message);
             }
             *result = Ok(0)
         }
@@ -405,7 +407,7 @@ impl<'a> SyscallObject<UserError> for SolLogPubKey<'a> {
         let message = account[0].to_base58();
         println!("log pubkey: {}", message);
         if let Ok(mut context) = self.context.try_borrow_mut() {
-            context.printbuf.push_str(&message);
+            context.logs.push_str(&message);
         }
         *result = Ok(0)
     }
@@ -570,6 +572,49 @@ impl<'a> SyscallObject<UserError> for SyscallGetReturnData<'a> {
     }
 }
 
+struct SyscallLogData<'a> {
+    context: Rc<RefCell<&'a mut VirtualMachine>>,
+}
+
+impl<'a> SyscallObject<UserError> for SyscallLogData<'a> {
+    fn call(
+        &mut self,
+        addr: u64,
+        len: u64,
+        _arg3: u64,
+        _arg4: u64,
+        _arg5: u64,
+        memory_mapping: &MemoryMapping,
+        result: &mut Result<u64, EbpfError<UserError>>,
+    ) {
+        if let Ok(mut context) = self.context.try_borrow_mut() {
+            let untranslated_events =
+                question_mark!(translate_slice::<&[u8]>(memory_mapping, addr, len), result);
+
+            let mut events = Vec::with_capacity(untranslated_events.len());
+
+            for untranslated_event in untranslated_events {
+                let event = question_mark!(
+                    translate_slice_mut::<u8>(
+                        memory_mapping,
+                        untranslated_event.as_ptr() as u64,
+                        untranslated_event.len() as u64,
+                    ),
+                    result
+                );
+
+                events.push(event.to_vec());
+            }
+
+            context.events.push(events.to_vec());
+
+            *result = Ok(0);
+        } else {
+            panic!();
+        }
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum Ed25519SigCheckError {
     InvalidPublicKey,
@@ -1177,6 +1222,10 @@ impl VirtualMachine {
             .register_syscall_by_name(b"sol_get_return_data", SyscallGetReturnData::call)
             .unwrap();
 
+        syscall_registry
+            .register_syscall_by_name(b"sol_log_data", SyscallLogData::call)
+            .unwrap();
+
         let executable = <dyn Executable<UserError, TestInstructionMeter>>::from_elf(
             &self.account_data[&program.program].data,
             None,
@@ -1245,6 +1294,14 @@ impl VirtualMachine {
         )
         .unwrap();
 
+        vm.bind_syscall_context_object(
+            Box::new(SyscallLogData {
+                context: context.clone(),
+            }),
+            None,
+        )
+        .unwrap();
+
         let res = vm
             .execute_program_interpreted(&mut TestInstructionMeter { remaining: 1000000 })
             .unwrap();
@@ -1438,6 +1495,30 @@ impl VirtualMachine {
             offset = next;
         }
     }
+
+    pub fn events(&self) -> Vec<RawLog> {
+        self.events
+            .iter()
+            .map(|fields| {
+                assert_eq!(fields.len(), 2);
+
+                assert_eq!(fields[0].len() % 32, 0);
+                assert!(fields[0].len() <= 4 * 32);
+
+                let topics = fields[0]
+                    .chunks_exact(32)
+                    .map(|topic| {
+                        let topic: [u8; 32] = topic.try_into().unwrap();
+
+                        ethereum_types::H256::from(topic)
+                    })
+                    .collect();
+                let data = fields[1].clone();
+
+                RawLog { data, topics }
+            })
+            .collect()
+    }
 }
 
 pub fn parse_and_resolve(src: &'static str, target: Target) -> ast::Namespace {

+ 5 - 5
tests/solana_tests/call.rs

@@ -81,9 +81,9 @@ fn simple_external_call() {
 
     vm.function("test_bar", &[Token::String(String::from("yo"))], &[]);
 
-    assert_eq!(vm.printbuf, "bar1 says: yo");
+    assert_eq!(vm.logs, "bar1 says: yo");
 
-    vm.printbuf.truncate(0);
+    vm.logs.truncate(0);
 
     let bar1_account = vm.stack[0].data;
 
@@ -97,9 +97,9 @@ fn simple_external_call() {
         &[],
     );
 
-    assert_eq!(vm.printbuf, "bar0 says: uncle beau");
+    assert_eq!(vm.logs, "bar0 says: uncle beau");
 
-    vm.printbuf.truncate(0);
+    vm.logs.truncate(0);
 
     vm.function(
         "test_other",
@@ -107,7 +107,7 @@ fn simple_external_call() {
         &[],
     );
 
-    assert_eq!(vm.printbuf, "bar1 says: cross contract call");
+    assert_eq!(vm.logs, "bar1 says: cross contract call");
 }
 
 #[test]

+ 5 - 8
tests/solana_tests/create_contract.rs

@@ -37,9 +37,9 @@ fn simple_create_contract() {
 
     let bar1 = vm.function("test_other", &[], &[&seed]);
 
-    assert_eq!(vm.printbuf, "bar1 says: yo from bar0");
+    assert_eq!(vm.logs, "bar1 says: yo from bar0");
 
-    vm.printbuf.truncate(0);
+    vm.logs.truncate(0);
 
     println!("next test, {:?}", bar1);
 
@@ -49,7 +49,7 @@ fn simple_create_contract() {
         &[],
     );
 
-    assert_eq!(vm.printbuf, "Hello xywoleh");
+    assert_eq!(vm.logs, "Hello xywoleh");
 }
 
 #[test]
@@ -117,12 +117,9 @@ fn two_contracts() {
 
     let _bar1 = vm.function("test_other", &[], &[&seed1, &seed2]);
 
-    assert_eq!(
-        vm.printbuf,
-        "bar1 says: yo from bar0bar1 says: hi from bar0"
-    );
+    assert_eq!(vm.logs, "bar1 says: yo from bar0bar1 says: hi from bar0");
 
-    vm.printbuf.truncate(0);
+    vm.logs.truncate(0);
 }
 
 #[test]

+ 118 - 0
tests/solana_tests/events.rs

@@ -0,0 +1,118 @@
+use crate::build_solidity;
+use ethabi::Token;
+use tiny_keccak::{Hasher, Keccak};
+
+#[test]
+fn simple_event() {
+    let mut vm = build_solidity(
+        r#"
+        contract c {
+            event e(int indexed a, int b);
+
+            function go() public {
+                emit e(1, 2);
+            }
+        }"#,
+    );
+
+    vm.constructor("c", &[]);
+
+    vm.function("go", &[], &[]);
+
+    let log = vm.events();
+
+    assert_eq!(log.len(), 1);
+
+    let program = &vm.stack[0];
+
+    let abi = program.abi.as_ref().unwrap();
+
+    let event = &abi.events_by_name("e").unwrap()[0];
+
+    assert_eq!(log[0].topics[0], event.signature());
+
+    let decoded = event.parse_log(log[0].clone()).unwrap();
+
+    for log in &decoded.params {
+        match log.name.as_str() {
+            "a" => assert_eq!(log.value, Token::Int(ethereum_types::U256::from(1))),
+            "b" => assert_eq!(log.value, Token::Int(ethereum_types::U256::from(2))),
+            _ => panic!("unexpected field {}", log.name),
+        }
+    }
+}
+
+#[test]
+fn less_simple_event() {
+    let mut vm = build_solidity(
+        r#"
+        contract c {
+            struct S {
+                int64 f1;
+                bool f2;
+            }
+
+            event e(
+                int indexed a,
+                string indexed b,
+                int[2] indexed c,
+                S d);
+
+            function go() public {
+                emit e(-102, "foobar", [1, 2], S({ f1: 102, f2: true}));
+            }
+        }"#,
+    );
+
+    vm.constructor("c", &[]);
+
+    vm.function("go", &[], &[]);
+
+    let log = vm.events();
+
+    assert_eq!(log.len(), 1);
+
+    let program = &vm.stack[0];
+
+    let abi = program.abi.as_ref().unwrap();
+
+    let event = &abi.events_by_name("e").unwrap()[0];
+
+    assert_eq!(log[0].topics[0], event.signature());
+
+    let decoded = event.parse_log(log[0].clone()).unwrap();
+
+    for log in &decoded.params {
+        match log.name.as_str() {
+            "a" => assert_eq!(
+                log.value,
+                Token::Int(ethereum_types::U256::from_dec_str("115792089237316195423570985008687907853269984665640564039457584007913129639834").unwrap())
+            ),
+            "b" => {
+                let mut hasher = Keccak::v256();
+                hasher.update(b"foobar");
+                let mut hash = [0u8; 32];
+                hasher.finalize(&mut hash);
+
+                assert_eq!(log.value, Token::FixedBytes(hash.to_vec()));
+            }
+            "c" => {
+                let mut hasher = Keccak::v256();
+                let mut v = [0u8; 32];
+                v[31] = 1;
+                hasher.update(&v);
+                v[31] = 2;
+                hasher.update(&v);
+                let mut hash = [0u8; 32];
+                hasher.finalize(&mut hash);
+
+                assert_eq!(log.value, Token::FixedBytes(hash.to_vec()));
+            }
+            "d" => {
+                assert_eq!(log.value, Token::Tuple(vec![Token::Int(ethereum_types::U256::from(102)), Token::Bool(true)]));
+            }
+
+            _ => panic!("unexpected field {}", log.name),
+        }
+    }
+}

+ 1 - 0
tests/solana_tests/mod.rs

@@ -6,6 +6,7 @@ mod builtin;
 mod call;
 mod create_contract;
 mod destructure;
+mod events;
 mod expressions;
 mod hash;
 mod mappings;

+ 7 - 7
tests/solana_tests/simple.rs

@@ -17,13 +17,13 @@ fn simple() {
 
     vm.constructor("foo", &[]);
 
-    assert_eq!(vm.printbuf, "Hello from constructor");
+    assert_eq!(vm.logs, "Hello from constructor");
 
-    vm.printbuf.truncate(0);
+    vm.logs.truncate(0);
 
     vm.function("test", &[], &[]);
 
-    assert_eq!(vm.printbuf, "Hello from function");
+    assert_eq!(vm.logs, "Hello from function");
 }
 
 #[test]
@@ -42,7 +42,7 @@ fn format() {
     vm.constructor("foo", &[]);
 
     assert_eq!(
-        vm.printbuf,
+        vm.logs,
         "x = 21847450052839212624230656502990235142567050104912751880812823948662932355201"
     );
 }
@@ -75,9 +75,9 @@ fn parameters() {
         &[],
     );
 
-    assert_eq!(vm.printbuf, "x is 10");
+    assert_eq!(vm.logs, "x is 10");
 
-    vm.printbuf.truncate(0);
+    vm.logs.truncate(0);
 
     vm.function(
         "test",
@@ -88,7 +88,7 @@ fn parameters() {
         &[],
     );
 
-    assert_eq!(vm.printbuf, "y is 102");
+    assert_eq!(vm.logs, "y is 102");
 }
 
 #[test]

+ 2 - 2
tests/solana_tests/using.rs

@@ -30,7 +30,7 @@ fn using_for_contracts() {
     runtime.constructor("C", &[]);
     runtime.function("test", &[], &[]);
 
-    assert_eq!(runtime.printbuf, "Hello");
+    assert_eq!(runtime.logs, "Hello");
 
     let mut runtime = build_solidity(
         r#"
@@ -73,5 +73,5 @@ fn using_for_contracts() {
     runtime.constructor("foo", &[]);
     runtime.function("test", &[], &[]);
 
-    assert_eq!(runtime.printbuf, "X libX contractx:2");
+    assert_eq!(runtime.logs, "X libX contractx:2");
 }