本文參考了fabric官網的nodejs版本:https://hyperledger-fabric.readthedocs.io/en/latest/write_first_app.html
使用應用時,網絡中必須要有CA,因為我們需要用CA來注冊管理者和app使用者,然後再以他們的身份去調用智能合約,如以test-network為例,啟動時必須使用如下指令:
./network.sh up createChannel -c mychannel -ca
鍊碼這裡仍然使用上篇部落格所編寫的atcc,在編寫應用之前,我已經把他們送出到了網絡中。
Fabric 2.0,編寫及使用鍊碼
然後建立一個Maven項目,這裡我建立的是一個springboot項目,并将Fabric相關操作封裝在Dao層中。
1.引入必要的依賴
這裡包括springboot的依賴和fabric的依賴。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hyperledger.fabric/fabric-gateway-java -->
<dependency>
<groupId>org.hyperledger.fabric</groupId>
<artifactId>fabric-gateway-java</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.使用者相關的Dao層
首先定義相關的接口,這裡包括一個管理者建立接口和一個根據使用者名建立使用者接口,使用者建立相關的操作是應用群組織的CA進行互動的,不會涉及到鍊碼,即隻是該組織本身進行,不會有其他組織參與,這個過程完成之後,會産生該組織的一個操作者的身份來代表這個組織去調用鍊碼。這個過程會在組織的CA資料庫中留下記錄,是以如果要重新開機項目,務必要删除這一步生成的wallet檔案夾,否則會産生錯誤,因為test-network中新啟動的CA會把資料庫也清除,當然如果是自己啟動并且持久化過的CA應該就不用做這一步了。
/** 建立相關的使用者
* Create by zekdot on 2021/9/21.
*/
public interface UserDao {
// 建立管理者使用者
boolean createAdmin() throws Exception;
// 建立app使用者
boolean createUser(String username) throws Exception;
}
在實作中,admin的建立用的是enroll這個詞,普通使用者的建立用的是register這個詞,我查到StackOverflow中有一個回答:https://stackoverflow.com/questions/55990837/what-is-the-meaning-of-register-and-enroll-in-the-context-of-fabric-ca,大意是register由CA管理者完成,隻需要給一個身份賦予使用者名和密碼以及相關的屬性,在CA資料庫進行記錄,就算完成了registration,這個過程沒有證書被生成,而enroll則涉及到證書以及相關的公私鑰對的生成。
兩個操作都涉及連結到Org1的CA,這裡我把連接配接傳回用戶端的方法進行了封裝:
private HFCAClient getCaclient() throws Exception {
// Create a CA client for interacting with the CA.
Properties props = new Properties();
props.put("pemFile",
"/home/xxx/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem");
props.put("allowAllHostNames", "true");
HFCAClient caClient = HFCAClient.createNewInstance("https://localhost:7054", props);
CryptoSuite cryptoSuite = CryptoSuiteFactory.getDefault().getCryptoSuite();
caClient.setCryptoSuite(cryptoSuite);
return caClient;
}
2.1.管理者使用者的建立
代碼如下,是對createAdmin的實作:
public boolean createAdmin() throws Exception {
// 建立CA用戶端執行個體
HFCAClient caClient = getCaclient();
// 擷取管理身份的錢包,沒有則建立,這裡的寫法會建立在項目所在目錄下,也可以寫成絕對路徑形式
Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet"));
// 檢查是否已經注冊過了管理者身份的使用者,是的話直接退出
if (wallet.get("admin") != null) {
System.out.println("An identity for the admin user \"admin\" already exists in the wallet");
return false;
}
// Enroll the admin user, and import the new identity into the wallet.
final EnrollmentRequest enrollmentRequestTLS = new EnrollmentRequest();
enrollmentRequestTLS.addHost("localhost");
enrollmentRequestTLS.setProfile("tls");
// 進行注冊,得到注冊結果
Enrollment enrollment = caClient.enroll("admin", "adminpw", enrollmentRequestTLS);
// 利用注冊結果生成新的證書
Identity user = Identities.newX509Identity("Org1MSP", enrollment);
// 把證書加入錢包中
wallet.put("admin", user);
System.out.println("Successfully enrolled user \"admin\" and imported it into the wallet");
return true;
}
管理者這裡隻涉及一個enroll操作,因為在ca啟動的時候就已經為管理者進行了register操作
上面這句話是官網上說的,我在fabric-samples/test-network/organizations/fabric-ca的腳本下發現了這條指令
fabric-ca-client enroll -u https://admin:[email protected]:7054 --caname ca-org1 --tls.certfiles "${PWD}/organizations/fabric-ca/org1/tls-cert.pem"
可以說明在建立CA的時候就已經注冊了使用者名為admin,密碼為adminpw的使用者,上述的代碼隻是利用這個使用者向CA申請一張證書放到錢包(wallet)目錄下。上述代碼執行完成之後,在項目目錄下會多出一個wallet檔案夾,裡面有一個admin.id檔案,裡面包括admin的公私鑰等資訊,可以代表admin的身份。
2.2.app使用者的建立
在錢包中有了管理者的身份之後,應用就可以使用管理者身份來注冊一個app使用者用于和區塊鍊網絡進行互動,代碼如下,是對createUser方法的實作:
public boolean createUser() throws Exception {
HFCAClient caClient = getCaclient();
// 擷取的錢包
Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet"));
// 檢查是否已經注冊過
if (wallet.get(username) != null) {
System.out.println("An identity for the user \"appUser\" already exists in the wallet");
return false;
}
// 擷取管理者證書
X509Identity adminIdentity = (X509Identity)wallet.get("admin");
if (adminIdentity == null) {
System.out.println("\"admin\" needs to be enrolled and added to the wallet first");
return false;
}
// 根據管理者的資訊建立使用者實體
User admin = new User() {
@Override
public String getName() {
return "admin";
}
@Override
public Set<String> getRoles() {
return null;
}
@Override
public String getAccount() {
return null;
}
@Override
public String getAffiliation() {
return "org1.department1";
}
@Override
public Enrollment getEnrollment() {
return new Enrollment() {
@Override
public PrivateKey getKey() {
return adminIdentity.getPrivateKey();
}
@Override
public String getCert() {
return Identities.toPemString(adminIdentity.getCertificate());
}
};
}
@Override
public String getMspId() {
return "Org1MSP";
}
};
// Register the user, enroll the user, and import the new identity into the wallet.
RegistrationRequest registrationRequest = new RegistrationRequest("appUser");
registrationRequest.setAffiliation("org1.department1");
registrationRequest.setEnrollmentID("appUser");
String enrollmentSecret = caClient.register(registrationRequest, admin);
Enrollment enrollment = caClient.enroll(username, enrollmentSecret);
Identity user = Identities.newX509Identity("Org1MSP", enrollment);
wallet.put("appUser", user);
System.out.println("Successfully enrolled user \"appUser\" and imported it into the wallet");
return true;
}
可以看到,這裡利用管理者的身份才能發起注冊的請求,注冊成功之後,會生成一張證書代表appUser的身份,同時在CA資料庫中也可以看到相應的記錄,CA資料庫存在于fabric-samples/test-network/organizations/fabric-ca/org1中的哪個db檔案,使用sqlitebrowser可以檢視到其中的内容:
3.合約相關的Dao層
在有了可以和網絡進行互動的身份之後,我們就可以編寫合約相關的方法了,這裡首先對合約引用的擷取進行一個封裝,之後隻需要調用getContract就可以擷取合約引用了。
static {
System.setProperty("org.hyperledger.fabric.sdk.service_discovery.as_localhost", "true");
}
private Gateway connect() throws Exception {
Wallet wallet = Wallets.newFileSystemWallet(Paths.get("wallet"));
Path networkConfigPath = Paths.get("/media/xxx/fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/connection-org1.yaml");
Gateway.Builder builder = Gateway.createBuilder();
builder.identity(wallet, "appUser").networkConfig(networkConfigPath).discovery(true);
return builder.connect();
}
private Contract getContract() throws Exception {
Gateway gateway = connect();
Network network = gateway.getNetwork("mychannel");
Contract contract = network.getContract("atcc");
return contract;
}
可以看到這裡首先擷取了錢包,利用裡面的appUser的身份才能夠連接配接到網絡,另外這裡設定discovery(true)目的是為了讓應用能夠擷取到其他目前線上的peer上的服務。在getContract方法中,我們指明了要互動的通道名稱與鍊碼名稱,這裡如果一個鍊碼中包含多個智能合約,隻需要在network.getContract方法中使用逗号進行隔開。
如果在部署鍊碼的時候沒有調用過初始化賬本的方法,那麼這裡首先需要實作一個初始化賬本的方法,但是我在部署的時候已經對賬本進行了初始化,是以,這裡隻需要實作兩個方法對功能進行示範即可,下面定義了合約Dao層的接口,這裡的傳回值都是String類型,但是實際上也可以是一個類或者是其他正常類型。
最上面那個靜态塊的目的是為了讓應用能夠把fabric的peer等的域名解析為localhost,因為我們的服務都是部署在本地,如果不加這句話會造成peer0和peer1相關域名無法解析的錯誤。
public interface ATCCDao {
/**
* 擷取全部資産
* @return
*/
String getAllAssets() throws Exception;
/**
* 增加一個新資産
* @param ID 資産id
* @param Color 資産顔色
* @param Size 資産大小
* @param Owner 資産所有者
* @param AppraisedValue 估值
* @return
* @throws Exception
*/
String addNewAssets(String ID, String Color, String Size, String Owner, String AppraisedValue) throws Exception;
}
3.1.擷取全部資産
實作getAllAssets方法即可,這裡調用的是evaluateTransaction方法(這個方法隻會從一個peer中去查詢到所需要的資料),傳回的是byte數組,需要進一步轉換成字元串進行傳回。
public String getAllAssets() throws Exception {
Contract contract = getContract();
byte[] result;
result = contract.evaluateTransaction("GetAllAssets");
return new String(result);
}
調用之後可以傳回如下的内容:
[{"AppraisedValue":300,"Color":"blue","ID":"asset1","Owner":"Tomoko","Size":5},{"AppraisedValue":400,"Color":"red","ID":"asset2","Owner":"Brad","Size":5},{"AppraisedValue":500,"Color":"green","ID":"asset3","Owner":"Jin Soo","Size":10},{"AppraisedValue":600,"Color":"yellow","ID":"asset4","Owner":"Max","Size":10},{"AppraisedValue":700,"Color":"black","ID":"asset5","Owner":"Adriana","Size":15},{"AppraisedValue":800,"Color":"white","ID":"asset6","Owner":"Michel","Size":15}]
3.2.增加新資産
實作addNewAssets方法,這裡需要注意兩點,第一是調用的是submitTransaction方法(這個方法會讓所有peer參與到背書操作中,并且内置時間監聽器等來協助整個背書操作的完成),該方法也會傳回一個byte數組,第二點傳入參數的順序需要和智能合約傳入時的一緻,智能合約的更新方法如下:
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error
...
可見,傳入時,需要按照相對應的順序傳入參數。
public String addNewAssets(String Id, String Color, String Size, String Owner, String AppraisedValue) throws Exception {
Contract contract = getContract();
byte[] result;
result = contract.submitTransaction("CreateAsset", Id, Color, Size, Owner, AppraisedValue);
return new String(result);
}
調用之後,我發現這句話的傳回值為空。
然後我們再檢視全部資産,可以看到新加的資産已經生效了:
[{"AppraisedValue":300,"Color":"blue","ID":"asset1","Owner":"Tomoko","Size":5},{"AppraisedValue":400,"Color":"red","ID":"asset2","Owner":"Brad","Size":5},{"AppraisedValue":500,"Color":"green","ID":"asset3","Owner":"Jin Soo","Size":10},{"AppraisedValue":600,"Color":"yellow","ID":"asset4","Owner":"Max","Size":10},{"AppraisedValue":700,"Color":"black","ID":"asset5","Owner":"Adriana","Size":15},{"AppraisedValue":800,"Color":"white","ID":"asset6","Owner":"Michel","Size":15},{"AppraisedValue":1000,"Color":"Blue","ID":"assets7","Owner":"zekdot","Size":20}]
這時我再測試一次加入和剛加入的資産相同的資産試試,因為智能合約的邏輯中如果此資産對應的id已經存在,會傳回一個錯誤資訊。
這時有異常抛出:
org.hyperledger.fabric.gateway.ContractException: No valid proposal responses received. 2 peer error responses: the asset assets7 already exists; the asset assets7 already exists
說明如果我們想知道智能合約的調用是否成功隻能通過對異常的捕捉來進行判斷。