導(dǎo)讀:由于 區(qū)塊鏈具有去中心雷客、不可逆等特點(diǎn),天然適合作為證據(jù)儲存的載體。360存證云是360區(qū)塊鏈實(shí)驗(yàn)室基于以太坊開發(fā)的電子證據(jù)存證系統(tǒng),本文簡單介紹了其中鏈存儲部分的設(shè)計(jì)思路和一些實(shí)現(xiàn)細(xì)節(jié)吐葱。
一、電子存證技術(shù)概述
傳統(tǒng)的電子存證簡單來說就是將源信息經(jīng)過加密存儲在一個(gè)具有公信力的獨(dú)立第三方處校翔,并綁定時(shí)間戳弟跑、創(chuàng)建人等信息用來證明在某個(gè)時(shí)間點(diǎn)存在這樣的信息。舉例來說防症,對于原創(chuàng)作品的保護(hù)孟辑,作者可以在創(chuàng)作完成后第一時(shí)間進(jìn)行電子存證哎甲,以保證在以后出現(xiàn)侵權(quán)后證明自己最早創(chuàng)作了作品從而保護(hù)自己的權(quán)益。
電子存證的源信息可以是一段文本饲嗽,文檔炭玫,圖片,視頻等形式貌虾。對于這種各式各樣的形式吞加,電子存證一般存儲的是源信息的哈希摘要;哈希是一段定長的比特串酝惧,類似于源信息的指紋榴鼎,源信息只要改變,哈希就會和原來的完全不一樣晚唇,由于哈衔撞疲基本上是不可遍歷的,所以在現(xiàn)實(shí)中可以認(rèn)為哈希和源信息一一對應(yīng)哩陕。哈希的這種特性普遍應(yīng)用在文件指紋等場景平项,例如下載文件中的哈希校驗(yàn)。電子存證存儲的哈希值是可以證明源信息真實(shí)未經(jīng)篡改悍及。哈希的另一個(gè)特性是無法從哈希摘要反推出原始信息闽瓢,所以這樣也保證了一些敏感信息的隱私性。
二心赶、區(qū)塊鏈電子存證的優(yōu)勢
對于傳統(tǒng)的電子存證扣讼,具有公信力的獨(dú)立第三方是一個(gè)很重要的角色;需要存證缨叫、取證椭符、驗(yàn)證的各方都無條件信任。這樣的第三方權(quán)限過于集中耻姥,如果第三方惡意修改數(shù)據(jù)销钝,則基本無從查證;所以這樣的第三方只能通過非技術(shù)的其他手段保證不去作惡琐簇。而區(qū)塊鏈本身通過一環(huán)套一環(huán)的鏈?zhǔn)浇Y(jié)構(gòu)蒸健、分布式的存儲、分布式的共識機(jī)制將這樣過大的權(quán)利分散到所有參與者身上婉商,保證了不產(chǎn)生這樣一個(gè)權(quán)限過大的中心化第三方來具有作惡的可能似忧。
通過區(qū)塊鏈解決的存證中的信任問題,基于這樣的一個(gè)前提据某,我們設(shè)計(jì)了基于以太坊的電子存證應(yīng)用橡娄。
三、區(qū)塊鏈存證合約設(shè)計(jì)
區(qū)塊鏈上的數(shù)據(jù)經(jīng)過礦工打包進(jìn)區(qū)塊中后基本上不可能更改癣籽,所以存證合約設(shè)計(jì)時(shí)候只需要做簡單的讀寫操作挽唉。我們設(shè)計(jì)的存證合約讀寫的數(shù)據(jù)結(jié)構(gòu)為:
struct Abstract {
uint timestamp;
address sender;
uint version;
bytes32 hash;
byte[512] extend;
}
數(shù)據(jù)結(jié)構(gòu)中包含時(shí)間戳滤祖,調(diào)用存證合約的地址、存證的哈希值瓶籽、擴(kuò)展字段和標(biāo)識版本的 version 字段匠童。擴(kuò)展字段和版本由使用方自定義編碼和解碼方式。通過這樣的數(shù)據(jù)結(jié)構(gòu)塑顺,構(gòu)造一個(gè) mapping(bytes32 => Abstract) Map 來用于分別保存存證信息汤求,Map 結(jié)構(gòu)的 Key 可以簡單設(shè)置為存證的哈希值或者其他可追溯的值。
在這樣結(jié)構(gòu)上再封裝對 Map 的讀寫操作就是一個(gè)簡單的存證合約严拒⊙镄鳎可是由于區(qū)塊鏈的特性,合約一旦上鏈后就不能更改了裤唠,所以如果合約邏輯出現(xiàn)漏洞就影響比較大挤牛,并且不能修復(fù),重新部署合約又會丟失原有的數(shù)據(jù)种蘸,這樣設(shè)計(jì)的合約是不可維護(hù)的墓赴。所以設(shè)計(jì)對這樣的合約進(jìn)行更改,將使用方直接調(diào)用 Map 操作的讀寫進(jìn)行切斷航瞭,在中間加入一個(gè)訪問控制的合約層诫硕,這樣經(jīng)過修改的合約結(jié)構(gòu)如下:
底層數(shù)據(jù)層合約:僅封裝對 Map 結(jié)構(gòu)的讀寫操作,不設(shè)計(jì)具體的業(yè)務(wù)邏輯刊侯;在合約層加入權(quán)限控制章办,維護(hù)訪問地址的白名單,僅白名單內(nèi)部的地址具有操作合約數(shù)據(jù)的權(quán)限滨彻;僅合約部署者具有控制白名單的權(quán)限纲菌。
上層邏輯合約:封裝了簡單的存證業(yè)務(wù)邏輯,上層邏輯沒有數(shù)據(jù)存儲操作疮绷,在合約部署時(shí)候傳入底層合約的地址作為參數(shù),數(shù)據(jù)存儲通過合約調(diào)用底層合約來實(shí)現(xiàn)嚣潜。
這樣分層后冬骚,一旦上層邏輯出現(xiàn)問題,可以通過管理員吊銷上層合約的讀寫訪問權(quán)限來阻止進(jìn)一步的損失懂算;合約升級是通過部署新的上層合約只冻,賦予新的上層合約權(quán)限,吊銷舊上層合約權(quán)限來實(shí)現(xiàn)计技;底層合約出現(xiàn)問題喜德,也可以通過升級上層合約,在邏輯上繞過垮媒。
具體底層合約的代碼如下:
pragma solidity ^0.4.17;
contract DataModel {
struct Abstract {
uint timestamp;
address sender;
uint version;
bytes32 hash;
byte[512] extend;
}
mapping(bytes32 => Abstract) abstractData;
mapping(address => bool) public allowedMap;
address[] public allowedArray;
event AddressAllowed(address _handler, address _address);
event AddressDenied(address _handler, address _address);
event DataSaved(address indexed _handler, uint timestamp, address indexed sender, uint version, bytes32 hash);
event ExtendSaved(address indexed _handler, byte[512] extend);
event ExtendNotSave(address indexed _handler, uint version, byte[512] extend);
function DataModel() public {
allowedMap[msg.sender] = true;
allowedArray.push(msg.sender);
}
modifier allow() {
require(allowedMap[msg.sender] == true);
_;
}
function allowAccess(address _address) allow public {
allowedMap[_address] = true;
allowedArray.push(_address);
AddressAllowed(msg.sender, _address);
}
function denyAccess(address _address) allow public {
allowedMap[_address] = false;
AddressDenied(msg.sender, _address);
}
function getData(bytes32 _key) public view returns(uint, address, uint, bytes32, byte[512]) {
return (
abstractData[_key].timestamp,
abstractData[_key].sender,
abstractData[_key].version,
abstractData[_key].hash,
abstractData[_key].extend
);
}
function setData(bytes32 _key, uint timestamp, address sender, uint version, bytes32 hash) allow public {
abstractData[_key].timestamp = timestamp;
abstractData[_key].sender = sender;
abstractData[_key].version = version;
abstractData[_key].hash = hash;
DataSaved(msg.sender, timestamp, sender, version, hash);
}
function setExtend(bytes32 _key, byte[512] extend) allow public {
if (abstractData[_key].version > 0) {
for (uint256 i; i < 512; i++) {
abstractData[_key].extend[i] = extend[i];
}
ExtendSaved(msg.sender, extend);
} else {
ExtendNotSave(msg.sender, abstractData[_key].version, extend);
}
}
}
上層合約的代碼如下:
pragma solidity ^0.4.20;
import "./data-model.sol";
contract Storage {
DataModel dataModel;
uint currentVersion = 1;
event StorageSaved(address handler, bytes32 indexed hashKey, uint timestamp, uint version, byte[512] extend);
function Storage(address dataModelAddress) public {
dataModel = DataModel(dataModelAddress);
// require(dataModelAddress.delegatecall(bytes4(keccak256("allowAccess(address)")), this));
}
function getData(bytes32 key) public view returns(uint timestamp, address sender, uint version, bytes32 hashKey, string extend) {
byte[512] memory extendByte;
(timestamp, sender, version, hashKey, extendByte) = dataModel.getData(key);
bytes memory bytesArray = new bytes(512);
for (uint256 i; i < 512; i++) {
bytesArray[i] = extendByte[i];
}
extend = string(bytesArray);
return(timestamp, sender, version, hashKey, extend);
}
function saveData(bytes32 hashKey, byte[512] extend) public {
dataModel.setData(hashKey, block.timestamp, msg.sender, currentVersion, hashKey);
dataModel.setExtend(hashKey, extend);
StorageSaved(msg.sender, hashKey, block.timestamp, currentVersion, extend);
}
}
四舍悯、存證應(yīng)用和以太坊區(qū)塊鏈的交互
我們存證應(yīng)用采用的是 Go 語言開發(fā)航棱,通過 RPC 調(diào)用和鏈進(jìn)行交互;由于采用 Go 語言開發(fā)萌衬,而正好以太坊官方提供 go-ethereum 的開源代碼饮醇,所以以太坊 SDK 這塊就直接選用這份開源代碼;代碼中不僅有主動(dòng)調(diào)用 RPC 接口秕豫,而且需要接收節(jié)點(diǎn)推送的合約事件朴艰,所以 RPC 調(diào)用基于的是 WebSocket 協(xié)議,需要節(jié)點(diǎn)開啟 WebSocket RPC 調(diào)用支持混移,可以通過啟動(dòng)參數(shù) --ws --wsaddr value --wsport value --wsapi value 來實(shí)現(xiàn)WIKI祠墅,或者通過 JavaScript Console 的 Admin API來開啟。
調(diào)用 Go SDK 的基本流程如下(代碼省略錯(cuò)誤處理等邏輯歌径,僅保留核心流程):
import (
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
// 初始化 RPC 連接
RPCClient, _ := rpc.Dial(conf.BlockChainConf.RPCUrl)
// 初始化 ethclient
cli := ethclient.NewClient(RPCClient)
// 導(dǎo)入 ABI 接口字符串
parsedABI, _ := abi.JSON(strings.NewReader(evidenceABI))
// 初始化合約實(shí)例
evidence := bind.NewBoundContract(conf.BlockChainConf.ContractAddress, parsedABI, cli, cli, nil)
// 初始化上下文
ctx, cancel := context.WithTimeout(context.Background(), conf.BlockChainConf.ConnTimeout)
defer cancel()
// 交易簽名私鑰
auth := bind.NewKeyedTransactor(account.PrivateKey)
auth.Context = ctx
// 調(diào)用 RPC 發(fā)送存證合約交易
tx, _ := evidence.Transact(auth, "saveData", hash, stringToBytes512(extend))
最終返回的 tx 則為交易信息毁嗦,這時(shí)候交易并沒有即時(shí)出塊,需要等待出塊節(jié)點(diǎn)出塊沮脖;這里通過監(jiān)聽合約的日志事件來實(shí)現(xiàn):
// 訂閱事件的過濾條件金矛,這里傳入合約的地址
query := ethereum.FilterQuery{
Addresses: []common.Address{conf.BlockChainConf.ContractAddress},
}
// Log 通道接收
var logChan = make(chan types.Log)
ctx := context.Background()
// 初始化客戶端
client, _ := blockchain.InitClient()
// 初始化事件監(jiān)聽
subscribe, _ := client.SubscribeFilterLogs(ctx, query, logChan)
// 同樣解析出 ABI 合約接口
parsedABI, _ := abi.JSON(strings.NewReader(evidenceABI))
// 收到的事件結(jié)構(gòu),和合約代碼中數(shù)據(jù)結(jié)構(gòu)對應(yīng)
var receivedData struct {
Handler common.Address
HashKey common.Hash
Timestamp *big.Int
Version *big.Int
Extend Bytes512}for {
select {
case err := <-evt.Subscribe.Err():
fmt.Printf("receive Error: %s\n", err.Error())
case log := <-LogChan:
// 解包收到的 Log勺届,receivedData 則為接收事件的數(shù)據(jù)
err := parsedABI.Unpack(&receivedData, "StorageSaved", log.Data)
}
}
通過這樣子驶俊,就可以在區(qū)塊鏈出塊后接受到事件,保證合約方法的成功調(diào)用
取證一種方式是通過調(diào)用合約的 getData 方法來做免姿,和寫入存證數(shù)據(jù)代碼大同小異饼酿,如下:
// 對應(yīng)存證的 evidence.Transact 方法
err = evidence.Call(callOpts, &output, "getData", key)
另一種方式是通過合約的 Log 過濾來實(shí)現(xiàn),如下:
// 這里過濾條件選用合約中 Map 數(shù)據(jù)的 Key
query := ethereum.FilterQuery{
Topics: [][]common.Hash{[]common.Hash{}, []common.Hash{hashKey}},
}
// 調(diào)用 Client 的 FilterLogs 方法
logs, err := client.FilterLogs(ctx, query)
// 接著類似于監(jiān)聽事件那里胚膊,解包收到的 Log 得到數(shù)據(jù)
五故俐、結(jié)語
存證和區(qū)塊鏈結(jié)合是一個(gè)和合適透明的場景,利用區(qū)塊鏈解決的存證中存在的第三方信任問題紊婉;可是司法并沒有跟上技術(shù)進(jìn)步的節(jié)奏药版;存證現(xiàn)在還處于技術(shù)實(shí)現(xiàn)階段,距離真正落地使用應(yīng)該還有一段距離喻犁,這些都需要我們時(shí)刻關(guān)注相關(guān)信息槽片。
本文摘自 360區(qū)塊鏈實(shí)驗(yàn)室 公眾號文章基于以太坊區(qū)塊鏈的電子存證應(yīng)用,版權(quán)歸原作者所有