微服务与区块链的智能合约有很多共同之处。二者都独立运行(on-chain),并通过基于消息的通道与外界通信(off-chain)。二者的体积都很小,开发者希望他们都自主地、独立地运行,而且当布署在去中心化的网络上时,表现更好。
本文主要阐述使用微服务架构构建区块链应用的设计原则,及代码示例。涉及到:
微服务完美地体现了 Unix 哲学的精神:只做一件事,并且把它做好。微服务是一个独立的,可布署的有边界的组件,这个组件支持基于消息的交互通信。基于这个前提,微服务架构是一种工程模式,这种模式用于构建高度自动化,可进化的单一功能的软件系统。
区块链应用与微服务的共同之处是什么?什么样的设计原则可以从微服务架构用于去中心化的世界?上面的图表比较了微服务和智能合约的特定设计属性。
把区块链应用设计成微服务可以带来这些福利:
微服务通常使用一种通用的语言API与外界通信,一般是JSON或SOAP。r提供一种基于消息的共通,来跨越不同技术(.NET,Java,Node.js等)和不同平台(Windows,Linux)。
如果你知道DevOps的话,它把服务器当成畜牲而不是宠物来对待。你也许会用同样的方式对待你的代码,简单地可丢弃的代码可以减少技术债。通过优化基础架构来提升工程流程的现代化水平,减少运营成本(比如放进容器或者完全使用无服务配置)。
用微服务架构原则设计区块链应用还可以有业务福利。通过减少基础设施的成本及扩能相关的风险可以改进软件系统的效率。这些方面对于私有的区块链有特殊的价值,对于业务来说成本和服务的持续性是关键需求。
微服务架构原则还支持使用可替换的组件,以减少技术债。Solidity,Ethereum 中用于智能合约的编程语言,有一种机制可以用于为每一个执行的合约指定准确的运行时版本。随着时间的推移,智能合约的版本号可以用于识别过时的区块链,之前的代码可以被替换了。要注意在区块链中,已经被处理的智能合约(也就是说,这个智能合约是被挖到的矿的区块的一部分)可以被删除掉,必须发布一个新版本的智能合约用于将来的交易。
另一个好处是可以更好的扩展运行时,让软件系统可以按需调整。以微服务实现的智能合约可以让区块链在私有的业务中以更灵活的方式分发用于挖矿交易的负载。
在最基础的层面,微服务架构把应用打散成更小的部分并获得发布式安装的福利。同样地,运行于区块链的智能合约从分布式的P2P网络中获利。使用面向微服务架构设计的智能合约可以有效的、可扩展的、可管理的、交付。
写一个用于去中心的区块链数字账本应用,就需要一个典型的分布式系统,比如独立存储,异步消息及分布式交易。区块链应用还需要用户和设备的验证,以及验证执行智能合约中特定的动作。扩展到流行的DDD(领域驱动设计)方式,我指的是在区块链上实践,即 DDDD(去中心化的领域驱动设计)。
我们来设计这个领域,或者说一个数字账本中执行的智能合约的上下文。智能合约把区块链应用的业务逻辑表示成工作流,由一个消息发送者(用户或执行合约函数的设备)和一个状态(合约的参数)区分工作流的每一个环节,其他实体(用户或设备)可能会被执行智能合约中的函数所影响。
在本文中,第一个创建合约的应用角色叫做 发起者(initiator)。合约的内部状态发生变化时,会触发一个事件,发信号给智能合约的其他部分,或者执行应用。这是一个典型的模式,用于抛出 off-chain 数据,使用服务总线处理智能合约的事件,然后分布消息给接收者。
上图区分了一个智能合约的工作流中引用的实体。
Ethereum 使用 Solidity 作为编程语言,编写自制的用于智能合约的业务逻辑,智能合约在 Solidity 中类似面向对象语言中的类。每个合约包含角色,状态,函数,环节以及业务流程的执行动作。
pragma solidity ^0.4.20;
contract Betting
{
//Roles
address public Gamler;
address public Bookmaker;
//State
enum BetType { Placed, Won, Lost }
BetType public State;
//Properties
uint public BetAmount;
}
上同的代码段展示了 Solidity 中声明的不同类型的变量,可以用于赌博应用。角色(游戏者,发牌者)被定义为地址,是Ethereum中的用户或合约的唯一标识符。状态 是一个枚举标记,用于区分当前的赌局状态。函数,后面会介绍,定义状态的变化。赌资额用一个uint数字表示(目前Solidity不支持decimal值)。
要注意Solidity的版本声明,是0.4.20,这是为了避免与以后版本的Solidity编译器产生兼容问题。还可以区分智能合约中的老代码,这些老代码可能需要更新。从区块链中移除现存智能合约的过程叫做“自销毁”。
一个智能合约应该是单一职责并且尽可能只包含最少的业务逻辑。在这个赌博应用中,一个智能合约可以给赌徒暴露一些函数用于放置一个赌局,然后发牌者来判定输或赢。货币可以在两个角色之间交换,这也是赌博工作流的一部分。需要用合约验证的货币交易的常用模式一般可以看作两个阶段。如下图:
发牌者(合约的发起者)下一定量的注,然后被保存进智能合约,如果这一注赢了,由发牌者标记为赢,然后赌资转移给赌徒,否则,发牌者就收回赌资。
constructor() public{
Gambler = msg.sender;
}
function Bet(uint amount) public payable{
require(msg.sender == Gambler, "Only a gambler can place a bet.");
require(amout > 0, "Amount should be greater than zero.");
BetAmount = amount;
address(this).transfer(amount);
State = BetType.Placed;
}
function Bet(uint amount) public payable{
require(msg.sender != Gambler, "Only the bookmaker can mark a bet as won.");
require(amout > 0, "Amount should be greater than zero.");
Gambler.transfer(amount);
Close(BetType.Won);
}
function Lost() public payable{
require(msg.sender != Gambler, "Only the bookmaker can mark a bet as won.");
Bookmaker = msg.sender;
Bookmaker.transfer(BetAmount);
Close(BetType.Lost);
}
function Close(BetType state) internal{
Gambler = 0x0;
BetAmount = 0;
State = state;
}
如上面的代码所示,这个用Solidity实现的智能合约需要为工作流中的每个动作定义几个函数:
- 构造函数把消息发送者保存为发牌者,这是智能合约的初创者。
- Bet 函数,接收赌资作为输入,执行一些验证(这个函数只能被赌徒调用,且赌资必须大于0)。然后把赌资转移给合约。因为on-chain货币转移的要求,函数需要标记为 payable。
- Won 函数,验证了调用者不是发牌者之后,把赢得的赌资转移给赌徒,然后关闭赌局,标记为“Won”。
- Lost 函数,只能被发牌者调用,转移最初的赌徒输掉的赌资给发牌者,然后关闭赌局,标记为“Lost”。
- 关闭赌局后,赌徒被移除(address被设置成0x0),赌资也设置成0,准备下一局。
虽然实现比较简单,但这个场景实现了一个典型的区块链应用中管理钱的消费的模式。其他场景中,可能需要使用证明文件,比如文档,表格,证书,或者图片。由于诸多原因,主要是存储的限制,把文件放入区块链是不合适的。通用的作法是执行一个加密算法的哈希计算(比如SHA-256),然后把这个哈希值分享到分布式账本中,然后外部系统来保存这个文件。
后面什么时候执行这个哈希算法都会得到同样的结果,除非这个文件被改了,哪怕是一个像素。这个流程授予了一个存证(proof of existence),可以是邮件,文件,文档,通话记录或者视频。也授予了身份证明(proof of authenticity),你知道一个数字资产没有改变过,因为数字账本是不可变的,独立的,所有交易的可验证记录。
之前说过,建议让智能合约只有单一职能,所以对于智能合约来说,面向职能的设计是一项重要的技能。多个智能合约可能操作系统领域中的同一个数据模型,而执行是独立的。比如,在一个应用中,可能有一个智能合约用于管理赌局,另一个管理体育事件,赌局的合约可能引用体育事件,创建一个二者之间的依赖是不可能的。有办法生成一个模型来帮助避免在多个智能合约之间共享数据吗?
不管我们用什么格式的数据(SQL,NoSQL,JSON),我们都会用CRUD(增删改查)操作来实现数据模型。这里不保存领域状态的数据结构,我们可以保存那些导致当前状态的事件。这种模式方法叫做“事件源”(Event Sourceing)https://bit.ly/2068nrt。
Event Sourceing 全都是关于存储fact的,fact是一个事件发生的代表值。就像生活中,我们无法回到过去也不能改变过去,我们只能做在当下来补偿过去。数据是不可变的:所以我们永远会发布新的命令或事件来补偿,而不是更新一个实体的状态。这个方式也叫做 CRAB(Create,Retrieve,Append,Burn)(https://bit.ly/2MbpUOb),这也是一个区块链允许的操作:没有数据的更新的删除,只能附加到链上。从一个区块链中删除东西与它的不变性有冲突,但是你可以通过“burning”接收者的地址来停止转移资产。
这种方式有一个疑虑:性能。如果任何状态值是事件的函数,你可以假设每次访问这个值都需要重新计算当前源事件的状态。明显,会很慢。在事件源中,你可以避免这样高成本的操作,可以使用一个叫做 回滚快照(实体状态在某个时间点的映射)的方式。比如,银行会在月末预先计算你的银行账户余额,这样你就不需要计算从开户时的所有贷款和信用记录。
上图说明了用于赌局应用的结构化数据模型,这有时也叫:雪花模型,因为每个实体(一个数据表)都与其他的不同。
这个结构化数据只保存当前系统的状态,而事件源方式保存单独的fact。事件源中的状态,是所有发生的相关fact的函数。
为了更远的推动职责单一,CQRS(Command Query Responsibility Segregation)补充事件源作一种设计模式用于数据存储。CQRS鼓励有效的单一职责以及微服务的可布署性。意思是你可以(应该)分离数据的更新和查询到单独的模型中。
当使用CQRS时,需要跨上下文访问数据的形式会被淘汰。智能合约可以拥有和封装任何对模型状态的更新和状态变化的上报事件。通过订阅这些事件的通知,一个单独的智能合约可以构建一个完整的、独立的、优化了查询的模型,不需要与其他合约或外部服务共享。有关CQRS的介绍,可以看 Martin Fowler的博客 bit.ly/2Awoz33。
上图描述了我设计的用于赌局应用的基于事件源的数据模型。这个简单的模型使用一个不考虑事件处理的相似结构。不需要知道赌局的当前状态,来读取事件的序列。这个事件的数据结构依赖于事件自身。虽然状态序列在工作流中存在,但是从数据模型的角度来说是不相干的。
在多个智能合约之间共享数据模型并不是唯一导致紧耦合的使用场景,另一个威胁是工作流。很多真实生活中的流程不可能用单一的原子的操作来表示。在RDBMS中,事务中的一个步骤失败了,整个事务就失败了。
对于分布式工作流和无状态的微服务,传统的事务是用数据锁和原子性、一致性、独立性、持久性(ACID)的约束来实现是不切实际的。
Sages(bit.ly/2AzdKNR)是一个长期存在的分布式交易系统。这个工作流中的每一步执行一部分工作,用一个叫做 “routing slip” 的消息给“修正事务”注册一个备用,然后传递并更新消息到活动链上。如果后面的步骤失败了,这一步骤查看“routing slip”,然后调用最近的“修正事务”,回传给“routing slip”。上一步也做同样的事情,调用其前任的“修正事务”,就这样下去,直到所有的事务都被修正。这种模式最终会导致分布式交易中数据的一致性。Sagas是非常适合微服务架构的,当然也适用于区块链智能合约。
你可以用Solidity通过备用函数实现一种“routing slip”。备用函数是一个无名函数,无入参,也无返回值。当调用这个合约的函数时,如果没有其他函数符合给定的函数定义,或者当合约收到“以太坊”时,就调用这个函数。为了能收到“以太坊”,这个函数必须声明为payable,否则就不能通过正规的(address 到 address)的交易方式收到以太坊。
值得注意的是,不带 payable 的备用函数可以收到“以太坊” 作为挖矿交易的回执,比如挖矿的区块奖励。合约不能拒绝这种转移,这是“以太坊”的设计,而Solidity不能克服。一个合约可以有一个无名函数,如下:
// Fallback function
function() public payable{
emit AmountTransfered(msg.sender);
}
event AmountTransfered(address sender);
在“以太坊”中,备用函数是智能合约需要的,以实现账户-账户的直接转移。