这个版本的SDK用来给java语言开发者提供便捷生成metadata的服务。方法的具体使用请查看examples
原本链测试地址:https://testnet.yuanbenlian.com
https://github.com/yuanbenio/universe-java-sdk
NOTE 原本链中所有字节数组都以16进制的字符串存储,公钥为压缩格式。
<dependency>
<groupId>com.yuanbenlian</groupId>
<artifactId>universe-java-sdk</artifactId>
<version>1.4.4-SNAPSHOT</version>
</dependency>
Java-SDK提供三个处理器来生成metadata的相关参数:service/KeyProcessor、service/DTCPProcessor、service/NodeProcessor。
1. service/KeyProcessor
这是一个密钥处理器,支持密钥对生成、签名、签名验证以及通过私钥推导公钥。
2. service/DTCPProcessor
这是一个DTCP协议的处理器,可以用来计算metadata中的各项参数。
3. service/NodeProcessor
这是一个对接原本链node节点的处理器,可以对node节点发送http请求,主要用于注册metadata以及查询metadata、license和blockHash等数据。
NOTE 哈希函数的源码见com.yuanbenlian.crypto,原本链的公私密钥检查工具见:com.yuanben.util.SecretUtil.java
name | type | 必传 | comment | source |
---|---|---|---|---|
type | string | Y | 类型, image,article,audio,vedio,custom,private | 用户传入 |
language | string | Y | 语言 'zh-CN', | 默认zh-cn,可用户传入 |
title | string | N | 内容标题 | 用户传入 |
signature | string | Y | 内容签名, 算法(secp256k1) | 系统生成 |
abstract | string | N | 描述,内容摘要 | 用户传入,为空时,系统自动取内容的前200个字符 |
category | string | N | 分类集, 以逗号分隔 "新闻, 商业" | 用户传入 |
dna | string | Y | metadata dna | 系统生成 |
parent_dna | string | N | 该metadata修改前的dna | 用户传入,如果时修改前一个metadata的数据,则需要传入前一个metadata的dna |
block_hash | string | Y | 区块链上的一个block_hash值 | 用户传入,会到链上做校验 |
created | integer | Y | 创建的时间,时间戳,10位长度, 1506302092 | 系统生成 |
content_hash | string | Y | 内容哈希,hash算法(keccak256) | 可用户传入,如果没有,系统根据content生成 |
block_height | string | Y | block_hash对应的区块的height值 | 用户传入,会到链上做校验 |
extra | TreeMap<String, Object> | N | 扩展内容,自定义内容。 | 用户传入 |
license | Metadata.License | Y | 许可证 | 用户传入 |
license.type | string | Y | 许可证类型 | 用户传入 |
license.parameters | TreeMap<String, Object> | N | 许可证参数对象,自定义内容 | 用户传入 |
source | string | N | 原内容的链接, article,image的官网或者内容的链接 | 用户传入 |
data | TreeMap<String, Object> | N | 存放和原数据相关的内容。 | 用户传入 |
id | string | N | 业务编号 | 用户传入 |
pubkey | string | N | 记录所有权的公钥 | 用户传入 |
/**
* 生成密钥对
*
* @return secp256k1密钥
*/
public static SecretKey GeneratorSecp256k1Key() {
SecretKey secretKey = new SecretKey();
ECKey ecKey = new ECKey();
secretKey.setPrivateKey(Hex.toHexString(ecKey.getPrivKeyBytes()));
ECPoint pubKeyPoint = ecKey.getPubKeyPoint();
byte[] encoded = pubKeyPoint.getEncoded(true);
secretKey.setPublicKey(Hex.toHexString(encoded));
return secretKey;
}
该方法位于KeyProcessor.java,返回公私密钥,其中私钥为16进制的字符串,长度64,公钥为压缩格式的16进制字符串,长度66。
/**
* 根据私钥生成16进制对压缩公钥
*
* @param privateKey 16进制私钥
* @return 16进制的公钥
*/
public static String GetPubKeyFromPri(String privateKey) throws InvalidException {
if (!SecretUtil.CheckPrivateKey(privateKey)) {
throw new InvalidException("private key`s format is error");
}
ECKey ecKey = ECKey.fromPrivate(Hex.decode(privateKey));
ECPoint pubKeyPoint = ecKey.getPubKeyPoint();
byte[] encoded = pubKeyPoint.getEncoded(true);
return Hex.toHexString(encoded);
}
该方法位于KeyProcessor.java,需要传入16进制的私钥字符串,返回压缩格式的公钥。
/**
* 根据公钥生成16进制对压缩公钥
* calculating address by public key
*
* @param publicKey
* @return address
*/
public static String Address(String publicKey) throws InvalidException {
if (!SecretUtil.CheckPublicKey(publicKey)) {
throw new InvalidException("Incorrect public key");
}
ECKey ecKey = ECKey.fromPublicOnly(Hex.decode(publicKey));
return "0x"+Hex.toHexString(ecKey.getAddress()).toUpperCase();
}
该方法位于KeyProcessor.java,需要传入16进制的公钥字符串,返回address。
/**
* 签名
*
* @param privateKey 私钥
* @param content 签名内容 字节数组
* @return 16进制签名串
*/
public static String Sign(String privateKey, byte[]... content) throws InvalidException {
if (!SecretUtil.CheckPrivateKey(privateKey)) {
throw new InvalidException("private key`s format is error");
}
ECKey ecKey = ECKey.fromPrivate(Hex.decode(privateKey));
Keccak256 keccak256 = new Keccak256();
for (byte[] b : content) {
keccak256.update(b);
}
ECKey.ECDSASignature ecdsaSignature = ecKey.doSign(keccak256.digest());
return ecdsaSignature.toHex();
}
该方法位于KeyProcessor.java,需要传入16进制的私钥字符串和需要签名的内容,返回16进制的签名字符串。
/**
* 签名验证
*
* @param publicKey 16进制的压缩公钥
* @param signMsg 16进制的签名串
* @param data 被签名的原数据字节数组 keccak256哈希值
* @return 验证结果
*/
public static boolean VerifySignature(String publicKey, String signMsg, byte[] data) throws InvalidException {
if (!SecretUtil.CheckPublicKey(publicKey, true)) {
throw new InvalidException("public key`s format is error");
}
byte[] decode = Hex.decode(signMsg);
byte[] rBs = new byte[decode.length / 2];
byte[] sBs = new byte[decode.length / 2];
System.arraycopy(decode, 0, rBs, 0, decode.length / 2);
System.arraycopy(decode, decode.length / 2, sBs, 0, decode.length / 2);
BigInteger r = new BigInteger(Hex.toHexString(rBs), 16);
BigInteger s = new BigInteger(Hex.toHexString(sBs), 16);
ECKey.ECDSASignature sig = ECKey.ECDSASignature.fromComponents(r.toByteArray(), s.toByteArray(), (byte) 0x1b);
return ECKey.verify(data, sig, Hex.decode(publicKey));
}
该方法位于KeyProcessor.java,需要传入公钥、签名和原数据,返回签名验证结果。
/**
* keccak256哈希运算
*
* @param content 内容
* @return 哈希值 length=32
*/
public static byte[] Keccak256(byte[]... content) {
if (content == null) return null;
Keccak256 keccak256 = new Keccak256();
for (byte[] s : content) {
keccak256.update(s);
}
return keccak256.digest();
}
/**
* keccak256哈希运算
*
* @param content 内容
* @return 哈希值 length=32
*/
public static byte[] Keccak256(String... content) {
if (content == null) return null;
Keccak256 keccak256 = new Keccak256();
for (String s : content) {
keccak256.update(s.getBytes());
}
return keccak256.digest();
}
该方法位于KeyProcessor.java,需要传入需要哈希的数据,返回哈希值。
/**
* 生成contentHash
* contentHash = Keccak256(content)
* @return 16进制的contentHash
*/
public static String GenContentHash(String... content) {
if (content == null) return Constants.STRING_EMPTY;
Keccak256 keccak256 = new Keccak256();
for (String s : content) {
keccak256.update(s.getBytes());
}
return Hex.toHexString(keccak256.digest());
}
该方法位于DTCPProcessor.java,需要传入metadata的Content,返回contentHash。
/**
* 对metadata签名 (签名内容为metadata中除去dna\content和signature字段外的所有字段值)
* @param metadata metadata实例
* @param privateKey 16进制的私钥
* @return 16进制的metadata signature
* @throws InvalidException 入参为空
*/
public static String GenMetadataSignature(Metadata metadata, String privateKey) throws InvalidException {
if (metadata == null || !SecretUtil.CheckPrivateKey(privateKey)) {
throw new InvalidException("metadata or privateKey is illegal");
}
return ECKeyProcessor.Sign(privateKey, metadata.toJsonRmSign().getBytes());
}
该方法位于DTCPProcessor.java,需要传入metadata,返回metadata的签名。
/**
* 生成闪电dna
* @param signature 16进制的metadata签名串
* @return metadata的闪电dna
* @throws InvalidException 入参为空
*/
public static String GeneratorDNA(String signature) throws InvalidException {
if (StringUtils.isBlank(signature)) {
throw new InvalidException("signature is empty");
}
Keccak256 keccak256 = new Keccak256();
keccak256.update(signature.getBytes());
return Hex.toHexString(keccak256.digest());
}
该方法位于DTCPProcessor.java,需要传入metadata的Signature,返回DNA。
/**
* 对metadata进行签名验证 (签名内容为metadata中除去dna\content和signature字段外的所有字段值)
* @param metadata metadata
* @return 验证结果
* @throws InvalidException metadata为空
*/
public static boolean VerifyMetadataSignature(Metadata metadata) throws InvalidException {
if (metadata == null ) {
throw new InvalidException("metadata is null");
}
return ECKeyProcessor.VerifySignature(metadata.getPubKey(),metadata.getSignature(), ECKeyProcessor.Keccak256(metadata.toJsonRmSign()));
}
该方法位于DTCPProcessor.java,需要传入metadata,返回metadata的签名验证结果。
/**
* 对metadata进行补全
* @param privateKey 16进制的私钥,用于签名
* @param metadata 必须包含license\title\type\block_hash,如果contentHash为空,则必须传入content的值;如果type不是article,则必须传入contentHash
* @return 信息补全的metadata
* @throws InvalidException
*/
public static Metadata FullMetadata(String privateKey, Metadata metadata) throws InvalidException {
......
}
input metadata:
name | type | must | comment | source |
---|---|---|---|---|
type | string | Y | 存证记录类型,有:image,article,audio,video,custom,private,file | 用户指定 |
title | string | Y | 内容标题 | 用户指定,private类型可以为空 |
category | string | N | 内容类别,如:"news,article" | 用户指定,private类型可以为空 |
block_hash | string | Y | 双向锚定的区块哈希(可以通过QueryLatestBlockHash获取) | 用户指定 |
block_height | string | Y | 双向锚定的区块高度 (可以通过QueryLatestBlockHash获取) | 用户指定 |
content | string | N | 内容 | 用户指定,和contentHash不能同时为空 |
content_hash | string | N | 内容哈希,SDK中使用Keccak256哈希算法 | 用户指定,如果为空,则根据content生成,所以content和contentHash不能同时为空 |
data | TreeMap<String, Object> | Y | 类型相关的扩展信息,如图片的宽和高 | 用户指定,private\custom\article可以为空 |
license | Metadata.License | Y | 授权信息 | 用户指定 |
license.type | string | Y | 授权协议名称 | 用户指定 |
license.parameters | TreeMap<String, Object> | Y | 授权内容 | 用户指定,None协议可以为空 |
created | integer | N | 生成记录的时间, eg:1506302092 | 系统生成 |
language | string | N | 语言 | 用户指定,默认:zh-CN |
parent_dna | string | N | 链接到另外一条存证记录,表示关联关系(如对link的修改) | 用户指定 |
abstract | string | N | 内容摘要 | 用户指定,默认内容的前200字符 |
source | string | N | 原内容地址,如原文发布的URL | 用户指定 |
id | string | N | 业务编号,如购物单ID | 用户指定 |
pubkey | string | N | 签名者的公钥 | 由传入的私钥生成 |
extra | TreeMap<String, Object> | N | 用户需要添加的附加信息 | 用户指定 |
signature | string | N | 数字签名 | 数字签名,系统生成 |
dna | string | N | 记录DNA | 系统生成 |
该方法位于DTCPProcessor.java,需要传入metadata和私钥,返回可被node节点接收的完整metadata。
/**
* 生成用于注册公钥的请求体
*
* @param privateKey 16进制私钥
* @param subPubKeys 需要注册的公钥数组
* @return 请求体封装
* @throws InvalidException 参数错误
*/
public static RegisterAccountReq GenRegisterAccountReq(String privateKey, String[] subPubKeys) throws InvalidException {
if (subPubKeys == null || subPubKeys.length < 1 || !SecretUtil.CheckPrivateKey(privateKey)) {
throw new InvalidException("subPubKeys or privateKey is illegal");
}
RegisterAccountReq req = new RegisterAccountReq();
req.setSubPubKeys(subPubKeys);
String pubKey = ECKeyProcessor.GetPubKeyFromPri(privateKey);
String sign = ECKeyProcessor.Sign(privateKey, JSONArray.toJSONString(subPubKeys).getBytes());
req.setPubKey(pubKey);
req.setSignature(sign);
return req;
}
该方法位于NodeProcessor.java,需要传入16进制的私钥和需要注册的公钥,返回用于注册的请求体。
/**
* 向node节点注册metadata
*
* @param url node节点的地址 (http://119.23.22.129:9000)
* @param version node节点的版本 (默认v1)
* @param async 是否异步发送 默认async=true为异步发送,如果async=false为同步发送
* @param md 要注册的metadata,不需要传content
* @return metadata的注册结果体
* @throws InvalidException 参数有误或网络请求错误
*/
public static MetadataSaveResp SaveMetadata(String url, String version, Boolean async, Metadata md) throws InvalidException {
if (StringUtils.isBlank(url)) {
throw new InvalidException("url is empty");
}
if (StringUtils.isBlank(version)) {
version = "v1";
}
if (async == null ){
async = true;
}
url += "/" + version + "/metadata?async=" + async;
if (md == null) {
throw new InvalidException("metadata is null");
}
if (StringUtils.isBlank(md.getSignature())) {
throw new InvalidException("signature is null");
}
if (md.getLicense() == null || StringUtils.isBlank(md.getLicense().getType()) || MapUtils.isEmpty(md.getLicense().getParameters())) {
throw new InvalidException("license is null");
}
String s = HttpUtil.sendPost(url, md.toJson());
return JSONObject.parseObject(s, MetadataSaveResp.class);
}
输入的Metadata: 此方法传入的Metadata请预先使用FullMetadata进行处理,防止存证失败
name | type | must | comment |
---|---|---|---|
content_hash | string | Y | 内容哈希,hash(content) |
created | integer | Y | 记录生成的时间戳, eg:1506302092 |
license | Metadata.License | Y | 授权信息 |
license.type | string | Y | 授权协议 |
license.parameters | TreeMap<String, Object> | Y | 授权内容 |
type | string | Y | 存证记录类型,有:image,article,audio,video,custom,private,file |
block_hash | string | Y | 双向锚定区块的哈希 |
block_height | string | Y | 双向锚定区块高度 |
pubkey | string | Y | 签名者的公钥 |
signature | string | Y | 数据签名 |
language | string | Y | 语言,'zh-CN' |
dna | string | N | 本条记录的DNA |
title | string | N | 存证记录的标题 |
category | string | N | 存证内容的类别,如:"news,article" |
data | TreeMap<String, Object> | N | 不同类型数据的扩展信息描述 |
parent_dna | string | N | 链接到另外一条存证记录,表示关联关系(如对link的修改) |
abstract | string | N | 内容摘要信息 |
source | string | N | 原内容地址,如原文发布的URL |
id | string | N | 业务编号 |
extra | TreeMap<String, Object> | N | 附加信息 |
该方法位于NodeProcessor.java,需要传入metadata,注册成功则返回metadata的dna。
/**
* query metadata from YuanBen chain
*
* @param url node address (http://localhost:9000/v1)
* @param dna DNA
* @return result include metadata and transaction information
* @throws InvalidException
*/
public static MetadataQueryResp QueryMetadata(String url, String dna) throws InvalidException {
...
}
该方法位于NodeProcessor.java,查询DNA对应数据的上链信息,同时会返回上链的原始数据和数据所在区块信息。
/**
* 向node节点查询license
*
* @param url node节点的地址 (http://119.23.22.129:9000)
* @param version node节点的版本 (默认v1)
* @param licenseType license's type
* @return license的查询结果体
* @throws InvalidException 参数有误或网络请求错误
*/
public static LicenseQueryResp QueryLicense(String url, String version, String licenseType) throws InvalidException {
if (StringUtils.isBlank(url) || StringUtils.isBlank(licenseType)) {
throw new InvalidException("url or licenseType is empty");
}
if (StringUtils.isBlank(version)) {
version = "v1";
}
url += "/" + version + "/license/" + licenseType;
String s = HttpUtil.sendGet(url);
return JSONObject.parseObject(s, LicenseQueryResp.class);
}
该方法位于NodeProcessor.java,需要传入license的type,返回license的详细信息。
/**
* 向node节点查询最新的blockHash
*
* @param url node节点的地址 (http://119.23.22.129:9000)
* @param version node节点的版本 (默认v1)
* @return 最新的blcokHash封装
* @throws InvalidException 参数有误或网络请求错误
*/
public static BlockHashQueryResp QueryLatestBlockHash(String url, String version) throws InvalidException {
if (StringUtils.isBlank(url)) {
throw new InvalidException("url is empty");
}
if (StringUtils.isBlank(version)) {
version = "v1";
}
url += "/" + version + "/block_hash/";
String s = HttpUtil.sendGet(url);
return JSONObject.parseObject(s, BlockHashQueryResp.class);
}
该方法位于NodeProcessor.java,用于查询原本链最新的区块信息。 NOTE 原本链的处理的速度为毫秒级,由于网络延迟,获取到的可能不是最新的区块信息。该接口获取的值主要用于metadata中值的填充,只需要保证hash在链上即可,不需要最新的。
/**
* 向node节点查询blockHash是否在链上,并处于指定高度
*
* @param url node节点的地址 (http://119.23.22.129:9000)
* @param version node节点的版本 (默认v1)
* @param req 请求体 (包括blockHash和blockHeight)
* @return 查询结果封装
* @throws InvalidException 参数有误或网络请求错误
*/
public static BlockHashCheckResp CheckBlockHash(String url, String version, BlockHashCheckReq req) throws InvalidException {
if (StringUtils.isBlank(url) || req == null || StringUtils.isBlank(req.getHash())) {
throw new InvalidException("url or request body is empty");
}
if (StringUtils.isBlank(version)) {
version = "v1";
}
url += "/" + version + "/check_block_hash/";
String s = HttpUtil.sendPost(url, req.toJson());
return JSONObject.parseObject(s, BlockHashCheckResp.class);
}
该方法位于NodeProcessor.java,用于检查blockHash是否在链上,并处于指定高度。
/**
* 注册公钥
*
* @param url node节点的地址 (http://localhost:9000)
* @param version node节点的版本 (默认v1)
* @param req 请求体
* @return 返回结果封装
* @throws InvalidException 参数有误或网络请求错误
*/
public static RegisterAccountResp RegisterAccount(String url, String version, RegisterAccountReq req) throws InvalidException {
if (StringUtils.isBlank(url) || req == null) {
throw new InvalidException("url or request body is empty");
}
if (!SecretUtil.CheckPublicKey(req.getPubKey(), true) ||
StringUtils.isEmpty(req.getSignature()) ||
req.getSubPubKeys() == null || req.getSubPubKeys().length < 1) {
throw new InvalidException("request parameters error");
}
if (StringUtils.isBlank(version)) {
version = "v1";
}
url += "/" + version + "/accounts/";
String s = HttpUtil.sendPost(url, req.toJson());
return JSONObject.parseObject(s, RegisterAccountResp.class);
}
该方法位于NodeProcessor.java,用于注册公钥,返回体的code为ok,则表示注册成功。