Các ứng dụng sẽ gọi đến các chaincode để tương tác với ledger. Minh họa như sau:

Info

Ngôn ngữ lập trình của ứng dụng và chaincode có thể khác nhau. Ví dụ: chúng ta có thể viết ứng dụng bằng Go và viết chaincode bằng Java.

Setup the Blockchain Network

Trước tiên, ta sẽ thiết lập network để deploy chaincode.

Dọn dẹp network của lần chạy trước:

./network.sh down

Khởi động network và tạo channel với CA (Certificate Authorites) thay vì sử dụng cryptogen:

./network.sh up createChannel -c mychannel -ca

Ngoài orderer và hai peer, câu lệnh trên còn tạo ra 3 CA container cho ba node:

hyperledger/fabric-ca:latest | 0.0.0.0:9054->9054/tcp, :::9054->9054/tcp, 7054/tcp, 0.0.0.0:19054->19054/tcp, :::19054->19054/tcp | ca_orderer
hyperledger/fabric-ca:latest | 0.0.0.0:7054->7054/tcp, :::7054->7054/tcp, 0.0.0.0:17054->17054/tcp, :::17054->17054/tcp           | ca_org1
hyperledger/fabric-ca:latest | 0.0.0.0:8054->8054/tcp, :::8054->8054/tcp, 7054/tcp, 0.0.0.0:18054->18054/tcp, :::18054->18054/tcp | ca_org2

Các CA sẽ tự động tạo các crypto material (các cặp khóa, chứng chỉ, …) cho các node.

Deploy chaincode lên channel bằng câu lệnh sau:

./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go/ -ccl go

Câu lệnh trên sẽ đóng gói, cài đặt chaincode lên các peer cũng như là approve và commit chaincode definition xuống ledger.

Kế tiếp, ta sẽ sử dụng ứng dụng mẫu được viết bằng Java ở trong thư mục fabric-samples/asset-transfer-basic/application-gateway-java để tương tác với chaincode. Danh sách các dependency của ứng dụng này:

dependencies {
    implementation 'org.hyperledger.fabric:fabric-gateway:1.4.0'
    compileOnly 'io.grpc:grpc-api:1.59.0'
    runtimeOnly 'io.grpc:grpc-netty-shaded:1.59.0'
    implementation 'com.google.code.gson:gson:2.10.1'
}

Trong đó, dependency org.hyperledger.fabric:fabric-gateway là quan trọng nhất bởi vì nó cung cấp API của Fabric Gateway để ứng dụng có thể kết nối đến Fabric Gateway, sử dụng một client identity (định danh người dùng) cụ thể, xác thực và submit transaction cũng như là nhận các event.

Build mã nguồn:

./gradlew build

Run the Sample Application

Khi chúng ta khởi chạy network ở trên, sẽ có một vài identity được tạo ra bởi CA. Ứng dụng sẽ sử dụng một trong các identity đó để giao tiếp với blockchain network.

Do ứng dụng sử dụng plugin application của Gradle:

plugins {
    // Apply the application plugin to add support for building a CLI application.
    id 'application'
    
}

Nên ta có thể dùng lệnh sau để chạy ứng dụng:

./gradlew run

Chúng ta sẽ phân tích mã nguồn của ứng dụng mẫu để hiểu rõ hơn cách tương tác với chaincode thông qua code.

First, Establish a gRPC Connection to the Gateway

Trước tiên, ứng dụng mẫu sẽ tạo ra một kết nối gRPC đến Fabric Gateway dùng để thực hiện giao dịch ở trong blockchain network. Sẽ phải có một peer đứng ra chạy Fabric Gateway và sẽ an toàn hơn nếu ứng dụng kết nối đến peer chạy Fabric Gateway trong cùng một tổ chức. Nếu trong tổ chức không có peer nào chạy Fabric Gateway thì có thể dùng Fabric Gateway tin cậy của tổ chức khác.

Để thực hiện kết nối đến Fabric Gateway, ta cần có endpoint của Fabric Gateway và nếu nó có cấu hình TLS thì ta sẽ cần phải có thêm các chứng chỉ TLS:

import io.grpc.Grpc;
import io.grpc.TlsChannelCredentials;
//...
 
public final class App {
	// Path to crypto materials.
	private static final Path CRYPTO_PATH = Paths.get("../../test-network/organizations/peerOrganizations/org1.example.com");
	// Path to peer tls certificate.
	private static final Path TLS_CERT_PATH = CRYPTO_PATH.resolve(Paths.get("peers/peer0.org1.example.com/tls/ca.crt"));
	//...
 
	// Gateway peer end point.
	private static final String PEER_ENDPOINT = "localhost:7051";
	private static final String OVERRIDE_AUTH = "peer0.org1.example.com";
 
	private static ManagedChannel newGrpcConnection() throws IOException {
		var credentials = TlsChannelCredentials.newBuilder()
				.trustManager(TLS_CERT_PATH.toFile())
				.build();
		return Grpc.newChannelBuilder(PEER_ENDPOINT, credentials)
				.overrideAuthority(OVERRIDE_AUTH)
				.build();
	}
	// ...
}

Có thể thấy, peer0 của Org1 đang chạy Fabric Gateway.

Đoạn code trên thực hiện những công việc sau:

  • Nạp lên chứng chỉ TLS của peer0 thuộc Org1.
  • Sử dụng chứng chỉ này để tạo ra các credential cho gRPC channel.
  • Thiết lập kết nối gRPC với peer chạy Fabric Gateway với hostname là peer0.org1.example.com.

Second, Create a Gateway Connection

Kế đến, ứng dụng sẽ tạo ra một kết nối Gateway để truy cập vào các channel cũng như là các chaincode được deploy ở trong các channel đó. Một kết nối Gateway yêu cầu ba thứ:

  1. Một kết nối gRPC đến Fabric Gateway.
  2. Danh tính của client dùng để thực hiện giao dịch ở trong network.
  3. Signer để tạo chữ ký số cho danh tính của người dùng.

Ứng dụng mẫu sử dụng chứng chỉ thuộc chuẩn X.509 (X.509 Certificate) của Org1 để định danh và tạo ra signer dựa trên khóa riêng tư:

import org.hyperledger.fabric.client.identity.Identities;
import org.hyperledger.fabric.client.identity.Identity;
import org.hyperledger.fabric.client.identity.Signer;
 
import java.nio.file.Files;
//...
 
public final class App {
	private static final String MSP_ID = System.getenv().getOrDefault("MSP_ID", "Org1MSP");
	//...
 
	// Path to crypto materials.
	private static final Path CRYPTO_PATH = Paths.get("../../test-network/organizations/peerOrganizations/org1.example.com");
	// Path to user certificate.
	private static final Path CERT_PATH = CRYPTO_PATH.resolve(Paths.get("users/User1@org1.example.com/msp/signcerts/cert.pem"));
	// Path to user private key directory.
	private static final Path KEY_DIR_PATH = CRYPTO_PATH.resolve(Paths.get("users/User1@org1.example.com/msp/keystore"));
	//...
 
	private static Identity newIdentity() throws IOException, CertificateException {
		var certReader = Files.newBufferedReader(CERT_PATH);
		var certificate = Identities.readX509Certificate(certReader);
	
		return new X509Identity(MSP_ID, certificate);
	}
	
	private static Signer newSigner() throws IOException, InvalidKeyException {
		var keyReader = Files.newBufferedReader(getPrivateKeyPath());
		var privateKey = Identities.readPrivateKey(keyReader);
	
		return Signers.newPrivateKeySigner(privateKey);
	}
	
	private static Path getPrivateKeyPath() throws IOException {
		try (var keyFiles = Files.list(KEY_DIR_PATH)) {
			return keyFiles.findFirst().orElseThrow();
		}
	}
	//...
}

Sau khi có đủ ba thành phần trên thì ứng dụng thực hiện tạo kết nối Gateway:

import org.hyperledger.fabric.client.Gateway;
//...
 
public final class App {
	//...
 
	public static void main(final String[] args) throws Exception {
		// The gRPC client connection should be shared by all Gateway connections to
		// this endpoint.
		var channel = newGrpcConnection();
	
		var builder = Gateway.newInstance().identity(newIdentity()).signer(newSigner()).connection(channel)
				// Default timeouts for different gRPC calls
				.evaluateOptions(options -> options.withDeadlineAfter(5, TimeUnit.SECONDS))
				.endorseOptions(options -> options.withDeadlineAfter(15, TimeUnit.SECONDS))
				.submitOptions(options -> options.withDeadlineAfter(5, TimeUnit.SECONDS))
				.commitStatusOptions(options -> options.withDeadlineAfter(1, TimeUnit.MINUTES));
	
		try (var gateway = builder.connect()) {
			new App(gateway).run();
		} finally {
			channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
		}
	}
	//...
}

Constructor của lớp App sẽ được mô tả ở bên dưới.

Phương thức run là một tập các lời gọi đến các phương thức của chaincode chẳng hạn như InitLedger(), GetAllAssets(), CreateAsset(), …

Third, Access the Smart Contract to Be Invoked

Sau khi tạo kết nối Gateway, ứng dụng mẫu sẽ truy cập đến channel mychannel và truy xuất chaincode basic từ channel đó:

import org.hyperledger.fabric.client.Gateway;
//...
 
public final class App {
	private static final String CHANNEL_NAME = System.getenv().getOrDefault("CHANNEL_NAME", "mychannel");
	private static final String CHAINCODE_NAME = System.getenv().getOrDefault("CHAINCODE_NAME", "basic");
	//...
 
	private final Contract contract;
	//...
 
	public App(final Gateway gateway) {
		// Get a network instance representing the channel where the smart contract is
		// deployed.
		var network = gateway.getNetwork(CHANNEL_NAME);
	
		// Get the smart contract from the network.
		contract = network.getContract(CHAINCODE_NAME);
	}
	//...
}

Fourth, Populate the Ledger with Sample Assets

Sau khi chaincode được deploy, trạng thái của ledger là rỗng. Ứng dụng mẫu sẽ sử dụng phương thức submitTransaction() để invoke phương thức InitLedger() của chaincode nhằm khởi tạo dữ liệu ban đầu cho ledger:

/**
 * This type of transaction would typically only be run once by an application
 * the first time it was started after its initial deployment. A new version of
 * the chaincode deployed later would likely not need to run an "init" function.
 */
private void initLedger() throws EndorseException, SubmitException, CommitStatusException, CommitException {
	System.out.println("\n--> Submit Transaction: InitLedger, function creates the initial set of assets on the ledger");
 
	contract.submitTransaction("InitLedger");
 
	System.out.println("*** Transaction committed successfully");
}

Về bản chất, submitTransaction() sẽ sử dụng Fabric Gateway để:

  1. Chứng thực transaction được đề xuất (transaction proposal).
  2. Gửi transaction đã được chứng thực cho ordering service.
  3. Chờ cho transaction được commit và cập nhật trạng thái của ledger.

Fifth, Invoke Transaction Functions to Read and Write Assets

Đến bước này, ứng dụng đã có thể invoke các hàm của chaincode thông qua object contract.

Query All Assets

Để gọi read-only method của contract, ta sử dụng phương thức evaluateTransaction():

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParser;
//...
 
import java.nio.charset.StandardCharsets;
//...
 
public final class App {
	private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
	//...
 
	/**
	 * Evaluate a transaction to query ledger state.
	 */
	private void getAllAssets() throws GatewayException {
		System.out.println("\n--> Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger");
	
		var result = contract.evaluateTransaction("GetAllAssets");
		
		System.out.println("*** Result: " + prettyJson(result));
	}
	
	private String prettyJson(final byte[] json) {
		return prettyJson(new String(json, StandardCharsets.UTF_8));
	}
	
	private String prettyJson(final String json) {
		var parsedJson = JsonParser.parseString(json);
		return gson.toJson(parsedJson);
	}
}

Tóm tắt đoạn code trên:

  1. Gọi phương thức GetAllAssets() của chaincode, phương thức này sẽ trả về một mảng byte.
  2. Chuyển mảng byte thành một chuỗi có encoding type là UTF-8.
  3. Chuyển chuỗi đã được encode thành một JsonElement bằng thư viện Gson.
  4. Chuyển JsonElement thành dạng chuỗi JSON.

Note

Kết quả trả về của các hàm giao dịch (transaction function) như evaluateTransaction() luôn là một mảng byte bởi vì kiểu dữ liệu trả về có thể là bất cứ thứ gì. Ứng dụng ở phía client có trách nhiệm diễn dịch các byte đó.

Create a New Asset

Đối với các phương thức có tạo transaction, ta cần dùng phương thức submitTransaction() của Contract:

/**
 * Submit a transaction synchronously, blocking until it has been committed to
 * the ledger.
 */
private void createAsset() throws EndorseException, SubmitException, CommitStatusException, CommitException {
	System.out.println("\n--> Submit Transaction: CreateAsset, creates new asset with ID, Color, Size, Owner and AppraisedValue arguments");
 
	contract.submitTransaction("CreateAsset", assetId, "yellow", "5", "Tom", "1300");
 
	System.out.println("*** Transaction committed successfully");
}

Đối số của phương thức submitTransaction() bao gồm:

  • Tên phương thức của chaincode cần invoke: CreateAsset.
  • Các đối số còn lại là đối số truyền vào phương thức của chaincode, mà cụ thể là assetId, "yellow", "5", "Tom", "1300". Thứ tự và kiểu dữ liệu của các đối số này cần phải match với phương thức cần invoke.

Transfer an Asset

Khác với việc thêm mới một asset, việc chuyển quyền sở hữu của asset ở trong ứng dụng mẫu sẽ không block mà sẽ được thực thi một cách bất đồng bộ. Cụ thể hơn, ứng dụng sẽ không cần phải chờ transaction được commit xuống ledger để nhận kết quả trả về của phương thức đang invoke. Thay vào đó, kết quả sẽ được trả về ngay khi transaction được chứng thực và được gửi đến cho ordering service (trước lúc commit). Điều này giúp cho ứng dụng có thể thực hiện những việc khác trong khi chờ transaction được commit.

/**
 * Submit transaction asynchronously, allowing the application to process the
 * smart contract response (e.g. update a UI) while waiting for the commit
 * notification.
 */
private void transferAssetAsync() throws EndorseException, SubmitException, CommitStatusException {
	System.out.println("\n--> Async Submit Transaction: TransferAsset, updates existing asset owner");
 
	var commit = contract.newProposal("TransferAsset")
			.addArguments(assetId, "Saptha")
			.build()
			.endorse()
			.submitAsync();
 
	var result = commit.getResult();
	var oldOwner = new String(result, StandardCharsets.UTF_8);
 
	System.out.println("*** Successfully submitted transaction to transfer ownership from " + oldOwner + " to Saptha");
	System.out.println("*** Waiting for transaction commit");
 
	var status = commit.getStatus();
	if (!status.isSuccessful()) {
		throw new RuntimeException("Transaction " + status.getTransactionId() +
				" failed to commit with status code " + status.getCode());
	}
	
	System.out.println("*** Transaction committed successfully");
}

Tóm tắt đoạn code trên:

  1. Tạo ra một transaction proposal với đối số truyền vào là tên phương thức của chaincode cần invoke (TransferAsset).
  2. Thêm vào hai đối số của TransferAsset.
  3. Thực hiện chứng thực bằng phương thức endorse().
  4. Submit transaction đã được chứng thực cho ordering service một cách bất đồng bộ và nhận về kết quả.
  5. Chờ transaction được commit xuống ledger bởi orderer.
  6. Xử lý lỗi nếu có.

Query the Updated Asset

Ứng dụng mẫu sau đó truy vấn thông tin của asset đã được cập nhật bởi phương thức TransferAsset thông qua phương thức ReadAsset:

private void readAssetById() throws GatewayException {
	System.out.println("\n--> Evaluate Transaction: ReadAsset, function returns asset attributes");
 
	var evaluateResult = contract.evaluateTransaction("ReadAsset", assetId);
	
	System.out.println("*** Result:" + prettyJson(evaluateResult));
}

Kết quả mong muốn:

*** Result:{
  "AppraisedValue": 1300,
  "Color": "yellow",
  "ID": "asset1706079275332",
  "Owner": "Saptha",
  "Size": 5
}

Handle Transaction Errors

Để minh họa cho việc xử lý lỗi khi invoke transaction, ứng dụng mẫu invoke phương thức TransferAsset nhưng truyền vào một asset ID không tồn tại:

/**
 * submitTransaction() will throw an error containing details of any error
 * responses from the smart contract.
 */
private void updateNonExistentAsset() {
	try {
		System.out.println("\n--> Submit Transaction: UpdateAsset asset70, asset70 does not exist and should return an error");
		
		contract.submitTransaction("UpdateAsset", "asset70", "blue", "5", "Tomoko", "300");
		
		System.out.println("******** FAILED to return an error");
	} catch (EndorseException | SubmitException | CommitStatusException e) {
		System.out.println("*** Successfully caught the error: ");
		e.printStackTrace(System.out);
		System.out.println("Transaction ID: " + e.getTransactionId());
 
		var details = e.getDetails();
		if (!details.isEmpty()) {
			System.out.println("Error Details:");
			for (var detail : details) {
				System.out.println("- address: " + detail.getAddress() + ", mspId: " + detail.getMspId()
						+ ", message: " + detail.getMessage());
			}
		}
	} catch (CommitException e) {
		System.out.println("*** Successfully caught the error: " + e);
		e.printStackTrace(System.out);
		System.out.println("Transaction ID: " + e.getTransactionId());
		System.out.println("Status code: " + e.getCode());
	}
}

Output:

*** Successfully caught the error: 
Transaction ID: 4faa334eb4691d2b074a8b9ec669d05e02f352246eb089303e77e7e8e04513ba
Error Details:
- address: peer0.org1.example.com:7051, mspId: Org1MSP, message: chaincode response 500, the asset asset70 does not exist
list
from [[Hyperledger Fabric - Running a Fabric Application]]
sort file.ctime asc

Resources