如何在多个智能合约之间实现共享公共数据服务

在多个智能合约之间有许多可能的交互模型。在本文的示例中,我们设计了一个简单的生产者到消费者模型,以便我们能够集中讨论共享公共数据的主题。

如何在多个智能合约之间实现共享公共数据服务

典型的生产者与消费者模型

例如生产商可以是提供抵押贷款产品的银行。消费者可以是审计部门。

因此我们需要生产者和消费者模型:

1. 生产者和消费者以不同的速度工作
2. 生产商和消费者的工作时间不同
3. 生产者和消费者是不同的智能合约,由不同的团队或在不同的时间制定
4. 还有一系列其他原因

在这个模型中,生产者将产生一个数据对象,当消费者准备消费时,这个数据对象被发送给消费者进行消费。也就是说,两个智能合约必须处理相同的数据对象,但时间不同。

为此,我们将使用一个队列。

既然我们已经确定了公共队列数据必须在智能合约之间共享,让我们继续进行下去。

公共队列数据位置

在所有情况下,智能合约在以太坊虚拟机(EVM)中共享任何公共数据的唯一方法是,在提供公共数据空间的调用合约的上下文中调用智能合约的函数。这与通过Solidity的delegatecall功能实现的调用库函数相同。

下面我们针对公共队列数据位置依次考虑以下每个选项:

1) Common Data within a Producer contract
2) Common Data within a Consumer contract
3) Common Data within a Router contract
4) Common Data within a Queue contract

1)生产者智能合约中的公共数据

如果消费者尚未达成一致/实施/最终确定等,这可能会很有用。它还有利于多个消费者。

如何在多个智能合约之间实现共享公共数据服务
在生产者中查找公共队列数据

但是从上面的代码和数据位置图中可以看到,为了使Consumer代码能够访问公共队列数据,必须在Producer合约的上下文中调用它。

这也意味着,通常需要的任何消费者数据也必须保存在该上下文中,因为在生产者或消费者合约中分配存储数据是不可取的,因为这可能会覆盖另一个合约中的存储数据。

在某些情况下这可能是合适的,但在此示例中不适用。

2)消费者合约中的公共数据

如果生产者想要控制哪个消费者正在消费哪个物品,这可能很有用。它也有利于多个生产者。

但是与上面的说明类似,为了使生产者代码能够访问公共队列数据,必须在消费者合约的上下文中调用它。

上面的警告也适用。

同样在某些情况下,这可能是合适的,但在本例中不适用。

3)路由器合约中的公共数据

这需要路由合约。它为多个生产者和消费者提供了便利。它还有助于更改(或升级)生产者和消费者智能合约。

如何在多个智能合约之间实现共享公共数据服务
路由包含所有智能合约的数据

生产者和消费者可以是智能合约或库,但是它们的功能必须始终在路由环境中执行。

通常也需要在此环境中保留所有通常需要的生产者,消费者或路由数据,因为在生产者、消费者或路由器合约中分配存储数据是不可取的,因为这可能会覆盖其他合约中的存储数据。

该技术还要求智能合约使用公共数据进行约束,以便每个智能合约仅更改与其操作有关的数据,而不更改共享的公共数据中的其他任何内容。

4)队列合约中的公共数据

这也需要一个路由合约。它与前面的选项类似,但强制安全访问附加和删除项。

如何在多个智能合约之间实现共享公共数据服务
附加的队列合约控制对队列数据的访问

该解决方案稍微复杂一点:必须同时向生产者和消费者提供对队列合约的引用,并且队列合约还需要知道其地址,以检查是否允许访问。

这可能是大多数情况下的理想解决方案,但是为了使本例的代码更简单,我们将创建并测量选项(3),即路由合约中的公共数据。

共享公共数据的方法

在任何数量的Solidity智能合约和库之间共享路由器中的公共数据的方法,例如基于OpenZeppelin的“非结构化存储”的“ EIP-2535:钻石标准”和“代理合约和钻石的新存储布局”中所述的钻石存储代理”和其他文献。

该代码有效地为每个公共数据对象选择了随机存储插槽。每个对象的插槽均固定。由于插槽是从2²⁵⁶的虚拟地址空间中随机选择的,因此位置冲突的可能性很小。我们在本文中将不作进一步考虑。

每当任何合约或库需要访问公共Queue数据时,都会执行示例函数action()中显示的代码:

function action() public {

// Code Fragment (1)
QueueData storage qds = queueData();

}

这将获得对本例中我们共享的公共队列数据的专门引用。然后可以根据需要使用队列,如下所示。

queueData()函数为:

// Code Fragment (2)
function queueData() internal pure returns (QueueData storage) {
return queueDataAt(QUEUE_DATA_LOCATION);
}

这使用QueueDataAt()获得对QUEUE_DATA_LOCATION处的公共队列数据的引用,这看似简单的代码:

// Code Fragment (3)
function QueueDataAt(uint location) internal pure returns
(QueueData storage qds) {
assembly { qds.slot := location }
}

此函数返回QueueData存储变量。由于QueueData是结构,因此此函数返回引用(指向该结构的指针)。汇编语言语句只是将返回的引用的存储插槽设置为给定位置。这具有将位置参数(即uint)转换(有效地转换)到QueueData存储中的效果。在0.7.0和更低版本的编译器之间,访问插槽的程序集语法略有不同。

必须为QueueDataAt()提供一个固定的随机位置,在这种情况下为QUEUE_DATA_LOCATION,该位置采用以下方式编码:

// Code Fragment (4)
uint constant QUEUE_DATA_LOCATION =
uint(keccak256(“queue.data.location”));

keccak256函数用于有效生成数据的随机位置。

如果解决方案中其他地方需要另一个队列,则需要将其放置在其他位置,这需要对keccak256函数使用不同的参数。

如果以上四个代码段存在于路由器或从属合约或库中的任何功能中,则可以访问相同的路由数据。

我们可以将这些代码片段组合在一起以产生:

function action() public {

// Code Fragment (1)
QueueData storage qds = queueData();

}
// Code Fragments (2)(3)(4) combined
function queueData() internal pure returns
(QueueData storage qds) {
uint location = uint(keccak256(“queue.data.location”));
assembly { qds.slot := location }
}

甚至:

function action() public {

// Code Fragments (1)(2)(3)(4) combine
QueueData storage qds;
uint location = uint(keccak256(“queue.data.location”));
assembly { qds.slot := location }

}

我们将在下面的示例代码中使用这些组合的代码片段。

私有智能合约数据的方法

如上所述,由于在任何智能合约中分配传统存储数据是不可取的,因为它可能覆盖任何其他智能合约中的传统存储数据,因此任何私有智能合约数据也必须通过采用上述方法保持在路由环境中。

如果代码片段是智能合约或库专用的,则其他智能合约(例如路由智能合约)将无法访问数据。

示例实现

为了说明该方法,我们将在选项(3)中实现简单的Producer和Consumer模型,该模型在Router合约中的Common Data(选项(3))中进行,并着重于访问私有合约数据和共享Common队列数据的机制。

如何在多个智能合约之间实现共享公共数据服务

智能合约是:

The Producer contract
The Consumer contract
The Router contract
The QueueData library
The QueueDataLocation contract

私有智能合约数据为:

The Producer Data – within the Producer contract
The Consumer Data – within the Consumer contract
The Router Data – within the Router contract

共享的公共数据是:

The QueueData – within the QueueData library

生产者智能合约

生产者智能合约包含在路由合约的上下文中操纵其自己的私有合约数据以及将项目追加到公共队列数据中的代码。

contract Producer is QueueDataLocation {
struct ProducerData {
uint count;
}
function produce() public {
// Code Fragment (1) for private Producer data
ProducerData storage pds = producerData();
pds.count++;
// Code Fragment (1) for common queue data
QueueData storage qds = QueueDataLocation.queueData();
QueueDataLib.append(qds, pds.count);
}

// Code Fragments (2)(3)(4) combined for private Producer data
function producerData() internal pure returns
(ProducerData storage pds) {
uint location = uint(keccak256(“produce.data.location”));
assembly { pds.slot := location }
}
}

稍后将显示QueueDataLocation合约。显示了访问两个不同数据结构(私有合约数据和共享公共数据)的代码片段(1)。代码片段(2),(3)和(4)与私有合约数据的方法相同。

消费者合约

消费者合约与生产者合约非常相似,包含在路由合约的环境中从公共队列数据中删除项目以及操纵其自己的私有合约数据的代码。

contract Consumer is QueueDataLocation {
struct ConsumerData {
uint total;
}

function consume() public returns (uint count) {
// Code Fragment (1) for common queue data
(bool success, uint item) =
QueueDataLib.remove(QueueDataLocation.queueData());
if (success) {
// Code Fragment (1) for private Consumer data
ConsumerData storage cds = consumerData();
cds.total += item;
return cds.total;
}
}
// Code Fragments (2)(3)(4) combined for private Consumer data
function consumerData() internal pure returns
(ConsumerData storage cds) {
uint location = uint(keccak256(“consumer.data.location”));
assembly { cds.slot := location }
}
}

同样,代码片段(1)访问两个不同的数据结构,共享的公共数据和私有的合约数据。

路由合约

路由合约包含将调用路由到生产者合约和消费者合约的代码,并且还操纵其自己的私有合约数据。

contract Router is CallLib, QueueDataLocation {
struct RouterData {
Producer producer;
Consumer consumer;
}

constructor(Producer producer, Consumer consumer, uint qSize) {
// Code Fragment (1) for private Router data
RouterData storage rds = routerData();
rds.producer = producer;
rds.consumer = consumer;
// Code Fragment (1) for common queue data
QueueDataLib.create(queueData(), qSize);
}

function produce() public {
// Code Fragment (1) for private Router data
callLib(address(routerData().producer));
}

function consume() public returns (uint total) {
// Code Fragment (1) for private Router data
RouterData storage rds = routerData();
(bool ok, bytes memory bm) = callLib(address(rds.consumer));
if (ok) {
return abi.decode(bm, (uint256));
}
}
// Code Fragment (2)(3)(4) combined for private Router data
function routerData() internal pure returns
(RouterData storage rds) {
uint location = uint(keccak256(“router.data.location”));
assembly { rds.slot := location }
}
}

代码片段(1)显示访问两个不同的数据结构,其私有合约数据和共享公共数据。

提供了produce()和consume()公共函数来执行此解决方案的实际操作。请注意,produce()直接使用routerData()函数的返回值,而消耗()将其分配给变量。

这两个函数中也有一个技巧,我们将在下面揭示。

代码片段(2),(3)和(4)与私有合约数据的方法相同。

调用库(Call Library)

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())}
}
}
}

队列数据和库

该库包含在所有使用它的合约中。生产者合约附加项目,消费者合约删除项目,路由器合约设置队列大小。编译器将确保最终字节码中仅提供所需的那些功能。

QueueDataLocation合约

该合约提供公共队列数据位置。

contract QueueDataLocation {
// Code Fragment (2)(3)(4) combined for common queue data
function queueData() internal pure returns
(QueueData storage qds) {
uint location = uint(keccak256(“queue.data.location”));
assembly { qds.slot := location }
}
}

代码片段(2)、(3)和(4)按照共享公共数据的方法。

测试

这个简单、交互式、智能合约使部署人员能够生产和消费物品。

contract TestRouter {
Router router;

constructor() {
Producer producer = new Producer();
Consumer consumer = new Consumer();
uint queueSize = 2;
router = new Router(producer, consumer, queueSize);
}
function produce() public {
router.produce();
}
function consume() public returns (uint) {
return router.consume();
}
}

耗气量

还构造了另一个合约,该合约包括继承的生产者和消费者合同,以方便进行天然气消耗量比较。从属合约使用相同的分配存储方法。

contract Combined is Consumer, Producer {
constructor(uint32 queueSize) {
QueueDataLib.create(queueData(), queueSize);
}
}

智能合约实施

单个智能合约具有间接费用,因此我们可以预期建筑用气量将大于合并智能合约。

如何在多个智能合约之间实现共享公共数据服务
路由、生产商和消费者合约的天然气消耗量与联合合约

是的。由于构建的耗气量会随着时间摊销,所以这可能根本不是问题。

典型合约用法

路由合约的Produce()和消耗()公共功能的耗气量如何?

如何在多个智能合约之间实现共享公共数据服务
路由Produce()和Consume()函数的耗气量

正如预期的那样,使用callLib()重定向函数调用的成本(如前所示)是最小的。这可能是可以接受的额外灵活性。

进一步的可能性

路由器合约的Produce()和Consumer()公共函数使用callLib()来调用从属Producer和Consumer合约函数。路由和从属合约均使用Solidity功能签名(称为msg.sig)。这就是我们前面提到的技巧。目标功能必须具有与路由协定中的公共功能相同的名称和参数:

如何在多个智能合约之间实现共享公共数据服务
路由将功能调用重定向到下级合约

在这个例子中,不可能在路由协定中使用回退功能,因为目的地函数在不同的下级协定中。如果只有一个从属合约,那么可以使用fallback函数,正如“编码可升级智能合约”一文所述。

如果只有一种简单的方法让路由知道哪些契约支持哪些函数,那么它们的代码将由回退函数调用,如下所示:

如何在多个智能合约之间实现共享公共数据服务
使用回退函数调用下级协定的路由

结论

有很多原因可以将智能合约拆分为多份合约,并且需要共享公共数据并在安全的地方找到私有合约数据。

我们已经展示了使用一个简单的路由器(有一些注意事项)可以很容易地做到这一点。

该内容来自于互联网公开内容,非区块链原创内容,如若转载,请注明出处:https://htzkw.com/archives/24828

联系我们

aliyinhang@gmail.com