Event Topics
If you’ve read the previous post in this series, you may be wondering at the following question.
What is the difference between each version of LOGn
?
The answer is indexed arguments. Strictly, the value n is the number of topics in the event, and we’ve already seen that we can easily produce events with 1 topic.
This happens to be the keccak256
hash of the event’s function signature. I mean function signature in the sense it is commonly used in programming, that is, a list of its name and argument types. For example, if you have the event definition
event LogEmptyEvent();
the corresponding signature will be
keccak256('LogEmptyEvent()') == '0x4b35438fb193bbfb629456ab5c5e73cff989b7f1cb731b9387ae83853da2a68e'
There are some simple rules on how to normalize this signature before calling the hash: argument names are ignored and spaces are removed. Plus, the type uint
is made explicit by using instead the full type it represents, uint256
. More about this can be found here. (Todo: add link to read the docs).
For example, for the two cases of LogEventData
above, the signatures are computed like this:
keccak256('LogEventData(uint256,uint8)') == '0x8604f2dc82d77e997bcc9f6b4a10c9175a9be5fb69a9ef44bbb5c4eaa66ca364' keccak256('LogEventData(uint8,uint16,uint32,uint64,uint128,uint256)') == '0x99986dab9136c96a7db07b9393684e3a152ce628162b6ebbdb6fbdca3ae2d581'
This tells us how to create the first topic. A look at the EVMs suggests we can create events without topics, basically anonymous events, since they don’t have a corresponding signature, but I don’t know how to create them in Solidity apart from invoking assembly code directly. But what about the remaining topics?
Event’s Indexed Arguments
We can have up to 3 topics beyond the signature, and these can be created very simply in Solidity, by making arguments indexed. Indexed arguments allow for efficient searches. Let’s see an example.
Add the following event to your contract:
event LogIndexedEvent(uint8 data1, uint8 indexed index1 , uint32 data2, uint16 indexed index2); function testIndexedEvents() public { emit LogIndexedEvent(10, 20, 24234, 10000); emit LogIndexedEvent(17, 30, 12000, 10000); emit LogIndexedEvent(34, 80, 7, 10000); emit LogIndexedEvent(10, 20, 12000, 20000); emit LogIndexedEvent(48, 30, 12000, 20000); emit LogIndexedEvent(17, 80, 24234, 20000); }
and invoke this method from your test script with something like this:
let receipt = await instance.methods.testIndexedEvents().send({from: accounts[0]}); let events = receipt.events.LogIndexedEvent; console.log('Events: %o', events); console.log('Signatures: %o', events.map(e => e.signature)); console.log('First topic: %o', events.map(e => e.raw.topics[0])); console.log('All topics: %o', events.map(e => e.raw.topics)); console.log('Returns: %o', events.map(e => e.returnValues));
If you look carefully at the output of the above, or play around with it for a while, you’ll find that:
events
is a list with data for 6 events, as expected (weemit
6 times)- all of the events have the same signature and therefore the same first topic
- they all have 2 other topics (and hence will use the opcode LOG3, which you can verify in remix)
- the topics include the values of the indexed arguments, but not the others
- all the arguments are still returned in
returnValues
.
Indexed Search
The power of indexed event arguments is not visible inside a smart contract. The benefit is for those applications monitoring the activity on the blockchain. Because the blockchain is shared by many contracts and applications, at any one time lots of different transactions produce a large number of events. Indexed topics allow a watcher application to filter down this mess of data and extract only what is relevant for itself.
Keep in mind that because the main topic is just the hash of an event name and its argument list, it is relatively easy to end up with an event named the same as someone else did if your choice of name is too obvious.
These two events would have the same hash and you could be paying attention to someone else’s events, not just yours.
Full nodes keep in memory the whole state of the blockchain (remember the blockchain is just an idealized data structure, that is virtually unique around the world, but in reality is instantiated in each supporting node), including the emission of logs (which effectively change the contract state).
If you need to watch log events in the blockchain, you can connect to a full node and query it via its JSON-RPC API, issuing the command eth_newFilter
followed by eth_getFilterLogs.
But the focus of this series is in using the abilities of web3.js
.
The easiest way to start is perhaps to ask for all the past events. That can be done in this way:
async function retrieveAllPastEvents() { return await instance.getPastEvents("allEvents", { fromBlock: 0 }); } var events = await retrieveAllPastEvents(); console.log("All Events: %o", events.map(e => { return { event: e.event, block: e.blockNumber } }));
For my example, I create three transactions that produce 9 events:
All Events:
[ { event: 'LogIndexedEvent', block: 44 },
{ event: 'LogIndexedEvent', block: 44 },
{ event: 'LogIndexedEvent', block: 44 },
{ event: 'LogIndexedEvent', block: 44 },
{ event: 'LogIndexedEvent', block: 44 },
{ event: 'LogIndexedEvent', block: 44 },
{ event: 'LogEventData', block: 45 },
{ event: 'LogEventData', block: 45 },
{ event: 'LogEmptyEvent', block: 46 },
[length]: 9 ]
Advanced Searches
You probably will want something more refined than this, for example keeping track only of the most recent blocks and of selected events. It is useful to read the documentation online for other methods of accessing events. T
If you don’t want the full list of logs, you can pass a different value in the first argument. If you give it just the name of an event, the following call will return just the sublist of matching that name:
return await instance.getPastEvents("LogIndexedEvent", {
fromBlock: 0
});
All Events:
[ { event: 'LogIndexedEvent', block: 48 },
{ event: 'LogIndexedEvent', block: 48 },
{ event: 'LogIndexedEvent', block: 48 },
{ event: 'LogIndexedEvent', block: 48 },
{ event: 'LogIndexedEvent', block: 48 },
{ event: 'LogIndexedEvent', block: 48 },
[length]: 6 ]
We can obtain more useful information for each event by listing also the topics and the return values.
console.log("All Events:\n%o", events.map(e =>
{
return {
event: e.event,
block: e.blockNumber,
returns:e.returnValues,
topics: e.raw.topics}}
));
This displays all the data of the events and an identification of the topics available for indexing. For example:
{ returns:
Result {
'0': '10',
'1': '20',
'2': '24234',
'3': '10000',
data1: '10',
index1: '20',
data2: '24234',
index2: '10000' },
topics:
[ '0xc8ab967a2839f077f35f0ee827fdf57900f535485088fecd64337ad2b64e75e6',
'0x0000000000000000000000000000000000000000000000000000000000000014',
'0x0000000000000000000000000000000000000000000000000000000000002710',
[length]: 3 ] },
To filter on index1
for value 80, we can extend the second argument, where we specified the initial block like this:
return await instance.getPastEvents("LogIndexedEvent", { fromBlock: 0, filter: {index1: 80} });
and get only 2 events:
All Events: [ { returns: Result { '0': '34', '1': '80', '2': '7', '3': '10000', data1: '34', index1: '80', data2: '7', index2: '10000' }, topics: [ '0xc8ab967a2839f077f35f0ee827fdf57900f535485088fecd64337ad2b64e75e6', '0x0000000000000000000000000000000000000000000000000000000000000050', '0x0000000000000000000000000000000000000000000000000000000000002710', [length]: 3 ] }, { returns: Result { '0': '17', '1': '80', '2': '24234', '3': '20000', data1: '17', index1: '80', data2: '24234', index2: '20000' }, topics: [ '0xc8ab967a2839f077f35f0ee827fdf57900f535485088fecd64337ad2b64e75e6', '0x0000000000000000000000000000000000000000000000000000000000000050', '0x0000000000000000000000000000000000000000000000000000000000004e20', [length]: 3 ] }, [length]: 2 ]
We can specify any argument that has been indexed, in order to quickly scan the blockchain and retrieve just the events we need. For example, this filter will return two events, but not the same as before:
filter: {index1: [80, 20], index2: 10000}
All Events: [ { returns: Result { '0': '10', '1': '20', '2': '24234', '3': '10000', data1: '10', index1: '20', data2: '24234', index2: '10000' }, topics: [ '0xc8ab967a2839f077f35f0ee827fdf57900f535485088fecd64337ad2b64e75e6', '0x0000000000000000000000000000000000000000000000000000000000000014', '0x0000000000000000000000000000000000000000000000000000000000002710', [length]: 3 ] }, { returns: Result { '0': '34', '1': '80', '2': '7', '3': '10000', data1: '34', index1: '80', data2: '7', index2: '10000' }, topics: [ '0xc8ab967a2839f077f35f0ee827fdf57900f535485088fecd64337ad2b64e75e6', '0x0000000000000000000000000000000000000000000000000000000000000050', '0x0000000000000000000000000000000000000000000000000000000000002710', [length]: 3 ] }, [length]: 2 ]
Conclusion
The above should give you the grounds to start using events to obtain and filter information from the blockchain. It is also a good point to close this series. I hope that it inspires you to start coding Apps for Ethereum, or indeed take the courageous step to become involved with blockchains and crypto-currencies.
If you have further questions, please let me know, so that I can clarify them. And if you like what I write, let me know as well, so I can better judge what to write about. Meanwhile, as always, thank you for reading, and have fun coding.