aelf合约代码打包和合约代码检查

circle-info

合约代码打包即生成{合约名}.dll.patched文件的过程;

合约代码检查即检查待部署的合约是否符合黑白名单机制和是否注入OpCode计数器的过程。

相关工程共计三个:

  • AElf.Kernel.CodeCheck:包含合约代码检查和合约代码打包接口定义(IContractAuditor、IContractPatcher等)、Code Check Service实现。aelf节点每次尝试同步区块的时候,也会通过CodeCheckValidationProvider.ValidateBlockAfterExecuteAsync验证这个区块中新部署的合约是否通过了节点本地的代码检查。

  • AElf.CSharp.CodeOps:包含代码检查规则。

  • AElf.ContractDeployer:合约代码打包的可执行文件,有一个Program.Main方法作为执行入口,能够基于合约的dll文件生成dll.patched文件,从而能够部署在aelf网络中。

代码检查规则

代码都在AElf.CSharp.CodeOps工程里。 通过appsettings.json可以配置代码检查的超时时间,如果不配置,默认为60秒。(见Constants.DefaultAuditTimeoutDuration。) 代码检查的调用路径为:

  • 节点常规调用:合约执行过程中抛出CodeCheckRequired事件 -> CodeCheckRequiredLogEventProcessor.ProcessAsync -> CodeCheckService.PerformCodeCheckAsync -> 所有实现IContractAuditor接口并注册过的实例调用Audit方法(看起来是这样,因为是从ContractAuditorContainer里取的,但是实际上只有CSharpContractAuditor这一个实现)

  • 打包合约代码时候的调用:AElf.ContractDeployer.Program.Main -> CSharpContractAuditor.Audit

当前系统中,代码检查逻辑在CSharpContractAuditor.Audit中。代码检查中发现的异常结果会添加进findings中:

  • 使用Mono.Cecil加载代码的ModuleDefinition(变量modDef),调用针对ModuleDefinition的代码检查;

  • 使用System.Reflection加载代码的Assembly(变量asm),调用针对Assembly的代码检查; (其实现在还没有针对Assembly的代码检查器)

  • 检查modDef中包含的Types,从TypeDefinition中递归地获取MethodDefinition,调用针对MethodDefinition的代码检查;

  • 如果系统中强制要求合约必须实现某个acs,会调用一下AcsValidator.Validate检查合约是否实现过acs;

  • 最后,如果findings数量大于0,就意味着验证失败。

综上,针对三个类型有代码检查:

验证类型
验证器
说明

ModuleDefinition

ContractStructureValidator

用于验证合约代码的结构,要保证它能构成一个aelf合约,并且构造函数符合要求。

  1. 有且只有一个类实现了ISmartContract(IsContractImplementation);

  2. 有且只有一个类继承自ContractState;

  3. 实现了ISmartContract的类的泛型是ContractState的子类;

  4. 把Module的所有Types里拿出来,逐个判断:

    1. 如果这个类型的父类是StateBase,那这个类型必须包含在_allowedStateTypes中;

    2. 如果这个类型在合约的实现代码中(不在实现了ISmartContract的类里),转到ValidateContractType(看一下代码注释);

    3. 如果这个类型不在合约的代码实现中,转到ValidateRegularType(这里跳过了ExecutionObserverProxy,因为ExecutionObserverProxy是在打包合约代码的时候注入的,免检)

    4. 对于所有类型的nexted types,递归调用ValidateType。

InstructionInjectionValidator

关联注入器:StateWrittenSizeLimitMethodInjector

在打包合约代码的时候会针对方法注入一些IL指令,这个验证器用于验证ModuleDifinition中所有类型的Methods(ValidateMethod)是否正确完成了注入。判断某一个指令是否应该注入,调用StateWrittenInstructionInjector.IdentifyInstruction;执行IL指令注入,调用StateWrittenInstructionInjector.InjectInstruction。这里调用StateWrittenInstructionInjector.ValidateInstruction进行检验。

ObserverProxyValidator

关联注入器:ExecutionObserverInjector

验证在每一个IL指令之前、branding指令之后是否正确注入了ExecutionObserverProxy的方法调用。

ResetFieldsValidator

关联注入器:ResetFieldsMethodInjector

遍历合约实现的每一个类,看有没有注入ResetFields方法。

WhitelistValidatorSystemContractWhitelistValidator

出于合约执行环境可用性和安全性考虑,需要对合约的实现做一定的限制,比如不可以使用浮点数(不同机器的浮点运算结果可能不一致)、不可以使用随机数生成器(那state必然无法验证)、不可以调用System.Runtime.CompilerServices中的任何一个方法(不然合约会拿到具体执行合约的机器的信息)等等。这些规则都硬编码在WhitelistProvider中,通过对程序集指定Trust.All和Trust.Partial、对系统类型指定Permission.Allowed和Permission.Denied等,完成白名单的定义,构造出Whitelist实例。普通合约和系统合约的白名单做了区分。因此可以说,aelf的合约语言实质上是C#语言的一个子集,学习难度远远小于学习C#语言本身。应该考虑出一个aelf合约语言的教程(以前开过一个文档,不过没时间写:aelf合约中的C#arrow-up-right )。

MethodDefinition

ArrayValidator

针对合约中数组使用的验证,AllowedTypes包含了允许的数组元素类型,随后根据把常量存入评估堆栈的IL指令(OpCodes.Ldc_I4_0到OpCodes.Ldc_I4_8)判断数组大小是否符合要求。

DescriptorAccessValidator

FileDescriptor属于是合约执行的元数据,不允许在合约实现中修改。

FloatOpsValidator

禁用掉所有浮点操作(定义在FloatOpCodes中)

GetHashCodeValidator

验证合约中的GetHashCode方法的实现是否符合要求(见ValidateGetHashCodeMethod方法);验证合约中的非GetHashCode方法是否调用了GetHashCode方法(不可以调)。

MultiDimArrayValidator

合约里不可以使用多维数组。

UncheckedMathValidator

合约里的所有加、减、乘、除操作都应该使用SafeMath中提供的方法,否则这里会校验失败。

代码打包时对IL指令执行注入

执行注入的代码:

  • ILProcessor.InsertBefore(Instruction target, Instruction instruction):在target指令之前注入instruction指令。

  • ILProcessor.InsertAfter(Instruction target, Instruction instruction):在target指令之后注入instruction指令。

注入器
说明

ExecutionObserverInjector

关联验证器:ObserverProxyValidator

在打包合约代码的时候会对IL指令的前面或后面注入一个ExecutionObserverProxy的静态方法调用。所有的IL指令在执行之前都要调用一下ExecutionObserverProxy.CallCount,如果是branding指令,要在执行之后调用一下ExecutionObserverProxy.BranchCount。要注意,ExecutionObserverProxy这个class虽然定义在aelf的代码里,但是在实际注入的时候,利用Mono.Cecil重新构造了一遍这个class(见ExecutionObserverInjector.ConstructCounterProxy方法)。ExecutionObserverProxy的代码只用于验证。

MethodCallReplacer

根据IL指令做一下类型和IL指令替换,比如:

  • string -> AElfString

  • OpCodes.Add -> OpCodes.Add_Ovf(增加了溢出检查)

  • OpCodes.Sub -> OpCodes.Sub_Ovf

  • OpCodes.Mul -> OpCodes.Mul_Ovf

ResetFieldsMethodInjector

关联验证器:ResetFieldsValidator

合约实现中的成员变量都不允许保留其状态,不然会影响交易执行上下文(Executor会连带着状态一起缓存)。这个注入器会寻找所有的成员变量,把它们都重置为默认值(0或者null),然后把设置为默认值的语句放进一个ResetFields()方法里。对于继承自CSharpSmartContract的类,会注入一个非静态的ResetFields方法;对于其他类,会注入一个静态的ResetFields方法。在每一次交易执行完毕后,会遍历所有的类,逐个调用ResetFields方法,完成对成员变量的重置。给两个例子,实现了共识合约的类中,通过ResetFieldsMethodInjector注入的方法为:

共识合约里的Round类,注入的方法为:

StateWrittenSizeLimitMethodInjector

关联验证器:InstructionInjectionValidator

在目前的代码中,实际上是寻找所有针对Contract State字段(SingletonState、MappedState这些)的操作的指令,比如callvirt,在InjectInstruction阶段往前面插入一个评估要操作的状态大小的方法ValidateStateSize,然后在InstructionInjectionValidator中检验ValidateStateSize方法有没有被注入。

Last updated