Neo核心开发者:我为N3开发的第一个智能合约|Neo专栏

上一篇Neo专栏中
作者为我们揭秘了引入随机数的具体困难有哪些
以及Neo在随机数算法领域究竟有何发展目标💡
本篇Neo专栏,
Neo核心开发者廖京辉🙋♂️
带您走近他「为N3开发的第一个智能合约」
在此过程中遇到的困难和解决方案
及对N3区块链开发平台的理解与想法😎
快来一起看看吧👇
前两篇文章里介绍了为N3引入安全随机数的事情,虽然最终版本的随机数算法还在不断的开发迭代中,但是现有的N3的区块内已经引入了一个由议长生成的随机数,可以用来测试相关合约。因此,我开发了一个剪刀石头布的游戏。一是用来学习N3的合约开发,二来是测试一下运行时随机数。(合约源码地👉https://github.com/Liaojinghui/RPC)
这个剪刀石头布的游戏很简单,设计如下:
用户每轮的投注需要大于1个GAS,同时需要小于之前合约从用户那里赢走的钱。也就是说用户不能期待一把回本,否则合约几乎永远都是输家。
在别的平台如果开发这样的合约,在没有可靠的运行时随机数的前提下,用户需要先将手势的摘要信息发送到合约里,然后等待下一个区块取某种线上数据作为随机数,再等待一个区块才能将用户手势的明文发送到合约里。为了完成一个简单的小游戏,这样的操作不可谓不繁琐复杂。而在N3里,只需要一笔交易,所有的过程都可以直接在一个区块内完成。
工具方法
为了方便合约开发,我把常用的权限控制整理成了OwnerOnly()方法,将条件控制逻辑整理成了Require()方法。
OwnerOnly()方法是为了对合约接口的调用权限进行约束。我们把只允许管理员进行调用的接口称为管理员接口,标记为***I_owner***。通常,我们需要对合约调用的交易的签名进行验证,只有来自合约拥有者,也就是在合约里标记的Owner账户的签名才能合法调用I_owner接口。这样的验证逻辑通常是一致的代码逻辑,因此,为了方便合约开发,对其进行代码进行归纳整理为:
private static void OwnerOnly() { if (!Verify()) throw new Exception("No authorization."); }
Require() 方法则是因为各种合约方法内部都需要对条件进行非常繁琐的验证,为了合约安全,我们通常希望合约的验证越完善越好。但是验证的过程一般需要引入if语句,而为了逻辑清晰,我们又不能将所有的条件整理成一种。当大量的if语句堆叠到一起的时候,代码就会变得臃肿。
因此,为了让合约在进行逻辑和条件检查的时候能更加“优雅”,我定义了private static void Require(bool isTrue, string msg = "Invalid")(后文称Require())。Require() 接收两个参数,第一个参数为条件或者条件语句,第二个为可选的错误信息。当第一个参数的结果不是true的时候,Require()就会抛出异常,FAULT这笔交易。同时用户可以通过第二个参数来自定义异常信息。方法实现为:
private static void Require(bool isTrue, string msg = "Invalid") { if (!isTrue) throw new Exception($"RPC::{msg}"); }
此外,为了减少由方法调用而增加的额外开销,我将这两个方法都设置为了内联函数。因此两个方法的最终实现版本为:
[MethodImpl(MethodImplOptions.AggressiveInlining)]private static void Require(bool isTrue, string msg = "Invalid") { if (!isTrue) throw new Exception($"RPC::{msg}"); }
[MethodImpl(MethodImplOptions.AggressiveInlining)]private static void OwnerOnly() { if (!Verify()) throw new Exception("No authorization."); }
细粒度安全要求与严格的安全校验
虽然开发这个小游戏的代码量并不大,逻辑更是非常简单,但是为了给以后的合约开发者积累更多的经验,我还是按照最高标准来去实现了这个合约的每一个功能。对于合约来说,最高的标准就是安全标准——这一点是毫无争议的。我始终认为合约的开发应该在最小的单位——方法里,就对安全要求进行定义并且验证。并且每个单位应该有自己完全独立的安全逻辑,比如参数的验证一定要在方法内部独立完成,不能对上层的calling方法传入的参数进行任何形式的假设,否则肯定会增加的重复验证的开销。但是,对于合约来说,安全大过天,为了保证安全,合约有时不得不“臃肿”。搭乘过飞机的小伙伴肯定都知道坐飞机有非常繁琐的安检流程,尤其是跨国转机,几乎每次中转都要重新安检。这就是为了排除各种风险。而合约的开发就需要这样对安全认真严谨的态度,在每一个最小节点里定义安全行为,引入安全检查。
在该合约里,我是在每个方法的注释里加入Security Requirements 内容来对方法的安全进行文字层面的约束,主要就是定义一下参数的范围,方法权限等等。然后在开发的时候逐一对安全要求进行确认。再在合约部署在测试网之后对每一个安全要求进行测试网上实际交易的检验,当检验通过时再进行最终的确认标记。也就是在功能测试之外,独立进行安全测试。举个例子,合约里的OnNEP17Payment回调方法:

OnNEP17Payment回调方法
方法的注释里有四条安全约束,这四条安全约束都在代码中进行了体现,具体到代码所在的行:
// <2> -- confirmed by jinghuiRequire(!Paused());
// <3> -- yet to confirmRequire(Runtime.CallingScriptHash == GAS.Hash, "Script format error.");//Require(Runtime.EntryScriptHash == GAS.Hash, "Runtime.EntryScriptHash == ((Transaction)Runtime.ScriptContainer).Hash");if (((Transaction)Runtime.ScriptContainer).Script.Length > 96) throw new Exception("RPC::Transaction script length error. No wrapper contract or extra script allowed.");// should not be called from a contract// --confirmedRequire(ContractManagement.GetContract(from) is null, "ContractManagement.GetContract(from) is null");
// <1> -- confirmed by jinghuiRequire(move == 0 || move == 1 || move == 2, "Invalid move.");
// <0> -- confirmed by jinghuiRequire(amount >= 1_0000_0000, "Please at least bet 1 GAS.");
虽然这样的方式不能排除合约存在漏洞的可能,但是却可以强制合约开发者对合约安全进行思考并予以约束,进而为合约提供最低限度的安全保障。
最后,特别感谢NGD的陈志同同学和鸭脖小朋友提供的大量帮助和指点。这个合约部署在N3的测试网上👉https://neo3.testnet.neotube.io/contract/0x9c01a8640dff7c086dca99758d71645f57164d7c。感兴趣的朋友可以去试试。不过调用合约最好不要用Neo-cli,这个里面的send命令有点问题,必须传from字段才能传data,而且data还只能是string类型。建议大家用别的工具来进行调用。
合约代码开源且完全授权,任何同学都可以对其进行任何形式的更改,进行任何方式的使用。希望能够帮助到想要进行N3合约开发的同学。
All in One · All in Neo
Neo是一个由社区驱动的开源平台。利用区块链技术与数字身份,开发者可以通过智能合约实现资产管理数字化与自动化。Neo致力于通过分布式网络建设下一代互联网基础设施,为区块链技术大规模落地奠定基础,以实现智能经济的宏大愿景。
自2016年上线至今,Neo主网已稳定运行超过四年。全新版本Neo N3已于2021年发布,能够提供更高吞吐量、更强稳定性与安全性,带来优化的智能合约系统及功能丰富的基础设施集合,赋能开发者并加速企业级区块链创新。


