本文讨论了Solidity智能合约使用库共享公共数据。
简介
在我们的文章“使用智能合约共享公共数据”中,我们描述了为什么以及如何可靠地使用智能合约共享公共数据。
然而用于在Solidity智能合约之间共享公共数据的技术也适用于Solidity库。本文描述了如何实现这一点,并比较了智能合约和库解决方案的耗气量。
Solidity v0.6.4及更高版本支持此方法。
示例模型
对于本文,我们假设我们正在构建一个与“共享公共数据的智能合约”相匹配的解决方案,也就是说,路由应包含一个生产者,该生产者产生一些排队给消费者使用的对象。在这篇文章中,我们并不真正关心附属库的实际功能和使用。重点介绍数据共享的常用方法。
下图说明了智能合约或库的代码和数据的位置:
智能合约/库的代码和数据位置
在这种情况下,路由必须是Solidity智能合约,因为它为来自其自身和从属库的所有数据提供存储空间管理。
在此示例中,生产者和消费者是Solidity库。它们的功能将始终在路由的环境中执行。
生产者和消费者专用数据也必须保留在路由器环境中,因为不可能在生产者库或消费者库中分配存储数据。
建议使用该文章中介绍的“存储私有合约数据的存储方法”来分配路由数据,以降低如果将任何从属智能合约添加到路由时覆盖存储的风险。此方法适用于库以及智能合约。
智能合约与库
智能合约与库:
The Producer library
The Consumer library
The Router contract
The QueueData library
The QueueDataLocation contract
私有合约数据为:
The Producer Data – within the Router contract
The Consumer Data – within the Router contract
The Router Data – within the Router contract
共享的公共数据是:
The QueueData – within the Router contract
生产者库(Producer Library)
将其私有数据附加到路由的环境中,并将它们附加到自己的数据库中。
library Producer {
struct ProducerData {
uint count;
}
function produce() public {
ProducerData storage pds = producerData();
pds.count++;
QueueData storage qds = queueData();
QueueDataLib.append(qds, pds.count);
}
function producerData() internal pure returns
(ProducerData storage pds) {
uint location = uint(keccak256(“produce.data.location”));
assembly { pds.slot := location }
}
function queueData() internal pure returns
(QueueData storage qds) {
uint location = uint(keccak256(“queue.data.location”));
assembly { qds.slot := location }
}
}
producerData()函数提供对生产者数据的引用,该引用仅由该库使用。
queueData()函数提供对队列数据的引用,这将与路由合约和使用者库共同使用。
这些函数也可以根据开发人员的需要进行扩展或内联。不幸的是,必须在此库中提供queueData()函数,并且必须将相同的代码复制到使用者库中,因为Solidity库不可能从公共基础合约或库继承。
消费者库(Consumer Library)
消费者合约与生产者库非常相似,包含在路由合约的环境中从公共队列数据中删除项目以及操纵其自己的私有数据的代码。
library Consumer {
struct ConsumerData {
uint total;
}
function consume() public returns (uint count) {
QueueData storage qds = queueData();
(bool success, uint item) = QueueDataLib.remove(qds);
if (success) {
ConsumerData storage cds = consumerData();
cds.total += item;
return cds.total;
}
}
function queueData() internal pure returns
(QueueData storage qds) {
uint location = uint(keccak256(“queue.data.location”));
assembly { qds.slot := location }
}
function consumerData() internal pure returns
(ConsumerData storage cds) {
uint location = uint(keccak256(“consumer.data.location”));
assembly { cds.slot := location }
}
}
同样queueData()函数提供对队列数据的引用,这将与路由合约和使用者库一起使用。确定数据位置的算法[在这种情况下为keccak256(“ queue.data.location”)]必须与Producer库中使用的算法相同,以便定位相同的数据。
ConsumerData()函数提供对消费者数据的引用,该数据仅由该库使用。
路由合约(Router Contract)
路由合约包含将调用路由到生产者和消费者库的代码,它还可以操作自己的私有合约数据。
contract Router is CallLib {
constructor(uint32 qSize) {
QueueDataLib.create(Producer.queueData(), qSize);
}
function produce() public {
callLib(address(Producer));
}
function consume() public returns (uint total) {
(bool ok, bytes memory bm) = callLib(address(Consumer));
if (ok) {
return abi.decode(bm, (uint256));
}
}
}
生产者库和使用者库中都可以使用生产者使用的queueData()函数。
提供了produce()和consume()公共函数来执行此解决方案的实际操作。目标函数必须具有与Router协定中的公共函数相同的功能签名(name和parameters),因为使用消息中提供的功能签名来调用库。
调用库
CallLib合约中提供的callLib()函数与文章“编码可升级的智能合约”和OpenZeppelin的“代理转发”中提供的后备函数类似。这是代码:
contract CallLib {
function callLib(address adrs) internal returns
(bool, bytes memory) {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), adrs, 0,
calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {revert(0, returndatasize())}
default {return (0, returndatasize())}
}
}
}
队列数据和库
此库将导入到使用它的所有合约和库中。将队列中的项目从队列中移除,并将实例从队列中移除。编译器将确保在最终字节码中只提供需要的那些函数。
测试
这个简单、交互式、智能合约部署人员能够调用生产和消费者。
contract TestRouter is DcReporter {
Router router;
constructor() {
uint queueSize = 2;
router = new Router(queueSize);
}
function produce() public {
router.produce();
}
function consume() public returns (uint total) {
return router.consume();
}
}
耗气量
还构造了另一个智能合约,该智能合约包括继承的生产者和消费者合约,以方便进行气体消耗量比较。从属合约使用相同的分配存储方法。
contract Combined is Consumer, Producer {
constructor(uint32 queueSize) {
QueueDataLib.create(Producer.queueData(), queueSize);
}
function produce() public {
Producer.produce();
}
function consume() public returns (uint total) {
return Consumer.consume();
}
}
在上一节文章,我们有对于此类合智能合约与库对气体消耗进行一个比较。
智能合约部署
创建单个智能合约也会产生开销,因此我们可以预期部署所需气体量将大于合并的智能合约和基于库的解决方案。
结果就是这样。请注意,库版本的路由消耗的气体要少得多。这是因为不需要创建单独的合约,另外部分原因是这些单独合约的数据引用不需要保存在以太坊虚拟机的高耗气量存储空间中。
典型智能合约用法
智能合约和库的Produce()和consume()公共函数的耗气量如何?
正如预期的那样,这些项目的成本大体上是相似的。
进一步的可能性
如“共享公共数据的智能合约”中所述,路由合约的produce()和consume()公共函数使用callLib()来调用从属的Producer和Consumer库函数:
如果只有一个从属合约或库,则按照“编码可升级的智能合约”一文,可以使用路由合约的回退函数。但是在此示例中,由于目标函数位于不同的从属库中,因此无法使用路由合约的回退函数。
《 EIP-2535:钻石标准》描述了路由如何存储类似于哪种合约或库支持哪些函数,从而通过回退函数调用代码的方式类似于以下方式:
结论
智能合约可能被分成多个组件,这些组件需要共享公共数据并将私有数据定位在安全的地方,原因有很多。
我们已经展示了使用一个简单的路由可以多么容易地做到这一点,但有一些注意事项。
这些组件中的每一个都可以是智能合约或库。智能合约比Solidity中的库更灵活,但在构造时消耗更多的天然气。