Optimism Rollup是目前最流行的以太坊L2解決方案。本文將解釋Optimism Rollup每個設計決策背后的動機, 剖析Optimism的系統實現,并提供指向每個分析組件的相應代碼的鏈接,適用于希望了解Optimism解決方案的 工作原理并評估所提議系統的性能和安全性的開發人員。
區塊鏈開發教程鏈接: 以太坊 | 比特幣 | EOS | Tendermint | Hyperledger Fabric | Omni/USDT | Ripple | Tron
以太坊已經圍繞其開發者生態系統發展了護城河。開發人員的技術棧包括:
由于Optimism Rollup將以太坊作為其第1層,因此如果我們可以無需修改即可重用現有工具,那就太好了。 這將改善開發人員的體驗,因為開發人員無需學習新技術。雖然已經多次提出,但是我想強調軟件重用 的另一個含義:安全性。
Optimism Rollup依賴于使用欺詐證明來防止發生無效的狀態轉換。這需要在以太坊上執行Optimsim交易。簡而言之, 如果交易結果存在爭議,例如修改了Alice的ETH余額,Alice將嘗試在以太坊上重放該確切的交易,以證明那里的 結果是正確的。但是,如果某些EVM操作碼依賴于系統范圍內的參數,這些參數可能隨時都會改變,例如加載或存儲狀態或 獲取當前時間戳,則它們在L1和L2上的行為將不同。
因此,Optimsim的第一個技術,就是處理L1上的L2爭端的機制,該機制保證可以重現在L1上執行L2事務時存在的 任何“上下文”,并且在理想情況下不引入太多開銷。
目標是實現一個沙盒環境,可確保在L1和L2之間確定性地執行智能合約。
Optimism的解決方案是Optimistic虛擬機。OVM是通過將上下文相關的EVM操作碼替換為其對應的OVM操作碼來實現的。
一個簡單的例子是:
所有與上下文相關的EVM操作碼在OVM核心合約在ExecutionManager中都有一個對應的ovm{OPCODE}。合約的執行是從EM的 入口點run函數開始的。這些操作碼也已修改為可以與可插拔狀態數據庫交互,其作用我們將在“欺詐證明”部分中進行介紹。
某些在OVM中“無意義”的操作碼會通過Optimism的SafetyChecker合約禁用,Optimism合約采用靜態分析技術,可以有效地 判斷合約是否OVM安全并返回1或0。
請查閱附錄部分以了解每個被修改/禁用的EVM操作碼。
Optimism Rollup看起來像這樣:
上圖中問號標注的組件將在下面的欺詐證明部分說明,但在此之前,我們需要進一步解釋一些基礎知識。
現在我們有了OVM沙箱,接下來要做的就是將智能合約編譯為OVM字節碼。下面是一些可選的方案:
Optimism當前使用的方法是第三種,Optimsim更改了socl大約500行代碼。
Solidity編譯器的工作原理是將Solidity轉換為Yul,然后轉換為EVM指令,最后轉換為字節碼。Optimism所做的更改 既簡單又優雅:對于每個操作碼,在編譯為EVM匯編后,如有必要,嘗試以ovm變體“重寫”它(如果被禁止則拋出錯誤)。
解釋起來有點復雜,下面讓我們比較一個簡單合約的EVM和OVM字節碼:
用solc編譯一下:
1
2
$ solc C.sol --bin-runtime --optimize --optimize-runs 200
6080604052348015600f57600080fd5b506004361060285760003560e01c8063c298557814602d575b600080fd5b60336035565b005b60008054600101905556fea264697066735822122001fa42ea2b3ac80487c9556a210c5bbbbc1b849ea597dd6c99fafbc988e2a9a164736f6c634300060c0033
我們可以反匯編此代碼看一下得到的匯編代碼,括號內表示Program Counter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
[025] 35 CALLDATALOAD
...
[030] 63 PUSH4 0xc2985578 // id("foo()")
[035] 14 EQ
[036] 60 PUSH1 0x2d // int: 45
[038] 57 JUMPI // jump to PC 45
...
[045] 60 PUSH1 0x33
[047] 60 PUSH1 0x35 // int: 53
[049] 56 JUMP // jump to PC 53
...
[053] 60 PUSH1 0x00
[055] 80 DUP1
[056] 54 SLOAD // load the 0th storage slot
[057] 60 PUSH1 0x01
[059] 01 ADD // add 1 to it
[060] 90 SWAP1
[061] 55 SSTORE // store it back
[062] 56 JUMP
...
上述匯編代碼的意思是,如果calldata匹配函數foo()的選擇器,則使用SLOAD操作碼載入0x00處的存儲變量, 加上0x01,最后將結果使用SSTORE操作碼存回去。聽起來不錯!
在OVM中看起來如何?首先用修改后的solc編譯:
1
2
$ osolc C.sol --bin-runtime --optimize --optimize-runs 200
60806040523480156100195760008061001661006e565b50505b50600436106100345760003560e01c8063c298557814610042575b60008061003f61006e565b50505b61004a61004c565b005b6001600080828261005b6100d9565b019250508190610069610134565b505050565b632a2a7adb598160e01b8152600481016020815285602082015260005b868110156100a657808601518282016040015260200161008b565b506020828760640184336000905af158601d01573d60011458600c01573d6000803e3d621234565260ea61109c52505050565b6303daa959598160e01b8152836004820152602081602483336000905af158601d01573d60011458600c01573d6000803e3d621234565260ea61109c528051935060005b60408110156100695760008282015260200161011d565b6322bd64c0598160e01b8152836004820152846024820152600081604483336000905af158601d01573d60011458600c01573d6000803e3d621234565260ea61109c5260008152602061011d56
得到的字節碼更長了,讓我們再次反匯編一下,看看有什么變化:
1
2
3
4
5
6
7
8
9
10
11
...
[036] 35 CALLDATALOAD
...
[041] 63 PUSH4 0xc2985578 // id("foo()")
[046] 14 EQ
[047] 61 PUSH2 0x0042
[050] 57 JUMPI // jump to PC 66
...
[066] 61 PUSH2 0x004a
[069] 61 PUSH2 0x004c // int: 76
[072] 56 JUMP // jump to PC 76
這一部分還是檢查是否匹配指定的函數選擇器,讓我們看看之后會發生什么。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
...
[076] 60 PUSH1 0x01 // Push 1 to the stack (to be used for the addition later)
[078] 60 PUSH1 0x00
[080] 80 DUP1
[081] 82 DUP3
[082] 82 DUP3
[083] 61 PUSH2 0x005b
[086] 61 PUSH2 0x00d9 (int: 217)
[089] 56 JUMP // jump to PC 217
...
[217] 63 PUSH4 0x03daa959 // <---| id("ovmSLOAD(bytes32)")
[222] 59 MSIZE // |
[223] 81 DUP2 // |
[224] 60 PUSH1 0xe0 // |
[226] 1b SHL // |
[227] 81 DUP2 // |
[228] 52 MSTORE // |
[229] 83 DUP4 // |
[230] 60 PUSH1 0x04 // | CALL to the CALLER's ovmSLOAD
[232] 82 DUP3 // |
[233] 01 ADD // |
[234] 52 MSTORE // |
[235] 60 PUSH1 0x20 // |
[237] 81 DUP2 // |
[238] 60 PUSH1 0x24 // |
[240] 83 DUP4 // |
[241] 33 CALLER // |
[242] 60 PUSH1 0x00 // |
[244] 90 SWAP1 // |
[245] 5a GAS // |
[246] f1 CALL // <---|
[247] 58 PC // <---|
[248] 60 PUSH1 0x1d // |
[250] 01 ADD // |
[251] 57 JUMPI // |
[252] 3d RETURNDATASIZE // |
[253] 60 PUSH1 0x01 // |
[255] 14 EQ // |
[256] 58 PC // |
[257] 60 PUSH1 0x0c // |
[259] 01 ADD // |
[260] 57 JUMPI // | Handle the returned data
[261] 3d RETURNDATASIZE // |
[262] 60 PUSH1 0x00 // |
[264] 80 DUP1 // |
[265] 3e RETURNDATACOPY // |
[266] 3d RETURNDATASIZE // |
[267] 62 PUSH3 0x123456 // |
[271] 52 MSTORE // |
[272] 60 PUSH1 0xea // |
[274] 61 PUSH2 0x109c // |
[277] 52 MSTORE // <---|
上面代碼包含很多操作,要點在于這里不是使用SLOAD操作碼,而是構造一個棧以便執行CALL操作碼。 調用的接收者通過CALLER操作碼被壓入棧。每一個調用都是來自EM,因此實際上CALLER是調用EM的有效方法。 調用的數據以ovmSLOAD(bytes32)函數的選擇器開頭,接下來是參數(在這個示例中,就是占用32字節的字)。 之后,將處理返回的數據并將其添加到內存中。
讓我們繼續:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
...
[297] 82 DUP3
[298] 01 ADD // Adds the 3rd item on the stack to the ovmSLOAD value
[299] 52 MSTORE
[308] 63 PUSH4 0x22bd64c0 // <---| id("ovmSSTORE(bytes32,bytes32)")
[313] 59 MSIZE // |
[314] 81 DUP2 // |
[315] 60 PUSH1 0xe0 // |
[317] 1b SHL // |
[318] 81 DUP2 // |
[319] 52 MSTORE // |
[320] 83 DUP4 // |
[321] 60 PUSH1 0x04 // |
[323] 82 DUP3 // |
[324] 01 ADD // | CALL to the CALLER's ovmSSTORE
[325] 52 MSTORE // | (RETURNDATA handling is omited
[326] 84 DUP5 // | because it is identical to ovmSSLOAD)
[327] 60 PUSH1 0x24 // |
[329] 82 DUP3 // |
[330] 01 ADD // |
[331] 52 MSTORE // |
[332] 60 PUSH1 0x00 // |
[334] 81 DUP2 // |
[335] 60 PUSH1 0x44 // |
[337] 83 DUP4 // |
[338] 33 CALLER // |
[339] 60 PUSH1 0x00 // |
[341] 90 SWAP1 // |
[342] 5a GAS // |
[343] f1 CALL // <---|
...
類似于將SLOAD調整到外部調用ovmSLOAD,SSTORE也調整到外部調用ovmSSTORE。調用的數據不同,因為ovmSSTORE 需要兩個參數,即存儲插槽和要存儲的值。下面是兩者的比較:
實際上,我們先調用Execution Manager的ovmSLOAD方法,然后再調用其ovmSTORE方法,而不是SLOAD和SSTORE。
通過比較EVM與OVM的執行(我們僅顯示執行的SLOAD一部分),我們可以看到通過Execution Manager進行的虛擬化:
這種虛擬化技術有一個“陷阱”:
會導致更快達到合約大小上限 :通常,以太坊合約的字節碼最大24KB 。使用Optimistic Solidity Compiler編譯的 合約最終比原來大,這意味著必須重構接近24KB限制的合約,以便其OVM大小仍適合24KB限制,因為它們需要在以太坊 主網上執行。
以太坊最流行的實現是go-ethereum(即geth)。讓我們看看通常如何在Geth中執行交易。
在每個塊上,調用狀態處理器的Process方法,該方法對每個交易執行ApplyTransaction方法。在內部,交易被轉換為 消息,消息被應用于當前狀態,最后將新產生的狀態存儲回數據庫中。
此核心數據流在Optimistic Geth上保持不變,但進行了一些修改以保持交易“對OVM友好”:
修改1:通過Sequencer入口點的OVM消息
交易被轉換為OVM消息。由于除去了消息的簽名,因此消息數據被修改為包括交易簽名以及原始交易的其余字段。to字段 將替換為“Sequencer入口點”合約的地址。這樣做是為了使交易格式緊湊,因為它將被發布到以太坊,并且我們已經確定, 好緊湊伸縮性就越好。
修改2:通過執行管理器的OVM沙箱
為了通過OVM沙箱運行交易,必須將它們發送到Execution Manager的run 功能。不要求用戶僅提交符合該限制的交易, 所有消息都被修改為在內部發送到Execution Manager。這里很簡單:消息的to字段被替換為執行管理器的地址,并且消息 的原始數據被打包為參數傳入run。
這可能有點不直觀,因此我們提供了代碼以給出一個具體示例:https : //github.com/gakonst/optimism-tx-format。
修改3:攔截對狀態管理器的調用
StateManager是一個特殊的合約,在Optimistic Geth 上并不存在。僅在欺詐證明期間部署它。細心的讀者會注意到, 當打包參數以進行run調用時,Optimism的geth還將打包一個硬編碼的State Manager地址。這就是最終被用作任何 ovmSSTORE或ovmSLOAD(或類似)調用的最終目的地的原因。在L2上運行時,以State Manager合約為目標的所有消息 都將被攔截,并且它們被連接為直接與Geth的StateDB對話(或不執行任何操作)。
對于尋求整體代碼更改的人們來說,最好的方法是搜索UsingOVM并比較geth 1.9.10的差異。
修改4:基于epoch的批次而不是塊
OVM沒有塊,它僅維護交易的有序列表。因此,沒有區塊gas限制的概念;取而代之的是,根據時間段(稱為epoch)限制 總的gas消耗率。在執行交易之前,要檢查是否需要啟動一個新的epoch,在執行之后,將其gas小號添加到該epoch所使用 的累積gas用量上。對于Equenecer提交的交易和“ L1至L2”交易,每個epoch都有單獨的gas限制。任何超過gas限值的交易 將提前返回。這意味著操作員可以在一個鏈上批次中發布多個具有不同時間戳的交易(時間戳由Sequencer定義,但有一些 限制,我們將在“數據可用性批處理”部分中說明)。
修改5:Rollup同步服務
該同步服務是一個新的進程運行,它與“正常” GETH同時運行。Rollup同步服務負責監視以太坊日志,對其進行處理, 并通過geth的worker注入要在L2狀態下應用的相應L2交易。
Optimistic Rollup的主要特性包括:
數據可用性批次
如前所述,交易數據被壓縮,然后發送到L2上的Sequencer Entrypoint合約。然后,Sequencer負責“匯總”這些交易, 并在以太坊上發布數據,提供數據可用性,以便即使Sequencer消失了,也可以啟動新的Sequencer以從中斷的地方繼續。
依靠以太坊實現該邏輯的智能合約稱為權威交易鏈(CTC:Canonical Transaction Chain)。權威交易鏈是一個追加型 日志,它代表Rollup鏈的“正式歷史”(所有交易以及其順序)。交易可以由Sequencer等提交給CTC。為了保留L1的抗審查 能力,任何人都可以將交易提交到此隊列,并在一定滯后期之后將其包括在CTC中。
CTC為每批發布的L2交易提供數據可用性。可以通過兩種方式創建批處理:
這里的一個極端情況是:如果Sequencer廣播了一個批次,則用戶可以強制包含涉及與該批次沖突的狀態的交易,從而 可能使該批次的某些交易無效。為了避免這種情況,我們引入了時間延遲,在此延遲之后可以由非Sequencer帳戶將批處理 追加到隊列中。對此進行考慮的另一種方法是,給利用appendeSequencerBatcher添加的交易一個“寬限期”,否則用戶 使用appendQueueBatch。
鑒于大多數交易預計將通過Sequencer提交,因此有必要深入研究批處理結構和執行流程。
你可能會注意到,appendSequencerBatch沒有任何參數。批次以緊密打包的格式提交,而使用ABI編碼和解碼則效率要 低得多。它使用內聯匯編來對calldata進行切片,并以預期的格式將其解壓縮。
一個批次由以下部分組成:
批次頭指定了上下文的數量,因此序列化的批處理看起來像是 [header, context1, context2, …, tx1, tx2, … ]
該函數繼續執行以下兩項操作:
如果通過了上下文驗證,則該批次將轉換為OVM鏈批次頭,然后將其存儲在CTC中。
存儲的批次頭包含該批次的merkle根,這意味著證明已包含交易是提供針對針對CTC中存儲的merkle根進行驗證的 merkle證明的簡單問題。
這里的自然問題是:這似乎太復雜了!為什么需要上下文?
上下文對于Sequencer來說是必要的,以便知道是否應在已排序交易之前或之后執行已排隊的交易。讓我們來看一個例子:
在時間T1,Sequencer已接收到2個交易,它們將包括在其批次中。在T2(> T1)用戶也排隊的交易時,將它添加到L1到 L2交易隊列(但不將其添加到批次!)。在T2,Sequencer又接收到1個交易,另外2個交易也入隊列。換句話說,待處理 交易的批處理看起來像:
1
[(sequencer, T1), (sequencer, T1), (queue, T2), (sequencer, T2), (queue, T3), (queue, T4)]
為了保持時間戳和塊號信息,同時又保持序列化格式的緊湊性,我們使用了“上下文”,即Sequencer和排隊交易之間的 共享信息集合。上下文必須嚴格增加塊數和時間戳。在上下文中,所有Sequencer交易共享相同的塊號和時間戳。 對于“隊列交易”,將時間戳和塊號設置為調用隊列時的值。在這種情況下,該批交易的上下文為:
1
[{ numSequencedTransactions: 2, numSubsequentQueueTransactions: 1, timestamp: T1}, {numSequencedTransactions: 1, numSubsequentQueueTransactions: 2, timestamp: T2}]
狀態承諾
在以太坊中,每個交易都會導致對狀態以及全局狀態根的修改。通過在某個區塊提供狀態根并通過默克爾證明來 證明某個帳戶在某個區塊擁有一些ETH,以證明該賬戶的狀態與所聲明的值匹配。因為每個塊包含多個交易,并且我們 只能訪問狀態根,所以這意味著我們只能在執行整個塊后才聲明狀態。
一段歷史:
在EIP98和Byzantium分叉之前,以太坊交易在每次執行后產生中間狀態根,這些根通過交易收據提供給用戶 刪除中間狀態根能夠提高性能,雖然有一點小缺陷,因此很快就采用了它。EIP PR658中提供的其他動機解決了該問題: 收據的PostState字段(指示與tx執行后的狀態相對應的狀態根)被布爾狀態字段(指示交易的成功狀態)替換。
事實證明,警告并非無關緊要。EIP98寫道:
所做的更改確實意味著,如果礦工創建了一個區塊,其中一個狀態轉換的處理不正確,那么就不可能針對該交易 提供欺詐證明;相反,欺詐證明必須包含整個區塊。
此更改的含義是,如果一個區塊有1000個交易,并且你在第988個交易中檢測到欺詐,則在實際執行你感興趣的交易 之前,需要在前一個區塊的狀態之上運行987個交易,這會使欺詐證明效率極低。以太坊本身沒有欺詐證明,所以沒關系!
另一方面,Optimism的欺詐證據是至關重要的。在前面,我們提到Optimism沒有區塊,那只是個小謊言:Optimism有區塊, 但是每個區塊只有1個交易,我們稱之為“微區塊”。由于每個微塊包含1個交易,因此每個塊的狀態根實際上是單個交易 產生的狀態根。烏拉!我們已經重新引入了中間狀態根,而不必對協議進行任何重大更改。當然,由于微塊在技術上 仍然是塊并且包含冗余的其他信息,因此當前當然具有恒定的性能開銷,但是這種冗余可以在將來刪除(例如,使所有 微塊都具有0x0作為塊哈希,并且僅填充RPC中的修剪字段以便向后兼容)。
現在,我們可以介紹狀態承諾鏈(SCC:State Commitment Chain)。SCC包含狀態根列表,在樂觀情況下,該列表對應于 針對先前狀態在CTC中應用每個交易的結果。如果不是這種情況,則欺詐驗證過程將刪除無效的狀態根,然后刪除所有 無效的狀態根,以便可以為這些交易提出正確的狀態根。
與CTC相反,SCC沒有任何酷炫的數據表示形式。它的目的很簡單:給定狀態根列表,它會對其進行存儲并保存批處理中 包含的中間狀態根的merkle根,以供以后通過appendStateBatch用作欺詐證明。
欺詐證明
既然我們了解了OVM的基本概念以及將其狀態錨定在以太坊上的支持功能,那么讓我們深入探討爭端解決程序, 也就是欺詐證明。
Sequencer執行3件事:
例如,如果在CTC中發布了8個交易,則對于每個狀態從S1到S8的轉換,在SCC中都會有8個狀態根。
但是,如果Sequencer是惡意的,他們可以在狀態Trie中將其帳戶余額設置為1000萬個ETH,這顯然是非法的操作, 從而使狀態根及其后面的所有狀態根均無效。他們可以通過發布看起來像這樣的數據來做到這一點:
我們注定要失敗嗎?我們必須做點什么!
眾所周知,Optimistic Rollup假定存在驗證者:對于Sequencer發布的每個交易,驗證者負責下載該交易并將其 應用于本地狀態。如果一切都匹配,它們什么也不做,但是如果不匹配,那就有問題了!為了解決該問題,他們 將嘗試在以太坊上重新執行T4以產生S4。然后,將修剪所有在S4之后發布的狀態根,因為無法保證它對應于有效狀態:
從較高層面來說,欺詐證明是“以S3作為我的開始狀態,我想證明在S3上應用T4會導致S4,這與Sequencer發布的內容 不同()。結果,我希望刪除S4及其之后的所有內容。”
如何實施?
在圖1中看到的是OVM在L2中以其“簡單”執行模式運行。在L1上運行時,OVM處于防欺詐模式,并且啟用了它的更多組件( 在L1和L2上都部署了Execution Manager和Safety Checker ):
在防欺詐模式下運行的OVM如下所示:
欺詐證明分為幾個步驟:
步驟1:聲明您要爭議的狀態轉換
State Transitioner現在處于PRE EXECUTION階段。
步驟2:上傳所有交易狀態
如果我們嘗試直接執行有爭議的交易,則該交易將立即失敗,并顯示INVALID_STATE_ACCESS錯誤,因為從步驟1開始, 在剛部署的L1狀態管理器上未加載任何涉及的L2狀態。OVM沙箱將檢測是否SM尚未填充某些觸摸狀態,并強制首先加載 所有觸摸狀態需求。
例如,如果有爭議的交易是簡單的ERC20代筆轉移,則初始步驟為:
步驟3:一旦提供所有預狀態,請運行交易
然后,用戶必須通過調用State Transitioner的applyTransaction來觸發交易的執行。在此步驟中,執行管理器開始使用 欺詐證明的狀態管理器執行交易。執行完成后,狀態轉換程序過渡到該POST EXECUTION階段。
步驟4:提供后期狀態
在L1上執行期間(步驟3),合同存儲位或帳戶狀態(例如,隨機數)中的值將更改,這將導致狀態轉換程序的后狀態根 更改。但是,由于狀態轉換器/狀態管理器對不知道整個L2狀態,因此它們無法自動計算新的后狀態根。
為了避免這種情況,如果存儲插槽或帳戶狀態的值發生更改,則將存儲插槽或帳戶標記為“ changed”,并增加未提交的 存儲插槽或帳戶的計數器。我們要求對于每個更改的項目,用戶還必須提供L2狀態的防彎證明,表明這確實是所觀察到的 值。每次“提交”存儲插槽更改時,都會更新合約帳戶的存儲根目錄。在提交所有更改的存儲插槽后,合約的狀態也將提交, 從而更新過渡器的后狀態root。對于發布的每個后期狀態數據,該計數器相應地遞減。
因此,可以預期,在交易中涉及的所有合約的狀態更改都已提交之后,結果后的狀態根是正確的。
步驟5:完成狀態轉換并最終確定欺詐證明
完成狀態轉換是一個簡單的completeTransition調用過程,它要求步驟4中的所有帳戶和存儲插槽都已提交(通過檢查 未提交狀態的計數器等于0來進行)。
最后,在Fraud Verifier合約上調用finalizeFraudVerification,該合約檢查狀態轉換程序是否完成,如果是, 則調用deleteStateBatch,該方法它繼續從SCC刪除(包括)有爭議的交易之后的所有狀態根批處理。CTC保持不變, 因此原始交易將以相同順序重新執行。
激勵+債券
為了使系統保持開放并無需許可,SCC旨在允許任何人成為Sequencer并發布狀態批。為避免SCC被垃圾數據淹沒, 我們引入了1個限制:
Sequencer必須由債券管理器智能合約標記為抵押品。你需要存入固定金額的抵押品,并且可以在7天后提取該金額。
但是,在抵押后,惡意的提議者可以反復創建欺詐性的狀態根源,希望沒有人對此提出異議,從而使他們有錢。 如果忽略用戶從Rollup和惡意Sequencer社交協調遷移的場景,那么這里的攻擊成本極低。
該解決方案在L2系統設計中是非常標準的:如果成功證明了欺詐,則X%的提議者的保證金會被燒掉13,剩余的(1-X)% 會按比例分配給每個為第2步和第4步提供數據的用戶。現在,Sequencer的背叛成本要高得多,并且假設它們的行為合理, 則有望創造足夠的誘因來防止它們惡意行為。即使有爭議狀態沒有直接影響他們,這也為用戶提供了一個誘人的誘因, 使他們提交數據以證明欺詐行為。
nuisance gas
有一個單獨的gas維度,稱為“有害gas”,用于限制欺詐證明的凈gas成本。特別是,L2 EVM gas成本表中未反映欺詐證明 建立階段的證人數據(例如,默克爾證明)。ovmOPCODES針對nuisance gas需要另外付費,每當觸摸一個新的存儲槽或 帳戶時,都會收取費用。如果消息嘗試使用超出消息上下文允許范圍的nuisance gas,則執行恢復。
原文鏈接:http://blog.hubwiz.com/2021/02/12/optimism-rollup/
1.開發語言選型
2.不同開發語言的資源占用對比
1.包結構說明
2.不同開發語言的資源占用對比
1.安裝go包依賴管理工具govendor
2.使用govendor下載包依賴
3.編譯RocketmqExporter
1.制作鏡像
2.提供yaml范例
2.1.使用者需要注意&修改的label
2.2.使用者需要注意&修改的環境變量
3.容器化命令
1.grafana/prometheus容器化
2.RocketmqExporter容器化
資源占用:K8S下,cpu占用0.01core, 內存占用10MB。
監控指標:消息堆積數,精確到進程粒度。
監控目的:實時掌控消息消費的健康程度。
數據來源:從rocketmq-console的http請求獲取數據。也就是說RocketmqExporter必須依賴rocketmq-console。好吧,我承認我圖省事兒了^_^。
為什么自己要重新實現: 官方exporter是java的,相對費資源;另外我們要求對消息堆積數有完備監控,且精確到進程級別。 從topic, consumerGroup, broker,queueId, consumerClientIP, consumerClientPID等維度對消息堆積數進行聚合,如下圖:
效果圖下載地址:
github.com/hepyu/k8s-app-config/blob/master/product/standard/grafana-prometheus-pro/exporter-mq-rocketmq/images/mesage-unconsumed-count.jpg
1.開發語言選型
golang是最適合的選擇。常用選型不外乎java, python, golang。
2.不同開發語言的資源占用對比
關于鏡像大小與實際資源占用的生產對比。
特別說明:
java很不適合開發exporter的重要原因有一點就是,"啟動時內存和CPU耗費"與"運行時內存和CPU耗費差異太大",這就導致容器資源分配時request和max有不小差值, 這個是很不好的,會留下隱患。
rocketmq實例不多還好,但是想象一下如果redis,mysql的exporter也是用java寫,那這個差值就大了,放大到整個集群將成為潛在風險,我想這也是mysql-exporter, redis-exporter官方為什么用golang寫的原因之一。
但是如果把request和max設置成一樣,又很浪費。
綜上所述,golang是最好的選擇。
1.包結構說明
2.重點文件說明
(4).如何編譯
有點麻煩,我從開發(IDE用vim)到編譯到image制作都是在linux服務器上,所以都是用的golang體系下原生命令進行操作的。
本工程目錄下提供一個已經編譯好的二進制文件:RocketmqExporter,可以直接使用。
1.安裝go包依賴管理工具govendor
go get -u -v github.com/kardianos/govendor
2.使用govendor下載包依賴
配置環境變量(注意source生效):export GOPATH=$HOME/go:$HOME/go-workspace
mkdir $HOME/go-workspace/src
然后將本工程clone到目錄$HOME/go-workspace/src。
進入$HOME/go-workspace/src執行govendor命令列出工程依賴:govendor list
m RocketmqExporter/constant
m RocketmqExporter/model
m RocketmqExporter/service
m RocketmqExporter/utils
m RocketmqExporter/wrapper
m github.com/go-kit/kit/log/level
m github.com/prometheus/client_golang/prometheus
m github.com/prometheus/client_golang/prometheus/promhttp
m github.com/prometheus/common/promlog
m github.com/prometheus/common/promlog/flag
m github.com/prometheus/common/version
m gopkg.in/alecthomas/kingpin.v2
然后執行govendor init,會生成一個vdendor目錄和vendor.json,后邊下載的包依賴都會放到這個目錄下。
vendor.json
{
"comment": "",
"ignore": "test",
"package": [],
"rootPath": "hpy-go-rocketmq-exporter"
}
下載包依賴到vendor目錄,執行命令:govendor fetch +out,時間比較長(本工程下提供一個已經編譯好的二進制文件:hpy-go-rocketmq-exporter,這個可以直接用于鏡像制作)。
執行完成后,vendor目錄下:
github.com
golang.org
gopkg.in
RocketmqExporter
vendor.json
vendor.json內容:
{
"comment": "",
"ignore": "test",
"package": [
{
"path": "RocketmqExporter/constant",
"revision": ""
},
{
"path": "RocketmqExporter/model",
"revision": ""
},
{
"path": "RocketmqExporter/service",
"revision": ""
},
{
"path": "RocketmqExporter/utils",
"revision": ""
},
{
"path": "RocketmqExporter/wrapper",
"revision": ""
},
{
"checksumSHA1": "MXqUZAuWyiMWV7HC0X2krRinZoI=",
"path": "github.com/alecthomas/template",
"revision": "fb15b899a75114aa79cc930e33c46b577cc664b1",
"revisionTime": "2019-07-18T01:26:54Z"
},
{
"checksumSHA1": "3wt0pTXXeS+S93unwhGoLIyGX/Q=",
"path": "github.com/alecthomas/template/parse",
"revision": "fb15b899a75114aa79cc930e33c46b577cc664b1",
"revisionTime": "2019-07-18T01:26:54Z"
},
{
"checksumSHA1": "VT42paM42J+M52CXStvRwsc1v6g=",
"path": "github.com/alecthomas/units",
"revision": "f65c72e2690dc4b403c8bd637baf4611cd4c069b",
"revisionTime": "2019-09-24T02:57:48Z"
},
{
"checksumSHA1": "0rido7hYHQtfq3UJzVT5LClLAWc=",
"path": "github.com/beorn7/perks/quantile",
"revision": "37c8de3658fcb183f997c4e13e8337516ab753e6",
"revisionTime": "2019-07-31T12:00:54Z"
},
{
"path": "github.com/cespare/xxhash/v2",
"revision": ""
},
{
"checksumSHA1": "eVc+4p1fDrG3e49wZuztY6D2txA=",
"path": "github.com/go-kit/kit/log",
"revision": "9f5354e50d79d79d865f684fe139811cf309870f",
"revisionTime": "2019-10-18T12:22:45Z"
},
{
"checksumSHA1": "dyVQWAYHLspsCzhDwwfQjvkOtMk=",
"path": "github.com/go-kit/kit/log/level",
"revision": "9f5354e50d79d79d865f684fe139811cf309870f",
"revisionTime": "2019-10-18T12:22:45Z"
},
{
"checksumSHA1": "g8yM1TRZyIjXtopiqbslzgLqtM0=",
"path": "github.com/go-logfmt/logfmt",
"revision": "07c9b44f60d7ffdfb7d8efe1ad539965737836dc",
"revisionTime": "2018-11-22T01:56:15Z"
},
{
"checksumSHA1": "Q3FteGbNvRRUMJqbYbmrcBd2DMo=",
"path": "github.com/golang/protobuf/proto",
"revision": "ed6926b37a637426117ccab59282c3839528a700",
"revisionTime": "2019-10-22T19:55:53Z"
},
{
"checksumSHA1": "abKzFXAn0KDr5U+JON1ZgJ2lUtU=",
"path": "github.com/kr/logfmt",
"revision": "b84e30acd515aadc4b783ad4ff83aff3299bdfe0",
"revisionTime": "2014-02-26T03:06:59Z"
},
{
"checksumSHA1": "bKMZjd2wPw13VwoE7mBeSv5djFA=",
"path": "github.com/matttproud/golang_protobuf_extensions/pbutil",
"revision": "c182affec369e30f25d3eb8cd8a478dee585ae7d",
"revisionTime": "2018-12-31T17:19:20Z"
},
{
"checksumSHA1": "I7hloldMJZTqUx6hbVDp5nk9fZQ=",
"path": "github.com/pkg/errors",
"revision": "27936f6d90f9c8e1145f11ed52ffffbfdb9e0af7",
"revisionTime": "2019-02-27T00:00:51Z"
},
{
"checksumSHA1": "HquvlxEmpILGOdePiJzqL/zMvUY=",
"path": "github.com/prometheus/client_golang/prometheus",
"revision": "333f01cef0d61f9ef05ada3d94e00e69c8d5cdda",
"revisionTime": "2019-10-24T23:19:15Z"
},
{
"checksumSHA1": "UBqhkyjCz47+S19MVTigxJ2VjVQ=",
"path": "github.com/prometheus/client_golang/prometheus/internal",
"revision": "333f01cef0d61f9ef05ada3d94e00e69c8d5cdda",
"revisionTime": "2019-10-24T23:19:15Z"
},
{
"checksumSHA1": "UcahVbxaRZ35Wh58lM9AWEbUEts=",
"path": "github.com/prometheus/client_golang/prometheus/promhttp",
"revision": "333f01cef0d61f9ef05ada3d94e00e69c8d5cdda",
"revisionTime": "2019-10-24T23:19:15Z"
},
{
"checksumSHA1": "V8xkqgmP66sq2ZW4QO5wi9a4oZE=",
"path": "github.com/prometheus/client_model/go",
"revision": "14fe0d1b01d4d5fc031dd4bec1823bd3ebbe8016",
"revisionTime": "2019-08-12T15:41:04Z"
},
{
"checksumSHA1": "vA545Z9FkjGvIHBTAKQOE0nap/k=",
"path": "github.com/prometheus/common/expfmt",
"revision": "b5fe7d854c42dc7842e48d1ca58f60feae09d77b",
"revisionTime": "2019-10-17T12:25:55Z"
},
{
"checksumSHA1": "1Mhfofk+wGZ94M0+Bd98K8imPD4=",
"path": "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg",
"revision": "b5fe7d854c42dc7842e48d1ca58f60feae09d77b",
"revisionTime": "2019-10-17T12:25:55Z"
},
{
"checksumSHA1": "ccmMs+h9Jo8kE7izqsUkWShD4d0=",
"path": "github.com/prometheus/common/model",
"revision": "b5fe7d854c42dc7842e48d1ca58f60feae09d77b",
"revisionTime": "2019-10-17T12:25:55Z"
},
{
"checksumSHA1": "Pj64Wsr2ji1uTv5l49J89Rff0hY=",
"path": "github.com/prometheus/common/promlog",
"revision": "b5fe7d854c42dc7842e48d1ca58f60feae09d77b",
"revisionTime": "2019-10-17T12:25:55Z"
},
{
"checksumSHA1": "3tSd7cWrq75N2PaoaqAe79Wa+Fw=",
"path": "github.com/prometheus/common/promlog/flag",
"revision": "b5fe7d854c42dc7842e48d1ca58f60feae09d77b",
"revisionTime": "2019-10-17T12:25:55Z"
},
{
"checksumSHA1": "91KYK0SpvkaMJJA2+BcxbVnyRO0=",
"path": "github.com/prometheus/common/version",
"revision": "b5fe7d854c42dc7842e48d1ca58f60feae09d77b",
"revisionTime": "2019-10-17T12:25:55Z"
},
{
"checksumSHA1": "/otbR/D9hWawJC2jDEqxLdYkryk=",
"path": "github.com/prometheus/procfs",
"revision": "34c83637414974b5e7d4bd700b49de3c66631989",
"revisionTime": "2019-10-22T16:02:49Z"
},
{
"checksumSHA1": "ax1TLBC8m/zLs8u//UHHdFf80q4=",
"path": "github.com/prometheus/procfs/internal/fs",
"revision": "34c83637414974b5e7d4bd700b49de3c66631989",
"revisionTime": "2019-10-22T16:02:49Z"
},
{
"checksumSHA1": "sxRjp2SwHqonjR+sHIEXCkfBglI=",
"path": "github.com/prometheus/procfs/internal/util",
"revision": "34c83637414974b5e7d4bd700b49de3c66631989",
"revisionTime": "2019-10-22T16:02:49Z"
},
{
"path": "golang.org/x/sys/windows",
"revision": ""
},
{
"checksumSHA1": "sToCp8GThnMnsBzsHv+L/tBYQrQ=",
"path": "gopkg.in/alecthomas/kingpin.v2",
"revision": "947dcec5ba9c011838740e680966fd7087a71d0d",
"revisionTime": "2017-12-17T18:08:21Z"
}
],
"rootPath": "hpy-go-rocketmq-exporter"
}
此時make會報錯,找不到包github.com/cespare/xxhash/v2,這個是因為prometheus基于依賴于該包,而prometheus是基于gomod構建的,gomod支持能夠識別xxhash后面的v2是指定的版本,常規方法無法下載,可以下載github.com/cespare/xxhash/,然后把該文件夾中的內容都copy到github.com/cespare/xxhash/v2目錄下即可。
在vendor目錄下執行:
git clone github.com/cespare/xxhash.git github.com/cespare/xxhash/v2
3.編譯RocketmqExporter
執行 make 進行編譯,打印信息如下:
>> building binaries
GO111MODULE=on /root/go/bin/promu build --prefix /root/go-workspace/src/RocketmqExporter
> RocketmqExporter
>> running all tests
GO111MODULE=on go test -race -mod=vendor ./...
? RocketmqExporter [no test files]
? RocketmqExporter/constant [no test files]
? RocketmqExporter/model [no test files]
? RocketmqExporter/service [no test files]
? RocketmqExporter/utils [no test files]
? RocketmqExporter/wrapper [no test files]
>> vetting code
GO111MODULE=on go vet -mod=vendor ./...
編譯成功后,在目錄下會生成一個二進制文件RocketmqExporter,可以直接執行:./RocketmqExporter,打印如下信息說明成功(不用關心報錯,因為沒有配置參數到環境變量,找不到rocketmq-console):
level=info ts=2019-11-01T09:19:57.879Z caller=RocketmqExporter.go:27 msg="Starting rocketmq_exporter" version="unsupported value type"
level=info ts=2019-11-01T09:19:57.879Z caller=RocketmqExporter.go:28 msg="Build contenxt" (gogo1.13.3,userroot@future,date111911090-09:17:41)=(MISSING)
level=info ts=2019-11-01T09:19:57.879Z caller=RocketmqExporter.go:34 msg=fmt.metricsPath:
panic: http: invalid pattern
goroutine 1 [running]:
net/http.(*ServeMux).Handle(0xd47080, 0x0, 0x0, 0x9f72c0, 0xc000091ec0)
/usr/local/go/src/net/http/server.go:2397 +0x33a
net/http.Handle(...)
/usr/local/go/src/net/http/server.go:2446
main.main()
/root/go-workspace/src/RocketmqExporter/RocketmqExporter.go:39 +0x720
(5).相關編譯文件說明
Makefile.common中的關鍵代碼:
.PHONY: common-all
#common-all: precheck style check_license lint unused build test
common-all: build test
上述代碼指明了構建過程,可以看到我只開啟了build和test兩個構建命令,其余關閉。
(6).如何進行容器化部署
1.制作鏡像
直接在目錄下執行:rocketmq_exporter.docker-build.sh
鏡像名稱為:hpy253215039/go-rocketmq-exporter,版本為:1.0.0。
鏡像名稱可以自己修改。
2.提供yaml范例
目錄下的范例文件是:go-exporter-deployment-mq-rocketmq-c0.yaml
2.1.使用者需要注意&修改的label
2.2.使用者需要注意&修改的環境變量
3.容器化命令
直接執行: kubectl apply -f go-exporter-deployment-mq-rocketmq-c0.yaml
(7).如何進行實體機部署
將目錄下env.default.config.backup中的內容拷貝到文件~/.bashrc中,然后執行 "source ~/.bashrc"使其生效。
然后運行目錄下的RocketmqExporter二進制文件即可,注意最好使用supervisor進行守護。
golang如果想要獲取自定義變量,必須把自定義變量放到這里定義:~/.bashrc ,放到/etc/profile中通過os.GetEnv是獲取不到的。
注意環境變量含義,要根據自己的實例情況進行修改:
(8).如何結合prometheus與grafana
筆者提供生產級容器化結合方式。
1.grafana/prometheus容器化
參照工程完成grafana/prometheus的容器化:
github.com/hepyu/k8s-app-config/tree/master/product/standard/grafana-prometheus-pro
上述工程包含消息堆積數的grafana的dashboard。
具體實施步驟和相關生產拓撲描述參見文章:
grafana&prometheus生產級容器化監控-1:生產級容器化
主要資源位置:
grafana消息堆積數dashboard位于:
github.com/hepyu/k8s-app-config/tree/master/product/standard/grafana-prometheus-pro/grafana/provisioning/dashboards/mq-rocketmq
prometheus.yml配置的抓取規則位于:
github.com/hepyu/k8s-app-config/blob/master/product/standard/grafana-prometheus-pro/prometheus-mq-rocketmq/prometheus-mq-rocketmq-configmap.yaml
2.RocketmqExporter容器化
具體參見文章:grafana&prometheus生產級容器化監控-2:監控rocketmq
3.使用注意事項
1.rocketmq-dashboard默認只顯示堆積數大于1000的metric項,主要是為了避免顯示太多而凌亂,可以自行修改。
2.當你使用broadcast模式發送消息時,消息堆積數是只增不減,因為consumer不會提交offset,所以你會看到這些topic的堆積數會很高,在做報警的時候要考慮過濾這類topic。
(9).其他相關文章
rocketmq-1:集群主要結構和監控,性能測試與成本控制
rocketmq-2:性能測試方案&壓測&選型&結論
rocketmq-3:rocketmq流控,重試機制與應對
rocketmq-4:線上rocketmq slave節點的ECS宕機恢復實記
rocketmq-5:生產級rocketmq集群部署
阿里云&kubernetes&微服務生產實踐-3:rocketmq生產主從容器化
golang實戰-1:搭建vim-go開發環境