diff --git a/chainbase/src/main/java/org/tron/common/zksnark/JLibrustzcash.java b/chainbase/src/main/java/org/tron/common/zksnark/JLibrustzcash.java index 88ef1991317..a3c7ccd8b5d 100644 --- a/chainbase/src/main/java/org/tron/common/zksnark/JLibrustzcash.java +++ b/chainbase/src/main/java/org/tron/common/zksnark/JLibrustzcash.java @@ -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()); } @@ -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(); return INSTANCE.librustzcashZip32XfvkAddress(params.getXfvk(), params.getJ(), params.getJ_ret(), params.getAddr_ret()); } @@ -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()); diff --git a/chainbase/src/main/java/org/tron/common/zksnark/LibrustzcashParam.java b/chainbase/src/main/java/org/tron/common/zksnark/LibrustzcashParam.java index 43db8cf0358..09e3f8aa1e9 100644 --- a/chainbase/src/main/java/org/tron/common/zksnark/LibrustzcashParam.java +++ b/chainbase/src/main/java/org/tron/common/zksnark/LibrustzcashParam.java @@ -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); @@ -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); } } @@ -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); } } @@ -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); } } @@ -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); diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index eea50879d9a..1caf44002e9 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -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) @@ -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(); } @@ -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"); } return BytesMessage.newBuilder() @@ -4538,14 +4543,19 @@ public BytesMessage getTriggerInputForShieldedTRC20Contract( ShieldedTRC20ParametersBuilder parametersBuilder = new ShieldedTRC20ParametersBuilder( parameterType); if (parametersBuilder.getShieldedTRC20ParametersType() == ShieldedTRC20ParametersType.BURN) { - byte[] burnCiper = ByteArray.fromHexString(shieldedTRC20Parameters.getTriggerContractInput()); - if (ArrayUtils.isEmpty(burnCiper) - || burnCiper.length != NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE) { - if (!ArrayUtils.isEmpty(burnCiper) && burnCiper.length == 80) { - throw new ZksnarkException( - "legacy 80-byte burn cipher is deprecated and rejected; expected " - + NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE + "-byte burn record"); - } + byte[] burnCiper; + try { + burnCiper = ByteArray.fromHexString(shieldedTRC20Parameters.getTriggerContractInput()); + } catch (DecoderException e) { + throw new ContractValidateException( + "invalid shielded TRC-20 trigger input: burn record must be valid hex", e); + } + if (burnCiper.length == 80) { + throw new ZksnarkException( + "legacy 80-byte burn cipher is deprecated and rejected; expected " + + NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE + "-byte burn record"); + } + if (burnCiper.length != NoteEncryption.Encryption.BURN_CIPHER_RECORD_SIZE) { throw new ZksnarkException( "invalid shielded TRC-20 contract parameters for burn trigger input"); } diff --git a/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java b/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java index a3e44187b2c..94824ea9c50 100644 --- a/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java +++ b/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java @@ -25,6 +25,7 @@ import org.tron.core.capsule.ReceiveDescriptionCapsule; import org.tron.core.capsule.SpendDescriptionCapsule; import org.tron.core.exception.ZksnarkException; +import org.tron.core.utils.ZenChainParams; import org.tron.core.zen.address.DiversifierT; import org.tron.core.zen.address.ExpandedSpendingKey; import org.tron.core.zen.address.PaymentAddress; @@ -222,11 +223,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)); } @@ -354,12 +357,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()); @@ -380,10 +385,15 @@ public ShieldedTRC20Parameters build(boolean withAsk) throws ZksnarkException { return builder.build(); } + /** + * Encodes generated parameters or an external request already checked by Wallet. For external + * requests (withAsk=false), Wallet checks signature counts and burn input count/nullifier size. + */ public String getTriggerContractInput(ShieldedTRC20Parameters shieldedTRC20Parameters, List spendAuthoritySignature, BigInteger value, boolean withAsk, byte[] transparentToAddress) { + validateTriggerContractParameters(shieldedTRC20Parameters, spendAuthoritySignature, withAsk); switch (shieldedTRC20ParametersType) { case MINT: return mintParamsToHexString(shieldedTRC20Parameters, value); @@ -397,14 +407,78 @@ public String getTriggerContractInput(ShieldedTRC20Parameters shieldedTRC20Param } } + /** Validates the structure of the fields consumed by the ABI encoder, not their proofs. */ + private void validateTriggerContractParameters(ShieldedTRC20Parameters parameters, + List spendAuthoritySignature, boolean withAsk) { + int spendCount = parameters.getSpendDescriptionCount(); + int receiveCount = parameters.getReceiveDescriptionCount(); + // Bound the lists before walking them or accessing individual descriptions. + switch (shieldedTRC20ParametersType) { + case MINT: + if (receiveCount != 1) { + throw new IllegalArgumentException("invalid mint output number"); + } + break; + case TRANSFER: + if (spendCount != 1 && spendCount != 2) { + throw new IllegalArgumentException("invalid transfer input number"); + } + if (receiveCount != 1 && receiveCount != 2) { + throw new IllegalArgumentException("invalid transfer output number"); + } + break; + case BURN: + if (withAsk && spendCount != 1) { + throw new IllegalArgumentException("invalid burn input number"); + } + if (receiveCount > 1) { + throw new IllegalArgumentException("invalid burn output number"); + } + break; + default: + throw new IllegalArgumentException("invalid shielded TRC-20 parameters type"); + } + + validateFieldLength(parameters.getBindingSignature(), 64, "binding_signature"); + if (shieldedTRC20ParametersType != ShieldedTRC20ParametersType.MINT) { + for (int i = 0; i < spendCount; i++) { + ShieldContract.SpendDescription spend = parameters.getSpendDescription(i); + if (withAsk || shieldedTRC20ParametersType != ShieldedTRC20ParametersType.BURN) { + validateFieldLength(spend.getNullifier(), 32, "spend_description.nullifier"); + } + validateFieldLength(spend.getAnchor(), 32, "spend_description.anchor"); + validateFieldLength(spend.getValueCommitment(), 32, "spend_description.value_commitment"); + validateFieldLength(spend.getRk(), 32, "spend_description.rk"); + validateFieldLength(spend.getZkproof(), 192, "spend_description.zkproof"); + // Without ask, the caller supplies the signatures separately from the descriptions. + ByteString signature = withAsk ? spend.getSpendAuthoritySignature() + : spendAuthoritySignature.get(i).getValue(); + validateFieldLength(signature, 64, "spend_authority_signature"); + } + } + for (ReceiveDescription receive : parameters.getReceiveDescriptionList()) { + validateFieldLength(receive.getNoteCommitment(), 32, "receive_description.note_commitment"); + validateFieldLength(receive.getValueCommitment(), 32, "receive_description.value_commitment"); + validateFieldLength(receive.getEpk(), 32, "receive_description.epk"); + validateFieldLength(receive.getZkproof(), 192, "receive_description.zkproof"); + validateFieldLength(receive.getCEnc(), ZenChainParams.ZC_ENCCIPHERTEXT_SIZE, + "receive_description.c_enc"); + validateFieldLength(receive.getCOut(), ZenChainParams.ZC_OUTCIPHERTEXT_SIZE, + "receive_description.c_out"); + } + } + + private static void validateFieldLength(ByteString value, int length, String field) { + if (value.size() != length) { + throw new IllegalArgumentException(field + " must be " + length + " bytes"); + } + } + private String mintParamsToHexString(GrpcAPI.ShieldedTRC20Parameters mintParams, BigInteger value) { if (value.compareTo(BigInteger.ZERO) <= 0) { throw new IllegalArgumentException("require the value be positive"); } - if (mintParams.getReceiveDescriptionCount() != 1) { - throw new IllegalArgumentException("invalid mint output number"); - } ShieldContract.ReceiveDescription revDesc = mintParams.getReceiveDescription(0); byte[] zeros = new byte[12]; @@ -429,12 +503,6 @@ private String transferParamsToHexString(GrpcAPI.ShieldedTRC20Parameters transfe List recvDescs = transferParams.getReceiveDescriptionList(); long spendCount = spendDescs.size(); long recvCount = recvDescs.size(); - if (spendCount != 1 && spendCount != 2) { - throw new IllegalArgumentException("invalid transfer input number"); - } - if (recvCount != 1 && recvCount != 2) { - throw new IllegalArgumentException("invalid transfer output number"); - } byte[] input = new byte[0]; byte[] spendAuthSig = new byte[0]; @@ -515,9 +583,6 @@ private String burnParamsToHexString(GrpcAPI.ShieldedTRC20Parameters burnParams, if (value.compareTo(BigInteger.ZERO) <= 0) { throw new IllegalArgumentException("the value must be positive"); } - if (burnParams.getReceiveDescriptionCount() > 1) { - throw new IllegalArgumentException("invalid burn output number"); - } byte[] transparentToAddressTvm = normalizeTransparentToAddress(transparentToAddress); diff --git a/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java b/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java index fc3be8352ee..2c76c66b466 100644 --- a/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java +++ b/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java @@ -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; @@ -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)); } diff --git a/framework/src/main/java/org/tron/core/zen/ZksnarkInitService.java b/framework/src/main/java/org/tron/core/zen/ZksnarkInitService.java index d8a400d7750..78b3d69e6b5 100644 --- a/framework/src/main/java/org/tron/core/zen/ZksnarkInitService.java +++ b/framework/src/main/java/org/tron/core/zen/ZksnarkInitService.java @@ -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; @@ -67,7 +68,14 @@ private static String getParamsFile(String fileName) { } FileUtils.copyToFile(in, fileOut); } 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(); diff --git a/framework/src/main/java/org/tron/core/zen/note/Note.java b/framework/src/main/java/org/tron/core/zen/note/Note.java index 79a9f04a1bb..34d60696313 100644 --- a/framework/src/main/java/org/tron/core/zen/note/Note.java +++ b/framework/src/main/java/org/tron/core/zen/note/Note.java @@ -121,6 +121,18 @@ public static Note decode(NoteEncryption.Encryption.EncPlaintext encPlaintext) return ret; } + private static Optional 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 */ @@ -131,7 +143,11 @@ public static Optional decrypt( if (!pt.isPresent()) { return Optional.empty(); } - Note ret = decode(pt.get()); + Optional 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))) { @@ -160,7 +176,11 @@ public static Optional decrypt( if (!pt.isPresent()) { return Optional.empty(); } - Note ret = decode(pt.get()); + Optional 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))) { diff --git a/framework/src/test/java/org/tron/common/zksnark/ShieldedNativeValidationTest.java b/framework/src/test/java/org/tron/common/zksnark/ShieldedNativeValidationTest.java new file mode 100644 index 00000000000..ca37e9a327e --- /dev/null +++ b/framework/src/test/java/org/tron/common/zksnark/ShieldedNativeValidationTest.java @@ -0,0 +1,231 @@ +package org.tron.common.zksnark; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; +import org.tron.common.zksnark.LibrustzcashParam.OutputProofParams; +import org.tron.common.zksnark.LibrustzcashParam.SpendProofParams; +import org.tron.common.zksnark.LibrustzcashParam.Zip32XfvkAddressParams; +import org.tron.common.zksnark.LibrustzcashParam.Zip32XskDeriveParams; +import org.tron.common.zksnark.LibrustzcashParam.Zip32XskMasterParams; +import org.tron.core.exception.ZksnarkException; + +public class ShieldedNativeValidationTest { + + private Librustzcash original; + private Librustzcash nativeApi; + + @Before + public void setUp() { + original = (Librustzcash) ReflectionTestUtils.getField(JLibrustzcash.class, "INSTANCE"); + nativeApi = mock(Librustzcash.class); + ReflectionTestUtils.setField(JLibrustzcash.class, "INSTANCE", nativeApi); + } + + @After + public void tearDown() { + ReflectionTestUtils.setField(JLibrustzcash.class, "INSTANCE", original); + } + + private static byte[] validAk() { + return Hex.decode("db8c9492f599646369a4855b4260fe3b957cec8e1c33de7036ec61d8059d52d1"); + } + + private static byte[] identity(boolean sign) { + byte[] ak = new byte[32]; + ak[0] = 1; + ak[31] = sign ? (byte) 0x80 : 0; + return ak; + } + + private static SpendProofParams spend(byte[] ak) throws ZksnarkException { + byte[] path = new byte[1065]; + path[0] = 32; + for (int i = 0; i < 32; i++) { + path[1 + i * 33] = 32; + } + return new SpendProofParams(1, ak, new byte[32], new byte[11], new byte[32], + new byte[32], 0, new byte[32], path, new byte[32], new byte[32], new byte[192]); + } + + @Test + public void rejectsBothIdentityEncodingsBeforeNativeProof() { + for (boolean sign : new boolean[]{false, true}) { + ZksnarkException error = assertThrows(ZksnarkException.class, + () -> spend(identity(sign))); + assertEquals("Invalid spend ak: identity point", error.getMessage()); + } + verifyNoInteractions(nativeApi); + } + + @Test + public void acceptsNonIdentityWithByte30SetForBothSignBits() throws ZksnarkException { + for (boolean sign : new boolean[]{false, true}) { + byte[] ak = identity(sign); + ak[30] = 1; + // Only the Java identity filter is under test, not native point or subgroup validity. + LibrustzcashParam.validSpendAk(ak); + } + verifyNoInteractions(nativeApi); + } + + @Test + public void rejectsInvalidAkLengths() { + for (byte[] ak : new byte[][]{null, new byte[0], new byte[31], new byte[33]}) { + assertThrows(ZksnarkException.class, () -> spend(ak)); + } + verifyNoInteractions(nativeApi); + } + + @Test + public void revalidatesSetterAndArrayMutationsAtNativeBoundary() throws Exception { + SpendProofParams params = spend(validAk()); + params.setAk(identity(true)); + assertThrows(ZksnarkException.class, + () -> JLibrustzcash.librustzcashSaplingSpendProof(params)); + + byte[] ak = validAk(); + SpendProofParams aliased = spend(ak); + System.arraycopy(identity(false), 0, ak, 0, ak.length); + assertThrows(ZksnarkException.class, + () -> JLibrustzcash.librustzcashSaplingSpendProof(aliased)); + verifyNoInteractions(nativeApi); + } + + @Test + public void rejectsIdentityDerivedFromZeroAsk() { + byte[] ak = new byte[32]; + original.librustzcashAskToAk(new byte[32], ak); + assertThrows(ZksnarkException.class, () -> spend(ak)); + verifyNoInteractions(nativeApi); + } + + @Test + public void acceptsNormalAkWithSignBitAndPreservesNativeFailure() throws Exception { + SpendProofParams params = spend(validAk()); + when(nativeApi.librustzcashSaplingSpendProof( + org.mockito.ArgumentMatchers.anyLong(), any(), any(), any(), any(), any(), + org.mockito.ArgumentMatchers.anyLong(), any(), any(), any(), any(), any())) + .thenReturn(true, false); + assertTrue(JLibrustzcash.librustzcashSaplingSpendProof(params)); + org.junit.Assert.assertFalse(JLibrustzcash.librustzcashSaplingSpendProof(params)); + org.junit.Assert.assertArrayEquals(validAk(), params.getAk()); + } + + private static OutputProofParams output() throws ZksnarkException { + return new OutputProofParams(1, new byte[32], new byte[11], new byte[32], + new byte[32], 0, new byte[32], new byte[192]); + } + + @Test + public void revalidatesOutputParametersBeforeNativeCalls() throws Exception { + for (byte[] d : new byte[][]{null, new byte[10], new byte[12]}) { + OutputProofParams params = output(); + params.setD(d); + assertThrows(ZksnarkException.class, + () -> JLibrustzcash.librustzcashSaplingOutputProof(params)); + } + OutputProofParams shortProof = output(); + shortProof.setZkproof(new byte[191]); + assertThrows(ZksnarkException.class, + () -> JLibrustzcash.librustzcashSaplingOutputProof(shortProof)); + OutputProofParams negativeValue = output(); + negativeValue.setValue(-1); + assertThrows(ZksnarkException.class, + () -> JLibrustzcash.librustzcashSaplingOutputProof(negativeValue)); + verifyNoInteractions(nativeApi); + } + + @Test + public void rejectsInvalidDiversifierBeforeNativeOutputProof() throws Exception { + OutputProofParams params = output(); + when(nativeApi.librustzcashCheckDiversifier(params.getD())).thenReturn(false); + + ZksnarkException error = assertThrows(ZksnarkException.class, + () -> JLibrustzcash.librustzcashSaplingOutputProof(params)); + + assertEquals("Invalid output diversifier", error.getMessage()); + verify(nativeApi).librustzcashCheckDiversifier(params.getD()); + verifyNoMoreInteractions(nativeApi); + } + + @Test + public void acceptsValidDiversifierAndPreservesNativeOutputProofResult() throws Exception { + OutputProofParams params = output(); + when(nativeApi.librustzcashCheckDiversifier(params.getD())).thenReturn(true); + when(nativeApi.librustzcashSaplingOutputProof(params.getCtx(), params.getEsk(), + params.getD(), params.getPkD(), params.getR(), params.getValue(), params.getCv(), + params.getZkproof())).thenReturn(true, false); + + assertTrue(JLibrustzcash.librustzcashSaplingOutputProof(params)); + assertFalse(JLibrustzcash.librustzcashSaplingOutputProof(params)); + } + + @Test + public void validatesZip32MasterSeedBoundsAndRechecksMutation() throws Exception { + assertThrows(ZksnarkException.class, + () -> new Zip32XskMasterParams(null, 0, new byte[169])); + assertThrows(ZksnarkException.class, + () -> new Zip32XskMasterParams(new byte[32], -1, new byte[169])); + assertThrows(ZksnarkException.class, + () -> new Zip32XskMasterParams(new byte[32], 33, new byte[169])); + Zip32XskMasterParams params = new Zip32XskMasterParams(new byte[32], 32, new byte[169]); + params.setSize(33); + assertThrows(ZksnarkException.class, + () -> JLibrustzcash.librustzcashZip32XskMaster(params)); + verifyNoInteractions(nativeApi); + } + + @Test + public void preservesEmptyAndPrefixSeedsAndHardenedChildIndices() throws Exception { + byte[] output = new byte[169]; + byte[] seed = new byte[32]; + JLibrustzcash.librustzcashZip32XskMaster(new Zip32XskMasterParams(seed, 16, output)); + verify(nativeApi).librustzcashZip32XskMaster(seed, 16, output); + byte[] emptySeed = new byte[0]; + JLibrustzcash.librustzcashZip32XskMaster(new Zip32XskMasterParams(emptySeed, 0, output)); + verify(nativeApi).librustzcashZip32XskMaster(emptySeed, 0, output); + byte[] parent = new byte[169]; + JLibrustzcash.librustzcashZip32XskDerive( + new Zip32XskDeriveParams(parent, Integer.MIN_VALUE, output)); + verify(nativeApi).librustzcashZip32XskDerive(parent, Integer.MIN_VALUE, output); + } + + @Test + public void rejectsShortZip32KeysAndOutputBuffers() throws Exception { + assertThrows(ZksnarkException.class, + () -> new Zip32XskDeriveParams(new byte[168], 0, new byte[169])); + assertThrows(ZksnarkException.class, + () -> new Zip32XskDeriveParams(new byte[169], 0, new byte[168])); + for (int field = 0; field < 4; field++) { + byte[][] arrays = {new byte[169], new byte[11], new byte[11], new byte[43]}; + arrays[field] = Arrays.copyOf(arrays[field], arrays[field].length - 1); + assertThrows(ZksnarkException.class, + () -> new Zip32XfvkAddressParams(arrays[0], arrays[1], arrays[2], arrays[3])); + } + Zip32XskDeriveParams derive = new Zip32XskDeriveParams(new byte[169], 0, new byte[169]); + derive.setData(new byte[1]); + assertThrows(ZksnarkException.class, + () -> JLibrustzcash.librustzcashZip32XskDerive(derive)); + Zip32XfvkAddressParams address = + new Zip32XfvkAddressParams(new byte[169], new byte[11], new byte[11], new byte[43]); + address.setAddr_ret(new byte[1]); + assertThrows(ZksnarkException.class, + () -> JLibrustzcash.librustzcashZip32XfvkAddress(address)); + verifyNoInteractions(nativeApi); + } +} diff --git a/framework/src/test/java/org/tron/core/ShieldedNullifierFailureTest.java b/framework/src/test/java/org/tron/core/ShieldedNullifierFailureTest.java new file mode 100644 index 00000000000..4459da64315 --- /dev/null +++ b/framework/src/test/java/org/tron/core/ShieldedNullifierFailureTest.java @@ -0,0 +1,111 @@ +package org.tron.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verifyNoInteractions; + +import com.google.protobuf.ByteString; +import java.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.springframework.test.util.ReflectionTestUtils; +import org.tron.api.GrpcAPI; +import org.tron.common.zksnark.JLibrustzcash; +import org.tron.common.zksnark.LibrustzcashParam.ComputeNfParams; +import org.tron.core.config.args.Args; +import org.tron.core.exception.ZksnarkException; +import org.tron.core.zen.address.DiversifierT; +import org.tron.core.zen.address.KeyIo; +import org.tron.core.zen.address.PaymentAddress; +import org.tron.protos.contract.ShieldContract.IncrementalMerkleTree; +import org.tron.protos.contract.ShieldContract.IncrementalMerkleVoucher; +import org.tron.protos.contract.ShieldContract.IncrementalMerkleVoucherInfo; +import org.tron.protos.contract.ShieldContract.PedersenHash; + +public class ShieldedNullifierFailureTest { + + private boolean allowShieldedApi; + + @Before + public void setUp() { + allowShieldedApi = Args.getInstance().isAllowShieldedTransactionApi(); + Args.getInstance().setAllowShieldedTransactionApi(true); + } + + @After + public void tearDown() { + Args.getInstance().setAllowShieldedTransactionApi(allowShieldedApi); + } + + private static GrpcAPI.Note note() { + PaymentAddress address = new PaymentAddress( + new DiversifierT(Hex.decode("4c418cf65a7210d9b66588")), + Hex.decode("4f05edbf1abe79773a46dbf16f43aa05290225487b656413533c405272b9bd5d")); + return GrpcAPI.Note.newBuilder().setPaymentAddress(KeyIo.encodePaymentAddress(address)) + .setRcm(ByteString.copyFrom(new byte[32])).build(); + } + + private static IncrementalMerkleVoucher voucher() { + return IncrementalMerkleVoucher.newBuilder().setTree(IncrementalMerkleTree.newBuilder() + .setLeft(PedersenHash.newBuilder().setContent(ByteString.copyFrom(new byte[32])))).build(); + } + + private static GrpcAPI.NfParameters request() { + return GrpcAPI.NfParameters.newBuilder().setNote(note()).setVoucher(voucher()) + .setAk(ByteString.copyFrom(new byte[32])).setNk(ByteString.copyFrom(new byte[32])).build(); + } + + @Test + public void nullifierApiReportsFailureInsteadOfReturningNull() { + try (MockedStatic nativeApi = mockStatic(JLibrustzcash.class)) { + ZksnarkException error = assertThrows(ZksnarkException.class, + () -> new Wallet().createShieldNullifier(request())); + assertEquals("compute nullifier error", error.getMessage()); + nativeApi.verify(() -> JLibrustzcash.librustzcashComputeNf(any())); + } + } + + @Test + public void failedNullifierNeverReachesSpentStore() throws Exception { + Wallet wallet = spy(new Wallet()); + ChainBaseManager manager = mock(ChainBaseManager.class); + ReflectionTestUtils.setField(wallet, "chainBaseManager", manager); + doReturn(IncrementalMerkleVoucherInfo.newBuilder().addVouchers(voucher()).build()) + .when(wallet).getMerkleTreeVoucherInfo(any()); + GrpcAPI.NoteParameters request = GrpcAPI.NoteParameters.newBuilder().setNote(note()) + .setAk(ByteString.copyFrom(new byte[32])).setNk(ByteString.copyFrom(new byte[32])) + .setTxid(ByteString.copyFrom(new byte[32])).build(); + try (MockedStatic nativeApi = mockStatic(JLibrustzcash.class)) { + ZksnarkException error = assertThrows(ZksnarkException.class, () -> wallet.isSpend(request)); + assertEquals("compute nullifier error", error.getMessage()); + verifyNoInteractions(manager); + nativeApi.verify(() -> JLibrustzcash.librustzcashComputeNf(any())); + } + } + + @Test + public void nullifierApiPreservesSuccessfulResultIncludingZero() throws Exception { + try (MockedStatic nativeApi = mockStatic(JLibrustzcash.class)) { + for (byte fill : new byte[]{0, 7}) { + byte[] expected = new byte[32]; + Arrays.fill(expected, fill); + nativeApi.when(() -> JLibrustzcash.librustzcashComputeNf(any())).thenAnswer(invocation -> { + ComputeNfParams params = invocation.getArgument(0); + System.arraycopy(expected, 0, params.getResult(), 0, expected.length); + return true; + }); + assertArrayEquals(expected, + new Wallet().createShieldNullifier(request()).getValue().toByteArray()); + } + } + } +} diff --git a/framework/src/test/java/org/tron/core/WalletMockTest.java b/framework/src/test/java/org/tron/core/WalletMockTest.java index d22d9775456..719a4b5a12f 100644 --- a/framework/src/test/java/org/tron/core/WalletMockTest.java +++ b/framework/src/test/java/org/tron/core/WalletMockTest.java @@ -1034,18 +1034,30 @@ public void testGetTriggerInputForShieldedTRC20Contract1() throws ZksnarkException, ContractValidateException { Wallet wallet = new Wallet(); ShieldContract.SpendDescription spendDescription = - ShieldContract.SpendDescription.newBuilder().build(); + ShieldContract.SpendDescription.newBuilder() + .setNullifier(ByteString.copyFrom(new byte[32])) + .setAnchor(ByteString.copyFrom(new byte[32])) + .setValueCommitment(ByteString.copyFrom(new byte[32])) + .setRk(ByteString.copyFrom(new byte[32])) + .setZkproof(ByteString.copyFrom(new byte[192])).build(); GrpcAPI.ShieldedTRC20TriggerContractParameters.Builder triggerParam = GrpcAPI.ShieldedTRC20TriggerContractParameters .newBuilder(); GrpcAPI.ShieldedTRC20Parameters shieldedTRC20Parameters = GrpcAPI.ShieldedTRC20Parameters.newBuilder() .addSpendDescription(spendDescription) - .addReceiveDescription(ShieldContract.ReceiveDescription.getDefaultInstance()) + .addReceiveDescription(ShieldContract.ReceiveDescription.newBuilder() + .setNoteCommitment(ByteString.copyFrom(new byte[32])) + .setValueCommitment(ByteString.copyFrom(new byte[32])) + .setEpk(ByteString.copyFrom(new byte[32])) + .setZkproof(ByteString.copyFrom(new byte[192])) + .setCEnc(ByteString.copyFrom(new byte[580])) + .setCOut(ByteString.copyFrom(new byte[80]))) .setParameterType("transfer") + .setBindingSignature(ByteString.copyFrom(new byte[64])) .build(); GrpcAPI.BytesMessage bytesMessage = - GrpcAPI.BytesMessage.newBuilder().build(); + GrpcAPI.BytesMessage.newBuilder().setValue(ByteString.copyFrom(new byte[64])).build(); triggerParam.setShieldedTRC20Parameters(shieldedTRC20Parameters); triggerParam.addSpendAuthoritySignature(bytesMessage); diff --git a/framework/src/test/java/org/tron/core/WalletShieldedDescriptionValidationTest.java b/framework/src/test/java/org/tron/core/WalletShieldedDescriptionValidationTest.java index a40dbc105c5..d90c6b7de37 100644 --- a/framework/src/test/java/org/tron/core/WalletShieldedDescriptionValidationTest.java +++ b/framework/src/test/java/org/tron/core/WalletShieldedDescriptionValidationTest.java @@ -8,8 +8,11 @@ import static org.mockito.Mockito.when; import com.google.protobuf.ByteString; +import java.io.PrintWriter; +import java.io.StringWriter; import java.math.BigInteger; import java.util.Arrays; +import javax.servlet.http.HttpServletResponse; import org.junit.BeforeClass; import org.junit.Test; import org.mockito.MockedStatic; @@ -20,6 +23,8 @@ import org.tron.common.utils.ByteUtil; import org.tron.core.config.args.Args; import org.tron.core.exception.ContractValidateException; +import org.tron.core.exception.ZksnarkException; +import org.tron.core.services.http.servlets.Util; import org.tron.core.zen.note.NoteEncryption.Encryption; import org.tron.protos.contract.ShieldContract; @@ -39,7 +44,8 @@ private static BytesMessage triggerInput(GrpcAPI.ShieldedTRC20Parameters paramet .setAmount(amount) .setShieldedTRC20Parameters(parameters); for (int i = 0; i < signatureCount; i++) { - request.addSpendAuthoritySignature(BytesMessage.getDefaultInstance()); + request.addSpendAuthoritySignature(BytesMessage.newBuilder() + .setValue(ByteString.copyFrom(new byte[64]))); } CommonParameter commonParameter = mock(Args.class); @@ -53,9 +59,24 @@ private static BytesMessage triggerInput(GrpcAPI.ShieldedTRC20Parameters paramet private static GrpcAPI.ShieldedTRC20Parameters withReceives( GrpcAPI.ShieldedTRC20Parameters.Builder parameters, int receives) { for (int i = 0; i < receives; i++) { - parameters.addReceiveDescription(ShieldContract.ReceiveDescription.getDefaultInstance()); + parameters.addReceiveDescription(ShieldContract.ReceiveDescription.newBuilder() + .setNoteCommitment(ByteString.copyFrom(new byte[32])) + .setValueCommitment(ByteString.copyFrom(new byte[32])) + .setEpk(ByteString.copyFrom(new byte[32])) + .setZkproof(ByteString.copyFrom(new byte[192])) + .setCEnc(ByteString.copyFrom(new byte[580])) + .setCOut(ByteString.copyFrom(new byte[80]))); } - return parameters.build(); + return parameters.setBindingSignature(ByteString.copyFrom(new byte[64])).build(); + } + + private static ShieldContract.SpendDescription.Builder spend() { + return ShieldContract.SpendDescription.newBuilder() + .setNullifier(ByteString.copyFrom(new byte[32])) + .setAnchor(ByteString.copyFrom(new byte[32])) + .setValueCommitment(ByteString.copyFrom(new byte[32])) + .setRk(ByteString.copyFrom(new byte[32])) + .setZkproof(ByteString.copyFrom(new byte[192])); } /** @@ -76,7 +97,7 @@ private static GrpcAPI.ShieldedTRC20Parameters burn(int receives, String amount) return withReceives(GrpcAPI.ShieldedTRC20Parameters.newBuilder() .setParameterType("burn") .setTriggerContractInput(ByteArray.toHexString(record)) - .addSpendDescription(ShieldContract.SpendDescription.newBuilder() + .addSpendDescription(spend() .setNullifier(ByteString.copyFrom(nullifier))), receives); } @@ -85,6 +106,7 @@ public void transferDescriptionCardinalityIsReportedAsValidationFailure() { GrpcAPI.ShieldedTRC20Parameters.Builder parameters = GrpcAPI.ShieldedTRC20Parameters.newBuilder() .setParameterType("transfer") + .setBindingSignature(ByteString.copyFrom(new byte[64])) .addSpendDescription(ShieldContract.SpendDescription.getDefaultInstance()); for (int i = 0; i < 3; i++) { parameters.addReceiveDescription(ShieldContract.ReceiveDescription.getDefaultInstance()); @@ -96,6 +118,122 @@ public void transferDescriptionCardinalityIsReportedAsValidationFailure() { exception.getMessage()); } + @Test + public void invalidBindingSignatureLengthsAreReportedAsValidationFailures() { + GrpcAPI.ShieldedTRC20Parameters[] parameters = { + withReceives(GrpcAPI.ShieldedTRC20Parameters.newBuilder().setParameterType("mint"), 1), + withReceives(GrpcAPI.ShieldedTRC20Parameters.newBuilder().setParameterType("transfer") + .addSpendDescription(spend()), 1), + burn(0, "1") + }; + for (GrpcAPI.ShieldedTRC20Parameters parameter : parameters) { + for (int length : new int[]{0, 63, 65}) { + GrpcAPI.ShieldedTRC20Parameters invalid = parameter.toBuilder() + .setBindingSignature(ByteString.copyFrom(new byte[length])).build(); + ContractValidateException exception = assertThrows(ContractValidateException.class, + () -> triggerInput(invalid, "1", invalid.getSpendDescriptionCount())); + assertEquals("invalid shielded TRC-20 trigger input: binding_signature must be 64 bytes", + exception.getMessage()); + } + } + } + + @Test + public void signatureCountValidationRetainsWalletError() { + GrpcAPI.ShieldedTRC20Parameters[] parameters = { + withReceives(GrpcAPI.ShieldedTRC20Parameters.newBuilder().setParameterType("transfer") + .addSpendDescription(spend()), 1), + burn(0, "1") + }; + for (GrpcAPI.ShieldedTRC20Parameters parameter : parameters) { + for (int count : new int[]{0, 2}) { + ZksnarkException exception = assertThrows(ZksnarkException.class, + () -> triggerInput(parameter, "1", count)); + assertEquals("the number of spendDescription and spendAuthoritySignature is not equal", + exception.getMessage()); + } + } + } + + @Test + public void malformedBurnHexIsReportedAsValidationFailure() throws Exception { + GrpcAPI.ShieldedTRC20Parameters valid = burn(0, "1"); + for (String hex : new String[]{"gg", "0xgg", "g", "ĀĀ"}) { + GrpcAPI.ShieldedTRC20Parameters invalid = valid.toBuilder() + .setTriggerContractInput(hex).build(); + ContractValidateException error = assertThrows(ContractValidateException.class, + () -> triggerInput(invalid, "1", 1)); + assertEquals("invalid shielded TRC-20 trigger input: burn record must be valid hex", + error.getMessage()); + + HttpServletResponse response = mock(HttpServletResponse.class); + StringWriter output = new StringWriter(); + when(response.getWriter()).thenReturn(new PrintWriter(output)); + Util.processError(error, response); + assertEquals("{\"Error\":\"invalid shielded TRC-20 trigger input: " + + "burn record must be valid hex\"}", output.toString().trim()); + } + } + + @Test + public void burnHexPrefixesAndOddLengthsPreserveTriggerInput() throws Exception { + GrpcAPI.ShieldedTRC20Parameters valid = burn(0, "1"); + String hex = valid.getTriggerContractInput(); + assertEquals('0', hex.charAt(0)); + BytesMessage expected = triggerInput(valid, "1", 1); + for (String compatible : new String[]{"0x" + hex, hex.substring(1), + "0x" + hex.substring(1)}) { + assertEquals(expected, triggerInput(valid.toBuilder() + .setTriggerContractInput(compatible).build(), "1", 1)); + } + } + + @Test + public void burnInputValidationRetainsWalletErrors() { + GrpcAPI.ShieldedTRC20Parameters valid = burn(0, "1"); + for (int count : new int[]{0, 2}) { + GrpcAPI.ShieldedTRC20Parameters.Builder parameters = valid.toBuilder() + .clearSpendDescription(); + for (int i = 0; i < count; i++) { + parameters.addSpendDescription(valid.getSpendDescription(0)); + } + ZksnarkException exception = assertThrows(ZksnarkException.class, + () -> triggerInput(parameters.build(), "1", count)); + assertEquals("burn trigger input requires exactly one spendDescription for nf-bound nonce", + exception.getMessage()); + } + for (int length : new int[]{0, 31, 33}) { + GrpcAPI.ShieldedTRC20Parameters.Builder parameters = valid.toBuilder(); + parameters.getSpendDescriptionBuilder(0).setNullifier(ByteString.copyFrom(new byte[length])); + ZksnarkException exception = assertThrows(ZksnarkException.class, + () -> triggerInput(parameters.build(), "1", 1)); + assertEquals("burn trigger input requires 32-byte spendDescription.nullifier", + exception.getMessage()); + } + } + + @Test + public void malformedDescriptionsAreReportedAsValidationFailures() { + GrpcAPI.ShieldedTRC20Parameters valid = withReceives( + GrpcAPI.ShieldedTRC20Parameters.newBuilder().setParameterType("transfer") + .addSpendDescription(spend()), 1); + GrpcAPI.ShieldedTRC20Parameters.Builder invalidSpend = valid.toBuilder(); + invalidSpend.getSpendDescriptionBuilder(0).setZkproof(ByteString.copyFrom(new byte[191])); + ContractValidateException spendError = assertThrows(ContractValidateException.class, + () -> triggerInput(invalidSpend.build(), "0", 1)); + assertEquals("invalid shielded TRC-20 trigger input: " + + "spend_description.zkproof must be 192 bytes", + spendError.getMessage()); + + GrpcAPI.ShieldedTRC20Parameters.Builder invalidReceive = valid.toBuilder(); + invalidReceive.getReceiveDescriptionBuilder(0).setCEnc(ByteString.copyFrom(new byte[579])); + ContractValidateException receiveError = assertThrows(ContractValidateException.class, + () -> triggerInput(invalidReceive.build(), "0", 1)); + assertEquals("invalid shielded TRC-20 trigger input: " + + "receive_description.c_enc must be 580 bytes", + receiveError.getMessage()); + } + @Test public void mintRequiresExactlyOneReceiveDescription() throws Exception { for (int receives : new int[]{0, 2}) { diff --git a/framework/src/test/java/org/tron/core/WalletShieldedNoteScanTest.java b/framework/src/test/java/org/tron/core/WalletShieldedNoteScanTest.java new file mode 100644 index 00000000000..849136ee551 --- /dev/null +++ b/framework/src/test/java/org/tron/core/WalletShieldedNoteScanTest.java @@ -0,0 +1,247 @@ +package org.tron.core; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import java.util.Arrays; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.api.GrpcAPI; +import org.tron.common.crypto.Hash; +import org.tron.common.utils.ByteArray; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.exception.ZksnarkException; +import org.tron.core.zen.address.FullViewingKey; +import org.tron.core.zen.address.KeyIo; +import org.tron.core.zen.address.PaymentAddress; +import org.tron.core.zen.address.SpendingKey; +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.NoteEncryption.Encryption.EncCiphertext; +import org.tron.core.zen.note.NoteEncryption.Encryption.EncPlaintext; +import org.tron.core.zen.note.OutgoingPlaintext; +import org.tron.protos.Protocol.Block; +import org.tron.protos.Protocol.Transaction; +import org.tron.protos.Protocol.Transaction.Contract; +import org.tron.protos.Protocol.TransactionInfo; +import org.tron.protos.contract.ShieldContract.ReceiveDescription; +import org.tron.protos.contract.ShieldContract.ShieldedTransferContract; + +public class WalletShieldedNoteScanTest { + + private static boolean previousAllow; + private static PaymentAddress address; + private static byte[] ivk; + private static byte[] ovk; + + @BeforeClass + public static void setUpClass() throws Exception { + previousAllow = Args.getInstance().isAllowShieldedTransactionApi(); + Args.getInstance().setAllowShieldedTransactionApi(true); + SpendingKey key = new SpendingKey(new byte[32]); + FullViewingKey viewingKey = key.fullViewingKey(); + address = key.defaultAddress(); + ivk = viewingKey.inViewingKey().getValue(); + ovk = viewingKey.getOvk(); + } + + @AfterClass + public static void tearDownClass() { + Args.getInstance().setAllowShieldedTransactionApi(previousAllow); + } + + private static Note note(long value) throws Exception { + Note note = new Note(address, value); + note.setMemo(new byte[]{7, 8, 9}); + return note; + } + + private static class EncryptedNote { + + private final Note note; + private final NoteEncryption encryptor; + private final EncCiphertext ciphertext; + private final byte[] commitment; + private final byte[] cout; + + private EncryptedNote(Note note, EncPlaintext plaintext) throws Exception { + this.note = note; + commitment = note.cm(); + encryptor = NoteEncryption.fromDiversifier(address.getD()).get(); + ciphertext = encryptor.encryptToRecipient(address.getPkD(), plaintext).get(); + cout = new OutgoingPlaintext(address.getPkD(), encryptor.getEsk()) + .encrypt(ovk, new byte[32], commitment, encryptor).getData(); + // A malformed authenticated plaintext, rather than damaged ciphertext, reaches the bug. + assertArrayEquals(plaintext.getData(), Encryption.attemptEncDecryption( + ciphertext.getData(), ivk, encryptor.getEpk()).get().getData()); + } + + private ReceiveDescription description() { + return ReceiveDescription.newBuilder() + .setNoteCommitment(ByteString.copyFrom(commitment)) + .setValueCommitment(ByteString.copyFrom(new byte[32])) + .setEpk(ByteString.copyFrom(encryptor.getEpk())) + .setCEnc(ByteString.copyFrom(ciphertext.getData())) + .setCOut(ByteString.copyFrom(cout)).build(); + } + + private TransactionInfo.Log log(byte[] contractAddress, int position) { + byte[] data = new byte[800]; + System.arraycopy(ByteArray.fromLong(position), 0, data, 24, 8); + System.arraycopy(commitment, 0, data, 32, 32); + System.arraycopy(encryptor.getEpk(), 0, data, 96, 32); + System.arraycopy(ciphertext.getData(), 0, data, 128, 580); + System.arraycopy(cout, 0, data, 708, 80); + return TransactionInfo.Log.newBuilder() + .setAddress(ByteString.copyFrom(Arrays.copyOfRange(contractAddress, 1, 21))) + .addTopics(ByteString.copyFrom(Hash.sha3(ByteArray.fromString( + "MintNewLeaf(uint256,bytes32,bytes32,bytes32,bytes32[21])")))) + .setData(ByteString.copyFrom(data)).build(); + } + + private void assertRejected() throws Exception { + assertFalse(Note.decrypt(ciphertext.getData(), ivk, encryptor.getEpk(), commitment) + .isPresent()); + assertFalse(Note.decrypt(ciphertext, encryptor.getEpk(), encryptor.getEsk(), + address.getPkD(), commitment).isPresent()); + } + } + + @Test + public void malformedAuthenticatedPlaintextsAreSkippedByBothDecryptors() throws Exception { + Note note = note(42); + for (int header : new int[]{0, 2, 255}) { + EncPlaintext plaintext = note.encode(); + plaintext.getData()[0] = (byte) header; + assertThrows(ZksnarkException.class, () -> Note.decode(plaintext)); + new EncryptedNote(note, plaintext).assertRejected(); + } + } + + @Test + public void negativeAuthenticatedValuesAreSkippedByBothDecryptors() throws Exception { + Note note = note(42); + for (long value : new long[]{Long.MIN_VALUE, -1}) { + Note invalid = new Note(address.getD(), address.getPkD(), value, note.getRcm()); + assertEquals(value, Note.decode(invalid.encode()).getValue()); + new EncryptedNote(note, invalid.encode()).assertRejected(); + } + } + + @Test + public void validNotesAndExistingRejectionsArePreserved() throws Exception { + for (long value : new long[]{0, 42, Long.MAX_VALUE}) { + Note note = note(value); + EncryptedNote encrypted = new EncryptedNote(note, note.encode()); + Note incoming = Note.decrypt(encrypted.ciphertext.getData(), ivk, + encrypted.encryptor.getEpk(), encrypted.commitment).get(); + Note outgoing = Note.decrypt(encrypted.ciphertext, encrypted.encryptor.getEpk(), + encrypted.encryptor.getEsk(), address.getPkD(), encrypted.commitment).get(); + for (Note decrypted : new Note[]{incoming, outgoing}) { + assertEquals(note.getValue(), decrypted.getValue()); + assertArrayEquals(note.getD().getData(), decrypted.getD().getData()); + assertArrayEquals(note.getRcm(), decrypted.getRcm()); + assertArrayEquals(note.getMemo(), decrypted.getMemo()); + } + assertThrows(ZksnarkException.class, () -> Note.decrypt(encrypted.ciphertext.getData(), + new byte[31], encrypted.encryptor.getEpk(), encrypted.commitment)); + encrypted.commitment[0] ^= 1; + encrypted.assertRejected(); + encrypted.commitment[0] ^= 1; + encrypted.ciphertext.getData()[0] ^= 1; + encrypted.assertRejected(); + } + } + + private static EncryptedNote[] mixedNotes() throws Exception { + Note valid = note(42); + EncPlaintext invalidHeader = valid.encode(); + invalidHeader.getData()[0] = 2; + Note invalidValue = new Note(address.getD(), address.getPkD(), -1, valid.getRcm()); + Note last = note(84); + return new EncryptedNote[]{new EncryptedNote(valid, valid.encode()), + new EncryptedNote(valid, invalidHeader), new EncryptedNote(valid, invalidValue.encode()), + new EncryptedNote(last, last.encode())}; + } + + private static Wallet scanner(Transaction transaction) { + Wallet scanner = spy(new Wallet()); + GrpcAPI.BlockList blocks = GrpcAPI.BlockList.newBuilder() + .addBlock(Block.newBuilder().addTransactions(transaction)).build(); + doReturn(blocks).when(scanner).getBlocksByLimitNext(0, 1); + return scanner; + } + + private static void assertScannedNote(Note expected, GrpcAPI.Note actual) { + assertEquals(expected.getValue(), actual.getValue()); + assertEquals(ByteString.copyFrom(expected.getRcm()), actual.getRcm()); + assertEquals(ByteString.copyFrom(new byte[]{7, 8, 9}), actual.getMemo()); + assertEquals(KeyIo.encodePaymentAddress(address), actual.getPaymentAddress()); + } + + @Test + public void trc20ScansReturnValidNotesOnBothSidesOfMalformedNotes() throws Exception { + byte[] contractAddress = new byte[21]; + contractAddress[0] = Wallet.getAddressPreFixByte(); + contractAddress[20] = 1; + EncryptedNote[] notes = mixedNotes(); + Transaction transaction = Transaction.getDefaultInstance(); + ByteString txid = new TransactionCapsule(transaction).getTransactionId().getByteString(); + Wallet scanner = scanner(transaction); + TransactionInfo.Builder info = TransactionInfo.newBuilder(); + for (int i = 0; i < notes.length; i++) { + info.addLog(notes[i].log(contractAddress, i)); + } + doReturn(info.build()).when(scanner).getTransactionInfoById(txid); + + GrpcAPI.DecryptNotesTRC20 incoming = scanner.scanShieldedTRC20NotesByIvk( + 0, 1, contractAddress, ivk, null, null); + GrpcAPI.DecryptNotesTRC20 outgoing = scanner.scanShieldedTRC20NotesByOvk( + 0, 1, ovk, contractAddress); + for (GrpcAPI.DecryptNotesTRC20 result : new GrpcAPI.DecryptNotesTRC20[]{incoming, outgoing}) { + assertEquals(2, result.getNoteTxsCount()); + for (int i = 0; i < 2; i++) { + int index = i * 3; + GrpcAPI.DecryptNotesTRC20.NoteTx actual = result.getNoteTxs(i); + assertEquals(txid, actual.getTxid()); + assertEquals(index, actual.getIndex()); + assertScannedNote(notes[index].note, actual.getNote()); + } + } + assertEquals(0, incoming.getNoteTxs(0).getPosition()); + assertEquals(3, incoming.getNoteTxs(1).getPosition()); + } + + @Test + public void nativeScansReturnValidNotesOnBothSidesOfMalformedNotes() throws Exception { + EncryptedNote[] notes = mixedNotes(); + ShieldedTransferContract.Builder shielded = ShieldedTransferContract.newBuilder(); + for (EncryptedNote note : notes) { + shielded.addReceiveDescription(note.description()); + } + Transaction transaction = Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().addContract(Contract.newBuilder() + .setType(Contract.ContractType.ShieldedTransferContract) + .setParameter(Any.pack(shielded.build())))).build(); + Wallet scanner = scanner(transaction); + GrpcAPI.DecryptNotes incoming = scanner.scanNoteByIvk(0, 1, ivk); + GrpcAPI.DecryptNotes outgoing = scanner.scanNoteByOvk(0, 1, ovk); + for (GrpcAPI.DecryptNotes result : new GrpcAPI.DecryptNotes[]{incoming, outgoing}) { + assertEquals(2, result.getNoteTxsCount()); + for (int i = 0; i < 2; i++) { + int index = i * 3; + assertEquals(index, result.getNoteTxs(i).getIndex()); + assertScannedNote(notes[index].note, result.getNoteTxs(i).getNote()); + } + } + } +} diff --git a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java index ab67437dfe7..f5b2f9b1711 100644 --- a/framework/src/test/java/org/tron/core/exception/TronErrorTest.java +++ b/framework/src/test/java/org/tron/core/exception/TronErrorTest.java @@ -1,9 +1,11 @@ package org.tron.core.exception; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mockStatic; @@ -20,11 +22,15 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.io.FileUtils; import org.junit.After; import org.junit.Assert; import org.junit.Rule; @@ -42,6 +48,7 @@ import org.tron.common.parameter.RateLimiterInitialization; import org.tron.common.utils.ReflectUtils; import org.tron.common.zksnark.JLibrustzcash; +import org.tron.common.zksnark.LibrustzcashParam.InitZksnarkParams; import org.tron.core.config.args.Args; import org.tron.core.services.http.servlets.GetBlockServlet; import org.tron.core.services.http.servlets.RateLimiterServlet; @@ -123,6 +130,73 @@ public InputStream getResourceAsStream(String name) { } } + @Test + public void spendCopyFailureStopsNativeInitializationAndAllowsRetry() throws Exception { + assertZksnarkCopyFailure(1); + } + + @Test + public void outputCopyFailureStopsNativeInitializationAndAllowsRetry() throws Exception { + assertZksnarkCopyFailure(2); + } + + private void assertZksnarkCopyFailure(int failedCopy) throws Exception { + Field field = ZksnarkInitService.class.getDeclaredField("initialized"); + field.setAccessible(true); + AtomicBoolean initialized = (AtomicBoolean) field.get(null); + boolean originalValue = initialized.get(); + List destinations = new ArrayList<>(); + byte[] contents = {1, 2, 3}; + IOException failure = new IOException("Parameter copy failed"); + + try (MockedStatic fileUtils = mockStatic(FileUtils.class); + MockedStatic nativeApi = mockStatic(JLibrustzcash.class)) { + initialized.set(false); + fileUtils.when(() -> FileUtils.copyToFile(any(InputStream.class), any(File.class))) + .thenAnswer(invocation -> { + File destination = invocation.getArgument(1); + destinations.add(destination); + Files.write(destination.toPath(), contents); + if (destinations.size() == failedCopy) { + throw failure; + } + return null; + }); + + TronError error = assertThrows(TronError.class, + ZksnarkInitService::librustzcashInitZksnarkParams); + + assertEquals(TronError.ErrCode.ZCASH_INIT, error.getErrCode()); + assertSame(failure, error.getCause()); + assertEquals(failedCopy, destinations.size()); + assertFalse(destinations.get(failedCopy - 1).exists()); + assertFalse(initialized.get()); + nativeApi.verifyNoInteractions(); + + nativeApi.when(() -> JLibrustzcash.librustzcashInitZksnarkParams(any())) + .thenAnswer(invocation -> { + InitZksnarkParams params = invocation.getArgument(0); + assertArrayEquals(contents, + Files.readAllBytes(new File(params.getSpend_path()).toPath())); + assertArrayEquals(contents, + Files.readAllBytes(new File(params.getOutput_path()).toPath())); + return null; + }); + ZksnarkInitService.librustzcashInitZksnarkParams(); + assertTrue(initialized.get()); + assertEquals(failedCopy + 2, destinations.size()); + nativeApi.verify(() -> JLibrustzcash.librustzcashInitZksnarkParams(any())); + ZksnarkInitService.librustzcashInitZksnarkParams(); + assertEquals(failedCopy + 2, destinations.size()); + nativeApi.verifyNoMoreInteractions(); + } finally { + initialized.set(originalValue); + for (File destination : destinations) { + Files.deleteIfExists(destination.toPath()); + } + } + } + @Test public void LogLoadTest() throws IOException { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); diff --git a/framework/src/test/java/org/tron/core/zen/ShieldedSignatureFailureTest.java b/framework/src/test/java/org/tron/core/zen/ShieldedSignatureFailureTest.java new file mode 100644 index 00000000000..97e2e356442 --- /dev/null +++ b/framework/src/test/java/org/tron/core/zen/ShieldedSignatureFailureTest.java @@ -0,0 +1,189 @@ +package org.tron.core.zen; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import java.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.tron.api.GrpcAPI.SpendAuthSigParameters; +import org.tron.common.zksnark.JLibrustzcash; +import org.tron.common.zksnark.LibrustzcashParam.SpendSigParams; +import org.tron.core.Wallet; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.exception.ZksnarkException; +import org.tron.core.zen.address.DiversifierT; +import org.tron.core.zen.address.ExpandedSpendingKey; +import org.tron.core.zen.address.FullViewingKey; +import org.tron.core.zen.note.Note; +import org.tron.protos.Protocol.Transaction.Contract.ContractType; + +public class ShieldedSignatureFailureTest { + + private static final long CONTEXT = 123L; + private boolean allowShieldedApi; + + @Before + public void setUp() { + allowShieldedApi = Args.getInstance().isAllowShieldedTransactionApi(); + Args.getInstance().setAllowShieldedTransactionApi(true); + } + + @After + public void tearDown() { + Args.getInstance().setAllowShieldedTransactionApi(allowShieldedApi); + } + + private static SpendAuthSigParameters signatureRequest() { + ByteString scalar = ByteString.copyFrom(new byte[32]); + return SpendAuthSigParameters.newBuilder() + .setAsk(scalar).setAlpha(scalar).setTxHash(scalar).build(); + } + + @Test + public void walletRejectsFailedSignatureInsteadOfReturningZeroBuffer() { + try (MockedStatic nativeApi = mockStatic(JLibrustzcash.class)) { + ZksnarkException error = assertThrows(ZksnarkException.class, + () -> new Wallet().createSpendAuthSig(signatureRequest())); + assertEquals("Spend signature generation failed", error.getMessage()); + nativeApi.verify(() -> JLibrustzcash.librustzcashSaplingSpendSig(any())); + } + } + + @Test + public void walletReturnsSuccessfulNativeSignatureUnchanged() throws Exception { + byte[] expected = new byte[64]; + Arrays.fill(expected, (byte) 7); + try (MockedStatic nativeApi = mockStatic(JLibrustzcash.class)) { + nativeApi.when(() -> JLibrustzcash.librustzcashSaplingSpendSig(any())) + .thenAnswer(invocation -> { + SpendSigParams params = invocation.getArgument(0); + System.arraycopy(expected, 0, params.getResult(), 0, expected.length); + return true; + }); + assertArrayEquals(expected, + new Wallet().createSpendAuthSig(signatureRequest()).getValue().toByteArray()); + } + } + + private static ExpandedSpendingKey spendingKey() throws Exception { + ExpandedSpendingKey key = mock(ExpandedSpendingKey.class); + byte[] ak = Hex.decode("db8c9492f599646369a4855b4260fe3b957cec8e1c33de7036ec61d8059d52d1"); + when(key.fullViewingKey()).thenReturn(new FullViewingKey(ak, new byte[32], new byte[32])); + when(key.getAsk()).thenReturn(new byte[32]); + when(key.getNsk()).thenReturn(new byte[32]); + return key; + } + + private static Note spendNote() throws Exception { + Note note = mock(Note.class); + when(note.cm()).thenReturn(new byte[32]); + when(note.nullifier(any(FullViewingKey.class), anyLong())).thenReturn(new byte[32]); + when(note.getD()).thenReturn(new DiversifierT()); + when(note.getRcm()).thenReturn(new byte[32]); + return note; + } + + private static void allowSpendProof(MockedStatic nativeApi) { + nativeApi.when(JLibrustzcash::librustzcashSaplingProvingCtxInit).thenReturn(CONTEXT); + nativeApi.when(() -> JLibrustzcash.librustzcashSaplingSpendProof(any())).thenReturn(true); + } + + private static Wallet transactionWallet() { + Wallet wallet = mock(Wallet.class); + when(wallet.createTransactionCapsuleWithoutValidate(any(Message.class), + eq(ContractType.ShieldedTransferContract), anyLong())).thenAnswer(invocation -> + new TransactionCapsule((Message) invocation.getArgument(0), + ContractType.ShieldedTransferContract)); + return wallet; + } + + @Test + public void trc20BuildStopsOnSpendSignatureFailureAndFreesContext() throws Exception { + ShieldedTRC20ParametersBuilder builder = new ShieldedTRC20ParametersBuilder("transfer"); + builder.setShieldedTRC20Address(new byte[20]); + builder.addSpend(spendingKey(), spendNote(), new byte[32], new byte[32], + new byte[1024], 0); + try (MockedStatic nativeApi = mockStatic(JLibrustzcash.class)) { + allowSpendProof(nativeApi); + ZksnarkException error = assertThrows(ZksnarkException.class, () -> builder.build(true)); + assertTrue(error.getMessage().contains("Spend signature generation failed")); + assertTrue(builder.getBuilder().getSpendDescription(0) + .getSpendAuthoritySignature().isEmpty()); + nativeApi.verify(() -> JLibrustzcash.librustzcashSaplingBindingSig(any()), never()); + nativeApi.verify(() -> JLibrustzcash.librustzcashSaplingProvingCtxFree(CONTEXT)); + } + } + + @Test + public void zenBuildStopsOnSpendSignatureFailureAndFreesContext() throws Exception { + ZenTransactionBuilder builder = new ZenTransactionBuilder(transactionWallet()); + org.tron.common.zksnark.IncrementalMerkleVoucherContainer voucher = + mock(org.tron.common.zksnark.IncrementalMerkleVoucherContainer.class, + org.mockito.Answers.RETURNS_DEEP_STUBS); + byte[] path = new byte[1065]; + path[0] = 32; + for (int i = 0; i < 32; i++) { + path[1 + i * 33] = 32; + } + when(voucher.path().encode()).thenReturn(path); + builder.addSpend(spendingKey(), spendNote(), new byte[32], new byte[32], voucher); + try (MockedStatic nativeApi = mockStatic(JLibrustzcash.class); + MockedStatic transactions = mockStatic(TransactionCapsule.class)) { + allowSpendProof(nativeApi); + transactions.when(() -> TransactionCapsule.getShieldTransactionHashIgnoreTypeException(any())) + .thenReturn(new byte[32]); + ZksnarkException error = assertThrows(ZksnarkException.class, builder::build); + assertEquals("Spend signature generation failed", error.getMessage()); + assertTrue(builder.getContractBuilder().getSpendDescription(0) + .getSpendAuthoritySignature().isEmpty()); + nativeApi.verify(() -> JLibrustzcash.librustzcashSaplingBindingSig(any()), never()); + nativeApi.verify(() -> JLibrustzcash.librustzcashSaplingProvingCtxFree(CONTEXT)); + } + } + + @Test + public void trc20BuildStopsOnBindingSignatureFailureAndFreesContext() throws Exception { + ShieldedTRC20ParametersBuilder builder = new ShieldedTRC20ParametersBuilder("transfer"); + builder.setShieldedTRC20Address(new byte[20]); + try (MockedStatic nativeApi = mockStatic(JLibrustzcash.class)) { + allowSpendProof(nativeApi); + ZksnarkException error = assertThrows(ZksnarkException.class, () -> builder.build(false)); + assertTrue(error.getMessage().contains("Binding signature generation failed")); + assertTrue(builder.getBuilder().getBindingSignature().isEmpty()); + nativeApi.verify(() -> JLibrustzcash.librustzcashSaplingBindingSig(any())); + nativeApi.verify(() -> JLibrustzcash.librustzcashSaplingProvingCtxFree(CONTEXT)); + } + } + + @Test + public void zenBuildStopsOnBindingSignatureFailureAndFreesContext() throws Exception { + ZenTransactionBuilder builder = new ZenTransactionBuilder(transactionWallet()); + try (MockedStatic nativeApi = mockStatic(JLibrustzcash.class); + MockedStatic transactions = mockStatic(TransactionCapsule.class)) { + allowSpendProof(nativeApi); + transactions.when(() -> TransactionCapsule.getShieldTransactionHashIgnoreTypeException(any())) + .thenReturn(new byte[32]); + ZksnarkException error = assertThrows(ZksnarkException.class, builder::buildWithoutAsk); + assertEquals("Binding signature generation failed", error.getMessage()); + assertTrue(builder.getContractBuilder().getBindingSignature().isEmpty()); + nativeApi.verify(() -> JLibrustzcash.librustzcashSaplingBindingSig(any())); + nativeApi.verify(() -> JLibrustzcash.librustzcashSaplingProvingCtxFree(CONTEXT)); + } + } +} diff --git a/framework/src/test/java/org/tron/core/zen/ShieldedTRC20ParametersBuilderTest.java b/framework/src/test/java/org/tron/core/zen/ShieldedTRC20ParametersBuilderTest.java index 83c82a4bda8..08f410c6c5f 100644 --- a/framework/src/test/java/org/tron/core/zen/ShieldedTRC20ParametersBuilderTest.java +++ b/framework/src/test/java/org/tron/core/zen/ShieldedTRC20ParametersBuilderTest.java @@ -1,5 +1,6 @@ package org.tron.core.zen; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mockStatic; @@ -8,6 +9,7 @@ import java.math.BigInteger; import java.security.MessageDigest; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import org.bouncycastle.util.encoders.Hex; @@ -90,12 +92,19 @@ static String encode(int spends, int receives, boolean withAsk) throws Exception new byte[21]); } + private static String encode(String type, ShieldedTRC20Parameters parameters, + List signatures, boolean withAsk) throws Exception { + return new ShieldedTRC20ParametersBuilder(type).getTriggerContractInput( + parameters, signatures, BigInteger.ONE, withAsk, new byte[20]); + } + static String digest(String triggerInput) throws Exception { return Hex.toHexString(MessageDigest.getInstance("SHA-256").digest(Hex.decode(triggerInput))); } private static String runEmpty(int spends, int receives) throws Exception { - ShieldedTRC20Parameters.Builder parameters = ShieldedTRC20Parameters.newBuilder(); + ShieldedTRC20Parameters.Builder parameters = ShieldedTRC20Parameters.newBuilder() + .setBindingSignature(fill(64, 13)); for (int i = 0; i < spends; i++) { parameters.addSpendDescription(ShieldContract.SpendDescription.getDefaultInstance()); } @@ -125,6 +134,162 @@ public void invalidCountsAreRejectedBeforeAnyMerge() throws Exception { } } + @Test + public void invalidBindingSignatureLengthsAreRejectedForEveryTypeAndSignaturePath() + throws Exception { + for (String type : new String[]{"mint", "transfer", "burn"}) { + ShieldedTRC20ParametersBuilder builder = new ShieldedTRC20ParametersBuilder(type); + for (boolean withAsk : new boolean[]{true, false}) { + for (int length : new int[]{0, 63, 65}) { + ShieldedTRC20Parameters parameters = populated(1, 1).toBuilder() + .setBindingSignature(fill(length, 13)).build(); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> builder.getTriggerContractInput(parameters, requestSignatures(1), + BigInteger.ONE, withAsk, new byte[20])); + assertEquals("binding_signature must be 64 bytes", exception.getMessage()); + } + } + } + } + + @Test + public void invalidSpendFieldLengthsAreRejected() throws Exception { + String[] fields = {"nullifier", "anchor", "value_commitment", "rk", "zkproof"}; + for (String type : new String[]{"transfer", "burn"}) { + int spends = "transfer".equals(type) ? 2 : 1; + for (int index = 0; index < spends; index++) { + for (String field : fields) { + int expected = "zkproof".equals(field) ? 192 : 32; + for (int length : new int[]{0, expected - 1, expected + 1}) { + ShieldedTRC20Parameters.Builder parameters = populated(spends, 1).toBuilder(); + ShieldContract.SpendDescription.Builder spend = + parameters.getSpendDescriptionBuilder(index); + spend.setField(spend.getDescriptorForType().findFieldByName(field), fill(length, 1)); + for (boolean withAsk : new boolean[]{true, false}) { + // Wallet checks external burn nullifiers before deriving the nonce. + if (!withAsk && "burn".equals(type) && "nullifier".equals(field)) { + continue; + } + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> encode(type, parameters.build(), requestSignatures(spends), withAsk)); + assertEquals("spend_description." + field + " must be " + expected + " bytes", + exception.getMessage()); + } + } + } + } + } + } + + @Test + public void invalidReceiveFieldLengthsAreRejected() throws Exception { + String[] fields = {"note_commitment", "value_commitment", "epk", "zkproof", "c_enc", "c_out"}; + int[] lengths = {32, 32, 32, 192, 580, 80}; + for (String type : new String[]{"mint", "transfer", "burn"}) { + int receives = "transfer".equals(type) ? 2 : 1; + int spends = "mint".equals(type) ? 0 : 1; + for (int index = 0; index < receives; index++) { + for (int fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) { + String field = fields[fieldIndex]; + int expected = lengths[fieldIndex]; + for (int length : new int[]{0, expected - 1, expected + 1}) { + ShieldedTRC20Parameters.Builder parameters = populated(spends, receives).toBuilder(); + ShieldContract.ReceiveDescription.Builder receive = + parameters.getReceiveDescriptionBuilder(index); + receive.setField(receive.getDescriptorForType().findFieldByName(field), + fill(length, 1)); + for (boolean withAsk : new boolean[]{true, false}) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> encode(type, parameters.build(), requestSignatures(spends), withAsk)); + assertEquals("receive_description." + field + " must be " + expected + " bytes", + exception.getMessage()); + } + } + } + } + } + } + + @Test + public void invalidSpendSignatureLengthsAreRejectedFromEitherSource() throws Exception { + for (String type : new String[]{"transfer", "burn"}) { + int spends = "transfer".equals(type) ? 2 : 1; + for (int index = 0; index < spends; index++) { + for (boolean withAsk : new boolean[]{true, false}) { + for (int length : new int[]{0, 63, 65}) { + ShieldedTRC20Parameters.Builder parameters = populated(spends, 1).toBuilder(); + List signatures = requestSignatures(spends); + if (withAsk) { + parameters.getSpendDescriptionBuilder(index) + .setSpendAuthoritySignature(fill(length, 6)); + } else { + signatures.set(index, BytesMessage.newBuilder().setValue(fill(length, 14)).build()); + } + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> encode(type, parameters.build(), signatures, withAsk)); + assertEquals("spend_authority_signature must be 64 bytes", exception.getMessage()); + } + } + } + } + } + + @Test + public void unusedSignatureSourcesDoNotAffectEncoding() throws Exception { + for (String type : new String[]{"transfer", "burn"}) { + ShieldedTRC20Parameters parameters = populated(1, 1); + String withAsk = encode(type, parameters, requestSignatures(1), true); + assertEquals(withAsk, encode(type, parameters, null, true)); + assertEquals(withAsk, encode(type, parameters, + Collections.singletonList(BytesMessage.getDefaultInstance()), true)); + + String withoutAsk = encode(type, parameters, requestSignatures(1), false); + ShieldedTRC20Parameters.Builder unsigned = parameters.toBuilder(); + unsigned.getSpendDescriptionBuilder(0).clearSpendAuthoritySignature(); + assertEquals(withoutAsk, encode(type, unsigned.build(), requestSignatures(1), false)); + } + ShieldedTRC20Parameters mint = populated(0, 1); + assertEquals(encode("mint", mint, null, true), encode("mint", mint.toBuilder() + .addSpendDescription(ShieldContract.SpendDescription.getDefaultInstance()).build(), + null, false)); + } + + @Test + public void internallyGeneratedBurnRequiresExactlyOneSpendDescription() throws Exception { + for (int spends : new int[]{0, 2}) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> encode("burn", populated(spends, 0), null, true)); + assertEquals("invalid burn input number", exception.getMessage()); + } + } + + @Test + public void validMintAndBurnParametersRetainTheirAbiSizes() throws Exception { + for (boolean withAsk : new boolean[]{true, false}) { + assertEquals(1056, Hex.decode(encode("mint", populated(0, 1), null, withAsk)).length); + assertEquals(736, + Hex.decode(encode("burn", populated(1, 0), requestSignatures(1), withAsk)).length); + assertEquals(1696, + Hex.decode(encode("burn", populated(1, 1), requestSignatures(1), withAsk)).length); + } + } + + @Test + public void transferWithValidBindingSignatureHasCanonicalAbiOffsets() throws Exception { + byte[] input = Hex.decode(encode(1, 1, false)); + assertEquals(1664, input.length); + // ABI head: three offsets, bytes32[2] binding signature, then the ciphertext offset. + int[][] offsetWords = {{0, 192}, {32, 544}, {64, 640}, {160, 960}}; + for (int[] word : offsetWords) { + assertEquals(BigInteger.valueOf(word[1]), + new BigInteger(1, Arrays.copyOfRange(input, word[0], word[0] + 32))); + assertEquals(BigInteger.ONE, + new BigInteger(1, Arrays.copyOfRange(input, word[1], word[1] + 32))); + } + assertArrayEquals(populated(1, 1).getBindingSignature().toByteArray(), + Arrays.copyOfRange(input, 96, 160)); + } + @Test public void populatedOutputsMatchTheBaseForEveryCountAndSignaturePath() throws Exception { for (String[] vector : BASE_DIGESTS) { diff --git a/framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java b/framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java index c37bd8f772d..64196b3f064 100644 --- a/framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/NoteEncDecryTest.java @@ -592,9 +592,14 @@ private TransactionInfo.Log buildBurnLog(byte[] ovk, BigInteger amount, byte[] t private GrpcAPI.ShieldedTRC20Parameters buildBurnTrc20Params(byte[] cipher, byte[] nf) { ShieldContract.SpendDescription spend = ShieldContract.SpendDescription.newBuilder() .setNullifier(ByteString.copyFrom(nf)) + .setAnchor(ByteString.copyFrom(new byte[32])) + .setValueCommitment(ByteString.copyFrom(new byte[32])) + .setRk(ByteString.copyFrom(new byte[32])) + .setZkproof(ByteString.copyFrom(new byte[192])) .build(); return GrpcAPI.ShieldedTRC20Parameters.newBuilder() .setParameterType("burn") + .setBindingSignature(ByteString.copyFrom(new byte[64])) .setTriggerContractInput(ByteArray.toHexString(cipher)) .addSpendDescription(spend) .build(); @@ -606,7 +611,8 @@ private GrpcAPI.ShieldedTRC20TriggerContractParameters buildBurnTriggerRequest( toAddress[0] = Wallet.getAddressPreFixByte(); return GrpcAPI.ShieldedTRC20TriggerContractParameters.newBuilder() .setShieldedTRC20Parameters(trc20Params) - .addSpendAuthoritySignature(GrpcAPI.BytesMessage.getDefaultInstance()) + .addSpendAuthoritySignature(GrpcAPI.BytesMessage.newBuilder() + .setValue(ByteString.copyFrom(new byte[64]))) .setAmount(value.toString()) .setTransparentToAddress(ByteString.copyFrom(toAddress)) .build(); diff --git a/framework/src/test/java/org/tron/core/zksnark/SendCoinShieldTest.java b/framework/src/test/java/org/tron/core/zksnark/SendCoinShieldTest.java index 7af3be0b2e1..8fcd681a6cf 100644 --- a/framework/src/test/java/org/tron/core/zksnark/SendCoinShieldTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/SendCoinShieldTest.java @@ -5,6 +5,7 @@ import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.io.Files; +import com.google.protobuf.Any; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import java.io.File; @@ -1415,16 +1416,8 @@ public SpendDescriptionCapsule generateSpendProof(SpendDescriptionInfo spend, lo addZeroValueOutputNote(builder); builder.setTransparentOutput(ByteArray.fromHexString(TO_ADDRESS), 10_000_000L); - TransactionCapsule transactionCap1 = builder.build(); - try { - executeTx(transactionCap1); - Assert.fail(); - } catch (ContractValidateException e) { - if (!e.getMessage().equals("librustzcashSaplingFinalCheck error")) { - throw e; - } - System.out.println("Done"); - } + ZksnarkException error = Assert.assertThrows(ZksnarkException.class, builder::build); + Assert.assertEquals("Binding signature generation failed", error.getMessage()); // 0L + 20_000_000L = 0L + 10_000_000L + 10_000_000L } @@ -1459,7 +1452,11 @@ public void TestGeneratesProofWithDiffCtx() throws Exception { public SpendDescriptionCapsule generateSpendProof(SpendDescriptionInfo spend, long ctx) throws ZksnarkException { long fakeCtx = JLibrustzcash.librustzcashSaplingProvingCtxInit(); - return super.generateSpendProof(spend, fakeCtx); + try { + return super.generateSpendProof(spend, fakeCtx); + } finally { + JLibrustzcash.librustzcashSaplingProvingCtxFree(fakeCtx); + } } }; @@ -1474,16 +1471,8 @@ public SpendDescriptionCapsule generateSpendProof(SpendDescriptionInfo spend, lo addZeroValueOutputNote(builder); builder.setTransparentOutput(ByteArray.fromHexString(TO_ADDRESS), 10_000_000L); - TransactionCapsule transactionCap1 = builder.build(); - try { - executeTx(transactionCap1); - Assert.fail(); - } catch (ContractValidateException e) { - if (!e.getMessage().equals("librustzcashSaplingFinalCheck error")) { - throw e; - } - System.out.println("Done"); - } + ZksnarkException error = Assert.assertThrows(ZksnarkException.class, builder::build); + Assert.assertEquals("Binding signature generation failed", error.getMessage()); // 0L + 20_000_000L = 0L + 10_000_000L + 10_000_000L } } @@ -1664,6 +1653,28 @@ public void TestDefaultBuilder() throws Exception { executeTx(transactionCapsule); } + @Test + public void testInvalidBindingSignatureIsRejectedByVerifier() throws Exception { + dbManager.getDynamicPropertiesStore().saveAllowShieldedTransaction(1); + dbManager.getDynamicPropertiesStore().saveTotalShieldedPoolValue(1000 * 1000000L); + + TransactionCapsule valid = generateDefaultBuilder(new ZenTransactionBuilder(wallet)); + Protocol.Transaction.Contract contract = valid.getInstance().getRawData().getContract(0); + ShieldedTransferContract invalidSignature = contract.getParameter() + .unpack(ShieldedTransferContract.class).toBuilder() + .setBindingSignature(ByteString.copyFrom(new byte[64])).build(); + TransactionCapsule invalid = new TransactionCapsule(valid.getInstance().toBuilder() + .setRawData(valid.getInstance().getRawData().toBuilder() + .setContract(0, contract.toBuilder().setParameter(Any.pack(invalidSignature)))) + .build()); + + ContractValidateException error = Assert.assertThrows(ContractValidateException.class, + () -> executeTx(invalid)); + Assert.assertEquals("librustzcashSaplingFinalCheck error", error.getMessage()); + // The same transaction with its original signature must still pass validation and execution. + executeTx(valid); + } + @Test public void TestWrongSpendRk() throws Exception { dbManager.getDynamicPropertiesStore().saveAllowShieldedTransaction(1);