存证数据写入

业务系统根据具体场景使用不同的交易类型写数据上链,具体参考存证交易模型

数据格式规范

为维护数据的可读性,区块链预先配置了不同业务场景下对应的数据格式规范,以约定统一的存证数据格式。上链数据必须满足规范要求,您可以在 client-sdk.zip 包内的schema.txt 中查看区块链的数据格式规范。数据格式规范由多个业务数据分类(Category)组成。您可以通过识别数据的 Category 在链上检索和过滤存证数据。

这里通过一个简单的 schema.txt 文件示例来演示如何构造、读取存证数据。具体操作如下:

  1. schema.txt 文件中声明一个名为 GuestInfo 的 Category,包含三个成员,分别是姓名、生日和邮件地址,均为字符串类型,并在注释中说明成员数据须满足的约束条件。

    注意:在实际开发过程中,请将 GuestInfo 替换为您本地 schema.txt 中实际定义的 Category。

    1. // schema.txt定义:商家顾客信息
    2. GuestInfo {
    3. String userName; // 姓名,要求非空,长度在20以内
    4. String birthday; // 生日,要求非空
    5. String email; // 邮件地址,要求非空,符合电子邮件格式
    6. }
  2. 对照规范要求,在client代码中调用GuestInfoBuilder构造一条存证数据。
    1. // client代码
    2. // 构造存证数据
    3. GuestInfoBuilder builder = new GuestInfoBuilder();
    4. byte[] bizData = builder.buildUserName("Bob")
    5. .buildBirthday("2000-01-01")
    6. .buildEmail("bob@inc.com")
    7. .build();
  3. 存证数据构造完成后,构造上链的 payload。这里需要了解一个枚举类 BizCategory,SDK 根据 schema.txt 的约定,预先生成了这一枚举类,以标识不同的 Category。本例中的 BizCategory 如下所示:
    1. // SDK的BizCategory声明
    2. public class BizCategory {
    3. public static final int GuestInfo = 0x00010001;
    4. ...
    5. }
  4. 把存证数据传入 payload,指定 Category 为 BizCategory.GuestInfo,构造一个数据存证交易,并通过 client 发送出去。至此,存证上链的过程已完成。
    1. // client代码
    2. // 创建ContentOnlyNotary Transaction
    3. TransactionDO tx = TransactionBuilder.getContentOnlyNotaryPayloadBuilder()
    4. .setContent(bizData)//设置需要存证的数据
    5. .setTimestamp(System.currentTimeMillis())//设置业务时间
    6. .setCategory(BizCategory.GuestInfo)//通过BizCategory设置业务分类
    7. .build();//build Transaction
    8. // 发送Transaction
    9. Response<TransactionDO> response = client.sendTransaction(tx);
  5. 读取数据。同样通过 BizCategory.GuestInfo 来识别数据对象类型,将存证数据转换回对象。

存证数据上链后,需要等待一段时间才能成块,出块后就可以查询数据了。获取块数据有两种模式:

  • 模式一:监听成块消息,处理新数据块,参见下文的示例 1:存证数据写入。
  • 模式二:通过定时任务,每隔一段时间拉取新的数据块,参见下文的示例 2:存证数据加密上链。

下面展示几个完整的示例。

代码示例

存证数据写入

  1. // 加载client配置文件
  2. Properties p = new Properties();
  3. p.load(new FileInputStream("sdk.properties"));
  4. ClientConfig config = new ClientPropertyConfig(p);
  5. // 使用指定client配置初始化client
  6. Client client = new Client(config);
  7. // 构造存证数据
  8. GuestInfoBuilder builder = new GuestInfoBuilder();
  9. byte[] bizData = builder.buildUserName("Bob")
  10. .buildBirthday("2000-01-01")
  11. .buildEmail("bob@inc.com")
  12. .build();
  13. // 创建ContentOnlyNotary Transaction
  14. TransactionDO tx = TransactionBuilder.getContentOnlyNotaryPayloadBuilder()
  15. .setContent(bizData)//设置需要存证的数据
  16. .setTimestamp(System.currentTimeMillis())//设置业务时间
  17. .setCategory(BizCategory.GuestInfo)//设置业务分类
  18. .build();//build Transaction
  19. // 发送Transaction
  20. Response<TransactionDO> response = client.sendTransaction(tx);
  21. if(response.isSuccess()){
  22. // 发送成功,业务系统保留Transaction Hash与业务数据关联
  23. return tx.getTxHashValue();
  24. }else{
  25. // 发送失败,抛异常。业务系统应该重试,rpc调用失败时区块链的状态是未知的。
  26. throw new RuntimeException("写入区块链失败");
  27. }

存证数据加密上链

  1. // 加载 client 配置文件
  2. Properties p = new Properties();
  3. p.load(new FileInputStream("client.properties"));
  4. ClientConfig config = new ClientPropertyConfig(p);
  5. // 使用指定 client 配置初始化client
  6. Client client = new Client(config);
  7. // 初始化链上加密 key repositry
  8. KeyRepositry keyRepositry = new KeyRepositry();
  9. // 设置虚拟 root key
  10. KeyPath key1 = new KeyPath("/ABS_ROOT");
  11. // 为BOC Bank类业务生成root加密key
  12. keyRepositry.setKey(key1.forName("ALIPAY").forName("BOC"), genKey());
  13. // 存证数据加密 key path
  14. String keyPathName = "/ABS_ROOT/ALIPAY/BOC/BILL";
  15. // 根据 key path 生成加密 key
  16. byte[] key = keyRepositry.getKeyFor(keyPathName);
  17. // 构造存证数据
  18. GuestInfoBuilder builder = new GuestInfoBuilder();
  19. byte[] bizData = builder.buildUserName("Bob")
  20. .buildBirthday("2000-01-01")
  21. .buildEmail("bob@inc.com")
  22. .build();
  23. // build transaction
  24. TransactionDO tx = TransactionBuilder.getEncryptShareNotaryPayloadBuilder()
  25. .encryptContent(bizData, key, keyPathName.getBytes())//设置存证内容,加密key,key path
  26. .setTimestamp(System.currentTimeMillis())//设置业务时间
  27. .setCategory(BizCategory.GuestInfo)//设置业务分类
  28. .build();
  29. // 发送Transaction
  30. Response<TransactionDO> response = client.sendTransaction(tx);
  31. if(response.isSuccess()){
  32. // 发送成功,业务系统保留 Transaction Hash 与业务数据关联
  33. return tx.getTxHashValue();
  34. }else{
  35. // 发送失败,抛异常。业务系统应该重试,RPC 调用失败时区块链的状态是未知的。
  36. throw new RuntimeException("写入区块链失败");
  37. }

存证数据读取

  1. // 获取当前最新区块header
  2. final Response<BlockHeader> blockHeader = client.getLatestBlockHeader();
  3. if(!blockHeader.isSuccess()) {
  4. LOGGER.error("Get block header fail: ", blockHeader.getErrorMsg());
  5. return;
  6. }
  7. // oldHeight = 当前本地区块高度;
  8. long beginHeight = oldHeight + 1;
  9. long finalHeight = blockHeader.getData().getHeight();
  10. // 发现新块则开始拉取
  11. while(beginHeight < finalHeight) {
  12. long endHeight = Math.min(beginHeight + FETCH_LIMIT - 1, finalHeight);
  13. Response<List<Block>> response = client.getBlocks((int)beginHeight, (int)endHeight);
  14. if (!response.isSuccess()) {
  15. LOGGER.error("Get response fail: ", response.getErrorMsg());
  16. return;
  17. }
  18. ConstructUtil constructUtil = new ConstructUtil();
  19. List<Block> blocks = response.getData();
  20. for (Block block : blocks) {
  21. // 获取区块的存证交易
  22. List<TransactionDO> transactions = block.getTransactions();
  23. for (TransactionDO transaction : transactions) {
  24. // 获取payload
  25. if (ContentOnlyNotaryPayloadDO.class.isInstance(transaction.getPayload())) {
  26. ContentOnlyNotaryPayloadDO contentOnlyNotaryPayloadDO =
  27. ContentOnlyNotaryPayloadDO.class.cast(transaction.getPayload());
  28. // 判断payload的业务数据分类
  29. if (contentOnlyNotaryPayloadDO.getCategory() == BizCategory.GuestInfo) {
  30. // 构造数据对象
  31. GuestInfo renter = GuestInfo.class.cast(constructUtil.construct(
  32. contentOnlyNotaryPayloadDO.getContent(), GuestInfo.class));
  33. // 读取renter数据
  34. System.out.println(renter.getUserName());
  35. }
  36. }
  37. }
  38. beginHeight = block.getHeader().getHeight() + 1;
  39. }
  40. }

最后如果用户的系统要退出,建议在退出前调用shutdown,client会在shutdown中优雅的关闭线程池。

  1. client.shutdown();