下圖說明了智慧合約或庫的程式碼和資料的位置:
智慧合約/庫的程式碼和資料位置
在這種情況下,路由必須是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中的庫更靈活,但在構造時消耗更多的天然氣。