教程 | 普遍化状态通道的代码速览

hongji   |     |   1642 次阅读

1

上一次我们发布了一套普遍化状态通道的架构。这是一种在链外(off-chain)完成已知参与者之间的资产转移的方法。人们可以安全地通过区块链交易来转移以太币或是代币、非同质物(non-fungible energy)比如以太猫、以及诸如此类的东西。我们对代码进行了整理,让它的 Gas 费用不至于太高,我们还加入了可读的测试代码,并已经准备好发布这一框架的实现。

代码放在 machinomy/mc2 库中。这是一个典型的用 Truffle 来管理的合约库,以常见的布局组织: /build/contractsmigrations 及其它。实际上,这是我们的支付通道合约的代码库的一个分支,以后它应该会与 upstream 合并。 /contracts/test 文件夹中有惊喜。

状态通道的构建要从部署多重签名合约开始。 如果所有参与者都同意,合约就由一个交易发送到区块链上。我们的多重签名仅限于两个参与者。如果想获得一个更详细的用例,大家可以使用一个更高级的多重签名合约,Gnosis Safe。

多重签名合约有三个方法:

function doCall(address destination, uint256 value, bytes data, bytes senderSig, bytes receiverSig);
function doDelegate(address destination, bytes data, bytes senderSig, bytes receiverSig);
function isUnanimous(bytes32 _hash, bytes _senderSig, bytes _receiverSig) public view returns(bool);

doCall 会向 destination 地址(译者注:即目标地址)执行一个普通调用(Ordinary Call)。 doDelegate 会执行一个 delegatecall 。后者对从多重签名合约到辅助通道合约(Subchannel Contract)的复杂资产转移来说是很有用的,比如要一次性向所有参与者转移资金。多重签名合约会使用一个独立的合约来处理转移逻辑。举个例子,DistributeToken 可以自动向所有参与者发送 ERC20 代币。

pragma solidity ^0.4.19;

import "zeppelin-solidity/contracts/token/ERC20/StandardToken.sol";


contract DistributeToken {
    function execute(address _token, address a, address b, uint256 amountA, uint256 amountB) public {
        StandardToken token = StandardToken(_token);
        require(token.transfer(a, amountA));
        require(token.transfer(b, amountB));
    }
}

合作的情况

在一个皆大欢喜的案例中,参与者只通过多重前面合约分发资金。达成资产分发的协议之后,一笔交易会到达多重签名合约以分发这些资金。一个合作化的测试用例编写了这个场景:

  1. 创建多重签名合约;
  2. 转移资产到该多重签名合约;
  3. 签署一笔多签名交易以将资产从多重签名合约中转出;
  4. 退出。

这种类型的行为在一个残酷的世界中是徒劳无功的。我们必须 强制 要求诚实、让欺骗的成本变得很高。这个框架会解决这个问题。

争端

在诚实的行为发生之前,参与者会置身于一个有争端解决机制的环境里。这包含虚拟地创建一个辅助通道合约。我们的冲突场景是一个双向的 ETH 转账辅助通道。为了简化处理,这将通过两笔交易完成,而不是一个“Uber实例”:

  1. 部署双向通道
  2. 将资金从多重签名合约转移到双向通道
  3. 完成一笔双向通道转账。
// 3: Prepare counterfactual deployment of Bidirectional, and update Lineup
let bidirectionalB = bytecodeManager.constructBytecode(Bidirectional, multisig.address, bidirectionalSettlementPeriod)
let bidirectionalA = await registry.counterfactualAddress(bidirectionalB, REGISTRY_NONCE)
let bidirectionalDeployment = registry.deploy.request(bidirectionalB, REGISTRY_NONCE).params[0].data
let bidirectionalCodehash = await conditional.callHash(registry.address, new BigNumber.BigNumber(0), bidirectionalDeployment)

// 4. Prepare counterfactual transfer, and update Lineup
let transferB = proxy.doCall.request(registry.address, bidirectionalA, depositA, '0x').params[0].data
let transferCodehash = await conditional.callHash(proxy.address, depositA, transferB)

// 5: Conditionally counterfactually deploy Bidirectional
let conditionalBidirectionalB = conditional.doCall.request(registry.address, lineupA, proof(lineupU, bidirectionalCodehash), registry.address, new BigNumber.BigNumber(0), bidirectionalDeployment)
let conditionalBidirectionalI = await counterFactory.call(conditionalBidirectionalB, 1)

// 5. Conditionally counterfactually move money to deployed Bidirectional
let conditionalTransferB = conditional.doDelegate.request(registry.address, lineupA, proof(lineupU, transferCodehash), proxy.address, depositA, transferB)
let conditionalTransferI = await counterFactory.delegatecall(conditionalTransferB, 2)

这两笔交易是经过签名的,但还没有发送到区块链上;但只要有需要就可以上链。同一组参与者可以在一段时间内支持多个通道。目前,防止重放攻击(Replay Attack)的最好方法就是使用顺序的多签名 nonce。辅助通道经过几轮的打开关闭之后,恶意的参与者可以用 一个已许可的 多签名 nonce 来部署一个 错误的 交易。为了防止这种情况,我们在一个 Lineup 中固定了交易的集合:

pragma solidity ^0.4.19;

import "./LibLineup.sol";
import "./Multisig.sol";


// Optimisation Idea: Use shared Lineup.
contract Lineup {
    LibLineup.State public state;

    event Trace(bytes32 a);
    function Lineup(bytes32 _merkleRoot, uint256 _updatePeriod, address _multisig) public {
        state.merkleRoot = _merkleRoot;
        state.updatePeriod = _updatePeriod;
        state.lastUpdate = block.number;
        state.multisig = Multisig(_multisig);
    }

    function update(uint256 _nonce, bytes32 _merkleRoot, bytes _senderSig, bytes _receiverSig) external {
        LibLineup.update(state, _nonce, _merkleRoot, _senderSig, _receiverSig);
    }

    function isContained(bytes proof, bytes32 hashlock) public view returns (bool) {
        return LibLineup.isContained(state, proof, hashlock);
    }
}

这些代码会存储交易列表的 merkleRoot(译者注:默克尔树的根节点哈希值)。然后人们就可以有条件地执行交易,如果该交易包含在这个 merkleRoot 所代表的交易列表中的话。。


pragma solidity ^0.4.19;

import "./Lineup.sol";
import "./PublicRegistry.sol";


contract Conditional {
    function doCall(
        address _registry,
        bytes32 _lineupCF,
        bytes _proof,
        address _destination,
        uint256 _value,
        bytes _data
    ) public
    {

        PublicRegistry registry = PublicRegistry(_registry);
        address lineupAddress = registry.resolve(_lineupCF);
        Lineup lineup = Lineup(lineupAddress);

        bytes32 hash = callHash(_destination, _value, _data);
        require(lineup.isContained(_proof, hash));
        require(_destination.call.value(_value)(_data)); // solium-disable-line security/no-call-value
    }

    function doDelegate(
        address _registry,
        bytes32 _lineupCF,
        bytes _proof,
        address _destination,
        uint256 _value,
        bytes _data
    ) public
    {

        PublicRegistry registry = PublicRegistry(_registry);
        address lineupAddress = registry.resolve(_lineupCF);
        Lineup lineup = Lineup(lineupAddress);

        bytes32 hash = callHash(_destination, _value, _data);
        require(lineup.isContained(_proof, hash));
        require(_destination.delegatecall(_data)); // solium-disable-line security/no-low-level-calls
    }

    function callHash(address _destination, uint256 _value, bytes _data) public pure returns (bytes32) {
        return keccak256(_destination, _value, _data);
    }
}

所以,实际上的准备步骤如下:

  1. 虚拟地部署 Lineup,
  2. 虚拟并且有条件地部署双向支付通道,
  3. 虚拟并且有条件地将资金转移到双向通道,
  4. 进行一笔双向转账。

现在,参与者可以放心地转移资金到多签名钱包里。在争端案例中,Lineup 和双向合约会部署到区块链上,然后参与方根据他们的转账状态更新双向通道状态。再然后,双向辅助通道会分发资金,因此解决了争端。

结论

这套方案覆盖了乐观情况和争议解决方案。然而,如果你仔细查看测试代码,你会看到通道管理有多么复杂。它看起来遵循了解释器(Interpreter)模式(校对注:一种设计模式)。因此,在下一次迭代中,将每一笔交易都打包成某些命令(校对注:Command,指一个抽象的处理单元)也许并不是好的拓展方向。实际上,解释器应该解析这些命令,然后决定是把它们部署到链上,还是在本地计算结果。到了那个时候,基于框架为第三方提供 API 来进行开发就变得可行了。那也将能处理基于 TCR(校对注:Token-Curated Registries,即代币代理注册) 的升级机制。在下一篇文章里,我们会看到它们是如何运转的。


原文链接: https://medium.com/machinomy/code-walkthrough-for-generalised-state-channels-f3cdd52d8172
作者: Machinomy
翻译&校对: 阿剑 & 风静縠纹平

本文由作者授权 EthFans 翻译及再出版。


你可能还会喜欢:

干货 | 2018年3月以太坊的扩展现状
干货 | 图灵完备的状态通道的实现办法, Part-1
观点 | 弄清加密经济学

 
0 人喜欢