Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions framework/src/main/java/org/tron/core/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ public class Wallet {
"BurnNewLeaf(uint256,bytes32,bytes32,bytes32,bytes32[21])"));
private static final byte[] SHIELDED_TRC20_LOG_TOPICS_BURN_TOKEN = Hash.sha3(ByteArray
.fromString("TokenBurn(address,uint256,bytes32[3])"));
private static final byte[] SHIELDED_TRC20_LOG_TOPICS_NOTE_SPENT = Hash.sha3(ByteArray
.fromString("NoteSpent(bytes32)"));
private static final String BROADCAST_TRANS_FAILED = "Broadcast transaction {} failed, {}.";

@Getter
Expand Down Expand Up @@ -3672,9 +3674,7 @@ public ShieldedTRC20Parameters createShieldedContractParameters(
builder.setTransparentToAddress(transparentToAddressTvm);
builder.setTransparentToAmount(toAmount);

Optional<byte[]> cipher = NoteEncryption.Encryption
.encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress);
cipher.ifPresent(builder::setBurnCiphertext);
builder.setOvk(ovk);

ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk);
GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0);
Expand Down Expand Up @@ -3799,9 +3799,7 @@ public ShieldedTRC20Parameters createShieldedContractParametersWithoutAsk(
System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20);
builder.setTransparentToAddress(transparentToAddressTvm);
builder.setTransparentToAmount(toAmount);
Optional<byte[]> cipher = NoteEncryption.Encryption
.encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress);
cipher.ifPresent(builder::setBurnCiphertext);
builder.setOvk(ovk);
GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0);
buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk);
if (receiveSize == 1) {
Expand Down Expand Up @@ -3838,6 +3836,8 @@ private int getShieldedTRC20LogType(TransactionInfo.Log log, byte[] contractAddr
return 3;
} else if (Arrays.equals(topicsBytes, SHIELDED_TRC20_LOG_TOPICS_BURN_TOKEN)) {
return 4;
} else if (Arrays.equals(topicsBytes, SHIELDED_TRC20_LOG_TOPICS_NOTE_SPENT)) {
return 5;
}
}
return 0;
Expand Down Expand Up @@ -4001,7 +4001,8 @@ public DecryptNotesTRC20 scanShieldedTRC20NotesByIvk(

private Optional<DecryptNotesTRC20.NoteTx> getNoteTxFromLogListByOvk(
DecryptNotesTRC20.NoteTx.Builder builder,
TransactionInfo.Log log, byte[] ovk, int logType) throws ZksnarkException {
TransactionInfo.Log log, byte[] ovk, int logType, byte[] pendingNf)
throws ZksnarkException {
byte[] logData = log.getData().toByteArray();
if (!ArrayUtils.isEmpty(logData)) {
if (logType > 0 && logType < 4) {
Expand Down Expand Up @@ -4040,18 +4041,27 @@ private Optional<DecryptNotesTRC20.NoteTx> getNoteTxFromLogListByOvk(
}
}
} else if (logType == 4) {
//Data = toAddress(32) + value(32) + ciphertext(80) + padding(16)
//Data = toAddress(32) + value(32) + cipher(80) + nonce(12) + reserved(4)
if (logData.length < 64 + NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE) {
return Optional.empty();
}
byte[] logToAddress = ByteArray.subArray(logData, 12, 32);
byte[] logAmountArray = ByteArray.subArray(logData, 32, 64);
byte[] cipher = ByteArray.subArray(logData, 64, 144);
byte[] nonceFromLog = ByteArray.subArray(logData, 144, 156);
BigInteger logAmount = ByteUtil.bytesToBigInteger(logAmountArray);
byte[] plaintext;
byte[] amountArray = new byte[32];
byte[] decryptedAddress = new byte[20];

Optional<byte[]> decryptedText = NoteEncryption.Encryption
.decryptBurnMessageByOvk(ovk, cipher);
.decryptBurnMessageByOvk(ovk, cipher, nonceFromLog, pendingNf);

if (decryptedText.isPresent()) {
plaintext = decryptedText.get();
if (plaintext[32] != Wallet.getAddressPreFixByte()) {
return Optional.empty();
}
System.arraycopy(plaintext, 0, amountArray, 0, 32);
System.arraycopy(plaintext, 33, decryptedAddress, 0, 20);
BigInteger decryptedAmount = ByteUtil.bytesToBigInteger(amountArray);
Expand Down Expand Up @@ -4091,15 +4101,24 @@ public DecryptNotesTRC20 scanShieldedTRC20NotesByOvk(long startNum, long endNum,
if (!Objects.isNull(logList)) {
Optional<DecryptNotesTRC20.NoteTx> noteTx;
int index = 0;
byte[] pendingNf = null;
for (TransactionInfo.Log log : logList) {
int logType = getShieldedTRC20LogType(log, shieldedTRC20ContractAddress);
if (logType > 0) {
if (logType == 5) {
byte[] logData = log.getData().toByteArray();
if (logData.length >= 32) {
pendingNf = ByteArray.subArray(logData, 0, 32);
}
} else if (logType > 0) {
noteBuilder = DecryptNotesTRC20.NoteTx.newBuilder();
noteBuilder.setTxid(ByteString.copyFrom(txid));
noteBuilder.setIndex(index);
index += 1;
noteTx = getNoteTxFromLogListByOvk(noteBuilder, log, ovk, logType);
noteTx = getNoteTxFromLogListByOvk(noteBuilder, log, ovk, logType, pendingNf);
noteTx.ifPresent(builder::addNoteTxs);
if (logType == 4) {
pendingNf = null;
}
}
}
}
Expand Down Expand Up @@ -4283,8 +4302,13 @@ public BytesMessage getTriggerInputForShieldedTRC20Contract(
parameterType);
if (parametersBuilder.getShieldedTRC20ParametersType() == ShieldedTRC20ParametersType.BURN) {
byte[] burnCiper = ByteArray.fromHexString(shieldedTRC20Parameters.getTriggerContractInput());
if (!ArrayUtils.isEmpty(burnCiper) && burnCiper.length == 80) {
if (!ArrayUtils.isEmpty(burnCiper)
&& burnCiper.length == NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE) {
parametersBuilder.setBurnCiphertext(burnCiper);
} else if (!ArrayUtils.isEmpty(burnCiper) && burnCiper.length == 80) {
throw new ZksnarkException(
"burn cipher length 80 is deprecated, expected "
+ NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE);
} else {
throw new ZksnarkException(
"invalid shielded TRC-20 contract parameters for burn trigger input");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.tron.core.zen.address.PaymentAddress;
import org.tron.core.zen.note.Note;
import org.tron.core.zen.note.NoteEncryption;
import org.tron.core.zen.note.NoteEncryption.Encryption;
import org.tron.core.zen.note.OutgoingPlaintext;
import org.tron.protos.contract.ShieldContract;
import org.tron.protos.contract.ShieldContract.ReceiveDescription;
Expand Down Expand Up @@ -61,7 +62,9 @@ public class ShieldedTRC20ParametersBuilder {
@Setter
private BigInteger transparentToAmount;
@Setter
private byte[] burnCiphertext = new byte[80];
private byte[] ovk;
@Setter
private byte[] burnCiphertext = new byte[Encryption.BURN_CIPHER_RECORD_SIZE];

public ShieldedTRC20ParametersBuilder() {

Expand Down Expand Up @@ -292,6 +295,21 @@ public ShieldedTRC20Parameters build(boolean withAsk) throws ZksnarkException {
SpendDescriptionInfo spend = spends.get(0);
spendDescription = generateSpendProof(spend, ctx).getInstance();
builder.addSpendDescription(spendDescription);

if (ovk == null && spend.expsk != null) {
ovk = spend.expsk.getOvk();
}
byte[] nf = spendDescription.getNullifier().toByteArray();
byte[] addr21 = new byte[21];
addr21[0] = Wallet.getAddressPreFixByte();
System.arraycopy(transparentToAddress, 0, addr21, 1, 20);
Optional<byte[]> cipherOpt = Encryption.encryptBurnMessageByOvk(
ovk, transparentToAmount, addr21, nf);
if (!cipherOpt.isPresent()) {
throw new ZksnarkException("encrypt burn message failed");
}
Comment thread
Federico2014 marked this conversation as resolved.
burnCiphertext = cipherOpt.get();

mergedBytes = ByteUtil.merge(shieldedTRC20Address,
encodeSpendDescriptionWithoutSpendAuthSig(spendDescription));
if (receives.size() == 1) {
Expand Down Expand Up @@ -492,7 +510,6 @@ private String burnParamsToHexString(GrpcAPI.ShieldedTRC20Parameters burnParams,
}

byte[] mergedBytes;
byte[] zeros = new byte[16];
mergedBytes = ByteUtil.merge(
spendDesc.getNullifier().toByteArray(),
spendDesc.getAnchor().toByteArray(),
Expand All @@ -503,8 +520,7 @@ private String burnParamsToHexString(GrpcAPI.ShieldedTRC20Parameters burnParams,
ByteUtil.bigIntegerToBytes(value, 32),
burnParams.getBindingSignature().toByteArray(),
payTo,
burnCiphertext,
zeros
burnCiphertext
);

byte[] outputOffsetBytes; // 32
Expand All @@ -524,7 +540,7 @@ private String burnParamsToHexString(GrpcAPI.ShieldedTRC20Parameters burnParams,
coffsetBytes = ByteUtil.longTo32Bytes(mergedBytes.length + 32 * 3 + 32L * 9);
countBytes = ByteUtil.longTo32Bytes(1L);
ReceiveDescription recvDesc = burnParams.getReceiveDescription(0);
zeros = new byte[12];
byte[] zeros = new byte[12];
mergedBytes = ByteUtil
.merge(mergedBytes,
outputOffsetBytes,
Expand Down
101 changes: 87 additions & 14 deletions framework/src/main/java/org/tron/core/zen/note/NoteEncryption.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
import static org.tron.core.zen.note.NoteEncryption.Encryption.NOTEENCRYPTION_CIPHER_KEYSIZE;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.tron.common.crypto.Hash;
import org.tron.common.utils.ByteUtil;
import org.tron.common.zksnark.JLibrustzcash;
import org.tron.common.zksnark.JLibsodium;
Expand Down Expand Up @@ -111,6 +113,11 @@ public OutCiphertext encryptToOurselves(
public static class Encryption {

public static final int NOTEENCRYPTION_CIPHER_KEYSIZE = 32;
public static final int BURN_CIPHER_LEN = 80;
public static final int BURN_NONCE_LEN = 12;
public static final int BURN_CIPHER_RECORD_SIZE = 96;
private static final byte[] BURN_NONCE_DOMAIN =
"Ztron_BurnNonce".getBytes(StandardCharsets.UTF_8);

/**
* generate ock by ovk, cv, cm, epk
Expand Down Expand Up @@ -246,47 +253,113 @@ public static Optional<OutPlaintext> attemptOutDecryption(
}

/**
* encrypt the message by ovk used for scanning
* encrypt burn message with nf-derived nonce, returns 96B record:
* cipher(80) + nonce(12) + reserved(4).
*/
public static Optional<byte[]> encryptBurnMessageByOvk(byte[] ovk, BigInteger toAmount,
byte[] transparentToAddress)
byte[] transparentToAddress, byte[] nf)
throws ZksnarkException {
if (ovk == null || ovk.length != NOTEENCRYPTION_CIPHER_KEYSIZE) {
throw new ZksnarkException("invalid ovk length");
}
if (transparentToAddress == null || transparentToAddress.length != 21) {
throw new ZksnarkException("invalid transparentToAddress length");
}
if (nf == null || nf.length != 32) {
throw new ZksnarkException("invalid nullifier length");
}
byte[] plaintext = new byte[64];
byte[] amountArray = ByteUtil.bigIntegerToBytes(toAmount, 32);
byte[] cipherNonce = new byte[12];
byte[] cipher = new byte[80];
byte[] nonce = deriveNonceFromNf(nf);
byte[] cipher = new byte[BURN_CIPHER_LEN];
System.arraycopy(amountArray, 0, plaintext, 0, 32);
System.arraycopy(transparentToAddress, 0, plaintext, 32,
21);
System.arraycopy(transparentToAddress, 0, plaintext, 32, 21);

if (JLibsodium.cryptoAeadChacha20Poly1305IetfEncrypt(new Chacha20Poly1305IetfEncryptParams(
cipher, null, plaintext,
64, null, 0, null, cipherNonce, ovk)) != 0) {
64, null, 0, null, nonce, ovk)) != 0) {
return Optional.empty();
}

return Optional.of(cipher);
byte[] record = new byte[BURN_CIPHER_RECORD_SIZE];
System.arraycopy(cipher, 0, record, 0, BURN_CIPHER_LEN);
System.arraycopy(nonce, 0, record, BURN_CIPHER_LEN, BURN_NONCE_LEN);
return Optional.of(record);
}

/**
* decrypt the message by ovk used for scanning
* Derive a 12-byte ChaCha20-Poly1305 nonce from the spend nullifier.
* The nullifier is prefixed with a fixed domain tag before hashing so the
* derivation cannot collide with any other SHA3-of-nf usage in the codebase.
*/
public static Optional<byte[]> decryptBurnMessageByOvk(byte[] ovk, byte[] ciphertext)
throws ZksnarkException {
private static byte[] deriveNonceFromNf(byte[] nf) {
byte[] tagged = new byte[BURN_NONCE_DOMAIN.length + nf.length];
System.arraycopy(BURN_NONCE_DOMAIN, 0, tagged, 0, BURN_NONCE_DOMAIN.length);
System.arraycopy(nf, 0, tagged, BURN_NONCE_DOMAIN.length, nf.length);
byte[] hash = Hash.sha3(tagged);
byte[] nonce = new byte[BURN_NONCE_LEN];
System.arraycopy(hash, 0, nonce, 0, BURN_NONCE_LEN);
return nonce;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* decrypt burn message. Uses nonceFromLog to determine v1/v2:
* - all-zero nonce → v1 legacy path (pre-upgrade burn), decrypt with zero nonce.
* - non-zero nonce + nf present → v2 strict mode, verify nonce == sha3(domain || nf)[:12]
* before decrypting; rejects any cipher whose nonce was not bound to this nf.
* - non-zero nonce + nf absent → v2 relaxed mode, decrypt without re-deriving nonce.
* This path exists so a scanner that lost the matching NoteSpent log within the
* transaction can still recover the plaintext. It is sound because (a) the AEAD tag
* is keyed by ovk, so only the legitimate ovk holder can decrypt at all, and
* (b) any forged nonce would be rejected by the AEAD tag under the same key.
* In other words, omitting nf weakens key↔nonce binding to AEAD-tag binding —
* it never weakens authenticity.
*/
public static Optional<byte[]> decryptBurnMessageByOvk(byte[] ovk, byte[] ciphertext,
byte[] nonceFromLog, byte[] nf) throws ZksnarkException {
if (ovk == null || ovk.length != NOTEENCRYPTION_CIPHER_KEYSIZE) {
throw new ZksnarkException("invalid ovk length");
}
if (ciphertext == null || ciphertext.length != BURN_CIPHER_LEN
|| nonceFromLog == null || nonceFromLog.length != BURN_NONCE_LEN) {
return Optional.empty();
}

byte[] effectiveNonce;
if (isAllZero(nonceFromLog)) {
effectiveNonce = nonceFromLog;
} else {
if (nf != null) {
byte[] derived = deriveNonceFromNf(nf);
if (!java.util.Arrays.equals(nonceFromLog, derived)) {
return Optional.empty();
}
}
effectiveNonce = nonceFromLog;
}

byte[] outPlaintext = new byte[64];
byte[] cipherNonce = new byte[12];
if (JLibsodium.cryptoAeadChacha20poly1305IetfDecrypt(new Chacha20poly1305IetfDecryptParams(
outPlaintext, null,
null,
ciphertext, 80,
ciphertext, BURN_CIPHER_LEN,
null,
0,
cipherNonce, ovk)) != 0) {
effectiveNonce, ovk)) != 0) {
return Optional.empty();
}
return Optional.of(outPlaintext);
}

private static boolean isAllZero(byte[] data) {
for (byte b : data) {
if (b != 0) {
return false;
}
}
return true;
}

public static class EncCiphertext {

@Getter
Expand Down
Loading
Loading