這篇文章是爲下一篇《NEO從源碼分析看UTXO轉帳交易》打前站,爲交易的構造及執行的一些技術基礎作個探索。因爲這個東西實在有點幹,幹到簡直咽不下,因此我來個自頂向下,從合約代碼開始慢慢深刻。此外,文中不免有些不詳盡或者疏漏偏頗的地方,還望大佬們不吝指教。html
在官方提供的三個合約示例中,這個鎖倉合約是惟一一個不須要Storage的,目前我是感受可能簡單些。若是這把坑了本身,我無怨無悔,畢竟別的合約早晚也要分析,/(ㄒoㄒ)/~~。 鎖倉合約的代碼和解釋均可以在官方文檔中找到,中文版地址在這裏,github地址在這裏git
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:00
return false;
return true;
}
}
複製代碼
我這裏把原來的時間戳改了,還把簽名驗證刪了。建立新合約項目的步驟我就再也不多說,官網上都有。 這個合約只有最新的區塊時間戳大於我既定的時間才能夠轉帳,不然轉帳失敗。理論是這樣的,官網解釋也基本就這麼言簡意賅。我接下來要作的,就是最苦逼的——追蹤這個合約腳本的生成和執行過程。下面涉及的代碼主要是三個項目:github
不得不說NEO開發團隊這塊作的仍是蠻好的,雖然這個編譯的過程灰常複雜,可是操做起來確實很簡單,直接右鍵項目選擇生成就能夠了:數組
從這裏能夠看到不少消息,每一步執行了什麼,生成了什麼,結果是什麼。最最重要的是,這裏有關鍵字啊,以前社區有人問我怎麼看源碼的,就這麼看的,可憐兮兮的找蛛絲馬跡,一個關鍵字一個關鍵字去查引用。 從這個日誌裏能夠看出,編譯的時候是先生成dll動態連接庫,這固然是.net的工做了。而後調用的是Neo.Compiler.MSIL這個東東。我就先找這個東西。緩存
根據上小結的關鍵字,我定位到neo-compiler項目的Program.cs文件,這個文件裏有編譯器的入口函數Main。不要問我怎麼調用的,不care,就這麼傲嬌(實在是沒找到)。Main方法會接收一個參數,就是dll文件的路徑:bash
源碼位置:neo/Compiler/Program.cs/Main(string[] args)app
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代碼:ide
.class public auto ansi beforefieldinit Lock
extends [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: 00
IL_0000: nop
// 0x025D: 28 10 00 00 0A
IL_0001: call uint32 [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Blockchain::GetHeight()
// 0x0262: 28 11 00 00 0A
IL_0006: call class [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Header [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Blockchain::GetHeader(uint32)
// 0x0267: 0A
IL_000b: stloc.0
// 0x0268: 06
IL_000c: ldloc.0
// 0x0269: 6F 12 00 00 0A
IL_000d: callvirt instance uint32 [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Header::get_Timestamp()
// 0x026E: 20 20 2F A1 5A
IL_0012: ldc.i4 1520512800
// 0x0273: FE 05
IL_0017: clt.un
// 0x0275: 0B
IL_0019: stloc.1
// 0x0276: 07
IL_001a: ldloc.1
// 0x0277: 2C 04
IL_001b: brfalse.s IL_0021
// 0x0279: 16
IL_001d: ldc.i4.0
// 0x027A: 0C
IL_001e: stloc.2
// 0x027B: 2B 1B
IL_001f: br.s IL_003c
// 0x027D: 02
IL_0021: ldarg.0
// 0x027E: 1F 21
IL_0022: ldc.i4.s 33
// 0x0280: 8D 16 00 00 01
IL_0024: newarr [mscorlib]System.Byte
// 0x0285: 25
IL_0029: dup
// 0x0286: D0 01 00 00 04
IL_002a: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=33' '<PrivateImplementationDetails>'::'09B200FB2B3E1BDC14112F99F08AA4576CF64321'
// 0x028B: 28 13 00 00 0A
IL_002f: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
// 0x0290: 28 14 00 00 0A
IL_0034: call bool [Neo.SmartContract.Framework]Neo.SmartContract.Framework.SmartContract::VerifySignature(uint8[], uint8[])
// 0x0295: 0C
IL_0039: stloc.2
// 0x0296: 2B 00
IL_003a: br.s IL_003c
// 0x0298: 08
IL_003c: ldloc.2
// 0x0299: 2A
IL_003d: ret
} // 方法 Lock::Main 結束
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
// 方法起始 RVA 地址 0x209a
// 方法起始地址(相對於文件絕對值:0x029a)
// 代碼長度 8 (0x8)
.maxstack 8
// 0x029B: 02
IL_0000: ldarg.0
// 0x029C: 28 15 00 00 0A
IL_0001: call instance void [Neo.SmartContract.Framework]Neo.SmartContract.Framework.SmartContract::.ctor()
// 0x02A1: 00
IL_0006: nop
// 0x02A2: 2A
IL_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合約如是。
由於這個鎖倉合約是個鑑權合約,不須要部署到區塊鏈上,因此我咱們只須要在本地進行部署就能夠了,這個過程用neo-GUI就能夠很方便的完成。爲了測試的直觀,我在本地只保留了一個有3.8gas的賬戶: AV5XmH49Gzz8puT5iMdv5ycmhqWGH5VNq7,下文中咱們把這個帳戶叫徐崢。 新建的合約地址是 Aaigh8uGWwsmPTWKkxfXx8ZRJNYk6RvnBQ,這個帳戶叫王寶強。除此以外,我還另外有一個帳戶ASCjW4xpfr8kyVHY1J2PgvcgFbPYa1qX7F,咱們叫黃渤,用於向徐崢帳戶轉帳,以確認徐崢帳戶收款功能正常。 故事背景以下,王寶強向徐崢借了3.8個GAS當回家路費,約定 3/9/2018 8:10:00 這個時間以後還。 故事發展:
在以上小故事中,因爲鎖倉合約約定取款時間爲8:10以後,在這個時間以前進行資產轉出都會失敗。在小故事中的全部交易都是真實的,能夠在測試網上查到交易信息。接下來咱們分析一下這個交易2是如何執行失敗的。
當咱們從鎖倉合約中轉出資產的交易廣播出去後,在新一輪共識中會被共識節點進行驗證(共識部分請移步個人博客《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。個人合約腳本是:
通過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工具的地址是 sdk.nel.group ,源碼github開放。這個逆向出的asm代碼是否是很像咱們的彙編代碼呢,除了這個指令不是像彙編那樣是三元的。這點在官方的文檔也有介紹,說是由於這個虛擬機上操做數是單獨維護在一個操做數棧上的,對於數據的操做只有簡單的push和pop,因此不必指定地址。我說我能一條條對照avm指令把整個合約執行流程走一遍你確定不信,我也不信,若是有人願意幫我翻譯一遍的話能夠從neo-vm/OpCode.cs這個文件中找到每條指令對應的定義。我我的的話是感受既然不想手擼avm腳本,那麼知道這個東西是這麼個過程就差很少了。
在上一節貼出來的avm代碼中有三個syscall指令,分別帶着一個字節數組,其實經過IL代碼也能看出來這三個字節數組中存放的確定就是系統調用的路徑了。可這個東西是如何來的呢?
能夠看出,系統調用的地址其實就是咱們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多Register
Register("Neo.Iterator.Next", Iterator_Next);
Register("Neo.Iterator.Key", Iterator_Key);
Register("Neo.Iterator.Value", Iterator_Value);
}
複製代碼
至於這些系統調用方法的返回值,則由各個系統調用接收的ExecutionEngine對象獲取。
好啦,以上就是NEO VM的大概流程和原理,因爲這個項目涉及的東西實在普遍,文章不能詳盡之處萬望見諒。
做者:暖冰