背景
Alice和Bob已经在他们选择的数据仓库中共享数据,而智能访问控制着用户访问,这些访问都通过互联网链接在一起。
我们前面的例子描述了一种解决方案,通过该解决方案,智能合约使用户能够将包含国际象棋棋子的文件附加到文件夹中。
在那个例子中,Alice是第一个出棋的人,在本例中,可以改为Bob是第一个出棋的人。而且他们可能会玩不止一种游戏。要实现这一点,需要修改智能合约,即创建一个新合约。
但是由于数据仓库的合约不能更改,因此创建新的智能合约也需要新的数据仓库。
这也可能意味着将旧的数据仓库移动(复制和删除)到新的数据仓库,并更改对控制智能合约的任何引用。
Alice和Bob希望能够升级控制其数据仓库的智能合约,而不必移动数据仓库并更新对新智能合约的任何其他引用。
这意味着保持相同的数据仓库,也要保持相同的智能合约。
可升级数据共享示例
在这个例子中,我们介绍了一个可升级的智能合约和一个路由器智能合约。
对于我们的特定示例,由于智能合约中的功能将由数据仓库调用,因此,智能合约必须符合datona-lib描述并指定为虚拟函数的智能数据访问合约(SDAC)接口规范,这一点很重要。在SDACinterface基本智能合约中。
因此,我们不需要通用的可升级智能合约,而是需要一个路由器智能合约和一个可升级的SDAC。
通过路由器访问的可升级智能合约(控制)
而且,我们选择仅可能的可升级SDAC:那些具有合约所有者和两个帐户用户的sdac。显然还有很多其他的可能性,比如一个合约所有者和一个其他帐户用户,或者多个帐户用户等等。
最后由于上述参考文献中的警告,我们还选择避免在新的智能合约中重复使用以前的智能合约的数据。在可能的情况下,我们将数据存储定位在链外,因为在链上存储数据的成本很高。
路由器“升级”到一个全新的智能合约,包括代码和数据
这意味着我们必须使用实现SDAC接口的路由器智能合约和可升级智能合约的方法,该方法调用支持两个用户的可升级SDAC。
· 创建可升级智能合约
下面是一个抽象的DuoSDAC,它将用于我们的示例可升级智能合约,它支持一个合约所有者和两个用户:
DuoSDAC
import "SDACinterface.sol";
abstract contract DuoSDAC is SDACinterface {
address public owner = msg.sender;
address public account1;
address public account2;
constructor( address _account1, address _account2 ) public {
account1 = _account1;
account2 = _account2;
}
}
必须将其声明为抽象智能合约,因为它不提供SDACinterface中指定的虚拟函数的实现。
我们可以重写之前的数据共享智能合约,然后跳入时间机器并压缩几周,这样我们就可以部署这些智能合约,而不是原来的智能合约。
以下是共享去中心化数据的示例解决方案,它简化了变量和构造函数(现在在DuoSDAC中),但在其他方面没有变化:
DataShareSDAC
import "DuoSDAC.sol";
contract DataShareSDAC is DuoSDAC {
bool terminated;
constructor( address _account1, address _account2 ) public
DuoSDAC(_account1, _account2) { }
...
在上一篇文章中,我们已经已对其进行了更改,以方便使用多个游戏文件夹,但在其他方面保持不变:
ChessDataShareSDAC
import "DataShareSDAC.sol";
contract ChessDataShareSDAC is DataShareSDAC {
uint gameFolder = 3;
...
function getPermissions( address account, uint object )
public view override virtual returns (uint) {
if (!terminated) {
if (object == gameFolder) {
...
这是本文的示例解决方案,它可以指定第一回合的用户,并由回合的用户开始新的游戏,从而有效地让步了当前游戏:
StartChessDataShareSDAC
import "ChessDataShareSDAC.sol";
contract StartChessDataShareSDAC is ChessDataShareSDAC {
constructor( address _account1, address _account2, bool _turn2 )
public ChessDataShareSDAC(_account1, _account2) {
turn2 = _turn2;
}
function newGame(uint _gameFolder) public {
require(msg.sender == (turn2 ? account2 : account1));
require(gameFolder < _gameFolder);
gameFolder = _gameFolder;
}
}
· 创建路由器智能合约
我们现在已经编写了几个符合DuoSDAC接口的可升级智能合约(上面的DataShareSDAC、ChessDataShareSDAC、StartChessDataShareSDAC)。
我们将部署一个可升级智能合约,然后升级到另一个可升级智能合约。
我们将使用路由器智能合约调用可升级的智能合约。由于路由器智能合约中的功能将由数据仓库调用,因此它必须符合datona-lib描述的智能数据访问合约(SDAC)接口规范。这些功能在下面的智能合约中被标识为数据仓库接口。
升级策略取决于智能合约作者决定升级权限的位置。在许多可升级的智能合约中,它由智能合约所有者承担(例如,请参阅开放Zeppelin升级)。
在我们的案例中,由于升级后的智能合约会影响两个用户共享的数据(顺便说一句,合约所有者实际上并不可见),因此我们决定,智能合约所有者和两个用户必须同意升级后的智能合约,然后才能强制执行。
我们将在下面的acceptUpgrade函数中强制执行该操作。如前所述,将通过将一个可升级的智能合约替换为另一个可升级的智能合约来升级完整的合约(代码和数据)。
下面是一个路由器智能合约的示例,它符合我们想要的数据仓库存储接口和升级策略:
DuoSDACrouter
import "DuoSDAC.sol";
contract DuoSDACrouter is SDAC {
DuoSDAC duoSDAC;
constructor( DuoSDAC _duoSDAC ) public {
duoSDAC = _duoSDAC;
}
// ---------- data vault interface ----------
function hasExpired() public override view virtual returns
(bool) {
return duoSDAC.hasExpired();
}
function terminate() public override virtual {
duoSDAC.terminate();
}
function getPermissions( address account, uint object ) public
override view virtual returns (uint) {
return duoSDAC.getPermissions( account, object );
}
// ---------- upgrade policy ----------
event newContract(address indexed _from, address indexed _to,
bytes32 _value);
event updated(address indexed _from, address indexed _to,
bytes32 _value);
DuoSDAC newDuoSDAC;
uint8 votes;
function agreeUpgrade(DuoSDAC _duoSDAC) public returns (bool) {
uint8 vote;
// (1)
if (msg.sender == duoSDAC.owner()) vote |= 1;
if (msg.sender == duoSDAC.account1()) vote |= 2;
if (msg.sender == duoSDAC.account2()) vote |= 4;
require(vote != 0, "Unknown account");
// (2)
if (newDuoSDAC != _duoSDAC) {
newDuoSDAC = _duoSDAC;
votes = vote;
emit newContract(msg.sender, address(newDuoSDAC),
"new contract");
} else {
votes |= vote;
// (3)
if (votes == (1 + 2 + 4)) {
terminate();
duoSDAC = newDuoSDAC;
votes = 0;
emit updated(msg.sender, address(newDuoSDAC),
"contract agreed and updated");
return true;
}
}
}
}
在我们之前的文章中已经描述过数据仓库接口函数。这里,我们集中讨论路由器的函数。
我们引入了一个必须由智能合约所有者和两个用户以任何顺序调用的函数acceptUpgrade。必须提供新的智能合约作为参数。
(1)之后的行确保为每个参与者的投票设置唯一的位。
(2)之后的检查可确保每当有人尝试升级到潜在的新合约时,将清除表决集并发出事件。
(3)之后的检查确保了所有3个参与者都同意同一个新的智能合约时,旧的智能合约终止,设置了新的智能合约,发出了一个事件并且函数返回true。
合约测试
为了测试解决方案合约,我们可以将其部署在区块链上-一个测试网就可以了。但是任何DuoSDAC智能合约构造函数都需要2个参数,即两个用户帐户。
为了协助自动化测试,我们可以创建代理用户智能合约来代表两个用户的帐户。这是ProxyUser智能合约的定义:
ProxyUser
import "DuoSDACrouter.sol";
contract ProxyUser {
function agreeUpdate(DuoSDACrouter duoSDACrouter,
DuoSDAC duoSDAC) public returns (bool) {
return duoSDACrouter.agreeUpgrade(duoSDAC);
}
}
测试合约本身会创建代理用户智能合约,然后创建路由器智能合约,并提供一些测试功能:
TestSDACrouter
import "DuoSDACrouter.sol";
contract TestSDACrouter {
ProxyUser user1 = new ProxyUser();
ProxyUser user2 = new ProxyUser();
DuoSDACrouter duoSDACrouter = new DuoSDACrouter();
function t1createRouter() public {
DataShareSDAC dsSDAC = new
DataShareSDAC(address(user1), address(user2));
duoSDACrouter = new DuoSDACrouter(dataShareSDAC);
}
function t2changeToDataShare() public {
DataShareSDAC dsSDAC = new
DataShareSDAC(address(user1), address(user2));
require(!duoSDACrouter.agreeUpgrade(dsSDAC));
require(!user1.agreeUpdate(duoSDACrouter, dsSDAC));
require(user2.agreeUpdate(duoSDACrouter, dsSDAC));
}
function t3changeToChess() public {
ChessDataShareSDAC cdsSDAC = new
ChessDataShareSDAC(address(user1), address(user2));
require(!duoSDACrouter.agreeUpgrade(cdsSDAC));
require(!user1.agreeUpdate(duoSDACrouter, cdsSDAC));
require(user2.agreeUpdate(duoSDACrouter, cdsSDAC));
}
function t4changeToStartChess() public {
StartChessDataShareSDAC scdsSDAC = new
StartChessDataShareSDAC(address(user1), address(user2),
true);
require(!user2.agreeUpdate(duoSDACrouter, scdsSDAC));
require(!user1.agreeUpdate(duoSDACrouter, scdsSDAC));
require(duoSDACrouter.agreeUpgrade(scdsSDAC));
}
function t5changeBack() public {
DataShareSDAC dsSDAC = new
DataShareSDAC(address(user1), address(user2));
require(!user2.agreeUpdate(duoSDACrouter, dsSDAC));
// reset votes
ChessDataShareSDAC cdsSDAC = new
ChessDataShareSDAC(address(user1), address(user2));
require(!user1.agreeUpdate(duoSDACrouter, cdsSDAC));
// start voting again
require(!duoSDACrouter.agreeUpgrade(dsSDAC));
require(!user2.agreeUpdate(duoSDACrouter, dsSDAC));
require(user1.agreeUpdate(duoSDACrouter, dsSDAC));
}
}
上面的示例提供了一个公共函数t1createRouter,它使用初始的DuoSDAC可升级智能合约创建路由器智能合约。
公用功能t2changeToDataShare使用同一DuoSDAC可升级智能合约的不同版本来升级路由器智能合约。检查同意更新的返回结果。
公用函数t3changeToChess和t4changeToStartChess使用不同的DuoSDAC可升级智能合约来升级路由器智能合约。检查同意更新的返回结果。
公共函数t5changeBack使用初始DuoSDAC可升级智能合约的新实例化升级路由器智能合约,并测试投票。检查agreeUpdate的返回结果。
建议进行其他测试,以确保DuoSDAC可升级智能合约的行为符合预期。
气体消耗量
天下没有免费的午餐。
· 部署合约
直接调用gas保险库函数,而不是直接调用gas接口来消耗更多的数据。
部署可升级智能合约的总耗气体量是部署智能合约的正常耗气体量,再加上升级智能合约的耗气体量,再加上以下数量的一部分(取决于升级合约的次数):部署路由器。
升级到DataShare(实际上是从初始DataShare)的耗气体量比后续升级要高一些,因为这是第一次升级,因此涉及新的存储位置。
· 使用合约
部署合同不仅消耗更多的气体量,而且数据仓库使用的公共函数也更加昂贵。
此图说明了DataShare getPermissions函数的耗气体量已从略高于5000的气体增加到将近7500气体。在更复杂的智能合约中,相同函数的气体消耗量也出现了类似的增长。
· 使用智能合约
如t1createRouter所示,您必须先部署可升级的智能合约,然后再部署路由器。您还需要创建一个数据仓库并使用示例智能合约。这些技术都在我们之前的文章中进行了讨论。
· 部署智能合约
· 创建数据仓库
· 使用开始棋盘数据共享
结论
Alice和Bob以及其他许多人都希望能够以分散的方式共享数据。这可能会使数据更安全、更私密、更可控。
但他们也希望能够升级智能合约而不必更改其数据保险库。
我们已经展示了一个实用的解决方案,它使用智能合约和数据仓库的组合来实现这一点,这些都符合datona-lib中描述的接口规范。