一种可升级的可信存证合约设计与实现

存证作为区块链的一个重要应用场景,在各个公链中都有已落地的应用和服务。本文将介绍在以太坊上的一种可升级的存证合约的设计与实现。

一、存证业务模型

存证业务的核心是确权,业务逻辑相对比较简单,一般分为存证方取证方

  • 存证方负责将需要确权的数据进行上链;
  • 取证方在需要时可以在区块链上查询到存证内容和该内容的所有者。

如果存证的内容本身能够自证真实性(如电子合同,已有相关参与方的签名),这种业务模型是可以满足需求的。

但是大多数存证场景的存证内容并不能够自证真实,比如你正在阅读的文章,并不能证明作者就是六天,那么为了保证存证方上链的内容是可信的,这时候就需要引入第三个角色审核方

审核方负责对存证方发起的存证内容进行审核,只有审核通过的内容才能够上链。

如果是在联盟环境中,审核方也有可能是取证方,联盟内的成员对自己审核通过并已上链的内容自然认为是可信的。

在公链的环境中,审核方一般由第三方公信机构担任,存证内容的真实性由公信机构负责审查。

二、需求分析

根据上边的存证业务模型介绍,存证合约需要能够满足以下需求。

  1. 只有存证方能够发起存证内容上链
  2. 链上的存证数据应该包含存证内容和内容的所有者
  3. 可以对已上链的存证进行检索
  4. 审核方需要对待上链的存证投票,投票数满足一定条件后存证才能上链

三、合约设计

1.0版

基于需求分析,我们根据最小可使用原则,设计第一版存证合约框架,如下图所示。

存证合约架构1.0

在存证合约架构1.0版本中,只需要两个合约,一个用于权限控制的Owner合约,一个用于存证业务的Evidence合约。如果说存证合约任何用户都能够调用,进行存证内容上链,权限控制都可以不需要。

2.0版

在第二版中,我们采用了类似MVC结构,将数据和逻辑分离,并且引入控制层。

对存证的所有请求,都通过控制层进行转发,控制层将请求通过代理转发给逻辑层,逻辑层按照业务逻辑处理后通过数据层进行数据上链。架构图如下图所示。

存证合约架构2.0

3.0版

3.0版本与2.0版本在架构上是一致的,核心区别在逻辑层。3.0版本在逻辑层增加了存证审核方的业务逻辑。

由于采用了控制层的代理结构,对于业务逻辑升级时,只需要部署新的业务逻辑,然后将新合约的地址注册到代理合约中,即可完成合约升级,并且对外提供服务的合约地址不变。

存证合约架构3.0

说明:合约架构图中的各个层级只列出了该层级的核心功能点。

四、 存证合约实现

接下来会详细讲解存证合约的实现。完整实现代码可访问:https://github.com/six-days/ethereum-contracts/tree/master/evidence

1、数据层

数据层核心定义了以下数据:

  • EvidenceData存证核心结构,包括存证内容content、所有者owner和存证时间timestamp
  • evidence存证hash与存证结构的mapping变量
  • evidenceAmount用于统计总的存证数
contract EvidenceData {
    struct EvidenceObject {
        bytes content;
        address owner;
        uint timestamp;
    }
    mapping(bytes32 => EvidenceObject) internal evidence;
    uint internal evidenceAmount;
}

存证数据的相关变量都被定义为internal类型,限制为只能合约内部访问。

2、逻辑层

2.1 无审核方审核的逻辑实现

contract EvidenceBaseSaveHandler is Ownable, EvidenceData {
    
    bool internal _initialized;

    function initialize(address owner) public {
        require(!_initialized);
        setOwner(owner);
        _initialized = true;
    }

    function createSaveEvidence(bytes32 _hash, bytes memory _content) public onlyOwner {
        require(keccak256(_content) == _hash, "Invalid hash!");
        require(evidence[_hash].owner == address(0), "Evidence exist!");
        evidence[_hash] = EvidenceObject({
            content: _content,
            owner: msg.sender,
            timestamp: now
        });
        evidenceAmount++;
    }

    function getEvidence(bytes32 _hash) public view returns(bytes memory content, uint256 timestamp) {
        return (evidence[_hash].content, evidence[_hash].timestamp);
    }

    function checkEvidenceExist(bytes32 _hash) public view returns(bool isExist) {
        isExist = false;
        if (evidence[_hash].owner != address(0)) {
            isExist = true;
        }
        return isExist;
    }

    function getEvidenceAmount() public view returns(uint256 amount) {
        return evidenceAmount;
    }
}

整体上比较简单,但有几个需要说明的地方:

  • 之所以需要有initialize方法来为权限合约的owner赋值,是因为代理合约在代理逻辑合约之后,逻辑合约自身通过构造函数初始化的值是无法获取到的,因此需要有一个方法能够为初始参数赋值。
  • createSaveEvidence创建存证合约时,参数_hash_content的hash。如果存证的内容本身就是一个文件的hash值,那么参数_hash相当于是hash的hash。

2.2 有审核方审核的逻辑实现

contract EvidenceVoteSaveHandler is EvidenceBaseSaveHandler, Caller {

    using SafeMath
    for uint256;
    struct VoteEvidenceObject {
        address owner;
        bytes content;
        uint8 voted; // 赞成票个数
        mapping(address => bool) voters; // 审核方投票记录
    }
    mapping(bytes32 => VoteEvidenceObject) private voteEvidence; // 存证方发起的存证
    
    uint8 public threshold; // 投票阈值,超过该阈值则说明存证内容可上链
    function setThreshold(uint8 _threshold) public isCaller {
        threshold = _threshold;
    }

    // 存证方发起存证,会先存储到待上链的voteEvidence中
    function createSaveEvidence(bytes32 _hash, bytes memory _content) public isCaller {
        require(keccak256(_content) == _hash, "Invalid hash!");
        require(voteEvidence[_hash].owner == address(0), "Vote evidence exist!");
        require(checkEvidenceExist(_hash) == false, "Evidence exist!");
        voteEvidence[_hash] = VoteEvidenceObject({
            content: _content,
            owner: msg.sender,
            voted: 0
        });
    }
    
    // 对待上链的存证进行投票
    function voteEvidenceToChain(bytes32 _hash) public isCaller {
        require(voteEvidence[_hash].owner != address(0), "Evidence not exist!");
        require(voteEvidence[_hash].voters[msg.sender] == false, "Already voted!");
        voteEvidence[_hash].voted++;
        voteEvidence[_hash].voters[msg.sender] = true;
    }

    // 对超过投票阈值的存证发起上链
    function saveEvidenceToChain(bytes32 _hash) public {
        require(voteEvidence[_hash].owner != address(0), "Evidence not exist!");
        require(checkEvidenceExist(_hash) == false, "Evidence exist!");
        require(uint256(voteEvidence[_hash].voted).mul(100).div(callerAmount) >= threshold, "Insufficient votes!");
        evidence[_hash] = EvidenceObject({
            content: voteEvidence[_hash].content,
            owner: msg.sender,
            timestamp: now
        });
        evidenceAmount++;
    }
}

合约说明:

  • 合约EvidenceVoteSaveHandler继承自无审核方的基础存证合约EvidenceBaseSaveHandler和存储审核方信息的Caller合约。
  • 存证方发起存证后会先存储到待上链的voteEvidence中,等审核方投票,投票数超过阈值后才可存储到存证数据中。
    -saveEvidenceToChain方法并没有加权限校验,因为只有大于等于阈值的存证才能最终上链。

3、控制层

控制层使用的是OpenZeppelin提供的“非结构化存储实现可升级”的代理框架。代理模式的详细内容可阅读我之前写的另一篇文章《以太坊智实现智能合约升级的三种代理模式》

代理合约的核心代码如下所示。

contract Proxy {
  /**
  * @dev Tells the address of the implementation where every call will be delegated.
  * @return address of the implementation to which it will be delegated
  */
  function implementation() public view returns (address);

  /**
  * @dev Fallback function allowing to perform a delegatecall to the given implementation.
  * This function will return whatever the implementation call returns
  */
  function () payable public {
    address _impl = implementation();
    require(_impl != address(0));

    assembly {
      let ptr := mload(0x40)
      calldatacopy(ptr, 0, calldatasize)
      let result := delegatecall(gas, _impl, ptr, calldatasize, 0, 0)
      let size := returndatasize
      returndatacopy(ptr, 0, size)

      switch result
      case 0 { revert(ptr, size) }
      default { return(ptr, size) }
    }
  }
}

五、合约部署

1、合约部署

  1. 先部署控制层的代理合约OwnedUpgradeabilityProxy
  2. 部署无审核方的逻辑层合约EvidenceBaseSaveHandler
  3. 调用EvidenceBaseSaveHandler合约的initialize为初始化参数赋值
  4. 调用OwnedUpgradeabilityProxy合约中的upgradeTo方法,将逻辑合约注册到代理合约中。

此时,通过代理合约,已能够调用EvidenceBaseSaveHandler合约中的相关方法。

2、合约升级

如需将逻辑层合约升级为有审核方的合约,此时需要

  1. 部署EvidenceVoteSaveHandler合约
  2. 调用OwnedUpgradeabilityProxy合约中的upgradeTo方法,将新部署的逻辑合约注册到代理合约中。

此时,已完成了逻辑合约的升级。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353