NEO从源码分析看NEOVM

作者:暖冰

原文链接:https://mp.weixin.qq.com/s?__biz=MzUzNDQwNDQ0Mw==&mid=2247484024&idx=1&sn=0ffb4a6b3b69b6525b7c4d9ab5fce5a3&chksm=fa940e4ccde3875a5ff772815c744fa34ffe025cf5c9e87d61317acb485deb3d8b37f6a8a6dd&scene=21#wechat_redirect


 0x00 前言

这篇文章是为下一篇《NEO从源码分析看UTXO转账交易》打前站,为交易的构造及执行的一些技术基础做个探索。由于这个东西实在有点干,干到简直咽不下,所以我来个自顶向下,从合约代码开始慢慢深入。此外,文中难免有些不详尽或者疏漏偏颇的地方,还望大佬们不吝指教。

0x01 锁仓合约(Lock

在官方提供的三个合约示例中,这个锁仓合约是唯一一个不需要Storage的,目前我是感觉可能简单些。如果这把坑了自己,我无怨无悔,毕竟别的合约迟早也要分析,/(o)/~~锁仓合约的代码和解释都可以在官方文档中找到,中文版地址在这里github地址在这里

 publicclassLock : SmartContract

{

    publicstaticboolMain(byte[] signature)

    {

        Headerheader = Blockchain.GetHeader(Blockchain.GetHeight());

        if(header.Timestamp < 1520554200// 2018-3-98:10:00

            returnfalse;

        returntrue;

   }

}

我这里把原来的时间戳改了,还把签名验证删了。创建新合约项目的步骤我就不再多说,官网上都有。这个合约只有最新的区块时间戳大于我既定的时间才可以转账,否则转账失败。理论是这样的,官网解释也基本就这么言简意赅。我接下来要做的,就是最苦逼的——追踪这个合约脚本的生成和执行过程。下面涉及的代码主要是三个项目:

·        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.MSILconsole app v" + Assembly.GetEntryAssembly().GetName().Version);

if (args.Length == 0)

{

      log.Log("need oneparam for DLL filename.");

      return;

}

string filename = args[0];

string onlyname =System.IO.Path.GetFileNameWithoutExtension(filename);

string filepdb = onlyname + ".pdb";

说实话我对C#的了解并没有深入到字节码的水平,使用经验也就止于鹅厂实习做游戏的那几个月,这从DLLAVM我只能尽全力而为。转换的主要函数是ModuleConverterConvert,这个方法接收一个ILModule类型的对象作为参数,而这个ILModule对象就是负责解析dll文件获取IL指令的。由于我没找到办法动态分析这个compiler,所以我直接将Lock.dll文件进行了逆向,直接对照IL指令静态分析compiler。逆向工具我用的是ILSPYgithub有售。以下是逆向IL代码:

.classpublic auto ansibeforefieldinit Lock

         extends[Neo.SmartContract.Framework]Neo.SmartContract.Framework.SmartContract

{

         // 方法

         .method public hidebysigstatic

                 boolMain (

                          uint8[]signature

                 )cil managed

         {

                 // 方法起始 RVA 地址 0x2050

                 // 方法起始地址(相对于文件绝对值:0x0250

                 // 代码长度 62 (0x3e)

                 .maxstack4

                 .localsinit (

                          [0class[Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Header,

                          [1] bool,

                          [2] bool

                 )

 

                 // 0x025C: 00

                 IL_0000:nop

                 // 0x025D: 28 1000 00 0A

                 IL_0001:call uint32[Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Blockchain::GetHeight()

                 // 0x0262: 28 1100 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 1200 00 0A

                 IL_000d:callvirt instance uint32 [Neo.SmartContract.Framework]Neo.SmartContract.Framework.Services.Neo.Header::get_Timestamp()

                 // 0x026E: 20 202F 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 1600 00 01

                 IL_0024:newarr [mscorlib]System.Byte

                 // 0x0285: 25

                 IL_0029:dup

                 // 0x0286: D0 0100 00 04

                 IL_002a:ldtoken field valuetype'<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=33''<PrivateImplementationDetails>'::'09B200FB2B3E1BDC14112F99F08AA4576CF64321'

                 // 0x028B: 28 1300 00 0A

                 IL_002f:call void[mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class[mscorlib]System.Arrayvaluetype[mscorlib]System.RuntimeFieldHandle)

                 // 0x0290: 28 1400 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 hidebysigspecialname rtspecialname

                 instancevoid .ctor () cil managed

         {

                 // 方法起始 RVA 地址 0x209a

                 // 方法起始地址(相对于文件绝对值:0x029a

                 // 代码长度 8 (0x8)

                 .maxstack8

 

                 // 0x029B: 02

                 IL_0000:ldarg.0

                 // 0x029C: 28 1500 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-vmC#字节码的解析就是根据这些东西。Compilerdll获取IL指令使用的是mono.cecil,这个工具的代码github也有售。基本上NEO-VM定义了自己的一套完整指令集,可以逐条来做翻译,把IL指令翻译成avm指令,这个翻译的结果就是avm脚本了。翻译的过程首先是把IL指令中的方法提取出来,提取的部分有些对自动生成代码及系统调用的判断,比较繁琐,而且对于我们理解这个转换过程帮助也不大,我就不讲了。对于每个方法的核心处理代码如下:

源码位置:neon/MSIL/Converter.cs/Convert(ILModule _in)

//方法参数获取

foreach (var src inm.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,这个方法里定义有完整的ILavm的映射关系,这里就不一一分析了。这里我就先假装这个转换过程已经讲完了,细节部分可能以后的博客中还会涉猎到,都以后再说。前面分析完了,到创建合约的时候我就凉了,这居然涉及到应用合约和鉴权合约(下下篇博客专题介绍),这个东西我简直一直以来都云里雾里,现在居然直接迎头撞上了,苦也。这里不明白的可以静待我接下来专门介绍合约的博客,我就先直接往下走了。锁仓合约本身是不需要部署在区块链上的,它跟账户合约一样都是鉴权合约。我在上一篇文章《从源码分析看nep2nep6》中详细分析过,NEO的账户本身其实就是一个合约,一个不需要部署在区块链上,在每次交易的时候执行的鉴权合约。Lock合约如是。

0x04 转账

因为这个锁仓合约是个鉴权合约,不需要部署到区块链上,所以我我们只需要在本地进行部署就可以了,这个过程用neo-GUI就可以很方便的完成。为了测试的直观,我在本地只保留了一个有3.8gas的帐户: AV5XmH49Gzz8puT5iMdv5ycmhqWGH5VNq7,下文中我们把这个账户叫徐峥。新建的合约地址是 Aaigh8uGWwsmPTWKkxfXx8ZRJNYk6RvnBQ,这个账户叫王宝强。除此之外,我还另外有一个账户ASCjW4xpfr8kyVHY1J2PgvcgFbPYa1qX7F,我们叫黄渤,用于向徐峥账户转账,以确认徐峥账户收款功能正常。故事背景如下,王宝强向徐峥借了3.8GAS当回家路费,约定 3/9/2018 8:10:00 这个时间之后还。故事发展:

·        第一幕:王宝强回家过年没路费,向徐峥借3.8GAS。于是徐峥借给王宝强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 id0xfa17a8d74a8ebf75f839286de21e011209177930551f7b52a09161250a39df66


·        终幕:在囧途历经坎坷共同患难之后,两人化干戈为玉帛感情更深一步从此不再争吵,从此幸福美满的生活在了一起。

在以上小故事中,由于锁仓合约约定取款时间为8:10之后,在这个时间之前进行资产转出都会失败。在小故事中的所有交易都是真实的,可以在测试网上查到交易信息。接下来我们分析一下这个交易2是如何执行失败的。

0x05 合约执行

当我们从锁仓合约中转出资产的交易广播出去后,在新一轮共识中会被共识节点进行验证(共识部分请移步我的博客《NEO从源码分析看共识协议》),如果验证成功,则会放在缓存中等待写入新的区块中,如果验证失败,这个交易就会被丢弃:

源码位置:neo/Core/Helper/VerifyScripts(this IVerifiableverifiable)

using (StateReader service = newStateReader())

{

       ApplicationEngine engine = newApplicationEngine(TriggerType.Verification, verifiable, Blockchain.Default, service,Fixed8.Zero);

       engine.LoadScript(verification, false);

       engine.LoadScript(verifiable.Scripts[i].InvocationScript, true);

        if (!engine.Execute())returnfalse;

        if(engine.EvaluationStack.Count != 1||!engine.EvaluationStack.Pop().GetBoolean())returnfalse;

}

ApplicationEngineneo-vm中用来执行脚本的类。可以看到这里设置了脚本执行引擎的triggertype为验证,并且传入了交易的脚本进去。这里我们跟进Execute方法。

源码位置:neo/SmartContract/ApplicationEngine/Execute()

while (!State.HasFlag(VMState.HALT) &&!State.HasFlag(VMState.FAULT)) {

    if(CurrentContext.InstructionPointer < CurrentContext.Script.Length) {

        //读取下一条指令

        OpCodenextOpcode = CurrentContext.NextInstruction;

        //按指令收费

       gas_consumed = checked(gas_consumed + GetPrice(nextOpcode) * ratio);

        if (!testMode&& gas_consumed > gas_amount) {

            State|= VMState.FAULT;

            returnfalse;

        }

 

        if(!CheckItemSize(nextOpcode) ||

           !CheckStackSize(nextOpcode) ||

           !CheckArraySize(nextOpcode) ||

           !CheckInvocationStack(nextOpcode) ||

           !CheckBigIntegers(nextOpcode) ||

            !CheckDynamicInvoke(nextOpcode)){

            State|= VMState.FAULT;

            returnfalse;

        }

    }

    //执行

    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代码是不是很像我们的汇编代码呢,除了这个指令不是像汇编那样是三元的。这点在官方的文档也有介绍,说是因为这个虚拟机上操作数是单独维护在一个操作数栈上的,对于数据的操作只有简单的pushpop,所以没必要指定地址。我说我能一条条对照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(OpCodesrc,NeoMethod to)

var bytes = Encoding.UTF8.GetBytes(callname);

if (bytes.Length > 252thrownew Exception("string isto long");

byte[] outbytes = newbyte[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

internalboolInvoke(string method,ExecutionEngine engine)

{

      if(!dictionary.ContainsKey(method)) returnfalse;

            return dictionary[method](engine);

}

可以看到这里是将地址作为key来从map中取对应的方法来执行。这个map里的内容定义在智能合约的StateReader类中,这个类继承了InteropService,并且在构造方法中向dictionary中添加了元素:

源码位置:neo/SmartContract/StateReader

publicStateReader()

{

    Register("Neo.Runtime.GetTrigger",Runtime_GetTrigger);

    Register("Neo.Runtime.CheckWitness",Runtime_CheckWitness);

    //省略NRegister

    Register("Neo.Iterator.Next",Iterator_Next);

    Register("Neo.Iterator.Key",Iterator_Key);

    Register("Neo.Iterator.Value",Iterator_Value);

}

至于这些系统调用方法的返回值,则由各个系统调用接收的ExecutionEngine对象获取。

好啦,以上就是NEO VM的大概流程和原理,由于这个项目涉及的东西实在广泛,文章不能详尽之处万望见谅。

本文由NEL内容奖励计划支持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值