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


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

引用網址: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相應變化且傳入值即可。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM