引介

FunFair:状态通道合约的参考实现,Part-2

Ajian   |     |   329 次阅读

FunFair:状态通道合约的参考实现,Part-1


推进状态(第一部分)

现在,我们已经有了一条处于初始状态的通道。要想把这条通道利用起来,我们需要具备推进状态的能力,最好不是通过链上交易,为此我们需要一个 Action (操作)功能。

Action(操作)

在这种模式下,我们暂定只能由一位参与者发起 action 。我们目前不会强制两位参与者轮流发起 action(之后会这么做)。不过可以明确的一点是,一个 action 只会有一个签名。我们的 Action 结构如下:

    uint256 constant ACTION_TYPE_ADVANCE_STATE = 0x01;
    uint256 constant ACTION_TYPE_CLOSE_CHANNEL = 0xff;
    struct ActionContents {
        bytes32 channelID;
        address channelAddress;
        uint256 stateNonce;
        uint256 participant;
        uint256 actionType;
        bytes packedActionData;
    }
    struct Action {
        ActionContents contents;
        Signature signature;
    }

为了识别身份并防止重放攻击,Action 依然需要用到通道 ID 和合约地址,其所应用状态的 nonce 值(这个非常重要),以及执行这个 action 的参与方 (简单地用 0 或 1 表示)。

然后我们还要定义 action 类型——目前只有两类——推进状态(Advance State)和关闭通道(Close Channel)。我们之所以要定义类型,是因为我可以想象得到可用于通道但并不能应用于状态机的其他 action ,尽管现在我们还没实现出来。

最后,我们还要输入状态机所需的数据,即一步棋、一张牌、一次赌注等等,不过状态通道对这些数据一无所知。

推进状态(第二部分)

现在万事俱备。状态通道会公开一个公共的只读函数,将一个状态、一个 action 和状态机地址输入这个函数,会有一个新的状态返回。参与者在链下调用该函数,以确定性的方式推进状态。让我们来看看代码:

    function advanceState(StateContents memory stateContents, ActionContents memory actionContents,
                          IStateMachine stateMachine) public view returns
                          (FFR memory isValid, StateContents memory newStateContents) {
        // advance the state using the state machine
        int256 balanceChange;
        bytes memory packedNewCustomState;
        (isValid, packedNewCustomState, balanceChange) =
                 stateMachine.advanceState(stateContents.packedStateMachineState,
                                           actionContents.packedActionData,
                                           actionContents.participant,
                                           stateContents.balances);
        // was the action valid?
        if (!isValid.b) {
            return (FFR(false, "Invalid Action"), newStateContents);
        }
        // check that the balance change is acceptable
        // this must *never* trigger
        // Does Participant #0 have enough funds?
        assert((balanceChange >= 0) || (int256(stateContents.balances[0]) >= (-balanceChange)));
        // Does Participant #1 have enough funds?
        assert((balanceChange <= 0) || (int256(stateContents.balances[1]) >= ( balanceChange))); 
        newStateContents.channelID = stateContents.channelID;
        newStateContents.channelAddress = stateContents.channelAddress;
        newStateContents.nonce = stateContents.nonce + 1;
        newStateContents.balances[0] = uint256(int256(stateContents.balances[0]) + balanceChange);
        newStateContents.balances[1] = uint256(int256(stateContents.balances[1]) - balanceChange);
        newStateContents.packedStateMachineState = packedNewCustomState;
    }

显而易见,该代码调用了状态机来更新其状态,然后创建了一个新的最高级状态以及一个新的 nonce 。你会发现,实际的调用比我最初对状态机的定义略复杂一些。实际上,我们的实现是这样进行调用的:

advanceState(state, action, participant, balances) => (isValid, newState, balanceChange)

状态机需要核实 action 是否在特定状态下有效(例如,在下象棋的时候,不能将王下到会被将军的位置;在玩轮盘赌的时候,绝对不能在 37 这个数字上下注)。状态通道需要知道参与者是谁,以便在状态通道存在争议之时,对签名及其它数据进行验证。因此,参与者这一数据是要明确输入状态通道中的,而非被编码到 action 中再输入状态机。也就是说(例如),强制玩家轮流进行操作这一要求是依靠状态机实现的。最后,由于维护参与者余额是状态通道的责任,当前余额需要传入状态机,然后会状态根据 Action 返回一个新的余额作为一个结果。

我们要求所有的 action 必须是有效的, 并且参与者双方的余额都不为负,然后返回新的状态。

关闭通道

若想关闭状态通道,参与者双方必须达成共识——目前有几种可行的方法,不过我们已经选了一个语义上简单的。参与双方根据理想最终状态的 nonce 签署了关闭通道的 action ,然后将这些数据连同双方共同签署过的最终状态一起传到链上。

合约需要对以下几点进行核实:

  • 该状态通道是开启的吗?
  • 被提议的最终状态是否有效?
    • 是否指向该合约
    • 是否指向该状态通道
    • 参与者的余额总量加起来是否是通道开启时的余额总量?
  • 是否所有的 action 都有效?
    • 它们是否都指向该合约?
    • 它们是否都指向该状态通道?
    • 它们是否都指向被提议状态的 nonce ?
  • 状态和 Action 上的签名是否有效?
    • 每个 action 签名是否都来自正确的参与者?

最后——这个状态是否可终结?加上这个概念是为了防止状态通道做出无谓的关闭行为——例如,在游戏进行到一半的时候关闭。这一点是由状态机决定的,状态机会通过另一种方法来判断特定状态是否可终结并返回该结果(参见注释)。

如果以上几点都核实无误,即可关闭状态通道,重新分配资金。

使用状态通道

我们已经了解过核心概念了,现在是时候说说状态通道的实际运行流程了。

单个状态通道需要两个参与者——可以是人、机器人、自动化软件——什么都行,真的,只要能够与区块链进行通信,而且彼此之间也能通信就好。因此,ta 们得有渠道连上区块链节点、使用 web 3.0(之类的基础设施),以及一些链下信道,例如 HTTP 、Websocket,或是专门的 TCP/IP 协议(如果你实在想的话,也可以使用邮箱——这些都无所谓的)。

首先,参与双方都要同意开启通道;主要就是对于 OpenChannelData 结构体的内容达成共识。这些内容包括参与双方的地址、资金、使用的状态机(包括初始化数据)、时间戳,最后是初始状态哈希值及其签名(通过链下调用状态通道代码得到)。

一旦参与双方都签署了这些数据,状态通道就可以得到资金(见下文)并开启。等到参与双方看到该交易上链,该通道就可被视为开启。

若要推进通道的状态,需由一方提议一个 action 。提议 action 的操作可以是让用户按一下按钮,也可以通过自动算法来完成。不管是通过哪种方法,只要能创建具体的 action 消息就可以(我们会在后文给出详细说明)。然后,该参与者从链下调用 advanceState() 函数,输入前一个状态和 action 。如果输入有效的话,代码会返回一个新的状态。

之后,该参与者对 action 和新状态进行签名,然后将它们发送给对手方。

接收到这两个签名数据之后,对手方必须从以下两个方面对其进行验证:

  • 通过调用 advanceState() ,被提议的 action 确实将前一个状态推进到了被提议的新状态

  • 新状态和 action 的签名都是有效的

验证完成后,对手方应亲自签署新状态。对手方可以即刻将其发送给刚才的参与方,也可以与下一个提议的 action 一起发送——这都不成问题。只不过,对于任何一方而言,只要某个状态还没得到对手方的签名(即他们还没收到签名),基于该状态的 action 就都不能接受。

之后就是不断重复这一流程,直到参与方同意关闭状态通道为止。再说一遍,提议 action 的操作可以是让用户按一下按钮,也可以是在达到预定点之后自动触发的。这个时候,参与双方都需签署一个关闭通道 action ,然后将其发送到链上,就可以关闭通道了。实际上,只要签名有效,任何人都可以将这个 action 消息提交到链上,不过我能想象到,提交信息的通常会是参与双方中的一方,在接收到由对手方签署的 action 之后,生成并签署了自己的 action ,然后提交了两个已签署的 action 。

尤其要注意的一点是,如果有一个参与方已经签署了关闭通道 action ,并将其发送给了对手方,那 ta 在任何情况下都不能再推进状态了。对手方后续可以使用该信息关闭通道——无论该对手方更新了多少状态,都可以回滚到这个关闭通道的 action 所在的状态。

听起来都很简单对吧?有可能出什么问题吗?答案是,几乎所有东西都有可能出现问题,在状态通道的开发过程中,有 80% 的工作都是在解决这一问题。首先要明白的一点是,状态通道的核心其实是协议。

协议

根据我在网上随机找到的定义,协议指的是 “被任何团体、组织或场合接受或确立的流程或行为规范”。

在从通道开启到关闭的过程中,状态通道之所以能够有效运行,是因为参与双方都遵守协议——主要就是一套明确了参与双方所需完成事项的规则,以及上文 “使用状态通道” 一节中所述操作的顺序。协议的核心规则如下:

  • 如果轮到我进行操作,我必须在合理的时间范围内完成操作
  • 如果我接收到一个新状态,我必须:
    • 验证状态转换是否按照链上代码进行
    • 验证新状态和 action 上的签名是否有效
    • 将我对新状态的签名发送给对手方

在通道开启的过程中,可能会发生很多情况。我可能会连不上网,与对手方失联;我的电脑可能会宕机,无法访问已签署状态 和/或 临时密钥。对手方可能会有意无意地向我发送错误数据。

我们可以写一些特殊案例来解决这几个错误类型,不过用 “参与一方没有遵守协议” 这种陈述来表达,所有问题都会清楚得多。感谢 Jeff Coleman 在开发初期就拦住了我,没让我越陷越深。

为了解决这一问题,我们增加了一个叫做“争议解决(Dispute Resolution)”的流程。

争议解决

如果参与一方因另一方不遵守协议而无法推进通道的状态,可以进入争议流程。如上文所述,以“争议”来命名可能不是最好的,不过暂时先这么定了。

从代码的角度来看,争议流程位于 DisputableStateChannel.sol——这个类源自 StateChannel.sol ,位于一个独立的文件中,以便更加清楚地阅读代码,并将争议与常规的通道操作明确区分开来。

从本质上来说,我们所做的尝试是利用区块链来强制对手方再次遵守协议。我与 Tom Close 详细讨论过这个方法,他根据另一种思路完成了一个叫做 “强制移动(Force Move)” 的实现。

发起争议

作为参与者,我们遇到最常见的情况是,得到了一个双方都签署过的状态 S ,并且想要利用 action A 来将其推进到 S' 。我们在链下生成了状态转换,签署了 S' 和 A ,并将其发送给对手方。到了这一步,对手方要是没能将其对 S' 的签名发送给我们,不管是出于什么原因,我们都只能卡在这儿。

虽然我们相信 S' 是有效的,但是我们没法用这个 S' 在链下做任何事,毕竟对手方还没有对 S' 进行签名。不过我们可以在链上验证这个状态转换,来强迫状态推进。

function disputeWithAction(bytes memory packedOpenChannelData, State memory state,
                           Action memory action, State memory proposedNewState)

该函数接收 3 个参数:双方共同签署的状态、已签署的 action 和单方签署的提议新状态。几乎所有关于该函数的代码(以及本小节提到的其他代码)会对输入数据进行有效性检查(validation check)。就本例来看,我们需要检查:

  • 状态通道是开启且不存在争议的吗?
  • 在状态通道下(根据最终通道(End Channel)调用),状态和 Action 是否有效?
  • 状态是否经过参与双方正确签署?
  • Action 是否经过争议发起方正确签署?
  • Action 是否是推进状态类型的?

然后,我们利用状态机在链上生成了一个新的状态,并检查:

  • 新状态是否与争议发起方提交的被提议新状态一致
  • 是否经过参与双方正确签署

我们还要检查该状态的 nonce 是否严格大于之前所有争议的 nonce ——这是为了防止有人重复对同一个状态提出争议来阻碍对手方这样的极端案例。

最后,我门要检查信息是否是由争议发起方发送到链上的。在这个代码中,几乎其他所有调用都可由第三方发起——只要它们在链下经过正确签署即可。但是,我还没能找到一种方法来说服自己,允许第三方代表我来发起争议是正确的做法——如果我这么做了,第三方很容易就能通过做恶来阻碍我推进状态。关于这点,我依然在思考中——欢迎大家踊跃评论!

如果有任何检查项未通过,我们就会“甩手不干(throw)”(指的是终止执行合约并回到修改前的状态),该争议基本上会被全盘否决。

如果检查项全部通过,该通道就会被认定存在争议。我们会在链上存储一些与该争议相关的数据(争议发起方、新状态和 action 的哈希、新状态的 nonce 、时间戳和交易的区块编号),并等待我们的对手方作出响应。

解决争议

在理想情况下,对手方会意识到发生了什么事(例如,他们的网络连接刚好恢复了),并尝试纠正错误。

这就引入了状态通道协议的另一个部分:

  • 我必须一直关注这条链的情况

要是没有注意到对手方发起争议的话,你就倒霉了(见下文)。

话说回来,一旦你注意到了,就得做点什么了——你可以从链上事件或交易记录里调出争议发起方的 Action 和单方签署过的新状态(注意:这就是为什么我们要将整个被提议状态而非单独的一个哈希发送给交易,因为我们已经发现追踪链上 event 是非常不靠谱的。严格来说并不一定要这么做,但是这么做是非常实际的!)

有了这些信息,你就能够自行推进状态,并解决该争议了。为解决争议,你需要生成下一个状态转换,向状态通道合约证明你又上线了并且想要遵守协议。

这里可能会涉及用户界面,而且会有某个人——你或者某个代理机构——将交易发布到链上。这方面的用户界面流程需要更仔细的考量,而且一点也不简单。

无论你是以哪种方式到了这一步,都需要另一个 action —— A'、你对(争议发起方已经签署过的)S' 的签名、下一个被提议的新状态 S''(记住,是你在链下生成的那个),以及你对 S'' 的签名。然后你需要调用:

function resolveDispute_WithAction(bytes memory packedOpenChannelData, State memory state, 
                                   Action memory action, State memory proposedNewState)

该代码要进行多项检查,很多都跟之前的一样:

  • 这个状态通道目前存在争议吗?
  • S' 的哈希跟存储在链上的哈希一致吗?
  • 在该状态通道下,Action A' 有效吗?

(请注意,我们无需验证 S' ,因为 S'' 已经在最开始的争议调用阶段验证过了,所以它本身就是正确的。)

  • A' 是否是“推进状态”类型的?
  • A' 是否由争议发起方的对手方发起?
  • A' 是否经过正确签署?

然后我们在链上生成了 S'' 并验证:

  • S'' 是否与对手方提交的新状态一致
  • 他们对 S'' 的签名是否有效

如果有任何检查项未通过,决议就会被拒绝,什么都不会发生。

如果检查项都通过了,决议就会生效。然后就是见证奇迹的时刻!(再次感谢 Tom Close 为我指引了正确方向。)因为 S'' 已经由对手方签署过了,S'' 和 A' 都可由争议发起方从链上获取,状态通道可以在链下继续运行。这样真的很酷!最初的发起方目前所处的位置,就是当两个状态转换根据协议在链下发生之时,他们所应处于的位置。此外,他们拥有完成下一个状态转换所需的一切信息,并可以通过链下信道发送给对手方。

(未完)


原文链接: https://funfair.io/a-reference-implementation-of-state-channel-contracts/
作者: Funfair
翻译&校对: 闵敏 & 阿剑


你可能还会喜欢:

在 Layer-2 和 Layer-1 上异曲同工的技术
Layer-2 中的有效性证明与错误性证明
菜鸟学习状态通道,Part-3:多跳交易/中心辐射通道

 
0 人喜欢