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
23 changes: 18 additions & 5 deletions chainbase/src/main/java/org/tron/common/zksnark/JLibrustzcash.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ public class JLibrustzcash {

private static Librustzcash INSTANCE = LibrustzcashWrapper.getInstance();

public static void librustzcashZip32XskMaster(Zip32XskMasterParams params) {
public static void librustzcashZip32XskMaster(Zip32XskMasterParams params)
throws ZksnarkException {
params.valid();
INSTANCE.librustzcashZip32XskMaster(params.getData(), params.getSize(), params.getM_bytes());
}

Expand All @@ -38,11 +40,15 @@ public static void librustzcashInitZksnarkParams(InitZksnarkParams params) {
params.getSpend_hash(), params.getOutput_path(), params.getOutput_hash());
}

public static void librustzcashZip32XskDerive(Zip32XskDeriveParams params) {
public static void librustzcashZip32XskDerive(Zip32XskDeriveParams params)
throws ZksnarkException {
params.valid();
INSTANCE.librustzcashZip32XskDerive(params.getData(), params.getSize(), params.getM_bytes());
}

public static boolean librustzcashZip32XfvkAddress(Zip32XfvkAddressParams params) {
public static boolean librustzcashZip32XfvkAddress(Zip32XfvkAddressParams params)
throws ZksnarkException {
params.valid();
Comment on lines +45 to +51

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT / SDK & Native Hardening] Handle invalid serialized ZIP32 keys without native panics.

The new params.valid() checks validate buffer lengths but do not validate the serialized extended-key contents:

Consequently, correctly sized buffers containing invalid serialized keys can still trigger a native panic during decoding.

I found no production callers of these wrappers in the current java-tron tree, so this can be tracked as a follow-up rather than blocking this PR.

Before accepting untrusted extended keys through these interfaces, please replace the native expect() calls with controlled error handling and propagate failures safely through JNI and the SDK. The derive API currently returns void, so its failure-reporting mechanism also needs to be addressed.

Please add regression tests using correctly sized but invalid serialized XSK/XFVK values, verifying that decoding failures are handled without native panics.

The absence of production callers in java-tron does not establish whether other SDK consumers expose these functions to untrusted inputs.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@3for Agreed. The length checks do not validate serialized key contents, so this remains incomplete, as noted in the PR description.

I'll address it in a separate SDK/native follow-up: replace both decoding expect() calls with controlled error handling, propagate failures through JNI and Java—including an explicit failure contract for XSK derivation—and add regression tests using correctly sized but invalid XSK/XFVK inputs. Other SDK consumers also need to be considered, even though these wrappers currently have no production callers in java-tron.

return INSTANCE.librustzcashZip32XfvkAddress(params.getXfvk(), params.getJ(),
params.getJ_ret(), params.getAddr_ret());
}
Expand Down Expand Up @@ -118,14 +124,21 @@ public static boolean librustzcashCheckDiversifier(byte[] d) throws ZksnarkExcep
return INSTANCE.librustzcashCheckDiversifier(d);
}

public static boolean librustzcashSaplingSpendProof(SpendProofParams params) {
public static boolean librustzcashSaplingSpendProof(SpendProofParams params)
throws ZksnarkException {
params.valid();
return INSTANCE.librustzcashSaplingSpendProof(params.getCtx(), params.getAk(),
params.getNsk(), params.getD(), params.getR(), params.getAlpha(), params.getValue(),
params.getAnchor(), params.getVoucherPath(), params.getCv(), params.getRk(),
params.getZkproof());
}

public static boolean librustzcashSaplingOutputProof(OutputProofParams params) {
public static boolean librustzcashSaplingOutputProof(OutputProofParams params)
throws ZksnarkException {
params.valid();
if (!librustzcashCheckDiversifier(params.getD())) {
throw new ZksnarkException("Invalid output diversifier");
}
return INSTANCE.librustzcashSaplingOutputProof(params.getCtx(), params.getEsk(),
params.getD(), params.getPkD(), params.getR(), params.getValue(), params.getCv(),
params.getZkproof());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ public static void valid32Params(byte[] value) throws ZksnarkException {
validParamLength(value, 32);
}

public static void validSpendAk(byte[] ak) throws ZksnarkException {
valid32Params(ak);
// Jubjub encodes y in little-endian order, with x's parity in the top bit of byte 31.
// The identity (x = 0, y = 1) has canonical encoding 01 00 ... 00 00.
// The current native decoder also accepts 01 00 ... 00 80: negating x = 0 still gives 0,
// and it does not reject this non-canonical sign bit. Mask it to catch both encodings.
// This only excludes the identity; native code still validates the point and subgroup.
if (ak[0] != 1 || (ak[31] & 0x7f) != 0) {
return;
}
for (int i = 1; i < 31; i++) {
if (ak[i] != 0) {
return;
}
}
throw new ZksnarkException("Invalid spend ak: identity point");
}

public static void validVoucherPath(byte[] voucherPath) throws ZksnarkException {
validParamLength(voucherPath, 1 + 33 * 32 + 8);
validByteValue(voucherPath[0], (byte) 0x20);
Expand Down Expand Up @@ -117,6 +135,9 @@ public Zip32XskMasterParams(byte[] data, int size, byte[] m_bytes) throws Zksnar

@Override
public void valid() throws ZksnarkException {
if (data == null || size < 0 || size > data.length) {
throw new ZksnarkException("Invalid ZIP32 seed length");
}
validParamLength(m_bytes, 169);
}
}
Expand All @@ -129,7 +150,8 @@ public Zip32XskDeriveParams(byte[] data, int size, byte[] m_bytes) throws Zksnar

@Override
public void valid() throws ZksnarkException {

validParamLength(getData(), 169);
validParamLength(getM_bytes(), 169);
}
}

Expand Down Expand Up @@ -159,7 +181,10 @@ public Zip32XfvkAddressParams(byte[] xfvk, byte[] j, byte[] j_ret, byte[] addr_r

@Override
public void valid() throws ZksnarkException {

validParamLength(xfvk, 169);
validParamLength(j, 11);
validParamLength(j_ret, 11);
validParamLength(addr_ret, 43);
}
}

Expand Down Expand Up @@ -451,7 +476,7 @@ public static SpendProofParams decode(long ctx, byte[] data)
@Override
public void valid() throws ZksnarkException {
validValueParams(value);
valid32Params(ak);
validSpendAk(ak);
valid32Params(nsk);
valid11Params(d);
valid32Params(r);
Expand Down
9 changes: 7 additions & 2 deletions framework/src/main/java/org/tron/core/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -2868,6 +2868,9 @@ public SpendResult isSpend(NoteParameters noteParameters) throws
note.getRcm().toByteArray());
byte[] nf = baseNote.nullifier(ak, nk, voucherContainer.position());

if (nf == null) {
throw new ZksnarkException("compute nullifier error");
}
if (chainBaseManager.getNullifierStore().has(nf)) {
result = SpendResult.newBuilder()
.setResult(true)
Expand All @@ -2893,7 +2896,9 @@ public BytesMessage createSpendAuthSig(SpendAuthSigParameters spendAuthSigParame
spendAuthSigParameters.getAlpha().toByteArray(),
spendAuthSigParameters.getTxHash().toByteArray(),
result);
JLibrustzcash.librustzcashSaplingSpendSig(spendSigParams);
if (!JLibrustzcash.librustzcashSaplingSpendSig(spendSigParams)) {
throw new ZksnarkException("Spend signature generation failed");
}

return BytesMessage.newBuilder().setValue(ByteString.copyFrom(result)).build();
}
Expand Down Expand Up @@ -2925,7 +2930,7 @@ public BytesMessage createShieldNullifier(NfParameters nfParameters) throws Zksn
incrementalMerkleVoucherContainer.position(),
result);
if (!JLibrustzcash.librustzcashComputeNf(computeNfParams)) {
return null;
throw new ZksnarkException("compute nullifier error");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SHOULD] Native nullifier failures still cannot reach this check.

JLibrustzcash.librustzcashComputeNf() calls the SDK and unconditionally returns true. The SDK Java method returns void, and the JNI bridge discards Rust’s boolean return value.

Rust can return false before writing the output buffer. The Java-side ComputeNfParams.valid() checks lengths and non-negative values, but does not cover all native semantic checks. Consequently, createShieldNullifier() can still return the untouched output buffer as a successful result.

The new null check in isSpend() has the same limitation: Note.nullifier() returns null only when that wrapper returns false.

Please preserve an explicit success/failure status through Rust → JNI → SDK Java → JLibrustzcash, or provide an equivalent reliable error contract. Add regression coverage across that boundary: the current failure tests mock JLibrustzcash itself, so they exercise the Wallet branches without detecting the status loss. An all-zero output check should not substitute for the success status.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@3for Agreed. The current checks do not complete the fix: JNI discards the native return value, the SDK method returns void, and librustzcashComputeNf() always returns true. The mocked tests only verify the Wallet failure branches.

As noted in the PR description, this remains incomplete until the SDK/JNI/native changes are published and the dependency is upgraded. I'll wire librustzcashSaplingComputeNfChecked through the Java wrapper and add regression coverage exercising a real native failure through JNI and the SDK, verifying that it cannot produce a successful nullifier result. An all-zero output check will not substitute for an explicit failure status.

}

return BytesMessage.newBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,13 @@ private void createSpendAuth(byte[] dataToBeSigned) throws ZksnarkException {
throw new ZksnarkException("missing expanded spending key for spend authorization");
}
byte[] result = new byte[64];
JLibrustzcash.librustzcashSaplingSpendSig(
if (!JLibrustzcash.librustzcashSaplingSpendSig(
new LibrustzcashParam.SpendSigParams(spends.get(i).expsk.getAsk(),
spends.get(i).alpha,
dataToBeSigned,
result));
result))) {
throw new ZksnarkException("Spend signature generation failed");
}
builder.getSpendDescriptionBuilder(i)
.setSpendAuthoritySignature(ByteString.copyFrom(result));
}
Expand Down Expand Up @@ -354,12 +356,14 @@ public ShieldedTRC20Parameters build(boolean withAsk) throws ZksnarkException {
builder.setMessageHash(ByteString.copyFrom(dataHashToBeSigned));

byte[] bindingSig = new byte[64];
JLibrustzcash.librustzcashSaplingBindingSig(
if (!JLibrustzcash.librustzcashSaplingBindingSig(
new LibrustzcashParam.BindingSigParams(ctx,
valueBalance,
dataHashToBeSigned,
bindingSig)
);
)) {
throw new ZksnarkException("Binding signature generation failed");
}
builder.setBindingSignature(ByteString.copyFrom(bindingSig));
} catch (Exception e) {
throw new ZksnarkException("build the shielded TRC-20 parameters error: " + e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,14 @@ public TransactionCapsule build(boolean withAsk) throws ZksnarkException {
}

byte[] bindingSig = new byte[64];
JLibrustzcash.librustzcashSaplingBindingSig(
if (!JLibrustzcash.librustzcashSaplingBindingSig(
new BindingSigParams(ctx,
valueBalance,
dataHashToBeSigned,
bindingSig)
);
)) {
throw new ZksnarkException("Binding signature generation failed");
}
contractBuilder.setBindingSignature(ByteString.copyFrom(bindingSig));
} catch (ZksnarkException e) {
throw e;
Expand All @@ -197,11 +199,13 @@ public TransactionCapsule build(boolean withAsk) throws ZksnarkException {
public void createSpendAuth(byte[] dataToBeSigned) throws ZksnarkException {
for (int i = 0; i < spends.size(); i++) {
byte[] result = new byte[64];
JLibrustzcash.librustzcashSaplingSpendSig(
if (!JLibrustzcash.librustzcashSaplingSpendSig(
new SpendSigParams(spends.get(i).expsk.getAsk(),
spends.get(i).alpha,
dataToBeSigned,
result));
result))) {
throw new ZksnarkException("Spend signature generation failed");
}
contractBuilder.getSpendDescriptionBuilder(i)
.setSpendAuthoritySignature(ByteString.copyFrom(result));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -67,7 +68,14 @@ private static String getParamsFile(String fileName) {
}
FileUtils.copyToFile(in, fileOut);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT] Could we allocate these parameter files atomically with Files.createTempFile, preferably in a node-private temporary directory, instead of constructing timestamp-based paths? Please preserve the new cleanup and error propagation behavior. The native hash check validates parameter contents, but does not establish ownership of the destination used by the earlier copy operation.

} catch (IOException e) {
logger.error(e.getMessage(), e);
try {
Files.deleteIfExists(fileOut.toPath());
} catch (IOException cleanupFailure) {
e.addSuppressed(cleanupFailure);
fileOut.deleteOnExit();
}
throw new TronError("Failed to copy ZK-SNARK parameter file: " + resourcePath,
e, TronError.ErrCode.ZCASH_INIT);
}
if (fileOut.exists()) {
fileOut.deleteOnExit();
Expand Down
24 changes: 22 additions & 2 deletions framework/src/main/java/org/tron/core/zen/note/Note.java
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ public static Note decode(NoteEncryption.Encryption.EncPlaintext encPlaintext)
return ret;
}

private static Optional<Note> decodeForDecryption(
NoteEncryption.Encryption.EncPlaintext plaintext) {
final Note note;
try {
note = decode(plaintext);
} catch (ZksnarkException e) {
return Optional.empty();
}
// Authenticated plaintext is still sender-controlled; invalid notes must not abort scanning.
return note.value < 0 ? Optional.empty() : Optional.of(note);
}

/**
* decrypt c_enc with ivk to plain_enc
*/
Expand All @@ -131,7 +143,11 @@ public static Optional<Note> decrypt(
if (!pt.isPresent()) {
return Optional.empty();
}
Note ret = decode(pt.get());
Optional<Note> decoded = decodeForDecryption(pt.get());
if (!decoded.isPresent()) {
return Optional.empty();
}
Note ret = decoded.get();
byte[] pkD = new byte[32];
if (!JLibrustzcash
.librustzcashIvkToPkd(new IvkToPkdParams(ivk, ret.d.getData(), pkD))) {
Expand Down Expand Up @@ -160,7 +176,11 @@ public static Optional<Note> decrypt(
if (!pt.isPresent()) {
return Optional.empty();
}
Note ret = decode(pt.get());
Optional<Note> decoded = decodeForDecryption(pt.get());
if (!decoded.isPresent()) {
return Optional.empty();
}
Note ret = decoded.get();
byte[] cmuExpected = new byte[32];
if (!JLibrustzcash.librustzcashComputeCm(
new ComputeCmParams(ret.d.getData(), pkD, ret.value, ret.rcm, cmuExpected))) {
Expand Down
Loading
Loading