基於WeBASE-Front為業務賬户分配合約的寫權限及部署合約和創建表權限
作者簡介: 孫運盛 吉科軟信息技術有限公司 大數據技術研究院架構師。負責吉科軟區塊鏈BaaS平台設計研發,基於FISCO BCOS的區塊鏈技術研究以及在智慧農業、智慧城市、智慧食安行業領域的區塊鏈技術應用研發。
一、背景
- FISCO的基於角色的權限控制機制,在不啓用任何鏈委員和運維角色賬户時,每一個賬户都相當於是管理員角色賬户,擁有鏈委員角色的管理權限,無所不能;
- 角色定義分為治理方、運維方、監管方和業務方。且各角色權責分離、角色互斥;各角色擁有不同的權限,不能相互交叉;所以一旦啓用了治理方、運維方角色的賬户,業務方角色的賬户權限將會縮水,受限,不再擁有無所不能的權限,在某些合約架構中有可能會觸發對合約寫權限的限制;
-
我司的區塊鏈BaaS平台中啓用了鏈委員會的治理角色、運維角色賬户,在對合約進行調用時,發現一個權限導致的調用失敗問題,利用運維角色賬户調用合約的寫操作方法可以成功,利用業務角色賬户調用合約的讀操作方法可以成功,調用合約的寫操作方法失敗,詳細的問題復現測試流程見下文。
本文將根據官方的一物一碼商品追溯合約,在開啓鏈委員會治理和運維角色的情況下,對業務角色賬户對合約寫操作的方法調用失敗問題進行詳細測試流程説明,及對應的解決方案説明。為同樣因為權限問題而困擾的童鞋,提供對FISCO基於角色的權限控制更好的理解案例。
二、實驗環境
| 名稱 | 版本號 |
|---|---|
| FISCO BCOS | v2.9.0 |
| WeBASE-Front | v1.5.4 |
| Solidity | ^0.6.10 |
三、權限問題復現
3.1 測試使用的一物一碼追溯合約
基於官方的一物一碼追溯合約,做了一些改動,合約分為三個,分別為Goods.sol,Traceability.sol,TraceabilityFactory.sol,代碼如下:
Goods.sol合約:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
pragma experimental ABIEncoderV2;
struct TraceData {
///上鍊賬户地址
address addr;
///商品流轉環節
int16 status;
///上鍊時間
uint timestamp;
///上鍊摘要數據
string remark;
}
contract Goods {
///@notice 商品當前狀態
int16 _status;
///@notice 商品唯一編碼
string _goodsId;
///@notice 商品追溯數據結構體數組
TraceData[] _traceData;
///@notice 商品追溯數據上鍊事件日誌
event NewStatus(address addr,int16 status,uint timestamp,string remark);
///@notice 商品構造器
constructor(string memory goodsId,int16 goodsStatus,string memory remark) public {
_status = goodsStatus;
_goodsId = goodsId;
_traceData.push(TraceData({addr:tx.origin,status:goodsStatus,timestamp:block.timestamp,remark:remark}));
emit NewStatus(tx.origin, goodsStatus, block.timestamp, remark);
}
///@notice 商品追溯數據上鍊
function changeStatus(int16 goodsStatus,string memory remark) public {
_status = goodsStatus;
_traceData.push(TraceData({addr:tx.origin,status:goodsStatus,timestamp:block.timestamp,remark:remark}));
emit NewStatus(tx.origin, goodsStatus, block.timestamp, remark);
}
///獲取商品當前環節狀態
function getStatus() public view returns(int16){
return _status;
}
///獲取商品已上鍊的各環節追溯數據
function getTraceInfo() public view returns(TraceData[] memory) {
return _traceData;
}
}
Traceability.sol合約:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
pragma experimental ABIEncoderV2;
import "./Goods.sol";
contract Traceability {
struct GoodsData{
Goods traceGoods;
bool isExists;
}
///@notice 商品品類
bytes32 _category;
///@notice 商品品類映射表,key為商品唯一編碼,value為商品結構體
mapping(string => GoodsData) private _goods;
constructor(bytes32 goodsCategory) public {
_category = goodsCategory;
}
///@notice 創建新商品的事件日誌
event NewGoodsEvent(string goodsId);
function changeGoodsStatus(string memory goodsId,int16 goodsStatus,string memory remark) public {
///如果商品編碼不存在,則初始化添加商品,並添加商品追溯信息
if(!_goods[goodsId].isExists){
_goods[goodsId].isExists = true;
Goods traceGoods = new Goods(goodsId,goodsStatus,remark);
emit NewGoodsEvent(goodsId);
_goods[goodsId].traceGoods = traceGoods;
}else {
///否則直接根據商品ID從mapping中找到對應的商品,調用商品的的改變狀態函數添加商品追溯數據
_goods[goodsId].traceGoods.changeStatus(goodsStatus, remark);
}
}
///@notice 獲取指定商品的追溯數據
function getTraceInfo(string memory goodsId) public view returns(TraceData[] memory _data){
require(_goods[goodsId].isExists,"The Goods is not exists");
return _goods[goodsId].traceGoods.getTraceInfo();
}
///@notice 獲取指定商品的當前環節狀態
function getStatus(string memory goodsId) public view returns (int16){
require(_goods[goodsId].isExists,"The Goods is not exists");
return _goods[goodsId].traceGoods.getStatus();
}
}
TraceabilityFactory.sol合約:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
pragma experimental ABIEncoderV2;
import "./Traceability.sol";
contract TraceabilityFactory {
struct GoodsTrace{
Traceability trace;
bool isExists;
}
///@notice 商品品類映射表
mapping(bytes32 => GoodsTrace) private _goodsCategory;
///@notice 新商品生成事件日誌
event NewTraceEvent(bytes32 goodsGroup);
///@notice 商品追溯信息上鍊
function changeTraceGoods(bytes32 goodsGroup,string memory goodsId,int16 goodsStatus,string memory remark) public {
Traceability category = getTraceability(goodsGroup);
category.changeGoodsStatus(goodsId, goodsStatus, remark);
}
///@notice 獲取某個商品的當前環節編碼
function getTraceStatus(bytes32 goodsGroup,string memory goodsId) public view returns(int16 ){
Traceability category = getTraceabilityForSearch(goodsGroup);
return category.getStatus(goodsId);
}
///@notice 獲取商品的追溯信息
function getTraceInfo(bytes32 goodsGroup,string memory goodsId) public view returns(TraceData[] memory _data) {
Traceability category = getTraceabilityForSearch(goodsGroup);
return category.getTraceInfo(goodsId);
}
///@notice 根據商品品類名創建產品組hash值,返回bytes32類型的goodsGroup
function getGoodsGroup(string memory goodsGroupName) public view returns(bytes32){
return keccak256(abi.encode(goodsGroupName));
}
function createTraceability(bytes32 goodsGroup) private returns(Traceability) {
require(!_goodsCategory[goodsGroup].isExists,"The trademark already exists");
Traceability category = new Traceability(goodsGroup);
_goodsCategory[goodsGroup].isExists = true;
_goodsCategory[goodsGroup].trace = category;
emit NewTraceEvent(goodsGroup);
return category;
}
function getTraceability(bytes32 goodsGroup) private returns(Traceability) {
if(!_goodsCategory[goodsGroup].isExists){
createTraceability(goodsGroup);
}
return _goodsCategory[goodsGroup].trace;
}
function getTraceabilityForSearch(bytes32 goodsGroup) private view returns(Traceability) {
require(_goodsCategory[goodsGroup].isExists,"The trademark has not exists" );
return _goodsCategory[goodsGroup].trace;
}
}
TraceabilityFactory.sol為業務調用的入口合約,對外公開的接口列表,如下表所示:
| 接口名稱 | 接口描述 | 讀/寫 |
|---|---|---|
| getGoodsGroup | 根據商品品類名創建產品組hash值 | 讀操作 |
| changeTraceGoods | 商品追溯信息上鍊 | 寫操作 |
| getTraceStatus | 獲取某個商品的當前環節編碼 | 讀操作 |
| getTraceInfo | 獲取商品的追溯信息 | 讀操作 |
3.2 治理、運維、業務角色的賬户地址
鏈委員治理角色賬户地址:0x6eac7c4ff88516ba72e4237bfc721e33af88a933
運維角色賬户地址:0x4c88e3e3764767aa398c29de440c3492df8d2747
業務賬户地址1:0xba7d313fac4caa48f7c937f8212b1b4cbb73697f
業務賬户地址2:0x4acb81ee8f9f777807fc18009f294cd29af4509e
3.3 使用運維角色賬户部署合約
使用運維賬户0x4c88e3e3764767aa398c29de440c3492df8d2747,部署TraceabilityFactory.sol合約,合約地址為:0x3683ac26ede6bdbb2d36712f04aa23b8e073cd14
3.4 使用運維角色賬户調用合約的數據上鍊方法,調用成功
TraceabilityFactory合約的changeTraceGoods方法為追溯數據上鍊,利用運維角色賬户 0x4c88e3e3764767aa398c29de440c3492df8d2747 ,發起交易,如下圖所示:
通過交易回執,可以看到使用運維角色賬户調用合約寫操作接口是成功的。
3.5 使用業務賬户1調用合約的數據上鍊方法,調用失敗
使用業務賬户1: 0xba7d313fac4caa48f7c937f8212b1b4cbb73697f 調用合約的追溯數據上鍊接口
通過交易回執,可以看到交易失敗,返回狀態0x16,RevertInstruction錯誤。
3.6 將業務賬户1升級為運維角色並調用合約數據上鍊方法,調用成功
將業務賬户1 :0xba7d313fac4caa48f7c937f8212b1b4cbb73697f,添加到運維角色,如下圖所示:
然後使用業務賬户1 ,再次調用合約的追溯數據上鍊方法,下圖直接展示交易回執結果:
3.7 使用業務賬户2調用合約的數據上鍊方法,調用失敗
然後再使用業務賬户2:0x4acb81ee8f9f777807fc18009f294cd29af4509e,調用合約的追溯數據上鍊方法,如下圖所示:
從交易回執結果看,業務賬户2調用合約寫操作方法失敗,返回0x16,RevertInstruction錯誤。
四、權限問題分析及解決方案
4.1 分析原因一
從以上測試過程看,運維角色賬户擁有合約的部署、創建、調用寫操作方法的權限,可以對寫操作的方法調用成功,而業務角色賬户調用失敗,但對於讀操作的合約方法可以調用成功,且升級為運維角色賬户後同樣可以對寫操作方法調用成功,推測可能的原因為業務角色賬户對合約寫操作的方法缺少寫權限。
4.2 解決方案一
嘗試為合約分配可寫權限的業務賬户,WeBASE-Front前置服務中的權限分配接口,權限類型分為六種,為權限管理權限permission, 用户表管理權限userTable, 部署合約和創建用户表權限deployAndCreate, 節點管理權限node, 使用CNS權限cns, 系統參數管理權限sysConfig。我們當前推測是業務賬户對合約缺少寫權限,所以以上六種權限分配沒有為合約分配寫權限的類型。
另外合約狀態管理接口中,已經廢棄了為合約分配管理權限的賬户的操作類型。所以我們可自行添加為合約分配寫權限的賬户的接口。以下是改造WeBASE-Front前置服務的分配權限接口的部分關鍵代碼:
PrecompiledUtils.java中增加權限類型:
//2023-07-27 sunyunsheng add
public static final String PERMISSION_TYPE_CONTRACT_WRITE = "contractWrite";
PermissionManagerController.java中增加:
/**
* 2023-07-27 sunyunsheng add
* 為業務賬户分配合約的寫權限
* @param groupId 羣組ID
* @param from 發起賬户
* @param contractAddress 合約地址
* @param userAddress 業務賬户地址
* @return
*/
public Object grantContractWritePermission(int groupId, String from, String contractAddress,
String userAddress) {
if(Objects.isNull(contractAddress)){
log.error("grantContractWritePermission error for contract address is empty");
return ConstantCode.PARAM_FAIL_TABLE_NAME_IS_EMPTY;
}else {
try{
Object res = permissionManageService
.grantContractWritePermission(groupId, from, contractAddress, userAddress);
return res;
} catch (Exception e) {
log.error("end grantContractWritePermission for startTime:{}, Exception:{}",
Instant.now().toEpochMilli(), e);
return new BaseResponse(ConstantCode.FAIL_TABLE_NOT_EXISTS, e.getMessage());
}
}
}
/**
* 2023-07-27 sunyunsheng add
* 撤銷業務賬户對合約的寫權限
* @param groupId 羣組ID
* @param from 發起賬户
* @param contractAddress 合約地址
* @param userAddress 業務賬户地址
* @return
*/
public Object revokeContractWritePermission(int groupId, String from, String contractAddress,
String userAddress) {
if(Objects.isNull(contractAddress)){
log.error("revokeContractWritePermission error for table name is empty");
return ConstantCode.PARAM_FAIL_TABLE_NAME_IS_EMPTY;
}else {
try {
Object res = permissionManageService
.revokeContractWritePermission(groupId, from, contractAddress, userAddress);
return res;
} catch (Exception e) {
log.error("end revokeContractWritePermission for startTime:{}, Exception:{}",
Instant.now().toEpochMilli(), e);
return new BaseResponse(ConstantCode.FAIL_TABLE_NOT_EXISTS, e.getMessage());
}
}
}
/**
* 2023-07-27 sunyunsheng add
* 查詢合約寫權限的權限列表
* @param groupId 羣組ID
* @param contractAddress 合約地址
* @param pageSize 每頁大小
* @param pageNumber 頁碼
* @return
*/
public Object listContractWritePermission(int groupId, String contractAddress, int pageSize, int pageNumber) {
if(Objects.isNull(contractAddress)){
return ConstantCode.PARAM_FAIL_TABLE_NAME_IS_EMPTY;
}else {
List<PermissionInfo> resList = permissionManageService.listContractWritePermission(groupId, contractAddress);
if(resList.size() != 0) {
List2Page<PermissionInfo> list2Page = new List2Page<>(resList, pageSize, pageNumber);
List<PermissionInfo> finalList = list2Page.getPagedList();
long totalCount = (long) resList.size();
return new BasePageResponse(ConstantCode.RET_SUCCESS, finalList, totalCount);
} else {
return new BasePageResponse(ConstantCode.RET_SUCCESS_EMPTY_LIST, resList, 0);
}
}
}
PermissionManagerService.java中增加:
/**
* 2023-07-28 sunyunsheng add
* 為合約分配可寫權限的賬户地址
* @param groupId 羣組ID
* @param signUserId 簽名服務用户ID
* @param contractAddress 合約地址
* @param userAddress 分配的用户賬户地址
* @return
* @throws Exception
*/
public Object grantContractWritePermission(int groupId, String signUserId, String contractAddress,
String userAddress) throws Exception {
String res = precompiledWithSignService.grantWrite(groupId, signUserId, contractAddress, userAddress);
return res;
}
/**
* 2023-07-28 sunyunsheng add
* 撤銷合約分配可寫權限的賬户地址
* @param groupId 羣組ID
* @param signUserId 簽名服務用户ID
* @param contractAddress 合約地址
* @param userAddress 分配的用户賬户地址
* @return
*/
public Object revokeContractWritePermission(int groupId, String signUserId, String contractAddress,
String userAddress) {
String res = precompiledWithSignService.revokeWrite(groupId, signUserId, contractAddress, userAddress);
return res;
}
/**
* 查詢合約寫權限的權限列表
* @param groupId
* @param constractAddress
* @return
*/
public List<PermissionInfo> listContractWritePermission(int groupId, String constractAddress) {
PermissionService permissionService = new PermissionService(web3ApiService.getWeb3j(groupId),
keyStoreService.getCredentialsForQuery());
try {
return permissionService.queryPermission(constractAddress);
} catch (Exception e) {
log.error("listContractWritePermission fail:[]", e);
throw new FrontException(ConstantCode.GET_LIST_MANAGER_FAIL);
}
}
然後在分配、去除、查詢權限的接口中,增加switch分支,以處理新增加的權限類型 contractWrite。
在區塊鏈BaaS平台中增加相應的為合約分配寫權限的業務賬户功能,如下圖所示:
為合約添加可寫權限的業務賬户2之後,再次測試業務賬户2發起上鍊交易請求,交易回執結果如下圖所示:
由此可見,單單為業務賬户分配合約的寫權限仍舊未解決問題。
4.3 分析原因二
根據交易回執結果的狀態為0x16,錯誤信息 RevertInstruction,官方給出的交易執行失敗問題排查方向中列舉了可引起此錯誤的情況,如下所示:
2. revert instruction
問題描述:
交易回滾,交易回執狀態值為0x16,錯誤描述revert instruction,這個錯誤是因為合約的邏輯問題,包括:
訪問調用未初始化的合約
訪問初始化為0x0的合約
數組越界訪問
除零錯誤
調用assert、revert
其他錯誤
解決方法:
檢查合約邏輯,修復漏洞。
既然是合約邏輯問題,那説明還是合約內的某些地方引起的,但運維賬户可以調用成功,所以合約方法本身的邏輯應該是不存在漏洞的, 引起該錯誤的其中一種情況有“訪問調用未初始化的合約”,根據這種情況,我們檢查合約的changeTraceGoods方法的源碼,
///@notice 商品追溯信息上鍊
function changeTraceGoods(bytes32 goodsGroup,string memory goodsId,int16 goodsStatus,string memory remark) public {
Traceability category = getTraceability(goodsGroup);
category.changeGoodsStatus(goodsId, goodsStatus, remark);
}
function getTraceability(bytes32 goodsGroup) private returns(Traceability) {
if(!_goodsCategory[goodsGroup].isExists){
createTraceability(goodsGroup);
}
return _goodsCategory[goodsGroup].trace;
}
function createTraceability(bytes32 goodsGroup) private returns(Traceability) {
require(!_goodsCategory[goodsGroup].isExists,"The trademark already exists");
Traceability category = new Traceability(goodsGroup);
_goodsCategory[goodsGroup].isExists = true;
_goodsCategory[goodsGroup].trace = category;
emit NewTraceEvent(goodsGroup);
return category;
}
通過合約源碼分析,當合約方法傳入的goodsGroup商品分組hash值不存在時,合約內部使用了 Traceability category = new Traceability(goodsGroup);進行創建Traceability合約,new關鍵字的作用是創建合約,由此可推測,如果當前發起調用的賬户不具有創建合約的權限的話,此處就會創建合約失敗,也就會造成 “訪問調用未初始化的合約”,從而引起回滾,RevertInstruction錯誤。
4.4 解決方案二
根據分析原因二,推測是由業務賬户不具有創建合約權限引起的,那麼就需要為業務賬户單獨分配創建合約權限,WeBASE-Front前置服務的權限分配類型中提供了一種權限類型為部署合約和創建用户表權限deployAndCreate 的權限,為默認提供支持的權限類型,可以直接調用權限分配接口為業務賬户分配該類型權限。
改造區塊鏈BaaS平台中的鏈管理模塊功能,增加業務賬户分配權限的功能,並給業務賬户2 :0x4acb81ee8f9f777807fc18009f294cd29af4509e分配 部署合約和創建表權限。
再次使用業務賬户2,調用合約的追溯數據上鍊方法,調用成功,交易回執結果如下圖所示:
通過交易回執結果,可以看出,當業務賬户2分配部署合約和創建表的權限後,合約追溯數據上鍊的方法調用執行成功,可斷定當前業務賬户在沒有分配部署合約和創建表權限之前,是不具有創建合約的權限,從而導致合約內使用new關鍵字創建合約時會造成失敗回滾。
五、測試流程總結
1、運維賬號調用合約的寫方法,調用成功;
2、業務賬號1調用合約的寫方法,調用失敗;
3、業務賬號1添加到運維角色,調用合約寫接口,成功;
4、使用業務賬號2調用合約寫方法,調用失敗;
5、分配業務賬號2對合約的寫權限,(重寫前置服務的權限分配接口,增加contractWrite權限類型的接口,包括分配、撤銷、查詢權限列表三個接口);
6、使用已分配合約寫權限的業務賬號2調用合約寫方法,調用失敗;
7、為業務賬號2分配deployAndCreate(部署合約和創建表的權限)
8、使用業務賬號2對合約的寫方法調用,調用成功
六、方案總結
FISCO-BCOS的基於角色權限控制機制,一旦啓用鏈委員治理角色、運維角色時,基於角色權限互斥原則,普通業務賬户的權限會縮減,在業務角色賬户調用合約時,遇到使用高於業務角色的權限執行時,就需要額外單獨給業務角色分配所需的權限。分配權限時可以利用WeBASE-Front前置服務的權限分配接口,接口地址是/permission。
注意事項:
- 在測試為合約分配寫權限的業務賬户時,要注意必須使用合約部署者來發起該權限分配的調用,該權限分配調用的是預編譯合約1005,所以部署業務合約時最好是使用運維賬號進行部署,調用權限分配的交易發起賬户為業務合約的部署者,否則會提示 “permission denied”的錯誤。
- 在排查調用合約出現的錯誤時,一定要注重錯誤碼的分類,根據錯誤碼找到可能引起該錯誤的所有原因,同時結合合約內的邏輯及發起調用的賬户角色、權限進行綜合性研判分析,逐步定位錯誤的真正原因。
- 要謹慎使用權限分配,小心角色權限之間的互斥機制,要充分了解角色權限互斥的原理,防止造成各角色間莫名的權限混亂