From f301e6f769314718978b7280c447a295bd3c1657 Mon Sep 17 00:00:00 2001 From: federico Date: Wed, 15 Apr 2026 15:58:01 +0800 Subject: [PATCH] feat(crypto): shielded transaction API security enhancement --- .../common/parameter/CommonParameter.java | 2 +- .../src/main/java/org/tron/core/Wallet.java | 408 +++++++++--------- .../java/org/tron/core/config/args/Args.java | 2 +- .../zen/ShieldedTRC20ParametersBuilder.java | 11 +- .../tron/core/zen/ZenTransactionBuilder.java | 13 +- framework/src/main/resources/config.conf | 8 +- .../tron/core/ShieldedTRC20BuilderTest.java | 10 + .../ShieldedTransferActuatorTest.java | 56 +++ .../org/tron/core/config/args/ArgsTest.java | 9 + .../core/services/RpcApiServicesTest.java | 1 + .../http/CreateSpendAuthSigServletTest.java | 15 + .../core/zksnark/MerkleContainerTest.java | 15 + sprout-verifying.key | Bin 1449 -> 0 bytes 13 files changed, 335 insertions(+), 215 deletions(-) delete mode 100644 sprout-verifying.key diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index a73158a718a..f853dfa648f 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -335,7 +335,7 @@ public class CommonParameter { @Getter @Setter - public boolean allowShieldedTransactionApi; // clearParam: true + public boolean allowShieldedTransactionApi; // clearParam: false @Getter @Setter public long blockNumForEnergyLimit; diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 39e8f06c281..c7435ef0bd8 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -780,7 +780,7 @@ public WitnessList getPaginatedNowWitnessList(long offset, long limit) throws if (limit > WITNESS_COUNT_LIMIT_MAX) { limit = WITNESS_COUNT_LIMIT_MAX; } - + /* In the maintenance period, the VoteStores will be cleared. To avoid the race condition of VoteStores deleted but Witness vote counts not updated, @@ -1502,8 +1502,8 @@ public Protocol.ChainParameters getChainParameters() { builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() .setKey("getAllowTvmSelfdestructRestriction") .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmSelfdestructRestriction()) - .build()); - + .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() .setKey("getProposalExpireTime") .setValue(dbManager.getDynamicPropertiesStore().getProposalExpireTime()) @@ -2320,58 +2320,58 @@ public TransactionCapsule createShieldedTransaction(PrivateParameters request) checkCmValid(shieldedSpends, shieldedReceives); - // add - if (!ArrayUtils.isEmpty(transparentFromAddress)) { - builder.setTransparentInput(transparentFromAddress, fromAmount); - } + try { + // add + if (!ArrayUtils.isEmpty(transparentFromAddress)) { + builder.setTransparentInput(transparentFromAddress, fromAmount); + } - if (!ArrayUtils.isEmpty(transparentToAddress)) { - builder.setTransparentOutput(transparentToAddress, toAmount); - } - - // from shielded to public, without shielded receive, will create a random shielded address - if (!shieldedSpends.isEmpty() - && !ArrayUtils.isEmpty(transparentToAddress) - && shieldedReceives.isEmpty()) { - shieldedReceives = new ArrayList<>(); - ReceiveNote receiveNote = createReceiveNoteRandom(0); - shieldedReceives.add(receiveNote); - } - - // input - if (!(ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); - for (SpendNote spendNote : shieldedSpends) { - GrpcAPI.Note note = spendNote.getNote(); - PaymentAddress paymentAddress = KeyIo.decodePaymentAddress(note.getPaymentAddress()); - if (paymentAddress == null) { - throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + if (!ArrayUtils.isEmpty(transparentToAddress)) { + builder.setTransparentOutput(transparentToAddress, toAmount); + } + + // from shielded to public, without shielded receive, will create a random shielded address + if (!shieldedSpends.isEmpty() + && !ArrayUtils.isEmpty(transparentToAddress) + && shieldedReceives.isEmpty()) { + shieldedReceives = new ArrayList<>(); + ReceiveNote receiveNote = createReceiveNoteRandom(0); + shieldedReceives.add(receiveNote); + } + + // input + if (!(ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); + for (SpendNote spendNote : shieldedSpends) { + GrpcAPI.Note note = spendNote.getNote(); + PaymentAddress paymentAddress = KeyIo.decodePaymentAddress(note.getPaymentAddress()); + if (paymentAddress == null) { + throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + } + Note baseNote = new Note(paymentAddress.getD(), + paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); + + IncrementalMerkleVoucherContainer voucherContainer = + new IncrementalMerkleVoucherCapsule( + spendNote.getVoucher()).toMerkleVoucherContainer(); + builder.addSpend(expsk, + baseNote, + spendNote.getAlpha().toByteArray(), + spendNote.getVoucher().getRt().toByteArray(), + voucherContainer); } - Note baseNote = new Note(paymentAddress.getD(), - paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); - - IncrementalMerkleVoucherContainer voucherContainer = new IncrementalMerkleVoucherCapsule( - spendNote.getVoucher()).toMerkleVoucherContainer(); - builder.addSpend(expsk, - baseNote, - spendNote.getAlpha().toByteArray(), - spendNote.getVoucher().getRt().toByteArray(), - voucherContainer); } - } - // output - shieldedOutput(shieldedReceives, builder, ovk); + // output + shieldedOutput(shieldedReceives, builder, ovk); - TransactionCapsule transactionCapsule = null; - try { - transactionCapsule = builder.build(); + return builder.build(); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow"); } catch (ZksnarkException e) { logger.error("createShieldedTransaction except, error is " + e.toString()); throw new ZksnarkException(e.toString()); } - return transactionCapsule; - } public TransactionCapsule createShieldedTransactionWithoutSpendAuthSig( @@ -2422,59 +2422,60 @@ public TransactionCapsule createShieldedTransactionWithoutSpendAuthSig( checkCmValid(shieldedSpends, shieldedReceives); - // add - if (!ArrayUtils.isEmpty(transparentFromAddress)) { - builder.setTransparentInput(transparentFromAddress, fromAmount); - } + try { + // add + if (!ArrayUtils.isEmpty(transparentFromAddress)) { + builder.setTransparentInput(transparentFromAddress, fromAmount); + } - if (!ArrayUtils.isEmpty(transparentToAddress)) { - builder.setTransparentOutput(transparentToAddress, toAmount); - } + if (!ArrayUtils.isEmpty(transparentToAddress)) { + builder.setTransparentOutput(transparentToAddress, toAmount); + } - // from shielded to public, without shielded receive, will create a random shielded address - if (!shieldedSpends.isEmpty() - && !ArrayUtils.isEmpty(transparentToAddress) - && shieldedReceives.isEmpty()) { - shieldedReceives = new ArrayList<>(); - ReceiveNote receiveNote = createReceiveNoteRandom(0); - shieldedReceives.add(receiveNote); - } + // from shielded to public, without shielded receive, will create a random shielded address + if (!shieldedSpends.isEmpty() + && !ArrayUtils.isEmpty(transparentToAddress) + && shieldedReceives.isEmpty()) { + shieldedReceives = new ArrayList<>(); + ReceiveNote receiveNote = createReceiveNoteRandom(0); + shieldedReceives.add(receiveNote); + } - // input - if (!(ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - for (SpendNote spendNote : shieldedSpends) { - GrpcAPI.Note note = spendNote.getNote(); - PaymentAddress paymentAddress = KeyIo.decodePaymentAddress(note.getPaymentAddress()); - if (paymentAddress == null) { - throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + // input + if (!(ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + for (SpendNote spendNote : shieldedSpends) { + GrpcAPI.Note note = spendNote.getNote(); + PaymentAddress paymentAddress = KeyIo.decodePaymentAddress( + note.getPaymentAddress()); + if (paymentAddress == null) { + throw new ZksnarkException(PAYMENT_ADDRESS_FORMAT_WRONG); + } + Note baseNote = new Note(paymentAddress.getD(), + paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); + + IncrementalMerkleVoucherContainer voucherContainer = + new IncrementalMerkleVoucherCapsule( + spendNote.getVoucher()).toMerkleVoucherContainer(); + builder.addSpend(ak, + nsk, + ovk, + baseNote, + spendNote.getAlpha().toByteArray(), + spendNote.getVoucher().getRt().toByteArray(), + voucherContainer); } - Note baseNote = new Note(paymentAddress.getD(), - paymentAddress.getPkD(), note.getValue(), note.getRcm().toByteArray()); - - IncrementalMerkleVoucherContainer voucherContainer = new IncrementalMerkleVoucherCapsule( - spendNote.getVoucher()).toMerkleVoucherContainer(); - builder.addSpend(ak, - nsk, - ovk, - baseNote, - spendNote.getAlpha().toByteArray(), - spendNote.getVoucher().getRt().toByteArray(), - voucherContainer); } - } - // output - shieldedOutput(shieldedReceives, builder, ovk); + // output + shieldedOutput(shieldedReceives, builder, ovk); - TransactionCapsule transactionCapsule = null; - try { - transactionCapsule = builder.buildWithoutAsk(); + return builder.buildWithoutAsk(); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow"); } catch (ZksnarkException e) { logger.error("createShieldedTransaction exception, error is " + e.toString()); throw new ZksnarkException(e.toString()); } - return transactionCapsule; - } private void shieldedOutput(List shieldedReceives, @@ -2492,7 +2493,6 @@ private void shieldedOutput(List shieldedReceives, } } - public ShieldedAddressInfo getNewShieldedAddress() throws BadItemException, ZksnarkException { checkAllowShieldedTransactionApi(); @@ -3658,77 +3658,80 @@ public ShieldedTRC20Parameters createShieldedContractParameters( scaledToAmount, shieldedReceives.get(0).getNote().getValue(), dbManager.getDynamicPropertiesStore().disableJavaLangMath())); } catch (ArithmeticException e) { - throw new ZksnarkException("Unbalanced burn!"); + throw new ZksnarkException("shielded amount overflow"); } } - if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 - && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() - && scaledToAmount == 0) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); - - byte[] ovk = request.getOvk().toByteArray(); - if (ArrayUtils.isEmpty(ovk)) { - ovk = SpendingKey.random().fullViewingKey().getOvk(); - } + try { + if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 + && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() + && scaledToAmount == 0) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); + + byte[] ovk = request.getOvk().toByteArray(); + if (ArrayUtils.isEmpty(ovk)) { + ovk = SpendingKey.random().fullViewingKey().getOvk(); + } - builder.setTransparentFromAmount(fromAmount); - buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); - } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 - && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); - - byte[] ask = request.getAsk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); - } + builder.setTransparentFromAmount(fromAmount); + buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 + && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); + + byte[] ask = request.getAsk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); + } - ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); - for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { - buildShieldedTRC20Input(builder, spendNote, expsk); - } + ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); + for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { + buildShieldedTRC20Input(builder, spendNote, expsk); + } - for (ReceiveNote receiveNote : shieldedReceives) { - buildShieldedTRC20Output(builder, receiveNote, ovk); - } - } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 - && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); - - byte[] ask = request.getAsk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); - } + for (ReceiveNote receiveNote : shieldedReceives) { + buildShieldedTRC20Output(builder, receiveNote, ovk); + } + } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 + && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); + + byte[] ask = request.getAsk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ask) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ask, nsk or ovk"); + } - byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); - if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { - throw new ContractValidateException("No valid transparent TRC-20 output address"); - } + byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); + if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { + throw new ContractValidateException("No valid transparent TRC-20 output address"); + } - byte[] transparentToAddressTvm = new byte[20]; - System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); - builder.setTransparentToAddress(transparentToAddressTvm); - builder.setTransparentToAmount(toAmount); + byte[] transparentToAddressTvm = new byte[20]; + System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); + builder.setTransparentToAddress(transparentToAddressTvm); + builder.setTransparentToAmount(toAmount); - Optional cipher = NoteEncryption.Encryption - .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); - cipher.ifPresent(builder::setBurnCiphertext); + Optional cipher = NoteEncryption.Encryption + .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); + cipher.ifPresent(builder::setBurnCiphertext); - ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); - GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); - buildShieldedTRC20Input(builder, spendNote, expsk); - if (receiveSize == 1) { - buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + ExpandedSpendingKey expsk = new ExpandedSpendingKey(ask, nsk, ovk); + GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); + buildShieldedTRC20Input(builder, spendNote, expsk); + if (receiveSize == 1) { + buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + } + } else { + throw new ContractValidateException("invalid shielded TRC-20 parameters"); } - } else { - throw new ContractValidateException("invalid shielded TRC-20 parameters"); + return builder.build(true); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow"); } - - return builder.build(true); } private void buildShieldedTRC20InputWithAK( @@ -3795,61 +3798,65 @@ public ShieldedTRC20Parameters createShieldedContractParametersWithoutAsk( } } - if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 - && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() - && scaledToAmount == 0) { - byte[] ovk = request.getOvk().toByteArray(); - if (ArrayUtils.isEmpty(ovk)) { - ovk = SpendingKey.random().fullViewingKey().getOvk(); - } - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); - builder.setTransparentFromAmount(fromAmount); - ReceiveNote receiveNote = shieldedReceives.get(0); - buildShieldedTRC20Output(builder, receiveNote, ovk); - } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 - && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); - byte[] ak = request.getAk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); - } - for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { - buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); - } - for (ReceiveNote receiveNote : shieldedReceives) { + try { + if (scaledFromAmount > 0 && spendSize == 0 && receiveSize == 1 + && scaledFromAmount == shieldedReceives.get(0).getNote().getValue() + && scaledToAmount == 0) { + byte[] ovk = request.getOvk().toByteArray(); + if (ArrayUtils.isEmpty(ovk)) { + ovk = SpendingKey.random().fullViewingKey().getOvk(); + } + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.MINT); + builder.setTransparentFromAmount(fromAmount); + ReceiveNote receiveNote = shieldedReceives.get(0); buildShieldedTRC20Output(builder, receiveNote, ovk); + } else if (scaledFromAmount == 0 && spendSize > 0 && spendSize < 3 + && receiveSize > 0 && receiveSize < 3 && scaledToAmount == 0) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.TRANSFER); + byte[] ak = request.getAk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); + } + for (GrpcAPI.SpendNoteTRC20 spendNote : shieldedSpends) { + buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); + } + for (ReceiveNote receiveNote : shieldedReceives) { + buildShieldedTRC20Output(builder, receiveNote, ovk); + } + } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 + && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { + builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); + byte[] ak = request.getAk().toByteArray(); + byte[] nsk = request.getNsk().toByteArray(); + byte[] ovk = request.getOvk().toByteArray(); + if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { + throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); + } + byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); + if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { + throw new ContractValidateException("No transparent TRC-20 output address"); + } + byte[] transparentToAddressTvm = new byte[20]; + System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); + builder.setTransparentToAddress(transparentToAddressTvm); + builder.setTransparentToAmount(toAmount); + Optional cipher = NoteEncryption.Encryption + .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); + cipher.ifPresent(builder::setBurnCiphertext); + GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); + buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); + if (receiveSize == 1) { + buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); + } + } else { + throw new ContractValidateException("invalid shielded TRC-20 parameters"); } - } else if (scaledFromAmount == 0 && spendSize == 1 && receiveSize >= 0 && receiveSize <= 1 - && scaledToAmount > 0 && totalToAmount == shieldedSpends.get(0).getNote().getValue()) { - builder.setShieldedTRC20ParametersType(ShieldedTRC20ParametersType.BURN); - byte[] ak = request.getAk().toByteArray(); - byte[] nsk = request.getNsk().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - if ((ArrayUtils.isEmpty(ak) || ArrayUtils.isEmpty(nsk) || ArrayUtils.isEmpty(ovk))) { - throw new ContractValidateException("No shielded TRC-20 ak, nsk or ovk"); - } - byte[] transparentToAddress = request.getTransparentToAddress().toByteArray(); - if (ArrayUtils.isEmpty(transparentToAddress) || transparentToAddress.length != 21) { - throw new ContractValidateException("No transparent TRC-20 output address"); - } - byte[] transparentToAddressTvm = new byte[20]; - System.arraycopy(transparentToAddress, 1, transparentToAddressTvm, 0, 20); - builder.setTransparentToAddress(transparentToAddressTvm); - builder.setTransparentToAmount(toAmount); - Optional cipher = NoteEncryption.Encryption - .encryptBurnMessageByOvk(ovk, toAmount, transparentToAddress); - cipher.ifPresent(builder::setBurnCiphertext); - GrpcAPI.SpendNoteTRC20 spendNote = shieldedSpends.get(0); - buildShieldedTRC20InputWithAK(builder, spendNote, ak, nsk); - if (receiveSize == 1) { - buildShieldedTRC20Output(builder, shieldedReceives.get(0), ovk); - } - } else { - throw new ContractValidateException("invalid shielded TRC-20 parameters"); + return builder.build(false); + } catch (ArithmeticException e) { + throw new ZksnarkException("shielded amount overflow"); } - return builder.build(false); } private int getShieldedTRC20LogType(TransactionInfo.Log log, byte[] contractAddress, @@ -4596,4 +4603,3 @@ public PricesResponseMessage getMemoFeePrices() { return null; } } - diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 83d7fd2c63d..39631e2a3f1 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -726,7 +726,7 @@ public static void applyConfigParams( logger.warn("Configuring [node.fullNodeAllowShieldedTransaction] will be deprecated. " + "Please use [node.allowShieldedTransactionApi] instead."); } else { - PARAMETER.allowShieldedTransactionApi = true; + PARAMETER.allowShieldedTransactionApi = false; } PARAMETER.zenTokenId = config.hasPath(ConfigKey.NODE_ZEN_TOKENID) 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 95e4eeb0ccd..4b980c7b7c9 100644 --- a/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java +++ b/framework/src/main/java/org/tron/core/zen/ShieldedTRC20ParametersBuilder.java @@ -14,6 +14,7 @@ import org.tron.api.GrpcAPI; import org.tron.api.GrpcAPI.BytesMessage; import org.tron.api.GrpcAPI.ShieldedTRC20Parameters; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.utils.ByteArray; import org.tron.common.utils.ByteUtil; import org.tron.common.utils.Sha256Hash; @@ -547,8 +548,8 @@ public void addSpend( byte[] anchor, byte[] path, long position) throws ZksnarkException { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, anchor, path, position)); - valueBalance += note.getValue(); } public void addSpend( @@ -558,8 +559,8 @@ public void addSpend( byte[] anchor, byte[] path, long position) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, alpha, anchor, path, position)); - valueBalance += note.getValue(); } public void addSpend( @@ -570,23 +571,23 @@ public void addSpend( byte[] anchor, byte[] path, long position) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(ak, nsk, note, alpha, anchor, path, position)); - valueBalance += note.getValue(); } public void addOutput(byte[] ovk, PaymentAddress to, long value, byte[] memo) throws ZksnarkException { Note note = new Note(to, value); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public void addOutput(byte[] ovk, DiversifierT d, byte[] pkD, long value, byte[] r, byte[] memo) { Note note = new Note(d, pkD, value, r); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public static class SpendDescriptionInfo { 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 2e531e44d44..fc3be8352ee 100644 --- a/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java +++ b/framework/src/main/java/org/tron/core/zen/ZenTransactionBuilder.java @@ -10,6 +10,7 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.utils.ByteArray; import org.tron.common.zksnark.IncrementalMerkleVoucherContainer; import org.tron.common.zksnark.JLibrustzcash; @@ -66,8 +67,8 @@ public ZenTransactionBuilder() { } public void addSpend(SpendDescriptionInfo spendDescriptionInfo) { + valueBalance = StrictMathWrapper.addExact(valueBalance, spendDescriptionInfo.note.getValue()); spends.add(spendDescriptionInfo); - valueBalance += spendDescriptionInfo.note.getValue(); } public void addSpend( @@ -75,8 +76,8 @@ public void addSpend( Note note, byte[] anchor, IncrementalMerkleVoucherContainer voucher) throws ZksnarkException { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, anchor, voucher)); - valueBalance += note.getValue(); } public void addSpend( @@ -85,8 +86,8 @@ public void addSpend( byte[] alpha, byte[] anchor, IncrementalMerkleVoucherContainer voucher) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(expsk, note, alpha, anchor, voucher)); - valueBalance += note.getValue(); } public void addSpend( @@ -97,23 +98,23 @@ public void addSpend( byte[] alpha, byte[] anchor, IncrementalMerkleVoucherContainer voucher) { + valueBalance = StrictMathWrapper.addExact(valueBalance, note.getValue()); spends.add(new SpendDescriptionInfo(ak, nsk, ovk, note, alpha, anchor, voucher)); - valueBalance += note.getValue(); } public void addOutput(byte[] ovk, PaymentAddress to, long value, byte[] memo) throws ZksnarkException { Note note = new Note(to, value); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public void addOutput(byte[] ovk, DiversifierT d, byte[] pkD, long value, byte[] r, byte[] memo) { Note note = new Note(d, pkD, value, r); note.setMemo(memo); + valueBalance = StrictMathWrapper.subtractExact(valueBalance, value); receives.add(new ReceiveDescriptionInfo(ovk, note)); - valueBalance -= value; } public void setTransparentInput(byte[] address, long value) { diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 369924074bc..05c7ab56378 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -178,7 +178,13 @@ node { minParticipationRate = 15 - # allowShieldedTransactionApi = true + # WARNING: Some shielded transaction APIs require sending private keys as parameters. + # Calling these APIs on untrusted or remote nodes may leak your private keys. + # It is recommended to invoke them locally for development and testing. + # To opt in, set: allowShieldedTransactionApi = true + # Migration: the legacy key node.fullNodeAllowShieldedTransaction is still supported + # but deprecated; please migrate to node.allowShieldedTransactionApi. + # allowShieldedTransactionApi = false # openPrintLog = true diff --git a/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java b/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java index c2c4bfe3006..71192706049 100644 --- a/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java +++ b/framework/src/test/java/org/tron/core/ShieldedTRC20BuilderTest.java @@ -9,6 +9,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.tuple.Pair; import org.bouncycastle.util.encoders.Hex; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Ignore; @@ -73,9 +74,18 @@ public class ShieldedTRC20BuilderTest extends BaseTest { VerifyTransferProof transferContract = new VerifyTransferProof(); VerifyBurnProof burnContract = new VerifyBurnProof(); + private static boolean origShieldedApi; + @BeforeClass public static void initZksnarkParams() { ZksnarkInitService.librustzcashInitZksnarkParams(); + origShieldedApi = Args.getInstance().allowShieldedTransactionApi; + Args.getInstance().allowShieldedTransactionApi = true; + } + + @AfterClass + public static void restoreShieldedApi() { + Args.getInstance().allowShieldedTransactionApi = origShieldedApi; } @Ignore diff --git a/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java b/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java index faec4c74039..578f9f5ebed 100755 --- a/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java +++ b/framework/src/test/java/org/tron/core/actuator/ShieldedTransferActuatorTest.java @@ -1157,6 +1157,9 @@ public void publicAddressToShieldNoteValueFailure() { actuator.validate(); actuator.execute(ret); Assert.assertTrue(false); + } catch (ArithmeticException e) { + // StrictMathWrapper.subtractExact throws ArithmeticException on overflow + Assert.assertTrue(true); } catch (ContractValidateException e) { Assert.assertTrue(e instanceof ContractValidateException); Assert.assertEquals("librustzcashSaplingFinalCheck error", e.getMessage()); @@ -1346,5 +1349,58 @@ public void shieldAddressToPublic() { Assert.assertTrue(false); } } + + /** + * Test that shielded transfer transaction validation works even when + * allowShieldedTransactionApi is disabled. This verifies that the API flag + * only gates wallet/helper APIs, not the core transaction validation logic. + */ + @Test + public void shieldedTransferValidationWorksWhenApiDisabled() { + boolean orig = Args.getInstance().isAllowShieldedTransactionApi(); + // Disable the shielded API (this should NOT affect transaction validation) + Args.getInstance().setAllowShieldedTransactionApi(false); + + dbManager.getDynamicPropertiesStore().saveAllowShieldedTransaction(1); + dbManager.getDynamicPropertiesStore().saveTotalShieldedPoolValue(AMOUNT); + + try { + ZenTransactionBuilder builder = new ZenTransactionBuilder(wallet); + SpendingKey sk = SpendingKey.random(); + ExpandedSpendingKey expsk = sk.expandedSpendingKey(); + PaymentAddress address = sk.defaultAddress(); + Note note = new Note(address, AMOUNT); + IncrementalMerkleVoucherContainer voucher = createSimpleMerkleVoucherContainer(note.cm()); + byte[] anchor = voucher.root().getContent().toByteArray(); + dbManager.getMerkleContainer() + .putMerkleTreeIntoStore(anchor, voucher.getVoucherCapsule().getTree()); + builder.addSpend(expsk, note, anchor, voucher); + + addZeroValueOutputNote(builder); + + long fee = dbManager.getDynamicPropertiesStore().getShieldedTransactionCreateAccountFee(); + String addressNotExist = + Wallet.getAddressPreFixString() + "8ba2aaae540c642e44e3bed5522c63bbc21f0000"; + + builder.setTransparentOutput(ByteArray.fromHexString(addressNotExist), AMOUNT - fee); + + TransactionCapsule transactionCap = builder.build(); + Contract contract = + transactionCap.getInstance().toBuilder().getRawDataBuilder().getContract(0); + ShieldedTransferActuator actuator = new ShieldedTransferActuator(); + actuator.setChainBaseManager(dbManager.getChainBaseManager()).setContract(contract) + .setTx(transactionCap); + + // Validation should succeed even when API is disabled + actuator.validate(); + } catch (ContractValidateException e) { + Assert.fail("Shielded transfer validation should not throw ContractValidateException: " + + e.getMessage()); + } catch (Exception e) { + Assert.fail("Shielded transfer should not throw Exception: " + e.getMessage()); + } finally { + Args.getInstance().setAllowShieldedTransactionApi(orig); + } + } } diff --git a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java index 88d893bcf97..59fdae00f89 100644 --- a/framework/src/test/java/org/tron/core/config/args/ArgsTest.java +++ b/framework/src/test/java/org/tron/core/config/args/ArgsTest.java @@ -360,5 +360,14 @@ public void testConfigStorageDefaults() { Args.clearParam(); } + + @Test + public void testAllowShieldedTransactionApiDefault() { + Args.setParam(new String[]{}, TestConstants.TEST_CONF); + Assert.assertFalse(Args.getInstance().isAllowShieldedTransactionApi()); + Args.getInstance().setAllowShieldedTransactionApi(true); + Assert.assertTrue(Args.getInstance().isAllowShieldedTransactionApi()); + Args.getInstance().setAllowShieldedTransactionApi(false); + } } diff --git a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java index f40ec48e035..7312b28ff5c 100644 --- a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java +++ b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java @@ -150,6 +150,7 @@ public class RpcApiServicesTest { public static void init() throws IOException { Args.setParam(new String[] {"-d", temporaryFolder.newFolder().toString()}, TestConstants.TEST_CONF); + getInstance().allowShieldedTransactionApi = true; Assert.assertEquals(5, getInstance().getRpcMaxRstStream()); Assert.assertEquals(10, getInstance().getRpcSecondsPerWindow()); String OWNER_ADDRESS = Wallet.getAddressPreFixString() diff --git a/framework/src/test/java/org/tron/core/services/http/CreateSpendAuthSigServletTest.java b/framework/src/test/java/org/tron/core/services/http/CreateSpendAuthSigServletTest.java index 85d6764132b..d3ebf26a261 100644 --- a/framework/src/test/java/org/tron/core/services/http/CreateSpendAuthSigServletTest.java +++ b/framework/src/test/java/org/tron/core/services/http/CreateSpendAuthSigServletTest.java @@ -8,7 +8,9 @@ import java.io.UnsupportedEncodingException; import javax.annotation.Resource; import org.apache.http.client.methods.HttpPost; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -18,6 +20,8 @@ public class CreateSpendAuthSigServletTest extends BaseTest { + private static boolean origShieldedApi; + static { Args.setParam( new String[]{ @@ -26,6 +30,17 @@ public class CreateSpendAuthSigServletTest extends BaseTest { ); } + @BeforeClass + public static void enableShieldedApi() { + origShieldedApi = Args.getInstance().allowShieldedTransactionApi; + Args.getInstance().allowShieldedTransactionApi = true; + } + + @AfterClass + public static void restoreShieldedApi() { + Args.getInstance().allowShieldedTransactionApi = origShieldedApi; + } + @Resource private CreateSpendAuthSigServlet createSpendAuthSigServlet; diff --git a/framework/src/test/java/org/tron/core/zksnark/MerkleContainerTest.java b/framework/src/test/java/org/tron/core/zksnark/MerkleContainerTest.java index ed52e014a7b..61fb36a9f68 100644 --- a/framework/src/test/java/org/tron/core/zksnark/MerkleContainerTest.java +++ b/framework/src/test/java/org/tron/core/zksnark/MerkleContainerTest.java @@ -3,7 +3,9 @@ import com.google.protobuf.Any; import com.google.protobuf.ByteString; import javax.annotation.Resource; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; @@ -38,10 +40,23 @@ public class MerkleContainerTest extends BaseTest { // private static MerkleContainer merkleContainer; + private static boolean origShieldedApi; + static { Args.setParam(new String[]{"-d", dbPath()}, TestConstants.TEST_CONF); } + @BeforeClass + public static void enableShieldedApi() { + origShieldedApi = Args.getInstance().allowShieldedTransactionApi; + Args.getInstance().allowShieldedTransactionApi = true; + } + + @AfterClass + public static void restoreShieldedApi() { + Args.getInstance().allowShieldedTransactionApi = origShieldedApi; + } + /*@Before public void init() { merkleContainer = MerkleContainer diff --git a/sprout-verifying.key b/sprout-verifying.key deleted file mode 100644 index 16655abb5d779ce6818a9b75bbc0cd7a1ac7040b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1449 zcmV;a1y=en2U9s<3m5x<&kWAU?`%6S?TQhqNdm<^h*lR5`rT3+g~LVmr3UcUFWO%F{OCp?w%Gq%gRxie6r&EN$~{E1sKuJEA8!J;u8h} z+$5p;!Hp3-GzC|PweD|f5xSSI47o*|7W^*EG8oJwFglKid%gTgi?K}GWXx+|edOWQ z!fJKO7!xcaJ>PuR0NRfThiRg1zNXxk*m^;)w-$u1djpPd50jl)zm57*C@?L2BtUsa zqH2t9IGzsSOVJ$Zf6Vc#N?U?XG%H6keZv@r%dL*YgkcXXwf1Ng3UB+7=Uj z=$@K~%>{ah2Y#^GDWlV<_dg326NOgJYWT^zTN{-1-jS>vodDN==xVHmQx)+>0n7GX zyoj;z!%3)~v>58i>&}B#AtW%uW5LE=DvupvGHUNHtVW!VYCS?a8h`NtGHAtK#D){f zFcu}5B11|qE5wCe!Ms8?kp(M9ZPKjpuSWXO1!@+2*Mc|BP!gp8$s|80TEzQ|GpWsqG^Mqm##y0I`*se7+0%9Tlh#)2X!vv9FFk`6(`qm~P zU(QG#Fp9jep8~Z5pEz$%uBtZiH;5cICKmp_h7DsQ+rNhW5I}X=dr%5_;~?F2?7*_c zi!^mYddpPd8+5wa5AB;K|z`Z56R&36noQ zzJ`fAMeb7vX5ZKC4K$>2w_=CS&Yl}zE3-3>1u)B;EO=YS<(2xf_>OzBi)8uiE(bvP zT&_MGo{h-D;v%}qNKIxFpscSACTV9PY8Oqc3Pwx~Nr`eQxhA%cl%hccNz9mF2Y z1}`w-l8y`h;E|X8h$8kyE33QWKF)n+V)|Mmks2uET*@8OER|p*H=VQpA-p`63(r?X z4L{)iT;#Yw3B77C17aOH3ONce3NZ>Y3Ns2c3N;Eg3O5Qk3ONcezCAy}v#)x!4=HC_OwJMfn0`If3$w*TtSWT%C`K#2!eCz7Nznrz^knJIyc@NjPtw0 zb677uz}@@}YE$hp7BEM`2FgM4#2T8?e_hH_jE(cnZ#3c&Eo?Hd2QyCQ+#P+SILVt~ zYAjzhqmT(Alu^O{+q3yHzR6n^D$~w5bO|u-e$<~}qHS^-)l^0Cr^&=d1yw+_TMmvD z_10QZ;Km5