整合IPFS星際檔案系統,並基於WeBASE-Front傳送交易介面實現檔案類上鍊存證的方案
作者簡介: 孫運盛 吉科軟資訊科技有限公司 大資料技術研究院架構師。負責吉科軟區塊鏈BaaS平臺設計研發,基於FISCO BCOS的區塊鏈技術研究以及在智慧農業、智慧城市、智慧食安行業領域的區塊鏈技術應用研發。
一、背景
- 隨著業務的發展,越來越多的業務訴求集中到檔案上鍊,比如圖片類、各類電子證明檔案、影片檔案等檔案型別資料上鍊。常見檔案類上鍊場景如電子證照類、醫療影像類、電子票據類、數字藏品類等等都涉及對檔案不可篡改特性的需求,藉助區塊鏈上鍊儲存實現檔案的不可篡改和一致性校驗;
- 對於檔案類上鍊有兩種方案:方案一為檔案內容直接上鍊,這樣生成的交易內容較大,包含交易的區塊大小增長較快,隨著業務量增加,對區塊鏈的網路和儲存帶來較大的壓力;方案二為檔案內容hash上鍊,具體方案為將檔案儲存在物件儲存伺服器上,將檔案內容hash以及相關資訊上鍊,上鍊資訊較小,針對大資料量和高併發場景適用;
其中方案二更優,當查詢鏈上檔案資訊時,校驗鏈上檔案內容hash與物件儲存伺服器上檔案內容hash是否一致,來確認檔案內容是否被篡改。這種方式可極大避免對區塊鏈產品的網路和儲存壓力,同時也能保證對檔案內容的可信與可驗證。
本文將根據IPFS星際檔案儲存系統和基於CRUD的代理模式合約,對檔案類上鍊的處理過程進行示例說明,如何將業務方的檔案儲存到IPFS,同時計算檔案內容雜湊,再透過智慧合約將檔案內容雜湊、IPFS檔案儲存雜湊以及業務方上送的其他檔案屬性資訊一併上鍊儲存。方案流程如下圖:
二、實驗環境
名稱 | 版本號 | 說明 |
---|---|---|
FISCO BCOS | v2.9.0 | |
WeBASE-Front | v1.5.4 | |
Solidity | ^0.6.10 | |
IPFS(kubo) | v0.21.0 | 最新IPFS版本已更名為kubo |
三、IPFS私有網路多節點部署
3.1 下載安裝檔案
ipfs官網地址:https://dist.ipfs.tech/
下載kubo(go-ipfs),安裝列表中適配的作業系統進行下載對應的安裝檔案。
3.2 安裝及初始化
將下載的檔案放到任意安裝目錄下,進行解壓
#1.解壓安裝包
tar -zxvf kubo_v0.21.0_linux-amd64.tar.gz
#2.進入解壓後的目錄
cd kubo
#3.執行install.sh指令碼
./install.sh
#4.執行初始化
ipfs init
#5.解決跨域問題
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST", "OPTIONS"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Headers '["Authorization"]'
ipfs config --json API.HTTPHeaders.Access-Control-Expose-Headers '["Location"]'
#6.啟動ipfs
ipfs daemon
#7.若啟動時與本地服務埠發生衝突,可以在IPFS的配置中對埠進行重新指定
3.3 私有網路部署多節點
3.3.1 安裝golang環境
官網下載go,地址 https://go.dev/dl/
下載符合作業系統的版本 ,如 https://go.dev/dl/go1.20.6.linux-amd64.tar.gz
下載後解壓檔案
tar -zxvf go1.20.6.linux-amd64.tar.gz
配置環境變數,修改/etc/profile檔案,增加go的bin目錄,如:
export GOROOT=/home/bigdata/go
export PATH=$PATH:$GOROOT/bin
執行 source /etc/profile,使更改生效。
3.3.2 各節點伺服器依次下載並安裝IPFS
參考上節的單機安裝ipfs,檢視節點的ID
[root@localhost .ipfs]# ipfs id
{
"ID": "12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"PublicKey": "CAESIMobs4cyoWjM0h980TSqNLp+FYEWpXrhKz+GTGJyWMk4",
"Addresses": null,
"AgentVersion": "kubo/0.21.0-rc3/",
"ProtocolVersion": "ipfs/0.1.0",
"Protocols": null
}
3.3.3 IPFS 配置檔案修改
IPFS多節點 才能構建一個本地的分散式檔案系統,在聯盟鏈開發環境下,多數會使用到IPFS多節點私有網儲存檔案。
為了搭建多節點的IPFS訪問,需要修改~/.ipfs目錄下的config 檔案
"Addresses": {
"API": "/ip4/192.168.189.102/tcp/5001",
"Announce": [],
"AppendAnnounce": [],
"Gateway": "/ip4/192.168.189.102/tcp/8181",
"NoAnnounce": [],
"Swarm": [
"/ip4/0.0.0.0/tcp/4001",
"/ip6/::/tcp/4001",
"/ip4/0.0.0.0/udp/4001/quic",
"/ip4/0.0.0.0/udp/4001/quic-v1",
"/ip4/0.0.0.0/udp/4001/quic-v1/webtransport",
"/ip6/::/udp/4001/quic",
"/ip6/::/udp/4001/quic-v1",
"/ip6/::/udp/4001/quic-v1/webtransport"
]
}
其中 API 和Gateway兩部分的ip地址改為本機IP地址。Gateway的埠地址預設為8080,可以修改為其他埠號 如 8181, 不與其他應用的8080埠號衝突即可。
3.3.4 刪除公共連線節點
ipfs bootstrap rm --all
3.3.5 建立共享KEY
下載go-ipfs-swarm-key-gen,由於linux下訪問github.com很難訪問,可以使用git命令將工程clone下來,並且github.com 用kgithub.com替換,即可成功訪問github。
git clone https://kgithub.com/Kubuxu/go-ipfs-swarm-key-gen.git
swarm.key 金鑰允許我們建立一個私有網路,並告訴網路節點只和擁有相同秘鑰的節點通訊,在一個節點上執行下面命令:
進入go-ipfs-swarm-key-gen/ipfs-swarm-key-gen目錄下執行 go build編譯。
[root@localhost ipfs-swarm-key-gen]# go build
[root@localhost ipfs-swarm-key-gen]# ls
ipfs-swarm-key-gen main.go
編譯成功會在目錄下生成 ipfs-swarm-key-gen的可執行檔案。
使用ipfs-swarm-key-gen 生成金鑰檔案:./ipfs-swarm-key-gen > ~/.ipfs/swarm.key
進入 ~/.ipfs/檢視生成的swarm.key檔案
[root@localhost ipfs-swarm-key-gen]# cd ~/.ipfs/
[root@localhost .ipfs]# ls
api blocks config datastore datastore_spec gateway keystore nohup.out repo.lock swarm.key version
a.如果編譯報錯,解決方案
[root@localhost ipfs-swarm-key-gen]# go build
error obtaining VCS status: directory "/home/bigdata" is not using a known version control system
Use -buildvcs=false to disable VCS stamping.
出現此錯誤時,執行 go env -w GOFLAGS=-buildvcs=false
3.3.6 將共享key複製到其他節點
[root@localhost .ipfs]# scp swarm.key root@192.168.189.103:~/.ipfs
swarm.key 100% 95 1.2KB/s 00:00
[root@localhost .ipfs]# scp swarm.key root@192.168.189.120:~/.ipfs
swarm.key 100% 95 0.9KB/s 00:00
[root@localhost .ipfs]# scp swarm.key root@192.168.189.121:~/.ipfs
swarm.key
3.3.7 檢視本節點的ID,並在其他節點中新增本節點
在本節點伺服器執行 ipfs id
[root@localhost .ipfs]# ipfs id
{
"ID": "12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"PublicKey": "CAESIMobs4cyoWjM0h980TSqNLp+FYEWpXrhKz+GTGJyWMk4",
"Addresses": [
"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"/ip4/127.0.0.1/udp/4001/quic-v1/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"/ip4/127.0.0.1/udp/4001/quic-v1/webtransport/certhash/uEiCl2iwFGuc_5lPbIW_1kx-sHE8StNUn1oyO9C92Fo_aMA/certhash/uEiAqpOGn8isK2l-fOPxkGKwk63FSkYVmlw1O3cMNO3pbVA/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"/ip4/127.0.0.1/udp/4001/quic/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"/ip4/192.168.189.102/tcp/4001/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"/ip4/192.168.189.102/udp/4001/quic-v1/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"/ip4/192.168.189.102/udp/4001/quic-v1/webtransport/certhash/uEiCl2iwFGuc_5lPbIW_1kx-sHE8StNUn1oyO9C92Fo_aMA/certhash/uEiAqpOGn8isK2l-fOPxkGKwk63FSkYVmlw1O3cMNO3pbVA/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"/ip4/192.168.189.102/udp/4001/quic/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"/ip6/::1/tcp/4001/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"/ip6/::1/udp/4001/quic-v1/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"/ip6/::1/udp/4001/quic-v1/webtransport/certhash/uEiCl2iwFGuc_5lPbIW_1kx-sHE8StNUn1oyO9C92Fo_aMA/certhash/uEiAqpOGn8isK2l-fOPxkGKwk63FSkYVmlw1O3cMNO3pbVA/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3",
"/ip6/::1/udp/4001/quic/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3"
],
"AgentVersion": "kubo/0.21.0-rc3/",
"ProtocolVersion": "ipfs/0.1.0",
"Protocols": [
"/ipfs/bitswap",
"/ipfs/bitswap/1.0.0",
"/ipfs/bitswap/1.1.0",
"/ipfs/bitswap/1.2.0",
"/ipfs/id/1.0.0",
"/ipfs/id/push/1.0.0",
"/ipfs/lan/kad/1.0.0",
"/ipfs/ping/1.0.0",
"/libp2p/autonat/1.0.0",
"/libp2p/circuit/relay/0.2.0/stop",
"/x/"
]
}
找到 /ip4/192.168.189.102/tcp/4001/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3 ,在其他節點執行新增節點,如:
ipfs bootstrap add /ip4/192.168.189.102/tcp/4001/p2p/12D3KooWPRK2HL5deq1wn8ZuoeJKqctzh6ywoTsg9cz7Fwzwzpn3
3.3.8 強制啟用私有網路
修改config配置檔案 ,在Routing部分新增 "Type":"dht",如下:
"Routing": {
"AcceleratedDHTClient": false,
"Methods": null,
"Routers": null,
"Type": "dht"
}
新增環境變數,修改/etc/profile 新增 export LIBP2P_FORCE_PNET=1, 重新整理檔案
source /etc/profile
3.3.9 檢視節點連線情況
ipfs swarm peers
3.3.10 在其中一個節點上傳檔案,其他節點檢視檔案
每個節點都新增其他幾個節點的ID資訊,如在其中一個節點 新增另外一個節點的命令
ipfs bootstrap add /ip4/192.168.189.121/tcp/4001/p2p/12D3KooWPRTod1R4ivaac8cSfXRnkXgivcKaXjfrNmBCadXY1ATi
全部新增完成後,使用 ipfs swarm peers檢視連線的節點資訊
[root@localhost .ipfs]# ipfs swarm peers
/ip4/192.168.189.103/tcp/4001/p2p/12D3KooWKjjNkV2tUw3FZUMnrDrkPqR72eqjMCDkAXKqo1Foth2e
/ip4/192.168.189.120/tcp/4001/p2p/12D3KooWB3EJA4m73qCWMXi9cWyZzfbsSEnn7LXsCFpHZX9JuQcb
/ip4/192.168.189.121/tcp/4001/p2p/12D3KooWPRTod1R4ivaac8cSfXRnkXgivcKaXjfrNmBCadXY1ATi
在192.168.189.102節點上新增檔案, 其他節點上檢視檔案
新增檔案:
[root@localhost home]# ipfs add haha.txt
added QmWEkNNf7XUJ2aS6BhNs9UNRnusVVyFfbmD7L9HmPFWnQP haha.txt
4 B / 4 B [===================================================================================] 100.00%
其他節點依次檢視 該雜湊對應的檔案內容
[root@localhost bigdata]# ipfs cat QmWEkNNf7XUJ2aS6BhNs9UNRnusVVyFfbmD7L9HmPFWnQP
hello
可以在任意節點上傳檔案,其他節點檢視檔案,進行測試所有節點是否均聯通。
3.3.11 透過瀏覽器訪問檔案
訪問檔案的地址為 http://節點IP:節點PORT/ipfs/檔案雜湊值
如:
http://192.168.189.102:8181/ipfs/QmSJYWJz9pXn6y4vXc5veEaQUQJQ...
3.3.12 將ipfs新增為系統服務
首先建立 /etc/systemd/system/ipfs.service,touch /etc/systemd/system/ipfs.service
修改ipfs.service檔案,新增以下內容:
[Unit]
Description=IPFS Daemon
After=syslog.target network.target remote-fs.target nss-lookup.target
[Service]
Type=simple
ExecStart=/usr/local/bin/ipfs daemon --enable-namesys-pubsub
User=root
[Install]
WantedBy=multi-user.target
執行 systemctl daemon-reload
使用以下命令進行啟動和停止 及檢視狀態
service ipfs start
service ipfs stop
service ipfs restart
service ipfs status
四、IPFS檔案上傳、下載JAVA服務介面
4.1 引入IPFS SDK依賴包
<dependency>
<groupId>com.github.ipfs</groupId>
<artifactId>java-ipfs-http-client</artifactId>
<version>v1.4.4</version>
</dependency>
4.2 初始化IPFS
package com.jkr.api.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* ipfs星際檔案系統的屬性配置
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "ipfs.config")
public class IpfsProperties {
private String multiAddr;
}
package com.jkr.config;
import com.jkr.api.config.IpfsProperties;
import io.ipfs.api.IPFS;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import java.io.IOException;
/**
* 初始化相關類
*/
@Log4j2
@Configuration
public class BeanConfig {
@Autowired
private IpfsProperties ipfsProperties;
@Bean
public IPFS ipfs() throws IOException {
IPFS ipfs = new IPFS(ipfsProperties.getMultiAddr());
ipfs.refs.local();
return ipfs;
}
}
4.3 IPFS 檔案上傳、下載服務實現類
package com.jkr.api.service.impl;
import cn.hutool.core.io.FileUtil;
import com.jkr.api.service.IpfsService;
import io.ipfs.api.IPFS;
import io.ipfs.api.MerkleNode;
import io.ipfs.api.NamedStreamable;
import io.ipfs.multihash.Multihash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException;
/**
* 連線ipfs星際檔案系統的檔案上傳和下載服務類
*/
@Service
public class IpfsServiceImpl implements IpfsService {
@Autowired
private IPFS ipfs;
/**
* 指定檔案路徑上傳檔案到ipfs
*
* @param filePath 檔案路徑
* @return 儲存檔案的定址雜湊
* @throws IOException
*/
@Override
public String uploadToIpfs(String filePath) throws IOException {
NamedStreamable.FileWrapper file = new NamedStreamable.FileWrapper(new File(filePath));
MerkleNode addResult = ipfs.add(file).get(0);
String hash = addResult.hash.toString();
return hash;
}
/**
* 將byte格式的資料,上傳至ipfs
*
* @param fileData
* @return 儲存檔案的定址雜湊
* @throws IOException
*/
@Override
public String uploadToIpfs(byte[] fileData) throws IOException {
NamedStreamable.ByteArrayWrapper file = new NamedStreamable.ByteArrayWrapper(fileData);
MerkleNode addResult = ipfs.add(file).get(0);
return addResult.hash.toString();
}
/**
* 根據Hash值,從ipfs下載內容,返回byte資料格式
* @param hash 檔案定址雜湊
* @return
*/
@Override
public byte[] downFromIpfs(String hash) {
byte[] data = null;
try {
data = ipfs.cat(Multihash.fromBase58(hash));
} catch (IOException e) {
e.printStackTrace();
}
return data;
}
/**
* 根據Hash值,從ipfs下載內容,並寫入指定檔案destFilePath
*
* @param hash 檔案定址雜湊
* @param destFilePath 目標檔案路徑
*/
@Override
public void downFromIpfs(String hash, String destFilePath) {
byte[] data = null;
try {
data = ipfs.cat(Multihash.fromBase58(hash));
} catch (IOException e) {
e.printStackTrace();
}
if (data != null && data.length > 0) {
FileUtil.del(destFilePath);
FileUtil.writeBytes(data, destFilePath);
}
}
}
五、檔案資料上鍊的合約樣例
對於檔案上鍊採用方案二,上鍊檔案內容雜湊,而不直接將檔案本身上鍊,所以智慧合約的編寫可採用文字資訊上鍊方式,本示例中是基於CRUD合約的代理模式的通用存證智慧合約。主合約為代理合約EvidenceProxy、邏輯合約EvidenceController、儲存合約EvidenceStorage。
5.1 代理合約EvidenceProxy
pragma solidity ^0.6.10;
import "./Proxy.sol";
import "./EvidenceStorageStateful.sol";
/**
* @title 入口代理合約
* @author sunyunsheng
* @notice 通用資料上鍊存證入口代理合約,實現設定具體實現的邏輯合約地址、返回儲存合約地址列表
*/
contract EvidenceProxy is EvidenceStorageStateful, Proxy {
event SetStorage(address operator, address evidenceStorage);
/**
* @notice 設定儲存合約地址
* @dev 限合約管理員呼叫
* @param _storageAddr 登記儲存合約
*/
function setStorage(address _storageAddr) external onlyOwner {
require(_storageAddr != address(0), "Proxy: storage address should not be 0x");
require(Address.isContract(_storageAddr), "Proxy: Cannot set a proxy storage to a non-contract address");
evidenceStorage = EvidenceStorage(_storageAddr);
storageAddrList.push(_storageAddr);
emit SetStorage(msg.sender, _storageAddr);
}
/**
* @notice 返回所有的儲存合約地址列表
*/
function getStorageVersions() public view returns (string memory result){
result="{";
for(uint i = 0; i < storageAddrList.length; i++) {
result = result.toSlice().concat(addressToString(storageAddrList[i]).toSlice());
if(i != (storageAddrList.length-1)) {
result = result.toSlice().concat(",".toSlice());
}
}
result = result.toSlice().concat("}".toSlice());
}
}
5.2 邏輯合約EvidenceController
pragma solidity ^0.6.10;
pragma experimental ABIEncoderV2;
import "./Ownable.sol";
import "./EvidenceStorageStateful.sol";
import "./Table.sol";
import "./Strings.sol";
/**
* @title 存證邏輯合約
* @author sunyunsheng
* @notice 繼承邏輯合約基類和控制權基類,實現動態建表、新增或更新資料、刪除資料、根據主鍵查詢資料的邏輯介面
*/
contract EvidenceController is EvidenceStorageStateful, Ownable {
using Strings for *;
event Create(string tableName);
event Put(string tableName, string key, string[] _data);
event Remove(string tableName, string key);
event MultiPutJson(string tableName, string keys, string values, string result);
/**
* @notice 動態建表
* @param tableName 表名
*/
function create(string memory tableName) external {
require(evidenceStorage.StorageCreate(tableName) == int(0), "EvidenceController: createTable error");
emit Create(tableName);
}
/**
* @notice 新增資訊,自動區分是新增資訊還是更新資訊
* @param tableName 表名
* @param key 主鍵
* @param data 包含value以及5個保留欄位的值集合
*/
function put(string memory tableName, string memory key, string[] memory data) public {
int _result ;
string memory _key;
string[] memory _dataValue;
(_result, _key, _dataValue) = evidenceStorage.StorageSelect(tableName, key);
require(keccak256(abi.encode(_dataValue)) != keccak256(abi.encode(data)),"EvidenceController: same value, not need put");
require(evidenceStorage.StoragePut(tableName, key, data) == int(1), "EvidenceController: put info error");
emit Put(tableName, key, data);
}
/*
* @notice 在表中刪除key對應的記錄
* 刪除所有版本中key對應的記錄
*
* @param tableName 表名
* @param keyValue 主鍵
*/
function remove(string memory tableName, string memory key) external {
require(evidenceStorage.StorageRemove(tableName, key) == int(1), "EvidenceController: remove info error");
emit Remove(tableName, key);
}
/**
* @notice 獲取鏈上資訊
* @param tableName 表名
* @param key 主鍵
*/
function get(string memory tableName, string memory key) external view returns(
int _result ,string memory _key,string[] memory _valueData){
return evidenceStorage.StorageSelect(tableName, key);
}
}
5.3 儲存合約EvidenceStorage
pragma solidity ^0.6.10;
pragma experimental ABIEncoderV2;
import "./Table.sol";
import "./BaseStorage.sol";
import "./Strings.sol";
/**
* @title 通用儲存合約
* @author sunyunsheng
* @notice 資料上鍊通用存證的儲存合約,實現動態建表、插入資料、修改資料、刪除資料、查詢資料、判斷給定的key是否存在的邏輯功能
*/
contract EvidenceStorage is BaseStorage {
using Strings for *;
// table列表
string[] internal tableList;
// table記錄的條目數
mapping(string => uint) internal tableRecords;
/**
* @return table列表
*/
function getTableList() public view returns (string[] memory) {
return tableList;
}
/**
* @return table中操作的條目數
*/
function getTableRecords(string memory key) public view returns (uint) {
return tableRecords[key];
}
/**
* @notice 動態建立表
* @param tableName 表的名稱
* 表結構:
* +--------------+---------------------+-------------------------+
* | Field | Type | Desc |
* +--------------+---------------------+-------------------------+
* | key | string | 主鍵 |
* | value | string | 對應value |
* | reserve1 | string | 保留欄位1 |
* | reserve2 | string | 保留欄位2 |
* | reserve3 | string | 保留欄位3 |
* | reserve4 | string | 保留欄位4 |
* | reserve5 | string | 保留欄位5 |
* +--------------+---------------------+-------------------------+
* count = 0 表示成功建立
* 其他值表示建立失敗
*/
function StorageCreate(string memory tableName)public onlyProxy returns(int) {
TableFactory tf = TableFactory(0x1001);
int count = tf.createTable(tableName, "key", "value, reserve1, reserve2, reserve3, reserve4, reserve5");
tableList.push(tableName);
emit CreateResult(count);
return count;
}
/**
* @notice 插入資料
* @dev 限入口合約呼叫
* @param tableName 表的名稱
* @param key 主鍵
* @param _data 包含除key主鍵之外的其他欄位的值集合
* count = 1 表示插入成功
* count = -1 表示key已存在
* 其他值 表示插入失敗
*/
function StorageInsert(string memory tableName, string memory key, string[] memory _data)public onlyProxy returns(int) {
TableFactory tf = TableFactory(0x1001);
Table table = tf.openTable(tableName);
int count = 0 ;
if(_isKeyExist(table, key)){
count = -1 ;
tableRecords[tableName.toSlice().concat("_Insert_BAD".toSlice())] += 1;
}else{
Entry entry = table.newEntry();
entry.set("key", key);
entry.set("value", _data[0]);
if(_data.length>1){
for(uint i=1; i<_data.length; i++){
entry.set("reserve".toSlice().concat(toString(i).toSlice()), _data[i]);
}
}
count = table.insert(key, entry);
tableRecords[tableName.toSlice().concat("_Insert_OK".toSlice())] += 1;
}
emit InsertResult(count);
return count;
}
/**
* @notice 透過key查詢資料,以結構體形式返回
* @param tableName 表的名稱
* @param key 主鍵
*
* result = 0 表示查詢成功
* result = -1 表示查詢失敗
*/
function StorageSelect(string memory tableName, string memory key) public view returns(int , string memory , string[] memory ){
TableFactory tf = TableFactory(0x1001);
Table table = tf.openTable(tableName);
int _result;
string memory _key;
string[] memory _data = new string[](6);
if(_isKeyExist(table, key)){
Condition condition = table.newCondition();
Entry entry = table.select(key, condition).get(int(0));
_data[0]=entry.getString("value");
_data[1]=entry.getString("reserve1");
_data[2]=entry.getString("reserve2");
_data[3]=entry.getString("reserve3");
_data[4]=entry.getString("reserve4");
_data[5]=entry.getString("reserve5");
_result = 0 ;
_key = key;
}else{
_result= -1 ;
_key = key;
_data[0]="";
_data[1]="";
_data[2]="";
_data[3]="";
_data[4]="";
_data[5]="";
}
return (_result, _key, _data);
}
/**
* @notice 透過key新增資料,自動區分是註冊還是更新
* @param tableName 表的名稱
* @param key 主鍵
* @param _data 包含除key主鍵之外的其他欄位的值集合
*
* count = 1 表示新增成功
* 其他值表示更新失敗
*/
function StoragePut(string memory tableName, string memory key, string[] memory _data) public onlyProxy returns(int) {
TableFactory tf = TableFactory(0x1001);
Table table = tf.openTable(tableName);
int count = 0 ;
( int _result, , string[] memory _valueData ) = StorageSelect(tableName, key);
Entry entry = table.newEntry();
entry.set("value", _data[0]);
if(_data.length>1){
for(uint i=1; i<_data.length; i++){
entry.set("reserve".toSlice().concat(toString(i).toSlice()), _data[i]);
}
}
if(_result == 0 ){
Condition condition = table.newCondition();
count = table.update(key, entry, condition);
}else{
count = table.insert(key, entry);
}
tableRecords[tableName.toSlice().concat("_Put".toSlice())] += 1;
emit PutResult(_result, count);
return count;
}
/**
* @notice 透過key更新資料
* @param tableName 表的名稱
* @param key 主鍵
* @param _data 包含除key主鍵之外的其他欄位的值集合
*
* count = 1 表示更新成功
* count = -1 表示key不存在
* 其他值表示更新失敗
*/
function StorageUpdate(string memory tableName, string memory key, string[] memory _data ) public onlyProxy returns(int) {
TableFactory tf = TableFactory(0x1001);
Table table = tf.openTable(tableName);
int count = 0 ;
( int _result, , string[] memory _valueData ) = StorageSelect(tableName, key);
if(_result == 0 ){
Entry entry = table.newEntry();
entry.set("value", _data[0]);
if(_data.length>1){
for(uint i=1; i<_data.length; i++){
entry.set("reserve".toSlice().concat(toString(i).toSlice()), _data[i]);
}
}
Condition condition = table.newCondition();
count = table.update(key, entry, condition);
tableRecords[tableName.toSlice().concat("_Update_OK".toSlice())] += 1;
}else{
count = -1;
tableRecords[tableName.toSlice().concat("_Update_BAD".toSlice())] += 1;
}
emit UpdateResult(count);
return count;
}
/**
* @notice 透過key刪除資料
* @param tableName 表的名稱
* @param key 主鍵
*
* count = 1 標識刪除成功
* count = -1 表示key不存在
* 其他值表示刪除失敗
*/
function StorageRemove(string memory tableName, string memory key) public onlyProxy returns(int){
TableFactory tf = TableFactory(0x1001);
Table table = tf.openTable(tableName);
int count = 0 ;
if(_isKeyExist(table, key)){
Condition condition = table.newCondition();
count = table.remove(key, condition);
tableRecords[tableName.toSlice().concat("_Remove_OK".toSlice())] += 1;
}else{
count = -1;
tableRecords[tableName.toSlice().concat("_Remove_BAD".toSlice())] += 1;
}
emit RemoveResult(count);
return count;
}
/**
* @notice 判讀key值是否存在
* @param tableName 表的名稱
* @param key 主鍵
*/
function isKeyExist(string memory tableName, string memory key) external view returns(bool) {
TableFactory tf = TableFactory(0x1001);
Table table = tf.openTable(tableName);
return _isKeyExist(table, key);
}
function _isKeyExist(Table _table, string memory _id) internal view returns(bool) {
Condition condition = _table.newCondition();
return _table.select(_id, condition).size() != int(0);
}
}
5.4 其他輔助合約可下載附件
六、基於WeBASE-Front交易處理介面實現檔案資料上鍊
對於基於WeBASE-Front交易處理介面實現檔案資訊資料上鍊,主要邏輯是在區塊鏈BaaS介面平臺中接收業務方上送的檔案地址、檔案內容雜湊,檔案歸屬資訊等其他類業務資訊,透過檔案地址到業務方OSS平臺下載檔案,並計算檔案內容雜湊與業務方上送的檔案內容雜湊進行校驗,防止上鍊前的檔案內容篡改,校驗透過後將下載的檔案儲存至IPFS,將IPFS返回的定址雜湊,與業務上送的其他資訊,組裝傳送區塊鏈的交易報文,智慧合約採用CRUD方式,表中預留擴充套件欄位,可將介面平臺重新計算的檔案內容雜湊和IPFS定址雜湊存到預留擴充套件欄位,按此組裝傳送交易到區塊鏈的交易報文,呼叫WeBASE-Front前置服務的傳送交易介面,由WeBASE-Front呼叫WeBASE-Sign進行交易雲簽名,然後傳送交易到區塊鏈節點,實現檔案資訊的上鍊。
WeBASE-Front傳送交易介面的呼叫可參考本作者的另一篇文章《基於WeBASE-Front前置服務傳送交易REST介面呼叫可升級的智慧合約方案》。