计划用三篇文章,一个月左右的时间来实现一个蚂蚁开放联盟链上的区块链投票案例,本文是系列第二篇。
本文将结合具体的投票案例,设计一组区块链投票合约,并将合约部署到蚂蚁开放联盟链进行测试。重点在于结合工具展示蚂蚁链solidity合约的开发、升级、调用的过程,业务流程尽量简单,不会涉及投票的匿名性、抗胁迫性等指标。
本案例虚构了一个业主投票的场景,小区议事过程中,一般需要有业主提出议案事项,如“小区是否允许外卖小哥进入?”,议案交到业主大会,由业主大会整理并发起业主投票,计票规则一般是一户一票,权重按照房屋面积计算,如果超过一半面积的业主同意,议案则获得通过。因为投票的前后组织工作非常复杂繁重,可能需要线下扫楼等,所以一般一次投票不会只针对一个议案。每个议案下通常固定有三个选项,“同意”、“不同意”、“弃权”。因为是演示项目,流程和业务逻辑都尽量简单,例如我们在计票时不回采用面积权重的方法,只是简单的超过半数即可,不同意、弃权、未参与投票几种情况也不会做特殊处理(实际投票中,未参与投票可能会被记入同意)。
通过案例场景分析,系统中存在系统管理员、业主两种角色,每种角色又对应不同操作,本案例演示重点在合约的设计开发,所以不会区分系统的管理端和业主端,也不会做对应的鉴权等操作,统一配置一个swagger方便测试。一个完备的系统中应该将信息同时写到数据库中和数据合约中,但简单起见,本案例只向合约中写入数据,不涉及到Mysql等DB操作。下图为本案例中涉及到的用例。
接下来逐个分析:
系统管理员承担了系统基础信息的维护工作,其所进行的操作需要上到区块链留痕,这里的注册我们简化为系统账户与蚂蚁链的链上账户地址绑定。
系统管理员通过该功能向系统中添加小区和房屋信息,准备投票的基本信息。
主要用来演示如何对合约中数据进行编辑。
系统管理员组织并发起一次投票。需要初始化的内容包括投票起止时间、议案、可投票房屋地址等。
在投票结束后,调用合约对投票进行计票。
任何一个新业主账号在系统注册时,都会生成一个唯一业主数据合约与其对应。在系列上一篇中讲过,国内区块链应用实际是中介化,具体到这个用例,业主并不能用他的链上钱包地址直接对链上合约进行操作,总是要通过投票系统这个“中介”,所以业主也没必要具备自己的钱包地址,要完成数据上链,只要存到业主数据合约就可以了。
业主信息注册后需要业主进行人脸识别或者提交身份证正反面信息进行实名认证。业主合约这里做的主要是认证信息固化存证,可包括身份证号、照片哈希等。
业主实名认证后可像自己名下添加房屋,同样客房关系的证明,如房产证照片Hash等可以上链存证。
业主对管理员发起的议案进行投票。这里需要对业主身份进行判断,对重复投票进行检测等。
开始合约设计前,我们先明确下合约的设计原则。
智能合约的生命周期主要有设计、开发、部署、运行、升级、销毁。其中的设计和升级阶段是核心,相对于非区块链项目,区块链多了一个不可篡改的特性,升级时合约需要做到向下兼容。从业务视角来看,智能合约只需要做两件事,其一是如何定义数据的结构和读写方式,其二是如何处理数据并对外提供服务接口。这两件事有点类似于MVC中M和C的关系。
为了更好的做模型抽象和合约分层,也方便合约升级、回滚、兼容,将业务控制和数据从合约代码层面就做好分离,这样的处理在复杂业务逻辑场景中经过实践是当前被认为最佳的模式。这个模式简称为CD(Controller-Data)模式,将合约分为两类:控制器合约(Controller Contract)与数据合约(Data Contract)。
控制器合约通过入参和访问数据合约获得数据,并对数据做逻辑处理,然后写回数据合约。它专注于对数据的逻辑处理和对外提供服务。根据处理逻辑的不同,常见的有命名空间控制器合约、代理控制器合约、业务控制器合约、工厂控制器合约等。一般情况下,控制器合约不需要存储任何数据,它完全依赖外部的输入来决定对数据合约的访问。特殊情况下,控制器合约可以存储某个固定的数据合约的地址或者命名空间(通过命名空间在运行时获得合约地址)。
数据合约专注于数据结构定义与所存储数据的读写裸接口。为了达到数据统一访问管理和数据访问权限控制的目的,最好是将数据读写接口只暴露给对应的控制器合约。禁止其他方式的读写访问。
基于这个模式,遵循从上至下的分析方式,从对外提供的服务接口开始设计各类控制器合约,再逐步过渡到服务接口所需要的数据模型和存储方式,进而设计各类数据合约,可以较为快速的完成合约架构的设计。
下图是一个最基础的控制合约和数据合约示意图。合约的部署和升级过程会在后文实际操作中演示。
根据以上案例场景和用例的分析,我们可以抽象出以下几个对象:管理员、小区、房屋、业主、投票、议案。其中管理员被简化为一个线上账户,投票和议案简化为投票,我们可以得到4个合约对象:小区、房屋、业主、投票。按照CD模式,可拆分为四组共8个合约。如下图。
图中列出了各个合约的字段和方法。可以看到除了4组合约外还多出来一个代理控制合约,这里主要是方便对合约进行统一管理,方便升级和回滚。
补充说明:
代理设计模式在区块链中,一般用于控制合约的升级,比如在以太链中,合约的升级实际上是部署了一份新的合约,旧合约的内容无法被改写,升级会导致合约地址的改变。在DApp(Decentralized Application的缩写,中文直译为去中心化应用)中,后端建立在链上,没有像web应用一样的中心化后台,所以在DApp端的合约地址并不方便更新,为解决这一问题才设计了代理模式,但是在蚂蚁链上,因为国内政策原因,并不存在客户钱包这种真正去中心化的设计,蚂蚁链的中介化导致了中介后端的存在,中介后端可代替代理合约,所以本案例中我们不设置代理合约。
因为蚂蚁开放联盟链没有提供测试链,生产和测试的数据都放在同一条链上。防止测试和生产环境相互干扰,合约部署时需要在名称上加以区分。为方便区分版本,合约名称上需要带上版本号。我们定义的各个合约的版本号命名方式如下:
[package]_[env]_[name]_[yyyyMMdd]_[version]
例:
开发环境:
小区控制合约命名
VoteDemo_Dev_ProjectContral_20221101_1
四季花城小区数据合约命名
VoteDemo_Dev_Project_44030013_20221101_1
生产环境:
小区控制合约命名
VoteDemo_Online_ProjectContral_20221101_1
四季花城小区数据合约命名
VoteDemo_Online_Project_44030013_20221101_1
蚂蚁提供了在线和离线的开发工具,我们选用在线的Cloud IDE。合约工程的创建以及Cloud IDE的使用请参考蚂蚁的官方文档蚂蚁开放联盟链文档。
这里要说一个Cloud IDE的小问题
编译很简单,只要点击编译器左上角编译按钮即可。如下图,编译成功后点击左侧的部署按钮,再点击部署记录,可以看到合约之前的部署记录,因为我之前是用合并单文件编译发布过,所以右侧部署记录中的合约升级按钮是灰掉的。
蚂蚁链是支持通过api来部署合约的,api调用的方式会在下一节讲。先部署project_data合约,点击左侧文件下的部署合约按钮。按照前面讲到的合约命名方式,填写好对应的参数,
在本测试中,按照约定的命名规则,我部署了以下几个合约:
下图示意了构建测试合约数据的过程,因为蚂蚁链不允许在合约中创建合约,且还没有开发合约的调用后台,所以这里的测试合约全部是通过蚂蚁链CloudIDE手动部署。
蚂蚁链的升级是替换了原合约的代码,修改代码,编译后,可在编译器右侧对应合约的后面看到升级按钮,点击可对合约进行升级。升级后、合约地址、ID都不会改变,这一点是与以太链等公链最大不同的地方。
先将合约代码附在这里,待下一篇和后端代码整理好一起放到git上
pragma solidity ^0.4.20;
/*****************************************************************************************
* 小区控制合约
*****************************************************************************************/
contract ProjectControl {
//版本
uint8 public version = 1;
/**
*修改小区名称
*/
function modifyProject(identity _pdIdentity, bytes32 _code, string _name) {
ProjectData _data = ProjectData(_pdIdentity);
require(_data.code() == _code, "小区Code不相等");
_data.setName(_name);
}
/**
*添加房屋
*/
function addHouse(identity _pdIdentity, bytes32 _houseCode, identity _houseIdentity) {
ProjectData _data = ProjectData(_pdIdentity);
_data.addHouse(_houseCode,_houseIdentity);
}
}
/*****************************************************************************************
* 小区数据合约
*****************************************************************************************/
contract ProjectData {
//版本
uint8 public version = 1;
/**
*构造函数
*/
constructor(bytes32 _code, string _name) {
code = _code;
name = _name;
}
//小区在系统中的唯一编码
bytes32 public code;
//小区名称
string public name;
//小区房屋数据(系统中ID到链上地址的映射,防止系统问题导致房屋Code重复)
mapping(bytes32 => identity) public houses;
/**
*修改小区名称
*/
function setName(string _name) {
name = _name;
}
/**
*添加房屋
*/
function addHouse(bytes32 _houseCode, identity _houseIdentity) {
houses[_houseCode] = _houseIdentity;
}
/**
*查找房屋
*/
function houseExists(bytes32 _houseCode, identity _houseIdentity) view returns(bool) {
if (_houseIdentity == houses[_houseCode]) {
return true;
}
return false;
}
}
/*****************************************************************************************
* 房屋控制合约
*****************************************************************************************/
contract HouseControl {
//版本
uint public version = 1;
/**
*增加业主
*/
function addOwner(identity _houseIdentity, bytes32 _ownerCode, identity _ownerIdentity) {
HouseData h = HouseData(_houseIdentity);
h.addOwner(_ownerCode, _ownerIdentity);
}
/**
*删除业主
*/
function removeOwner(identity _houseIdentity, bytes32 _ownerCode) {
HouseData h = HouseData(_houseIdentity);
h.removeOwner(_ownerCode);
}
/**
*修改房屋信息
*/
function modifyHouse(identity _houseIdentity, string _name) {
HouseData h = HouseData(_houseIdentity);
h.setName(_name);
}
}
/*****************************************************************************************
* 房屋数据合约
*****************************************************************************************/
contract HouseData {
//版本
uint public version = 1;
//项目合约地址
identity public projectIdentity;
//房屋Code
bytes32 public code;
//房屋名称
string public name;
//房屋业主数据(系统中ID到链上地址的映射,防止系统问题导致业主Code重复)
mapping(bytes32 => identity) public owners;
/**
*构造函数
*/
constructor(bytes32 _code, string _name, identity _projectIdentity) {
code = _code;
name = _name;
projectIdentity = _projectIdentity;
}
/**
*修改房屋名称
*/
function setName(string _name) {
name = _name;
}
/**
*设置房屋code
*/
function setCode(bytes32 _code) {
code = _code;
}
/**
*添加业主
*/
function addOwner(bytes32 _ownerCode, identity _ownerIdentity) {
owners[_ownerCode] = _ownerIdentity;
}
/**
*删除业主
*/
function removeOwner(bytes32 _ownerCode) {
delete owners[_ownerCode];
}
}
/*****************************************************************************************
* 业主控制合约
*****************************************************************************************/
contract OwnerControl {
//版本
uint public version = 1;
/**
*业主身份认证
*/
function auth(identity _ownerIdentity, bytes32 _code, string _authFileHash) {
//设置认证标记位
OwnerData owner = OwnerData(_ownerIdentity);
owner.setAuthFileHash(_authFileHash);
owner.setAuthed(true);
}
/**
*添加房屋(房客关系认证)
*/
function addHouse(identity _ownerIdentity, identity _houseIdentity, bytes32 _houseCode, string _authFileHash, bool _authed){
OwnerData owner = OwnerData(_ownerIdentity);
owner.addHouse(_houseIdentity, _houseCode, _authFileHash, _authed);
}
/**
*删除房屋
*/
function removeHouse(identity _ownerIdentity, bytes32 _houseCode) {
OwnerData o = OwnerData(_ownerIdentity);
o.removeHouse(_houseCode);
}
}
/*****************************************************************************************
* 业主数据合约
*****************************************************************************************/
contract OwnerData {
//版本
uint public version = 1;
/**
*构造函数
*/
constructor(bytes32 _code, bytes32 _ID, string _name) {
ID = _ID;
code = _code;
name = _name;
authed = false;
}
//身份证号
bytes32 public ID;
//业主code
bytes32 public code;
//小区名称
string public name;
//是否认证
bool public authed;
//认证文件hash
string public authFileHash;
//房屋code到房屋的映射
mapping(bytes32 => House) houses;
struct House {
identity houseIdentity;
string authFileHash;
bool authed;
}
/**
*设置业主是否认证标记
*/
function setAuthed(bool _authed) {
authed = _authed;
}
/**
*设置业主认证文件Hash
*/
function setAuthFileHash(string _authFileHash) {
authFileHash = _authFileHash;
}
/**
*添加房屋
*
*此处房屋的认证标记,与业主身份认证标记不同,是指房屋与业主关系是否认证
*/
function addHouse(identity _houseIdentity, bytes32 _houseCode, string _authFileHash, bool _authed) {
houses[_houseCode] = House({
houseIdentity: _houseIdentity,
authFileHash: _authFileHash,
authed: _authed
});
}
/**
*删除房屋
*/
function removeHouse(bytes32 _houseCode) {
delete houses[_houseCode];
}
}
/*****************************************************************************************
* 投票控制合约
*****************************************************************************************/
contract VoteControl {
//版本
uint public version = 1;
/**
*投票
*/
function vote(identity _voteIdentity, identity _houseIdentity, identity _ownerIdentity, uint _proposalID, uint _optionID) {
VoteData v = VoteData(_voteIdentity);
v.vote(_houseIdentity, _ownerIdentity, _proposalID, _optionID);
}
/**
*添加房屋
*/
function addHouse(identity _voteIdentity, identity _houseIdentity) {
VoteData v = VoteData(_voteIdentity);
v.addHouse(_houseIdentity);
}
/**
*删除房屋
*/
function removeHouse(identity _voteIdentity, identity _houseIdentity) {
VoteData v = VoteData(_voteIdentity);
v.removeHouse(_houseIdentity);
}
/**
*添加议题
*/
function addProposal(identity _voteIdentity, uint _ID, string _content) {
VoteData v = VoteData(_voteIdentity);
v.addProposal(_ID, _content);
}
/**
*删除议题
*/
function removeProposal(identity _voteIdentity, uint _ID) {
VoteData v = VoteData(_voteIdentity);
v.removeProposal(_ID);
}
}
/*****************************************************************************************
* 投票数据合约
*****************************************************************************************/
contract VoteData {
//版本
uint public version = 1;
/**
*构造函数
*/
constructor(bytes32 _code, string _content, uint _startTime, uint _endTime) {
code = _code;
content = _content;
startTime = _startTime;
endTime = _endTime;
started = false;
}
//投票code
bytes32 public code;
//投票内容
string public content;
//投票开始时间(时间戳 10位)
uint public startTime;
//投票结束时间(时间戳 10位)
uint public endTime;
//投票是否开始?开始后不允许更改,这个判断应该放在控制合约中处理
bool public started;
//房屋地址到投票标记位的映射
mapping(identity => House) public houses;
//议案ID到议案的映射
mapping(uint => Proposal) public proposals;
struct Proposal {
uint ID;
string content;
mapping(uint => Option) options;
}
struct Option {
uint ID;
string content;
uint totalVotes;
}
struct House {
identity houseIdentity;
identity ownerIdentity;
bool hasVote;
}
/**
* 投票
*/
function vote(identity _houseIdentity, identity _ownerIdentity, uint _proposalID, uint _optionID){
House house = houses[_houseIdentity];
proposals[_proposalID].options[_optionID].totalVotes += 1;
house.ownerIdentity = _ownerIdentity;
house.hasVote = true;
}
/**
* 获取选项内容
*/
function getPOptions(uint _proposalID, uint _optionID) returns(string){
return proposals[_proposalID].options[_optionID].content;
}
/**
* 获取选项票数
*/
function getPOptionsVote(uint _proposalID, uint _optionID) returns(uint){
return proposals[_proposalID].options[_optionID].totalVotes;
}
/**
*添加议题
*/
function addProposal(uint _ID, string _content) {
proposals[_ID] = Proposal({
ID: _ID,
content: _content
});
}
/**
*删除议题
*/
function removeProposal(uint _ID) {
delete proposals[_ID];
}
/**
*添加议案选项
*/
function addOption(uint _proposalID, uint _optionID, string _content) {
proposals[_proposalID].options[_optionID] = Option({
ID: _optionID,
content: _content,
totalVotes: 0
});
}
/**
*添加可投票的房屋地址
*/
function addHouse(identity _houseIdentity) {
houses[_houseIdentity] = House({
houseIdentity: _houseIdentity,
ownerIdentity: 0x0,
hasVote: false
});
}
/**
*删除房屋
*/
function removeHouse(identity _houseIdentity) {
delete houses[_houseIdentity];
}
}