BubiChain Privacy Transaction Example
Scene Description
Suppose an investor wants to issue a private asset with the name Cocoon, the identifier CNT, and a total amount of 10,000. The specific information of the asset issuance is as follows:
Field Name | Required | Example | Description |
---|---|---|---|
name | yes | Cocoon | Asset name |
symbol | yes | CNT | Asset code |
totalSupply | yes | 10000 | Asset supply of assets |
version | yes | ETP1.0 | The version number |
The asset transaction process should ensure:
- Only the actual owner of the asset can transfer the asset, and the funds will not increase or decrease in a vacuum during the privacy transaction
- No one except the sender and receiver can get the details of the transaction amount.
- Users can easily get account balance
Development Process for Privacy
BubiChain privacy transactions are based on Pedersen commitments, scope proofs and other technologies. For details, please refer to [Term Description]. Construct a UTXO transaction model in a smart contract, which mainly includes the issuance and transfer of private assets.
note : Private transactions use a separate public-private key pair to support the elliptic curve algorithm related to private transactions, which is not the same as the public-private key pair of the original bub account.
Import JNI interface
The privacy module is implemented in C ++, and the Java layer is called through JAVA JNI. For details on import and call methods, please refer to Privacy JNI SDK
Create privacy contracts and trading accounts
Specific example codes for creating private smart contracts and trading accounts:
private static void createAccountOperation(BcOperationService operationService, BcQueryService queryService){
try {
Transaction transaction = operationService.newTransaction(address);
from_keypair = SecureKeyGenerator.generateBubiKeyPair();
to_keypair = SecureKeyGenerator.generateBubiKeyPair();
to_keypair1 = SecureKeyGenerator.generateBubiKeyPair();
String[] res = cn.bubi.Privacy.createKeyPair(cpp_obj);
if (Integer.valueOf(res[0]) == 0) {
ct_from_keypair[0] = res[2];
ct_from_keypair[1] = res[3];
}
res = cn.bubi.Privacy.createKeyPair(cpp_obj);
if (Integer.valueOf(res[0]) == 0) {
ct_from_keypair1[0] = res[2];
ct_from_keypair1[1] = res[3];
}
res = cn.bubi.Privacy.createKeyPair(cpp_obj);
if (Integer.valueOf(res[0]) == 0) {
ct_to_keypair[0] = res[2];
ct_to_keypair[1] = res[3];
}
res = cn.bubi.Privacy.createKeyPair(cpp_obj);
if (Integer.valueOf(res[0]) == 0) {
ct_to_keypair1[0] = res[2];
ct_to_keypair1[1] = res[3];
}
res = cn.bubi.Privacy.createKeyPair(cpp_obj);
if (Integer.valueOf(res[0]) == 0) {
ct_to_keypair2[0] = res[2];
ct_to_keypair2[1] = res[3];
}
System.out.println("from account: " + GsonUtil.toJson(from_keypair));
System.out.println("ct from account: " + ct_from_keypair[0] + ", " + ct_from_keypair[1]);
System.out.println("ct from account1: " + ct_from_keypair1[0] + ", " + ct_from_keypair1[1]);
System.out.println("to account: " + GsonUtil.toJson(to_keypair));
System.out.println("ct to account: " + ct_to_keypair[0] + ", " + ct_to_keypair[1]);
System.out.println("ct to account1: " + ct_to_keypair1[0] + ", " + ct_to_keypair1[1]);
System.out.println("ct to account2: " + ct_to_keypair2[0] + ", " + ct_to_keypair2[1]);
CreateContractOperation createContract = new CreateContractOperation.Builder()
.buildScript(getContractCodeFromFile(contract_file))
.buildInitBalance(100000000000L)
.build();
CreateAccountOperation createFrom = new CreateAccountOperation.Builder()
.buildDestAddress(from_keypair.getBubiAddress())
.buildInitBalance(100000000000L)
.build();
CreateAccountOperation createTo = new CreateAccountOperation.Builder()
.buildDestAddress(to_keypair.getBubiAddress())
.buildInitBalance(100000000000L)
.build();
TransactionCommittedResult result = transaction.buildAddOperation(createContract)
.buildAddOperation(createFrom)
.buildAddOperation(createTo)
.buildGasPrice(1000L).buildFeeLimit(10000000000L)
.buildAddSigner(BubiKey.getB16PublicKey(privateKey), privateKey)
.commit();
List<ContractAddress> addresses = queryService.getContractAddressByHash(result.getHash());
System.out.println("contract address:" + addresses.get(0).getContractAddress());
contract_address = addresses.get(0).getContractAddress();
System.out.println("\n------------------------------------------------");
System.out.println(GsonUtil.toJson(result));
} catch (SdkException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
Issue privacy assets
- Create privacy assets The structure of privacy assets is as follows:
message ConfidentialAsset
{
string id = 1; // Asset ID
string commit = 2; // Pedersen Commitment
string range_proof = 3; // Range Proof
string encrypt_value = 4; // The encrypted amount, the encryption key is the ECDH key generated by the sender's private key and the recipient's public key key=ECDH(r1, R2)
string from_pubkey = 5; // Sender public key
string to = 6; // Receiver address
}
- Issue Trigger a smart contract issuance interface to issue private assets
Create a Pedersen promise, proof of scope, encrypted amount, assembled into a private asset structure, and trigger the smart contract issuance interface with the JSON object as a parameter.
The private asset issuance example code is as follows:
private static void issueOperation(BcOperationService operationService, Long value){
try {
Transaction transaction = operationService.newTransaction(from_keypair.getBubiAddress());
Privacy.ConfidentialAsset.Builder token = Privacy.ConfidentialAsset.newBuilder();
token.setTo(from_keypair.getBubiAddress());
token.setFromPubkey(ct_from_keypair[0]);
String[] ecdh_key_ret = cn.bubi.Privacy.createEcdhKey(cpp_obj, ct_from_keypair[1], ct_from_keypair[0]);
String errCodeEcdhKeyRet = ecdh_key_ret[0];
String errDescEcdhKeyRet = ecdh_key_ret[1];
String ecdh_key = ecdh_key_ret[2];
if (Integer.valueOf(errCodeEcdhKeyRet) != 0) {
System.out.println("Error create ecdh key: " + errDescEcdhKeyRet);
}
String[] commit_ret = cn.bubi.Privacy.createPedersenCommit(cpp_obj, value, ecdh_key);
String errCodeCmtRet = commit_ret[0];
String errDescCmtRet = commit_ret[1];
String commit = commit_ret[2];
if (Integer.valueOf(errCodeCmtRet) != 0) {
System.out.println("Error create commit: " + errDescCmtRet);
}
token.setCommit(commit);
String enc = HexFormat.byteToHex(AesCbc.encrypt(Long.toString(value).getBytes(), ecdh_key.substring(0, 32).getBytes())).toLowerCase();
token.setEncryptValue(enc);
String[] proof_ret = cn.bubi.Privacy.bpRangeproofProve(cpp_obj, ecdh_key, value);
String errCodePrfRet = proof_ret[0];
String errDescPrfRet = proof_ret[1];
String proof = proof_ret[2];
if (Integer.valueOf(errCodeCmtRet) != 0) {
System.out.println("Error create range proof: " + errDescCmtRet);
}
String[] commits = new String[1];
commits[0] = commit;
String[] res = cn.bubi.Privacy.bpRangeproofVerify(cpp_obj, commit, proof);
if (Integer.valueOf(res[0]) != 0) {
System.out.println("Error create range proof: " + errDescCmtRet);
} else {
System.out.println("Verify range proof successfully");
}
token.setRangeProof(proof);
JsonFormat jsonFormat = new JsonFormat();
String tokenStr = jsonFormat.printToString(token.build());
JSONObject input = new JSONObject();
input.put("method", "issue");
JSONObject params = new JSONObject();
params.put("name", "Cocoon");
params.put("symbol", "CNT");
params.put("token", JSONObject.parseObject(tokenStr));
input.put("params", params);
System.out.println("input: " + JSON.toJSONString(input, true));
InvokeContractOperation invokeContractOperation = new InvokeContractOperation.Builder()
.buildDestAddress(contract_address)
.buildInputData(input.toJSONString())
.build();
TransactionCommittedResult result = transaction.buildAddOperation(invokeContractOperation)
.buildGasPrice(1000L).buildFeeLimit(100000000L)
.buildAddSigner(from_keypair.getPubKey(), from_keypair.getPriKey())
.commit();
System.out.println("\n------------------------------------------------");
System.out.println(GsonUtil.toJson(result));
} catch (SdkException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
Transfer of privacy assets
- The structure of the private asset transaction is as follows:
message ConfidentialTx
{
string excess_sig = 1; // Signature of blinding factor
string excess_msg = 2; // Sign message
repeated ConfidentialAsset inputs = 3; // Transaction input list, element is privacy asset, see privacy asset structure for details
repeated ConfidentialAsset outputs = 4; // Transaction output list, the element is a private asset, see the private asset structure for details
}
- Splitting private assets:
The privacy transaction is implemented based on the UTXO model. The split operation is equivalent to taking one asset voucher as input and two asset vouchers as output. The receiving address of the output voucher is specified as the sender itself.
private static void createSplitOperation(BcOperationService operationService, Long value) throws Exception {
String[] ids = {};
JSONObject input = createTx(from_keypair.getBubiAddress(), from_keypair.getBubiAddress(), ct_from_keypair, ct_from_keypair[0], value, ids);
try {
Transaction transaction = operationService.newTransaction(from_keypair.getBubiAddress());
InvokeContractOperation invokeContractOperation = new InvokeContractOperation.Builder()
.buildDestAddress(contract_address)
.buildInputData(input.toJSONString())
.build();
TransactionCommittedResult result = transaction.buildAddOperation(invokeContractOperation)
.buildGasPrice(1000L).buildFeeLimit(100000000L)
.buildTxMetadata("Transaction")
.buildAddSigner(from_keypair.getPubKey(), from_keypair.getPriKey())
.commit();
System.out.println("\n------------------------------------------------");
System.out.println(GsonUtil.toJson(result));
} catch (SdkException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
- Transfer of privacy assets Transferring private assets is also based on the UTXO structure, querying and obtaining the sender's private asset voucher ID, selecting it as the input voucher ID, and constructing an output voucher based on the transfer amount and receiving address and the recipient's public key (privacy-related public key). Then it is assembled into a private transaction structure, and the smart contract transfer interface is triggered with the JSON object as a parameter.
private static void createTransferOperation(BcOperationService operationService, Long value) throws Exception {
String[] ids = {};
JSONObject input = createTx(from_keypair.getBubiAddress(), to_keypair.getBubiAddress(), ct_from_keypair, ct_to_keypair[0], value, ids);
try {
Transaction transaction = operationService.newTransaction(from_keypair.getBubiAddress());
InvokeContractOperation invokeContractOperation = new InvokeContractOperation.Builder()
.buildDestAddress(contract_address)
.buildInputData(input.toJSONString())
.build();
TransactionCommittedResult result = transaction.buildAddOperation(invokeContractOperation)
.buildGasPrice(1000L).buildFeeLimit(100000000L)
.buildTxMetadata("Transaction")
.buildAddSigner(from_keypair.getPubKey(), from_keypair.getPriKey())
.commit();
System.out.println("\n------------------------------------------------");
System.out.println(GsonUtil.toJson(result));
} catch (SdkException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
- createTx code example In the sample code, in order to test easily, the information about sender and receiver, as well as the amount, voucher ID, etc. are passed in as parameters, and then the JSON object of the private transaction is constructed. An example code for constructing a privacy transaction is as follows:
// bubi address mapping only one confidential transcation account
public static JSONObject createTx(String from, String to, String[] ct_from, String ct_to, long value, String[] ids) throws Exception {
// get input token from contract
String[] priv_list = new String[1];
priv_list[0] = ct_from_keypair[1];
Map input_assets = getTokenByValue(from, priv_list, value, ids);
String[] input_blinds = new String[input_assets.size()];
String[] inputs = new String[input_assets.size()];
String[] input_ids = new String[input_assets.size()];
int i = 0;
Long total_balance = 0L;
for (Object key: input_assets.keySet()) {
JSONObject token = (JSONObject) input_assets.get(key);
total_balance += token.getLong("value");
String from_pubkey = token.getString("from_pubkey");
String[] blind_ret = cn.bubi.Privacy.createEcdhKey(cpp_obj, token.getString("spend_key"), from_pubkey);
String errCodeBlindRet = blind_ret[0];
String errDescBlindRet = blind_ret[1];
String blind = blind_ret[2];
if (Integer.valueOf(errCodeBlindRet) != 0) {
System.out.println("Error create blind: " + errDescBlindRet);
i += 1;
continue;
}
input_blinds[i] = blind;
inputs[i] = token.getString("commit");
input_ids[i] = key.toString();
i += 1;
}
// build the spend token
String[] ecdh_key_ret = cn.bubi.Privacy.createEcdhKey(cpp_obj, ct_from[1], ct_to);
String errCodeEcdhRet = ecdh_key_ret[0];
String errDescEcdhRet = ecdh_key_ret[1];
String ecdh_key = ecdh_key_ret[2];
String commit = "";
if (Integer.valueOf(errCodeEcdhRet) != 0) {
System.out.println("Error create echd key: " + errDescEcdhRet);
} else {
String[] commit_ret = cn.bubi.Privacy.createPedersenCommit(cpp_obj, value, ecdh_key);
String errCodeCmtRet = commit_ret[0];
String errDescCmtRet = commit_ret[1];
if (Integer.valueOf(errCodeCmtRet) != 0) {
System.out.println("Error createPedersenCommit: " + errDescCmtRet);
} else {
commit = commit_ret[2];
}
}
String[] ret = cn.bubi.Privacy.bpRangeproofProve(cpp_obj, ecdh_key, value);
String errCodePrfCngRet = ret[0];
String errDescPrfCngRet = ret[1];
String proof = ret[2];
if (Integer.valueOf(errCodePrfCngRet) != 0) {
System.out.println("Error proof change: " + errDescPrfCngRet);
} else {
String[] bp_very_ret1 = cn.bubi.Privacy.bpRangeproofVerify(cpp_obj, commit, proof);
String errCodeBpRet1 = bp_very_ret1[0];
String errDescBpRet1 = bp_very_ret1[1];
if (Integer.valueOf(errCodeBpRet1) != 0) {
System.out.println("Failed to verify range proof:" + proof + ": " + errDescBpRet1 + "\n");
}
}
String encrypt_data = HexFormat.byteToHex(AesCbc.encrypt(Long.toString(value).getBytes(), ecdh_key.substring(0, 32).getBytes())).toLowerCase();
JSONObject token1 = buildToken(ct_from[0], to, encrypt_data, proof, commit);
JSONArray output_assets = null;
// build the change token
String commit_change = null;
String change_key = null;
String[] blinds;
String[] values;
String[] commits;
if (total_balance - value > 0) {
output_assets = new JSONArray(2);
blinds = new String[2];
values = new String[2];
commits = new String[2];
String[] change_key_ret = cn.bubi.Privacy.createEcdhKey(cpp_obj, ct_from[1], ct_from[0]);
String errCodeCngKeyRet = change_key_ret[0];
String errDescCngKeyRet = change_key_ret[1];
change_key = change_key_ret[2];
if (Integer.valueOf(errCodeCngKeyRet) != 0) {
System.out.println("Error create change key: " + errDescCngKeyRet);
}
String[] commit_change_ret = cn.bubi.Privacy.createPedersenCommit(cpp_obj, total_balance - value, change_key);
String errCodeCmtCngRet = commit_change_ret[0];
String errDescCmtCngRet = commit_change_ret[1];
commit_change = commit_change_ret[2];
if (Integer.valueOf(errCodeCmtCngRet) != 0) {
System.out.println("Error create commit change: " + errDescCmtCngRet);
}
String encrypt_change = HexFormat.byteToHex(AesCbc.encrypt(Long.toString(total_balance - value).getBytes(), change_key.substring(0, 32).getBytes())).toLowerCase();
String[] change_ret = cn.bubi.Privacy.bpRangeproofProve(cpp_obj, change_key, total_balance - value);
errCodePrfCngRet = change_ret[0];
errDescPrfCngRet = change_ret[1];
String proof_change = change_ret[2];
if (Integer.valueOf(errCodePrfCngRet) != 0) {
System.out.println("Error proof change: " + errDescPrfCngRet);
} else {
String[] bp_very_ret1 = cn.bubi.Privacy.bpRangeproofVerify(cpp_obj, commit_change, proof_change);
String errCodeBpRet1 = bp_very_ret1[0];
String errDescBpRet1 = bp_very_ret1[1];
if (Integer.valueOf(errCodeBpRet1) != 0) {
System.out.println("Failed to verify range proof:" + proof + ": " + errDescBpRet1 + "\n");
}
}
JSONObject token2 = buildToken(ct_from[0], from, encrypt_change, proof_change, commit_change);
output_assets.add(token1);
output_assets.add(token2);
blinds[1] = change_key;
values[1] = Long.toString(total_balance - value);
commits[1] = commit_change;
} else {
output_assets = new JSONArray(1);
blinds = new String[1];
values = new String[1];
commits = new String[1];
output_assets.add(token1);
}
blinds[0] = ecdh_key;
values[0] = Long.toString(value);
commits[0] = commit;
// create excess
String[] output_blinds;
if(total_balance - value > 0) {
output_blinds = new String[2];
output_blinds[1] = change_key;
} else {
output_blinds = new String[1];
}
output_blinds[0] = ecdh_key;
String data = "64e4a2fcf36693904a0d549303c6f35c";
String[] excess_sig_ret = cn.bubi.Privacy.excessSign(cpp_obj, input_blinds, output_blinds, data);
String errCodeExcSigRet = excess_sig_ret[0];
String errDescExcSigRet = excess_sig_ret[1];
String excess_sig = excess_sig_ret[2];
if (Integer.valueOf(errCodeExcSigRet) != 0) {
System.out.println("Error excess sig: " + errDescExcSigRet);
}
// verify tx data
String[] tally_very_ret = cn.bubi.Privacy.pedersenTallyVerify(cpp_obj, inputs, commits, data, excess_sig);
String errCodeTalVeryRet = tally_very_ret[0];
String errDescTalVeryRet = tally_very_ret[1];
if(Integer.valueOf(errCodeTalVeryRet) != 0) {
System.out.println("Failed to verify tally, " + errDescTalVeryRet);
} else {
System.out.println("Verify tally done\n");
}
// build tx
JSONObject txJson = buildTx(input_ids, output_assets, data, excess_sig);
JSONObject input = new JSONObject();
input.put("method", "transfer");
input.put("params", txJson);
System.out.println("The contrat input is:");
System.out.println(JSON.toJSONString(txJson, true));
return input;
}
Get account balance
In the smart contract, the user account address is used as the key to record the privacy asset certificate owned by the account. The balance of the account is equal to the sum of all vouchers. To obtain the balance, you need to use the privacy related private key to decrypt and accumulate the amount in the voucher. The code example is as follows:
public static void getBalance(String address, String[] privs) throws Exception {
SetMetadata data = queryService.getAccount(contract_address, address);
System.out.println("Balance of address:" + address);
if(data == null) {
System.out.println("0");
return;
}
JSONObject dataJson = JSONObject.parseObject(data.getValue());
for (Object token: dataJson.getJSONArray("assets")) {
JSONObject t = (JSONObject) token;
long balance = 0;
boolean decrypt_done = false;
for(String priv: privs) {
String[] dec_key_ret = cn.bubi.Privacy.createEcdhKey(cpp_obj, priv, t.getString("from_pubkey"));
String errCodeDecKeyRet = dec_key_ret[0];
String errDescDecKeyRet = dec_key_ret[1];
String dec_key = dec_key_ret[2];
if (Integer.valueOf(errCodeDecKeyRet) != 0) {
System.out.println("Error decrypt key:" + errDescDecKeyRet);
throw new UnsatisfiedLinkError(errDescDecKeyRet);
} {
String encrypt_value = t.getString("encrypt_value");
try {
balance = Long.parseLong(AesCbc.decrypt(HexFormat.hexToByte(encrypt_value), dec_key.substring(0, 32).getBytes()));
decrypt_done = true;
break;
}catch (Exception e) {
continue;
}
}
}
if(!decrypt_done) {
throw new UnsatisfiedLinkError("Failed to decrypt token of id: " + t.getString("id"));
};
System.out.println("id: " + t.getString("id") + ", value:" + balance);
}
return;
}