区块链-solidity智能合约架构探索

野性酷女 2022-03-25 07:40 549阅读 0赞

solidity语言教程:http://www.tryblockchain.org/
官方文档翻译:https://solidity-cn.readthedocs.io/zh/develop/

说明:通过本文的工厂模式、合约注册表、合约调用外部合约几种模式,可以实现合约的解耦,合约调用,合约升级,可以开发类似java中的大型dapp

工厂模式

工程模式:一个合约可以创建并管理多个合约。原理是工厂合约中记录并保存子合约的地址,通过工程合约调用子合约中的方法。
在这里插入图片描述
举个例子。

创建一个简单的计数器合约,每个调用者都有自己独有的计数器,记录访问次数。假如有1W个用户,需要创建1W个合约,管理1W个地址,我们这里不需要人工的管理这1W个地址,通过一个工厂来创建并且管理。

  1. contract Counter {
  2. address owner;
  3. address factory;
  4. uint count = 0;
  5. function Counter(address _owner) { // 构造方法
  6. owner = _owner;
  7. factory = msg.sender;
  8. }
  9. modifier isOwner(address _caller) { //判断是否是自己的计数器合约
  10. require(msg.sender == factory);
  11. require(_caller == owner);
  12. _;
  13. }
  14. function increment(address caller) public isOwner(caller) { // 通过继承的方式,实现每次调用都先进行权限判断
  15. count = SafeMath.add(count, 1); //这里使用类型安全的计算
  16. }
  17. function getCount() constant returns (uint) { // 获取自己的count数量
  18. return count;
  19. }
  20. }

Counter 是每个用户独有的计数器合约,一个没用一个。用于统计调用次数。
工厂模式首先要使用mapping来记录用户与合约地址的关系

mapping(address => address) counters;
创建合约使用new 关键字即可:

counters[msg.sender] = new Counter(msg.sender);

将地址转换为合约并且调用合约:

Counter(counters[msg.sender]).increment(msg.sender);

完整代码如下:

  1. pragma solidity ^0.4.10;
  2. /**
  3. * @title SafeMath
  4. * @dev Unsigned math operations with safety checks that revert on error
  5. */
  6. library SafeMath {
  7. /**
  8. * @dev Multiplies two unsigned integers, reverts on overflow.
  9. */
  10. function mul(uint256 a, uint256 b) internal pure returns (uint256) {
  11. // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
  12. // benefit is lost if 'b' is also tested.
  13. // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
  14. if (a == 0) {
  15. return 0;
  16. }
  17. uint256 c = a * b;
  18. require(c / a == b);
  19. return c;
  20. }
  21. /**
  22. * @dev Integer division of two unsigned integers truncating the quotient, reverts on division by zero.
  23. */
  24. function div(uint256 a, uint256 b) internal pure returns (uint256) {
  25. // Solidity only automatically asserts when dividing by 0
  26. require(b > 0);
  27. uint256 c = a / b;
  28. // assert(a == b * c + a % b); // There is no case in which this doesn't hold
  29. return c;
  30. }
  31. /**
  32. * @dev Subtracts two unsigned integers, reverts on overflow (i.e. if subtrahend is greater than minuend).
  33. */
  34. function sub(uint256 a, uint256 b) internal pure returns (uint256) {
  35. require(b <= a);
  36. uint256 c = a - b;
  37. return c;
  38. }
  39. /**
  40. * @dev Adds two unsigned integers, reverts on overflow.
  41. */
  42. function add(uint256 a, uint256 b) internal pure returns (uint256) {
  43. uint256 c = a + b;
  44. require(c >= a);
  45. return c;
  46. }
  47. /**
  48. * @dev Divides two unsigned integers and returns the remainder (unsigned integer modulo),
  49. * reverts when dividing by zero.
  50. */
  51. function mod(uint256 a, uint256 b) internal pure returns (uint256) {
  52. require(b != 0);
  53. return a % b;
  54. }
  55. }
  56. contract Counter {
  57. address owner;
  58. address factory;
  59. uint count = 0;
  60. function Counter(address _owner) { // 构造方法
  61. owner = _owner;
  62. factory = msg.sender;
  63. }
  64. modifier isOwner(address _caller) { //判断是否是自己的计数器合约
  65. require(msg.sender == factory);
  66. require(_caller == owner);
  67. _;
  68. }
  69. function increment(address caller) public isOwner(caller) { // 通过继承的方式,实现每次调用都先进行权限判断
  70. count = SafeMath.add(count, 1); //这里使用类型安全的计算
  71. }
  72. function getCount() constant returns (uint) { // 获取自己的count数量
  73. return count;
  74. }
  75. }
  76. contract CounterFactory {
  77. mapping(address => address) counters; //记录用户地址与合约地址的map <用户地址,合约地址>
  78. function createCounter() public {
  79. if (counters[msg.sender] == 0) {//首先判断是否已经创建了合约,没有创建Counter合约才创建
  80. counters[msg.sender] = new Counter(msg.sender);
  81. }
  82. }
  83. function increment() public {
  84. require (counters[msg.sender] != 0); //校验调用方是否已经创建了Counter合约
  85. Counter(counters[msg.sender]).increment(msg.sender);//将地址转换为合约并且调用合约
  86. }
  87. function getCount(address account) public constant returns (uint) {
  88. if (counters[account] != 0) {
  89. return (Counter(counters[account]).getCount());//返回Counter合约的count次数
  90. }
  91. }
  92. }

部署合约时主要要部署CounterFactory:
在这里插入图片描述
部署完合约后,通过切换不同的测试账号来进行测试即可。

合约注册表

很多时候,我们编写合约时并不是只有一个合约,通常都需要多个合约。如果其中一个合约升级,其他合约可以在没有任何变化没有任何改动的情况下调用最新的合约。这时就需要使用合约注册表了。

合约注册表是合约名称 => 合约地址的映射表。这样就可以获取合约并且调用相关合约内的方法了。

类似金链盟的CNS域名服务

完整合约实例

  1. pragma solidity ^0.4.10;
  2. contract NameRegistry {
  3. //合约信息结构体
  4. struct ContractDetails {
  5. address owner;
  6. address contractAddress;
  7. uint16 version;
  8. }
  9. mapping(string => ContractDetails) registry;//合约名称与合约信息的map: <合约名称, 合约信息ContractDetails>
  10. function registerName(string name, address addr, uint16 ver) constant returns (bool) { //注册
  11. require(ver >= 1);//这里表示合约的version必须>=1
  12. ContractDetails memory info = registry[name];
  13. if (info.contractAddress == address(0)) {//没有则新建
  14. info = ContractDetails({
  15. owner: msg.sender,
  16. contractAddress: addr,
  17. version: ver
  18. });
  19. } else {// 如果已经存在,则更新为最新的合约
  20. require(info.owner == msg.sender);
  21. info.version = ver;
  22. info.contractAddress = addr;
  23. }
  24. registry[name] = info;//记录map
  25. return true;
  26. }
  27. function getContractDetails(string name) constant returns(address, uint16) { //根据名称获取最新的合约,返回合约地址及version
  28. return (registry[name].contractAddress, registry[name].version);
  29. }
  30. }

合约调用另一个合约

这里演示的是合约分开部署,通过合约地址进行调用。

第一个合约:

  1. pragma solidity ^0.4.0;
  2. contract MyContract1 {
  3. function f(uint data) constant returns (uint){
  4. return data + 2;
  5. }
  6. }

将第一个合约部署,获得合约地址0x6bc7ea1c744185d0ded0141d08a4dbc2978b5991

编写第二个合约MyContract2,其中根据上面的MyContract1的合约地址对MyContract1进行初始化。这样就可以调用第一个合约的方法了。

  1. pragma solidity ^0.4.0;
  2. contract MyContract1{
  3. function f(uint data) constant returns (uint);
  4. }
  5. contract MyContract2{
  6. MyContract1 myContract1 = MyContract1(0x6bc7ea1c744185d0ded0141d08a4dbc2978b5991);
  7. function AddData(uint value) constant returns (uint){
  8. return myContract1.f(value);
  9. }
  10. }

部署第二个合约。
编写测试 demoMyContract2.js 代码(只列出核心代码):

  1. name=instance.AddData(31);
  2. console.log("=== " + name.toString());

执行代码:输出结果是31+2=33

与注册表结合实现分布式编程

如此一来,配合上面的合约注册表,就可以实现类似于java中的分布式编程了:

部署MyContract1 ,获取address,将该address注册到合约注册表NameRegistry 中,在合约MyContract2中调用合约MyContract1时,每次都去合约注册表NameRegistry中查询最新的MyContract1的地址,就可以实现合约升级。

合约之间解耦,灵活升级,这种模式可以开发大型DAPP。

完整操作流程:

1、首先部署NameRegistry合约,得到合约地址:0x2ad1ecffff78caa5f5a61398d077ec1856b54567
2、然后修改MyContract2合约并部署:

  1. pragma solidity ^0.4.0;
  2. contract MyContract1{
  3. function f(uint data) constant returns (uint);
  4. }
  5. contract NameRegistry {
  6. function getContractDetails(string name) constant returns(address);
  7. }
  8. contract MyContract2{
  9. //根据地质初始化注册表(这个address是NameRegistry部署的地址)
  10. NameRegistry nameRegistry = NameRegistry(0x2ad1ecffff78caa5f5a61398d077ec1856b54567);
  11. function AddData(uint value) constant returns (uint){
  12. //通过注册表获取最新的MyContract1地址并初始化MyContract1
  13. MyContract1 myContract1 = MyContract1(nameRegistry.getContractDetails("myContract1"));
  14. return myContract1.f(value);
  15. }
  16. }

3、部署MyContract1,得到合约地址0xe7a58975b465b96658f5db80f057f2e25c80f594:

4、使用nodejs将其注册到注册表中:

  1. var func = "registerName(string,address,uint16)";
  2. var params = ["myContract1", "0xe7a58975b465b96658f5db80f057f2e25c80f594", 2];
  3. var receipt = await web3sync.sendRawTransaction(config.account, config.privKey, address, func, params);
  4. name=instance.getContractDetails("myContract1");
  5. console.log("=== " + name.toString());

5、调用MyContract2,可以看到输出33.
6、测试单独修改升级合约MyContract1这里2改为了8,MyContract2不需要改动。

  1. pragma solidity ^0.4.0;
  2. contract MyContract1 {
  3. function f(uint data) constant returns (uint){
  4. return data + 8; //这里由2修改为8
  5. }
  6. }

7、部署MyContract1并将其注册到注册表。
8、调用合约 MyContract2,可以看到结果变成了 31+8 = 39

映射表mapping迭代器

solidity提供了mapping,但是没有提供友好的访问方式。

这里自己通过mapping外记录额外元数据信息的方式实现迭代。

实例(仅是个demo,作为演示用,安全性不够):

  1. pragma solidity ^0.4.10;
  2. contract MappingIterator {
  3. mapping(string => string) elements;//map,用于存储数据
  4. string[] keys;//记录map的key
  5. //在mapping插入数据的同时也记录keys
  6. function put(string key, string addr) returns (bool) {
  7. keys.push(key);
  8. elements[key] = addr;
  9. return true;
  10. }
  11. //通过keys获取总数,keys的数量与mapping一致
  12. function getKeyCount() constant returns (uint) {
  13. return keys.length;
  14. }
  15. function getElementAtIndex(uint index) constant returns (string) {
  16. return elements[keys[index]];//数组是可以通过下标取值的,故keys通过下标就可以获取到mapping的key了。然后通过这个key取mapping的值,就可以实现mapping的迭代了
  17. }
  18. function getElement(string name) constant returns (string) {
  19. return elements[name];
  20. }
  21. }

使用修饰符

通常在调用方法时都会进行校验,包括权限或者参数。如果都写在方法中,则会增加很多代码量,且不易读。

  1. function increment() public {
  2. if (owner == msg.sender) {
  3. count = count + 1;
  4. }
  5. }

使用modifier定义修饰符来提高程序可读性及可维护性:

  1. modifier onlyBy(address _account) {
  2. require(msg.sender == _account);
  3. _;
  4. }
  5. function increment() public onlyBy(owner) {
  6. count = count + 1;
  7. }

也可以将修饰符函数进行抽象,使其支持任何条件判断:

  1. modifier onlyIf(bool _condition) {
  2. require(_condition);
  3. _;
  4. }
  5. function increment() public onlyIf(msg.sender == owner) {
  6. count = count + 1;
  7. }

上面onlyIf接收任意的判断条件,具体的条件在调用方提供。

通过在空格分隔的列表中指定多个修饰符,将多个修饰符应用于函数,并按所显示的顺序进行评估。

  1. pragma solidity ^0.4.10;
  2. contract MappingIterator {
  3. address owner;
  4. uint _count;
  5. function MappingIterator() {//构造函数
  6. owner = msg.sender;
  7. }
  8. modifier onlyIf(bool _condition) {//高度抽象的修饰符
  9. require(_condition);
  10. _;
  11. }
  12. function increment(uint count) public onlyIf(msg.sender == owner) onlyIf(count < 20) {//多个条件
  13. _count = count + 1;
  14. }
  15. function get() constant returns (uint) {
  16. return _count;
  17. }
  18. }

使用修饰符不仅可以提高程序可读性,还可以验证权限,限制数据写入,数据校验等功能。

自毁合约

合约自毁模式用于终止一个合约,这意味着将从区块链上永久删除这个合约。 一旦被销毁,就不可能 调用合约的功能,也不会在账本中记录交易。

在处理一个被销毁的合约时,有一些需要注意的问题:

  • 合约销毁后,发送给该合约的交易将失败
    任何发送给被销毁合约的资金,都将永远丢失
  • 为避免资金损失,应当在发送资金前确保目标合约仍然存在,移除所有对已销毁合约的引用

    pragma solidity ^0.4.10;

    contract SelfDesctructionContract {

    address owner;

    string someValue;

    modifier ownerRestricted {

    1. require(owner == msg.sender);
    2. _;

    }
    // 构造函数
    function SelfDesctructionContract() {

    1. owner = msg.sender;

    }

    function setSomeValue(string value){

    1. someValue = value;

    }
    //销魂当前合约
    function destroyContract() ownerRestricted {

    1. suicide(owner);

    }
    }

部署合约后,可以调用合约的方法,一旦调用了suicide() 进行合约的销毁,那么在此调用合约方法将失败,而且如果其内部有资产,资产将永远消失。除非非常了解其含义,否则慎用。

模块化及合约升级

就像java应用一样,合约的编写也照样需要模块化,不同的功能编写不同的合约。合约之间通过接口相互调用。

升级合约时,也不需要全部合约重新部署,仅仅部署有修改的合约即可。

数据合约与操作合约分离

正是有了模块化与合约升级,所以才需要数据存储合约与数据操作合约进行分离。如果存储合约重新部署,老合约储存的数据将无法使用。

代码编写规范

本编码规范请参考:https://www.jianshu.com/p/d616f387d811

发表评论

表情:
评论列表 (有 0 条评论,549人围观)

还没有评论,来说两句吧...

相关阅读

    相关 区块 智能合约 简介

    根据谷歌趋势数据显示,目前,程序员对智能合约编程的兴趣已经处于历史最高水平,其中中国高居全球榜首,随着区块链技术的发展,相信日后智能合约将会与我们的生活密切相关,今天就为大家介