1. 程式人生 > >以太坊:事件、日誌和布隆過濾器

以太坊:事件、日誌和布隆過濾器

 版權宣告:本文系博主原創,未經博主許可,禁止轉載。保留所有權利。

引用網址:https://www.cnblogs.com/zhizaixingzou/p/10122288.html

 

目錄

 

1. 事件、日誌和布隆過濾器

本文Java原始碼截圖來自開源的以太坊Java版本實現:https://github.com/ethereum/ethereumj

1.1. Solidity中的事件在Java中被記錄為日誌

1.1.1. 包含事件定義和生成的Solidity原始碼

包含事件定義和生成的Solidity原始碼:

pragma solidity ^0.4.12;

contract EventDemo {
    event Sent(uint indexed value, address from, uint amount);

    constructor () public {
        emit Sent(25, msg.sender, 56);
    }
}

事件的引數可用indexed標記,標記後就可以生成布隆過濾器,以便合約的事件查詢。最多可以有3indexed標記的引數,且indexed標記的引數必須在最前頭。

另外,因為主題都是一個個的32位元組儲存的,因而能作為indexed的不能是動態型別。

 

編譯後得到位元組碼:

6080604052348015600f57600080fd5b50604080513381526038602082015281516019927f2e0c9b7721d4bcc1b5781e2248e010b07b94a614f855a3406b43d03aad9ad4d2928290030190a260358060586000396000f3006080604052600080fd00a165627a7a72305820a916c71db2597b4b3d7c2a8311ac8bc79e96edb3187542cd0d464813a12dd2600029

 

對應的彙編程式碼:

PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 0xF JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x40 DUP1 MLOAD CALLER DUP2 MSTORE PUSH1 0x38 PUSH1 0x20 DUP3 ADD MSTORE DUP2 MLOAD PUSH1 0x19 SWAP3 PUSH32 0x2E0C9B7721D4BCC1B5781E2248E010B07B94A614F855A3406B43D03AAD9AD4D2 SWAP3 DUP3 SWAP1 SUB ADD SWAP1 LOG2 PUSH1 0x35 DUP1 PUSH1 0x58 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 REVERT STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 0xa9 AND 0xc7 SAR 0xb2 MSIZE PUSH28 0x4B3D7C2A8311AC8BC79E96EDB3187542CD0D464813A12DD260002900

  

應用二進位制介面的JSON表示,包含了構造方法和事件的定義:

[
	{
		"inputs": [],
		"payable": false,
		"stateMutability": "nonpayable",
		"type": "constructor"
	},
	{
		"anonymous": false,
		"inputs": [
			{
				"indexed": true,
				"name": "value",
				"type": "uint256"
			},
			{
				"indexed": false,
				"name": "from",
				"type": "address"
			},
			{
				"indexed": false,
				"name": "amount",
				"type": "uint256"
			}
		],
		"name": "Sent",
		"type": "event"
	}
]

1.1.2. 執行合約得到日誌

在以太坊上執行上面的合約位元組碼:

 

日誌資訊通過LogInfo類記錄:

LogInfo{address=00a615668486da40f31fd050854fb137b317e056, topics=[2e0c9b7721d4bcc1b5781e2248e010b07b94a614f855a3406b43d03aad9ad4d2 0000000000000000000000000000000000000000000000000000000000000019 ], data=00000000000000000000000005792f204d45f061a5b68847534b428a127ae5830000000000000000000000000000000000000000000000000000000000000038}

address記錄事件生成所在的合約地址。

topics記錄主題列表,第一個是事件簽名的SHA3編碼,編譯時就定了,使用了計算公式keccak256("Sent(unit,address,uint)")。接下來的主題,則分別對應了indexed的引數值,如這裡的value25

data記錄沒有indexed標記的引數值,如這裡的fromamount的值。

 

LOGn,其中n0~4,表示的是主題數量,如上面使用了LOG2,表示有兩個主題,一個是事件簽名的SHA3編碼,一個是indexed修飾的引數。

1.1.3. 日誌資訊的解析

1.1.3.1. 通過ABI解析

可以看到,日誌資訊存放的資料並非直接可讀的,所以需要解析,方法如下:

注意,這裡的JSON按解析的要求使用的是陣列形式,只是我們只傳遞了一個事件的ABI

 

得到的結果如下,即args為事件引數值:

記下來分析下解析過程。

 

1)通過ABIJSON表示得到對應的Function[]例項。

這個Function[]實現是Contract的一個欄位。

 

2)根據日誌資訊的第一個主題得到對應的事件的Function

可以看到,其過程是拿出合約的Function[]中的每一個,計算出簽名的SHA3值與日誌資訊的第一個主題比較,如果相等就篩除了此Function

 

從這裡也可以看出事件簽名SHA3Java上是如何等價實現的,即用到了事件名和引數型別來獲取簽名,沒有用到事件的其他組成。

 

3)根據事件的Function,解析主題列表和日誌資訊的data部分。

對應主題的解析,則直接使用SolidityType解析其型別值,如例子中的value

org.ethereum.solidity.SolidityType.IntType#decode

對於未indexed的引數,則可能包含動態型別,解析過程如下:

可以看到data部分是每個非indexed引數各自ABI編碼後的簡單拼接,與合約呼叫時需要所有引數整體ABI編碼是不同的。

1.1.3.2. 通過事件簽名解析

 1 public static class Event extends Entry {
 2     private static final Pattern EVENT_SIGNATURE = Pattern.compile("^(\\w+?)\\((.+?)\\)$");
 3     private static final String PARAMETER_TYPE_SEPARATOR = ",";
 4     private static final String INDEXED_SEPARATOR = " ";
 5 
 6     /**
 7      * Add parsing event form signature.
 8      *
 9      * @param signature event signature
10      * @return Event instance
11      */
12     public static Event fromSignature(String signature) {
13         Matcher matcher = EVENT_SIGNATURE.matcher(signature);
14         if (!matcher.find()) {
15             throw new IllegalArgumentException("Event signature is illegal");
16         }
17 
18         String params = matcher.group(2).trim();
19         if (StringUtils.isEmpty(params)) {
20             throw new IllegalArgumentException("Event parameter list cannot be empty");
21         }
22         if (params.startsWith(PARAMETER_TYPE_SEPARATOR) || params.endsWith(PARAMETER_TYPE_SEPARATOR)) {
23             throw new IllegalArgumentException(
24                     String.format("Event signature can not begin or end with %s", PARAMETER_TYPE_SEPARATOR));
25         }
26 
27         String eventName = matcher.group(1).trim();
28         List<Param> eventInputs = new ArrayList<>();
29         boolean indexedOver = false;
30         for (String paramType : params.split(PARAMETER_TYPE_SEPARATOR)) {
31             String[] paramPart = paramType.split(INDEXED_SEPARATOR);
32             Param param = new Param();
33             if (paramPart.length == 1) {
34                 param.type = SolidityType.getType(paramPart[0]);
35                 param.indexed = false;
36                 indexedOver = true;
37             } else if (paramPart.length == 2 && !indexedOver) {
38                 param.type = SolidityType.getType(paramPart[0]);
39                 param.indexed = true;
40             } else {
41                 throw new IllegalArgumentException(
42                         String.format("Event parameter \"%s\" is illegal", paramType));
43             }
44             eventInputs.add(param);
45         }
46 
47         return new Event(false, eventName, eventInputs, null);
48     }

1.2. 布隆過濾器的應用

1.2.1. 布隆過濾器的簡介

在儲存一個數據時,我們將來可能要查詢它。為此,我們可以將此資料進行N個雜湊函式計算,得到N個值。假設這些數均勻分佈在M以內,那麼可以設定一個長度為M的位向量,根據得到的N個值,將位向量上對應的N個位置的為置為1。這就得到了一個布隆過濾器。對所有的資料都這樣,然後合併到這個布隆過濾器上。

 

要判斷一個數據是否儲存過,則也計算出這N個值,然後看布隆過濾器位向量相應位置是否都為1,如果不是,則一定沒有儲存過,否則可能儲存過(之所以是可能,因為不同的資料可能覆蓋位向量的同一位)。如果全為1,則再進行資料的具體比對。

 

可以看到,這可以大大加速資料的查詢,它可以快速排出未儲存的資料。

1.2.2. 為一個事件生成布隆過濾器

為一個事件,或者說一條日誌,生成布隆過濾器的過程如下:

org.ethereum.vm.LogInfo#getBloom

 

也就是說,將參與布隆過濾器生成的合約地址和各個主題,分別進行SHA3編碼,得到一個個32位元組的雜湊值。然後將根據這些雜湊值生成的布隆過濾器合併就得到了事件的布隆過濾器。

 

根據雜湊值生成布隆過濾器的過程是:

將布隆過濾器內部儲存的位向量設定為2048位,即256位元組。

取第0位元組的低3位和第1位元組組成intb,它的最大值為2047,布隆過濾器位向量的第b位設定為1

同理,取第23位元組,取45位元組,填充布隆過濾器位向量的指定位。

 

布隆過濾器的合併操作如下:

即將布隆過濾器的位向量同樣的位做或運算。

 

從上面的過程可知,一個事件因為最多4個主題,一個主題最多會設定3個位,所以一個事件在布隆過濾器中最多佔據位向量2048位中的12位。

1.2.3. 為一個交易生成布隆過濾器

預設使用256位元組的位向量。

交易的布隆過濾器是所有的事件布隆過濾器的合併。

1.2.4. 為一個區塊生成布隆過濾器

區塊的布隆過濾器是所有的交易隆過濾器的合併,最後區塊的布隆過濾器存放在區塊頭。

1.2.5. 事件查詢

使用布隆過濾器可以大大加速事件的查詢。我們知道,以太坊會先建立各個主題的布隆過濾器,然後合併得到事件的布隆過濾器,再合併得到交易的布隆過濾器,最後合併得到區塊的布隆過濾器。而查詢滿足指定特徵的事件的過程則正好相反,即先根據查詢條件得到布隆過濾器,如果其位向量是區塊布隆過濾器的子向量,則認為可能在此區塊有,如果不是則就能判斷不是了,如果是有可能,則繼續對比區塊下各個交易的布隆過濾器,以此類推。最後如果匹配事件的布隆過濾器,則在進行嚴格的資料驗證,驗證相同則通過了。

 

這裡只展示了從交易的布隆過濾器開始查詢。

 

如果事件的某個欄位能夠作為查詢條件,那麼它應該定義為indexed

 

下面看看以太坊的一個實現:布隆匹配的最終目標是日誌資訊,終極匹配為事件布隆過濾器的匹配。

1)布隆過濾器匹配。

org.ethereum.core.Bloom#matches

引數為傳進來的日誌特徵,如果它是本布隆過濾器的子布隆過濾器,則返回true

2)提供給外部的查詢引數。

org.ethereum.listener.LogFilter

外界如何表達自己的查詢要求?

I、要查詢合約A內包含有主題abc的事件,其布隆過濾器匹配過程要求的傳入是:

contractAddresses.length=1

contractAddresses[0]=A

topics.size=3

topic.get(0)=byte[][],其中byte[][]可以有多個值,表示任何一個值滿足,那麼該主題都是值滿足的,其他主題同理。

Aabc要同時存在且值滿足,則對應的日誌資訊是匹配的,就可以進入精確比對了,比對過則返回給查詢者。

II、如果要查詢的是多個合約含這些主題的事件,則contractAddresses.length相應變化且傳入值即可。

總之,對於是否匹配合約、指定主題則必須全滿足,而具體到某一個,則值中的任意一個匹配上就滿足了。