NEO从源码分析看NEOVM
2019獨角獸企業重金招聘Python工程師標準>>>
0x00 前言
這篇文章是為下一篇《NEO從源碼分析看UTXO轉賬交易》打前站,為交易的構造及執行的一些技術基礎做個探索。由于這個東西實在有點干,干到簡直咽不下,所以我來個自頂向下,從合約代碼開始慢慢深入。此外,文中難免有些不詳盡或者疏漏偏頗的地方,還望大佬們不吝指教。
0x01 鎖倉合約(Lock)
在官方提供的三個合約示例中,這個鎖倉合約是唯一一個不需要Storage的,目前我是感覺可能簡單些。如果這把坑了自己,我無怨無悔,畢竟別的合約遲早也要分析,/(ㄒoㄒ)/~~。 鎖倉合約的代碼和解釋都可以在官方文檔中找到,中文版地址在這里,github地址在這里
public class Lock : SmartContract {public static bool Main(byte[] signature){Header header = Blockchain.GetHeader(Blockchain.GetHeight());if (header.Timestamp < 1520554200) // 2018-3-9 8:10:00return false;return true;} }我這里把原來的時間戳改了,還把簽名驗證刪了。創建新合約項目的步驟我就不再多說,官網上都有。 這個合約只有最新的區塊時間戳大于我既定的時間才可以轉賬,否則轉賬失敗。理論是這樣的,官網解釋也基本就這么言簡意賅。我接下來要做的,就是最苦逼的——追蹤這個合約腳本的生成和執行過程。下面涉及的代碼主要是三個項目:
- neo-vm : https://github.com/neo-project/neo-vm
- neo-compiler: https://github.com/neo-project/neo-compiler
- neo-gui-nel: https://github.com/NewEconoLab/neo-gui-nel
0x02 編譯
不得不說NEO開發團隊這塊做的還是蠻好的,雖然這個編譯的過程灰常復雜,但是操作起來確實很簡單,直接右鍵項目選擇生成就可以了:
從這里可以看到很多消息,每一步執行了什么,生成了什么,結果是什么。最最重要的是,這里有關鍵字啊,之前社區有人問我怎么看源碼的,就這么看的,可憐兮兮的找蛛絲馬跡,一個關鍵字一個關鍵字去查引用。 從這個日志里可以看出,編譯的時候是先生成dll動態鏈接庫,這當然是.net的工作了。然后調用的是Neo.Compiler.MSIL這個東東。我就先找這個東西。
0x03 解析
根據上小結的關鍵字,我定位到neo-compiler項目的Program.cs文件,這個文件里有編譯器的入口函數Main。不要問我怎么調用的,不care,就這么傲嬌(實在是沒找到)。Main方法會接收一個參數,就是dll文件的路徑:
源碼位置:neo/Compiler/Program.cs/Main(string[] args)
log.Log("Neo.Compiler.MSIL console app v" + Assembly.GetEntryAssembly().GetName().Version); if (args.Length == 0) {log.Log("need one param for DLL filename.");return; } string filename = args[0]; string onlyname = System.IO.Path.GetFileNameWithoutExtension(filename); string filepdb = onlyname + ".pdb";說實話我對C#的了解并沒有深入到字節碼的水平,使用經驗也就止于鵝廠實習做游戲的那幾個月,這從DLL轉AVM我只能盡全力而為。 轉換的主要函數是ModuleConverter的Convert,這個方法接收一個ILModule類型的對象作為參數,而這個ILModule對象就是負責解析dll文件獲取IL指令的。由于我沒找到辦法動態分析這個compiler,所以我直接將Lock.dll文件進行了逆向,直接對照IL指令靜態分析compiler。逆向工具我用的是ILSPY,github有售。以下是逆向IL代碼:
.class public auto ansi beforefieldinit Lockextends [Neo.SmartContract.Framework]Neo.SmartContract.Framework.SmartContract {// 方法.method public hidebysig static bool Main (uint8[] signature) cil managed {// 方法起始 RVA 地址 0x2050// 方法起始地址(相對于文件絕對值:0x0250)// 代碼長度 62 (0x3e).maxstack 4.locals init ([0] class [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Header,[1] bool,[2] bool)// 0x025C: 00IL_0000: nop// 0x025D: 28 10 00 00 0AIL_0001: call uint32 [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Blockchain::GetHeight()// 0x0262: 28 11 00 00 0AIL_0006: call class [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Header [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Blockchain::GetHeader(uint32)// 0x0267: 0AIL_000b: stloc.0// 0x0268: 06IL_000c: ldloc.0// 0x0269: 6F 12 00 00 0AIL_000d: callvirt instance uint32 [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Header::get_Timestamp()// 0x026E: 20 20 2F A1 5AIL_0012: ldc.i4 1520512800// 0x0273: FE 05IL_0017: clt.un// 0x0275: 0BIL_0019: stloc.1// 0x0276: 07IL_001a: ldloc.1// 0x0277: 2C 04IL_001b: brfalse.s IL_0021// 0x0279: 16IL_001d: ldc.i4.0// 0x027A: 0CIL_001e: stloc.2// 0x027B: 2B 1BIL_001f: br.s IL_003c// 0x027D: 02IL_0021: ldarg.0// 0x027E: 1F 21IL_0022: ldc.i4.s 33// 0x0280: 8D 16 00 00 01IL_0024: newarr [mscorlib]System.Byte// 0x0285: 25IL_0029: dup// 0x0286: D0 01 00 00 04IL_002a: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=33' '<PrivateImplementationDetails>'::'09B200FB2B3E1BDC14112F99F08AA4576CF64321'// 0x028B: 28 13 00 00 0AIL_002f: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)// 0x0290: 28 14 00 00 0AIL_0034: call bool [Neo.SmartContract.Framework]Neo.SmartContract.Framework.SmartContract::VerifySignature(uint8[], uint8[])// 0x0295: 0CIL_0039: stloc.2// 0x0296: 2B 00IL_003a: br.s IL_003c// 0x0298: 08IL_003c: ldloc.2// 0x0299: 2AIL_003d: ret} // 方法 Lock::Main 結束.method public hidebysig specialname rtspecialname instance void .ctor () cil managed {// 方法起始 RVA 地址 0x209a// 方法起始地址(相對于文件絕對值:0x029a)// 代碼長度 8 (0x8).maxstack 8// 0x029B: 02IL_0000: ldarg.0// 0x029C: 28 15 00 00 0AIL_0001: call instance void [Neo.SmartContract.Framework]Neo.SmartContract.Framework.SmartContract::.ctor()// 0x02A1: 00IL_0006: nop// 0x02A2: 2AIL_0007: ret} // 方法 Lock::.ctor 結束} // 類 Lock 結束從上面ILSpy逆向出的IL代碼就可以很清晰的看出來函數名、參數、類型、系統調用等等關鍵信息,neo-vm對C#字節碼的解析就是根據這些東西。Compiler從dll獲取IL指令使用的是mono.cecil,這個工具的代碼github也有售。基本上NEO-VM定義了自己的一套完整指令集,可以逐條來做翻譯,把IL指令翻譯成avm指令,這個翻譯的結果就是avm腳本了。翻譯的過程首先是把IL指令中的方法提取出來,提取的部分有些對自動生成代碼及系統調用的判斷,比較繁瑣,而且對于我們理解這個轉換過程幫助也不大,我就不講了。對于每個方法的核心處理代碼如下:
源碼位置:neon/MSIL/Converter.cs/Convert(ILModule _in)
//方法參數獲取 foreach (var src in m.Value.paramtypes) {nm.paramtypes.Add(new NeoParam(src.name, src.type)); } //是否為neo系統調用 byte[] outcall; string name; if (IsAppCall(m.Value.method, out outcall))continue; if (IsNonCall(m.Value.method))continue; if (IsOpCall(m.Value.method, out name))continue; if (IsSysCall(m.Value.method, out name))continue; //方法代碼轉換為opcode this.ConvertMethod(m.Value, nm);在每個方法解析完之后會調用ConvertMethod方法來把方法內部的IL指令轉換為對應的avm指令,指令轉換的方法是ConvertCode,這個方法里定義有完整的IL到avm的映射關系,這里就不一一分析了。 這里我就先假裝這個轉換過程已經講完了,細節部分可能以后的博客中還會涉獵到,都以后再說。 前面分析完了,到創建合約的時候我就涼了,這居然涉及到應用合約和鑒權合約(下下篇博客專題介紹),這個東西我簡直一直以來都云里霧里,現在居然直接迎頭撞上了,苦也。這里不明白的可以靜待我接下來專門介紹合約的博客,我就先直接往下走了。鎖倉合約本身是不需要部署在區塊鏈上的,它跟賬戶合約一樣都是鑒權合約。我在上一篇文章《從源碼分析看nep2和nep6》中詳細分析過,NEO的賬戶本身其實就是一個合約,一個不需要部署在區塊鏈上,在每次交易的時候執行的鑒權合約。Lock合約如是。
0x04 轉賬
因為這個鎖倉合約是個鑒權合約,不需要部署到區塊鏈上,所以我我們只需要在本地進行部署就可以了,這個過程用neo-GUI就可以很方便的完成。為了測試的直觀,我在本地只保留了一個有3.8gas的帳戶: AV5XmH49Gzz8puT5iMdv5ycmhqWGH5VNq7,下文中我們把這個賬戶叫徐崢。 新建的合約地址是 Aaigh8uGWwsmPTWKkxfXx8ZRJNYk6RvnBQ,這個賬戶叫王寶強。除此之外,我還另外有一個賬戶ASCjW4xpfr8kyVHY1J2PgvcgFbPYa1qX7F,我們叫黃渤,用于向徐崢賬戶轉賬,以確認徐崢賬戶收款功能正常。 故事背景如下,王寶強向徐崢借了3.8個GAS當回家路費,約定 3/9/2018 8:10:00 這個時間之后還。 故事發展:
- 第一幕:王寶強回家過年沒路費,向徐崢借3.8個GAS。于是徐崢借給王寶強3.8GAS,并且約定歸還時間為8:10之后。沒辦法,只有回家了之后才有錢還。交易1 id為: 0x7f5be9b212c81958428a416f5afad3ca26d3d032e85330b6837f9fea559e1785
- 第二幕:徐崢路上和王寶強鬧翻,徐崢強行向王寶強索要3.8GAS。可憐的寶強無可奈何了么?徐崢索取3.8GAS是交易2 id為: 0xfa17a8d74a8ebf75f839286de21e011209177930551f7b52a09161250a39df66
- 第三幕:可憐的寶寶是執著的,是我的就是我的,不是我的我也不要,說好了8:10以后還就8:10以后還。兔子急了還咬人呢,寶寶堅決不退讓,孩子是我的,GAS也是我的。徐崢百般索要無果,交易2宣告失敗。
- 第四幕:最終在8:10之后的8:23,徐崢才成功拿走了借給寶寶的3.8GAS。取回GAS交易3 id: 0xfa17a8d74a8ebf75f839286de21e011209177930551f7b52a09161250a39df66
- 終幕:在囧途歷經坎坷共同患難之后,兩人化干戈為玉帛感情更深一步從此不再爭吵,從此幸福美滿的生活在了一起。
在以上小故事中,由于鎖倉合約約定取款時間為8:10之后,在這個時間之前進行資產轉出都會失敗。在小故事中的所有交易都是真實的,可以在測試網上查到交易信息。接下來我們分析一下這個交易2是如何執行失敗的。
0x05 合約執行
當我們從鎖倉合約中轉出資產的交易廣播出去后,在新一輪共識中會被共識節點進行驗證(共識部分請移步我的博客《NEO從源碼分析看共識協議》),如果驗證成功,則會放在緩存中等待寫入新的區塊中,如果驗證失敗,這個交易就會被丟棄:
源碼位置:neo/Core/Helper/VerifyScripts(this IVerifiable verifiable)
using (StateReader service = new StateReader()) {ApplicationEngine engine = new ApplicationEngine(TriggerType.Verification, verifiable, Blockchain.Default, service, Fixed8.Zero);engine.LoadScript(verification, false);engine.LoadScript(verifiable.Scripts[i].InvocationScript, true);if (!engine.Execute()) return false;if (engine.EvaluationStack.Count != 1 || !engine.EvaluationStack.Pop().GetBoolean()) return false; }ApplicationEngine是neo-vm中用來執行腳本的類。可以看到這里設置了腳本執行引擎的triggertype為驗證,并且傳入了交易的腳本進去。這里我們跟進Execute方法。
源碼位置:neo/SmartContract/ApplicationEngine/Execute()
while (!State.HasFlag(VMState.HALT) && !State.HasFlag(VMState.FAULT)) {if (CurrentContext.InstructionPointer < CurrentContext.Script.Length) {//讀取下一條指令OpCode nextOpcode = CurrentContext.NextInstruction;//按指令收費gas_consumed = checked(gas_consumed + GetPrice(nextOpcode) * ratio);if (!testMode && gas_consumed > gas_amount) {State |= VMState.FAULT;return false;}if (!CheckItemSize(nextOpcode) ||!CheckStackSize(nextOpcode) ||!CheckArraySize(nextOpcode) ||!CheckInvocationStack(nextOpcode) ||!CheckBigIntegers(nextOpcode) ||!CheckDynamicInvoke(nextOpcode)) {State |= VMState.FAULT;return false;}}//執行StepInto(); }不難看出這個engine執行avm腳本的方式和cpu差不多,都是每次取一條指令執行。由于跟著StepInto一條一條執行還不如直接看AVM指令代碼,所以這里我們就跳出源碼,來分析AVM。我的合約腳本是:
54c56b6c766b00527ac4616168184e656f2e426c6f636b636861696e2e4765744865696768746168184e656f2e426c6f636b636861696e2e4765744865616465726c766b51527ac46c766b51c36168174e656f2e4865616465722e47657454696d657374616d7004d8d0a15a9f6c766b52527ac46c766b52c3640e00006c766b53527ac4620e00516c766b53527ac46203006c766b53c3616c7566
經過NEL輕錢包工具轉ASM代碼如下:
0:PUSH4 1:NEWARRAY 2:TOALTSTACK 3:FROMALTSTACK 4:DUP 5:TOALTSTACK 6:PUSH0(false) 7:PUSH2 8:ROLL 9:SETITEM a:NOP b:NOP c:SYSCALL[781011114666108111991079910497105110467110111672101105103104116] 26:NOP 27:SYSCALL[78101111466610811199107991049710511046711011167210197100101114] 41:FROMALTSTACK 42:DUP 43:TOALTSTACK 44:PUSH1(true) 45:PUSH2 46:ROLL 47:SETITEM 48:FROMALTSTACK 49:DUP 4a:TOALTSTACK 4b:PUSH1(true) 4c:PICKITEM 4d:NOP 4e:SYSCALL[7810111146721019710010111446711011168410510910111511697109112] 67:PUSHBYTES4[0xd8d0a15a] 6c:LT 6d:FROMALTSTACK 6e:DUP 6f:TOALTSTACK 70:PUSH2 71:PUSH2 72:ROLL 73:SETITEM 74:FROMALTSTACK 75:DUP 76:TOALTSTACK 77:PUSH2 78:PICKITEM 79:JMPIFNOT[14] 7c:PUSH0(false) 7d:FROMALTSTACK 7e:DUP 7f:TOALTSTACK 80:PUSH3 81:PUSH2 82:ROLL 83:SETITEM 84:JMP[14] 87:PUSH1(true) 88:FROMALTSTACK 89:DUP 8a:TOALTSTACK 8b:PUSH3 8c:PUSH2 8d:ROLL 8e:SETITEM 8f:JMP[3] 92:FROMALTSTACK 93:DUP 94:TOALTSTACK 95:PUSH3 96:PICKITEM 97:NOP 98:FROMALTSTACK 99:DROP 9a:RET這個avm2asm工具的地址是 http://sdk.nel.group ,源碼github開放。這個逆向出的asm代碼是不是很像我們的匯編代碼呢,除了這個指令不是像匯編那樣是三元的。這點在官方的文檔也有介紹,說是因為這個虛擬機上操作數是單獨維護在一個操作數棧上的,對于數據的操作只有簡單的push和pop,所以沒必要指定地址。我說我能一條條對照avm指令把整個合約執行流程走一遍你肯定不信,我也不信,如果有人愿意幫我翻譯一遍的話可以從neo-vm/OpCode.cs這個文件中找到每條指令對應的定義。我個人的話是感覺既然不想手擼avm腳本,那么知道這個東西是這么個過程就差不多了。
0x06 系統調用
在上一節貼出來的avm代碼中有三個syscall指令,分別帶著一個字節數組,其實通過IL代碼也能看出來這三個字節數組中存放的肯定就是系統調用的路徑了。可這個東西是如何來的呢?
- 第一個syscall的地址是:781011114666108111991079910497105110467110111672101105103104116,對應16進制的:4e656f2e426c6f636b636861696e2e476574486569676874,這個轉換為字符串就是Neo.Blockchain.GetHeight。
- 第二個syscall的地址是:78101111466610811199107991049710511046711011167210197100101114,對應16進制的:4e656f2e426c6f636b636861696e2e476574486561646572,翻譯出來就是Neo.Blockchain.GetHeader。
- 第三個syscall地址是:7810111146721019710010111446711011168410510910111511697109112,對應的16進制是:4e656f2e4865616465722e47657454696d657374616d70,翻譯出來是Neo.Header.GetTimestamp。
可以看出,系統調用的地址其實就是我們C#中調用的方法的路徑。這塊的構造代碼如下:
源碼位置:neo/Compiler/MSIL/ModuleConverter/_ConverterCall(OpCode src,NeoMethod to)
var bytes = Encoding.UTF8.GetBytes(callname); if (bytes.Length > 252) throw new Exception("string is to long"); byte[] outbytes = new byte[bytes.Length + 1]; outbytes[0] = (byte)bytes.Length; Array.Copy(bytes, 0, outbytes, 1, bytes.Length); //bytes.Prepend 函數在 dotnet framework 4.6 編譯不過 _Convert1by1(VM.OpCode.SYSCALL, null, to, outbytes);從代碼中可以看出來,這個syscall指令的地址長度最大只能有252字節。 調用這個syscall指令的代碼在nep-vm 的ExecuteEngine類里:
源碼位置:neo/vm/ExecuteEngine/ExecuteOp
case OpCode.SYSCALL:if (!service.Invoke(Encoding.ASCII.GetString(context.OpReader.ReadVarBytes(252)), this))State |= VMState.FAULT;break;這里是調用了Invoke方法,并將系統調用的路徑傳過去,我們跟進去這個Invoke方法:
源碼位置:neo/vm/InteropService
internal bool Invoke(string method, ExecutionEngine engine) {if (!dictionary.ContainsKey(method)) return false;return dictionary[method](engine); }可以看到這里是將地址作為key來從map中取對應的方法來執行。這個map里的內容定義在智能合約的StateReader類中,這個類繼承了InteropService,并且在構造方法中向dictionary中添加了元素:
源碼位置:neo/SmartContract/StateReader
public StateReader() {Register("Neo.Runtime.GetTrigger", Runtime_GetTrigger);Register("Neo.Runtime.CheckWitness", Runtime_CheckWitness);//省略N多RegisterRegister("Neo.Iterator.Next", Iterator_Next);Register("Neo.Iterator.Key", Iterator_Key);Register("Neo.Iterator.Value", Iterator_Value); }至于這些系統調用方法的返回值,則由各個系統調用接收的ExecutionEngine對象獲取。
好啦,以上就是NEO VM的大概流程和原理,由于這個項目涉及的東西實在廣泛,文章不能詳盡之處萬望見諒。
轉載于:https://my.oschina.net/u/2276921/blog/1632561
總結
以上是生活随笔為你收集整理的NEO从源码分析看NEOVM的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LinQ中Skip()方法和Take()
- 下一篇: 如何利用阿里云安全产品加强你的网站防护能