本文参考了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,在编写应用之前,我已经把他们提交到了网络中。
然后创建一个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:adminpw@localhost: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
说明如果我们想知道智能合约的调用是否成功只能通过对异常的捕捉来进行判断。